Component model support
Context
https://github.com/bytecodealliance/wasmtime/tree/main/examples/component Add support for making the same example from C#.
https://github.com/WebAssembly/component-model/tree/main https://github.com/bytecodealliance/componentize-dotnet
Current state:
Only Wasm modules can be instantiated and functions ran from them. Loading .wasm file which is compiled as a component into a Module fails.
Steps
[ ] running components [ ] imports and exports (with WIT data types) [ ] converting modules to components [ ] multiple components interacting
Remarks
This is blocked by https://github.com/bytecodealliance/wasmtime/issues/8036#issuecomment-2180272305
Now that we released wasmtime v22 support, we should have the right version but may need to expose some new apis
Related: other language bindings have varying support https://github.com/bytecodealliance/wasmtime-py/issues/245 https://github.com/bytecodealliance/wasmtime-go/issues/204 https://github.com/bytecodealliance/wasmtime/pull/7801
Since we use the c-api we need https://github.com/bytecodealliance/wasmtime/issues/8036.
I'm also very much interested in supporting Wasm Component.
The wasmtime 33 release and 34 release expanded the C api to support (some aspects of) Wasm Components.
This is encouraging and it seems we can make some progress here as well?
Yes, the project could begin to consume those. Its does need a refresh to pick up the latest wasmtime versions
Yes, the project could begin to consume those. Its does need a refresh to pick up the latest wasmtime versions
I have created a PR for the refresh: #332
Good job on the refresh btw!
Thinking about the component model, how would it work? Could we dress up some classes with attributes like Microsoft's clearscript?
Good job on the refresh btw!
Thinking about the component model, how would it work? Could we dress up some classes with attributes like Microsoft's clearscript?
I did some experimental development efforts to support components. In contrast with core Wasm, the Component Model supports complex types such as string, list, result variant, option and record. The design seems to be influenced by ML-enthousiasts ;)
The challenge will be to have an ergonomic mapping between mainstream C# types (List<>, struct, class, enum, etc.) and these types in the Component Model. Some will be straight forward, such as List<> and string. Other mappings such as to variant and result will be less natural as there is no de-facto standard equivalent in C#. As another example, a subtle complication is that the char type in Component Model is defined as holding a unicode scalar value, which is 32 bits. Where as the char in C# is 16 bit. Should we map the char to a C# char and throw exceptions when out of range or should we map it to a byte[], uint or int ?
Besides the mechanics of mapping values there are also the WIT interface files. There are code generators for various languages to generate interface code based on a WIT file. Perhaps this functionality should be in wasmtime-dotnet as well, or perhaps it should be in a separate package?
So, with respect to wasmtime-dotnet I expect the implementation of the component model to be more opionated than the rest of the code. Imo we should strive towards a lower-level and flexible interface, with a higher-level interface on top. Not unlike the System.Text.JSON serialization where you can easily (de)serialize objects using the high-level interface but can utilize the low-level interface if you require that kind of control. For example, wasmtime-dotnet could provide a Result type out of the box for convenience, but also allow applications to supply their own Result type.
This feature is not trivial to implement and will be opionated by its nature. A way forward would be to implement the low-level mechanics first and then start building the high-level interfaces and WIT interfacing on top of that.
uint is likely the appropriate type for a unicode scalar. In C#, such a type is represented with the Rune struct. Encoding.UTF32 may be of help in case the char type can be used as parts of a string.
For example, wasmtime-dotnet could provide a Result type out of the box for convenience, but also allow applications to supply their own Result type.
Just a note on this, wasmtime-dotnet actually does something similar for functions already. See Result.cs and demo use in tests.
A function that returns int from wasm can be wrapped like this:
var func = instance.GetFunction<int>("func_name"); // Throws on trap
var func = instance.GetFunction<FunctionResult<int>>("func_name"); // Result indicates trap
Rune
One complication is that type doesn't seem to be supported in netstandard2.0 or netstandard2.1, which we support (see: https://github.com/bytecodealliance/wasmtime-dotnet/blob/main/src/Wasmtime.csproj#L4C6-L4C22)
Couldn't variant just be a dynamic?
Let's not fall into the trap of using dynamic for anything at all, ever. A setup like System.Text.Json which instead supports serialization as well as getting members by their name is more robust.
If variant types are only ever blittable, a straight up cast (via something like Unsafe.ReadUnaligned) would be even easier.
@just-ero I mentioned dynamic because other scripting libraries use it (ClearScript for example).
While I don't personally use it, what trap would that be if I may ask? Aside from no compiler intellisense and no native aot.
What if someone has unsafe off and wanted to use the method? I don't think JSON marshalling would make sense here, the performance alone would not be great..
Variant in the "spec" (at least on bytecodealliance) describes it as an enum like. Reminds me kinds like C's union. Pascal had a variant as well.
Could probably be marshalled as it's own class Variant and VariantDefinition?
Pseudo code:
class VariantDefinition { allowedTypes }
class Variant { VariantDefinition definition object value }
And both could be used in tandem. When setting the variant, it could be checked against the definition if it's the right value.
Aside from no compiler intellisense and no native aot.
I don't view sacrificing the user's experience for a "convenient" implementation as viable. dynamic would additionally box value types and use reflection for accessing values.
What if someone has unsafe off and wanted to use the method?
I don't understand this question. There is absolutely no influence <AllowUnsafeBlocks /> has on the code quality or the code generation. It does not make sense to force it off.
I don't think JSON marshalling would make sense here, the performance alone would not be great.
This is certainly true, but I never suggested using JSON, just a system similar to it.
Besides, System.Text.Json's performance is top of the line (in C#) and allows source-generating the serialization logic, which additionally makes it NativeAOT-compatible.
This is certainly true, but I never suggested using JSON, just a system similar to it.
Yup, something similar I'd agree with too for sure (without the string parsing of course haha)
I'm looking at the structures in wasmtime C api, and I see wasmtime_component_valvariant_t which seems like the definition (apologies, I'm fresh with the wasmtime C api but wanna try and contribute anyway).
The payload in it seems to have a name/wasmtime_component_val. The kind (wasmtime_component_valkind_t: Discriminant used in wasmtime_component_val_t::kind). I'm guessing references one of the following in the list of wasmtime_component_valunion_t union reference? (According to: https://docs.wasmtime.dev/c-api/unionwasmtime__component__valunion__t.html)
It would then be a mapping like (switch-like case), pseudo incoming:
var value = type switch { 0 => bool, // WASMTIME_COMPONENT_BOOL = 0 1 => signedByte, // WASMTIME_COMPONENT_S8 = 1 2 => unsignedByte, // WASMTIME_COMPONENT_U8 = 2 ... }
So the developer can check the kind to see what type, and then get the specific value from the payload? (if they need to, but it could just be an 'object' and they can check the type at runtime too).
Hi, component model support in c# in is being progressed with the wit-bindgen tool. It lowers and lifts calls to components to present a familiar dotnet interface.
https://github.com/bytecodealliance/wit-bindgen
Is wit-bindgen purely for the code that goes into the generated wasm, or does it also generate some host side code too?
It generates based on the wit, so it can generate imports and exports. It can be used for both the host and the guest.
It generates c# so doesn't inject directly into the wasm, but becomes part of the input c#.
Hi, component model support in c# in is being progressed with the wit-bindgen tool. It lowers and lifts calls to components to present a familiar dotnet interface.
https://github.com/bytecodealliance/wit-bindgen
In the documentation it states that the generated code is for C# guests and not for hosts?
From the point of view of a guest component I understand the requirement to lower/lift values according to the ABI. But from a host embedding wasmtime and invoking the C-API, it looks to me that this is not required because the Rust code in wasmtime already does this ?
So when invoking a Wasm component function from wasmtime-dotnet would imply the chained mapping: C# values -> C-API values -> lowered Wasm values. The generated C# code from the WIT file seems to do a single mapping: C# values -> lowered Wasm values.
I might be wrong on this and would love to hear other people's insights on this, but I do not think we can use that direct mapping for now as it is not (yet) supported in the C-API ?
wit-bindgen doesn't generate anything for hosting as far as I know, yes. It's precisely why I've been monitoring this issue; I want to load WASM components in a C# environment to make use of them as plugins in my app.
The code wit-bindgen currently generates for C# is also not idiomatic and not "a familiar dotnet interface". For example, some namespaces are (often partially) snake_case, which is not familiar .NET whatsoever. It additionally uses very weird practices for reading values from the WASM address space, some of which could be immensely improved for readability, usability, and performance.
I will likely write my own bindings wrapper once this is ready.
Feel free to submit PRs, note that writing to Wasm address spaces directly has alignment concerns. It is a work in progress, but the public API is where the aim is to get something natural, the internals will naturally be more complicated to fit the Wasm ABI, and performance concerns. Your comment on namespaces in snake case is noted, thanks.
I'm playing around in the following branch: https://github.com/GerardSmit/wasmtime-dotnet/tree/component-model
However, I made quite a lot of changes:
ClangSharp
Instead of hand-writing the bindings, I used ClangSharp to generate bindings (which is also a separate issue #329). The problem of moving to ClangSharp is that the current bindings are not compatible.
For example, we currently have: https://github.com/bytecodealliance/wasmtime-dotnet/blob/81435e40ed25c188936263d659d1620d8a300461/src/Engine.cs#L82-L83
But ClangSharp generates the following:
[DllImport("wasmtime", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern wasm_engine_t* wasm_engine_new();
Another example is out:
https://github.com/bytecodealliance/wasmtime-dotnet/blob/81435e40ed25c188936263d659d1620d8a300461/src/Module.cs#L416-L417
ClangSharp:
[DllImport("wasmtime", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
[return: NativeTypeName("wasmtime_error_t *")]
internal static extern wasmtime_error* wasmtime_module_serialize([NativeTypeName("wasmtime_module_t *")] wasmtime_module* module, wasm_byte_vec_t* ret);
Because of this, I started fresh: https://github.com/GerardSmit/wasmtime-dotnet/tree/component-model/src/Wasmtime
This isn't ideal and I don't know if we should migrate everything over to ClangSharp, or only do components here (and then cast the IntPtr's to the ClangSharp struct pointers) and migrate everything later.
Method calls
The API works as following:
// Create Engine, Linker and Store
using var engine = new Engine();
using var linker = new Linker(engine);
using var store = new Store(engine);
// Simple module that adds 2 ints together
const string addIntModule =
"""
(component
(core module $AddModule
(func (export "add") (param i32) (param i32) (result i32)
local.get 0
local.get 1
i32.add
)
)
(core instance $add_instance (instantiate $AddModule))
(func (export "add") (param "a" s32) (param "b" s32) (result s32)
(canon lift
(core func $add_instance "add")
)
)
)
""";
// Compile the component
using var component = Component.Compile(engine, addIntModule);
// Create a new instance
var instance = component.CreateInstance(linker, store);
// Set up the parameters (this is an using, so strings are freed after use)
using var a = ComponentValue.CreateInt32(40);
using var b = ComponentValue.CreateInt32(2);
// Call the method "add"
// The call must be disposed, because it calls 'wasmtime_component_func_post_return' to free the results.
using var result = instance.Call(
name: "add",
resultCount: 1,
values: [a, b]);
// Get the first result
Assert.Equal(42, result.GetInt32(0));
Source generator
Calling a method like this every time can become quite annoying, so I made a source generator that converts the WIT to a C# structs.
For example, the following WIT:
package tests:[email protected];
world test {
export add-s32: func(x: s32, y: s32) -> s32;
}
Gets source generated to:
using System;
using Wasmtime;
internal static partial class Wit
{
public static partial class Tests
{
public static partial class Test
{
public readonly partial struct Test_
{
private readonly ComponentInstance _instance;
public Test_(ComponentInstance instance)
{
_instance = instance;
}
public int AddS32(int x, int y)
{
using var p0 = ComponentValue.CreateInt32(x);
using var p1 = ComponentValue.CreateInt32(y);
Span<ComponentValue> parameters = stackalloc ComponentValue[2];
parameters[0] = p0;
parameters[1] = p1;
using var result = _instance.Call("add-s32", 1, parameters);
return result.GetInt32(0);
}
}
}
}
}
Now it's possible to call the method as following:
var test = new Wit.Tests.Test.Test_(instance);
Assert.Equal(42, test.AddS32(40, 2));
The benefit of having a source generator, is that changing the WIT-file instantly updates the C# code. You don't have to recompile first (so the MSBuild Task getting run) when you add a new function to the WIT file. And also, it's reflection free 😄
State
Currently, the basics work. I still need to implement special types (result, list etc.) and custom types.
The source generator also doesn't support type definitions yet (type custom-type = string;), so this is still work in progress.
I also need to make more tests for every type, and check thread-safety.
Is it possible to configure ClangSharp to work with SafeHandle instead of raw structs? That would make the autogenerated native API much safer to work with!
Is it possible to configure ClangSharp to work with SafeHandle instead of raw structs? That would make the autogenerated native API much safer to work with!
Is not SafeHandle used for win32 API mainly or to ensure finalization of objects containing unamanged resource? Is this the case?
Also I suppose it might add a significant amount of overhead when interoping.
What are the concerns about 'raw structs' in this specific scenario?
Wasmtime-dotnet already uses safe handles - in general anywhere that a resource is "owned" by the C# side it is wrapped up in a safe handle. For example Module.Handle. This ensures you can't mismanage the native resource lifetime (can't free an invalid handle, can't double free, can't forget to free).
The safe handle can be passed directly to the extern methods in-lieu of a pointer, which also makes those calls properly typed (better than using IntPtr everywhere). For example wasmtime_module_new takes an Engine, that's passed that as a Engine.Handle instead of an IntPtr or a wasm_engine*.
@GerardSmit Would there be an overload to call a method by an index or handle? String-lookup per call would be always adding an expensive lookup every time.
Edit: Or getting a reference to the func as well (via wasmtime_component_instance_get_func)
Would something like:
var addS32Index = component.GetExportIndex(store, "add-s32");
instance.Call(addS32Index);
Could we get away with making the Test_ into just Test and a struct? Like:
internal static partial class Wit
{
public static partial class Tests
{
public readonly partial struct Test
{
private readonly ComponentInstance _instance;
public Test_(ComponentInstance instance)
{
_instance = instance;
}
public int AddS32(int x, int y)
{
using var p0 = ComponentValue.CreateInt32(x);
using var p1 = ComponentValue.CreateInt32(y);
Span<ComponentValue> parameters = stackalloc ComponentValue[2];
parameters[0] = p0;
parameters[1] = p1;
using var result = _instance.Call("add-s32", 1, parameters);
return result.GetInt32(0);
}
}
}
}
So then in the end you could just do:
var test = new Wit.Tests.Test(instance);
Plus perhaps an overload with the new index? And also an overload with your own Span of component values, and also Component Values?
using var value0 = ComponentValue.CreateInt32(1);
using var value1 = ComponentValue.CreateInt32(2);
Span<ComponentValue> myParams = stackalloc ComponentValue[2];
myParams[0] = value0;
myParams[1] = value1;
System.Console.WriteLine(test.AddS32(addS32Index, 1, 2));
System.Console.WriteLine(test.AddS32(addS32Index, myParams);
System.Console.WriteLine(test.AddS32(addS32Index, value0, value1);
This way I could cache some values ahead of time for quicker calls.
@cyraid currently the export handle was cached in a ConcurrentDictionary. The function look-up was done every call.
I made it do a function lookup every call because I thought I read somewhere that wasmtime_component_instance_get_func cannot be shared between calls. But it seems I was wrong here.
I've added a method called GetFunction in the ComponentInstance.
After that, I also made it so Call is thread-safe.
Since not all applications are multi-threaded, I've also made CallUnsafe, which doesn't do any locking.
The API's are as following:
/// <summary>
/// Represents a function in a WebAssembly component instance.
/// </summary>
public readonly struct ComponentInstanceFunction {}
public class ComponentInstance
{
/// <summary>
/// Gets the function in the component instance with the specified name.
/// </summary>
/// <remarks>
/// This method is not thread-safe.
/// </remarks>
public ComponentInstanceFunction GetFunction(string name);
/// <summary>
/// Calls a function in the component instance with synchronization.
/// </summary>
public ComponentCallResults Call(ComponentInstanceFunction function, int resultCount, ReadOnlySpan<ComponentValue> values);
/// <summary>
/// Calls a function in the component instance without any synchronization.
/// The caller is responsible for ensuring that calls to the current component instance are not made concurrently.
/// </summary>
public ComponentCallResults CallUnsafe(ComponentInstanceFunction function, int resultCount, ReadOnlySpan<ComponentValue> values);
// Existing method "Call(string name, ...)" still exists, together with the new method "CallUnsafe(string name, ...)"
}
With this API, it's possible to do the following:
ComponentInstance instance
var function = instance.GetFunction("uppercase");
using var param = ComponentValue.CreateString("uppercase");
using var results = instance.CallUnsafe(function, 1, [param]);
Assert.Equal("UPPERCASE", results.GetString(0));
I changed the source generator so it generates the following methods:
class Test_
{
public string Uppercase(string s);
public string UppercaseUnsafe(string s);
public string Uppercase(ComponentInstanceFunction function, string s);
public string UppercaseUnsafe(ComponentInstanceFunction function, string s);
}
So, you can also use the ComponentInstanceFunction like:
ComponentInstance instance;
var test = new Wit.Tests.Test.Test_(instance);
var function = instance.GetFunction("uppercase");
var result = test.Uppercase(function, "uppercase");
This doesn't validate if the function you provide, is from the same component, or if the function is actually correct. This is something I maybe still need to do.
Component instance
I also made a mistake thinking wasmtime_component_linker_instantiate would allow multi-threaded calls. This is not the case.
When I made multiple threads with each their own component instance from the same store, and do a function calls, I would get an assert from Wasmtime that component ID 1 isn't component ID 2... So, you can only have one instance per store.
I moved Component.CreateInstance to Store.GetComponentInstance(Component component, Linker linker).
After the store is disposed, the function calls will now throw a ObjectDisposedException exception instead of crashing.
Updated code is in https://github.com/GerardSmit/wasmtime-dotnet/tree/component-model
I made some thread-safety tests in https://github.com/GerardSmit/wasmtime-dotnet/blob/component-model/tests/Wasmtime.Tests/ComponentCallTest.cs
Is it possible to configure ClangSharp to work with SafeHandle instead of raw structs? That would make the autogenerated native API much safer to work with!
It seems this is not possible.
The maintainer of ClangSharp said the following when this question was asked:
My own view is that having the higher level abstractions and forcing things like SafeHandle is undesirable. It forces users into what is typically allocation heavy and costly abstractions that they can't avoid. I believe it's better to expose the raw bindings and then incrementally build better support on top more piecemeal as needed ~ https://github.com/dotnet/ClangSharp/issues/427#issuecomment-1836370041
This was also mentioned in the other issue: https://github.com/bytecodealliance/wasmtime-dotnet/issues/329#issuecomment-2557923412
@GerardSmit Starting to look pretty good my friend!