Experiments in C# and WebAssembly Interface Types
All code at https://github.com/yowl/CSharpWitx
WebAssembly (Wasm) Interface types promise to open up a range of scenarios, not least faster access to the DOM. However they also promise to allow easier communication between Wasm modules in different languages. More about Wasm Interface types can be found at https://github.com/WebAssembly/interface-types/blob/master/proposals/interface-types/Explainer.md.
However Interface Types are a way off yet, so as a way to get some of the advantages in a shorter time frame the canonical ABI was proposed : https://github.com/WebAssembly/interface-types/pull/132 . This, together with module linking and multiple memories, allows us to communicate between modules in different languages via a common set of types. Its not fixed in stone and is going through some churn so things will change, but most of that churn will hopefully be done this year. If you are lucky your language will be supported by https://github.com/bytecodealliance/witx-bindgen . That lucky list is c and Rust, so no dotnet languages, this article describes some playing to make c# talk to Rust in WebAssembly.
For this we are going to use Wasi and need a Rust module, a c# module, and the glue to join them together. This glue will be done by witx-bindgen, for the Rust module, wasmlink to join the imports and exports, and some hand crafted interop code in c# to make up for the lack of support in witx-bindgen for c#. For the experiment we are going to take a c# string, pass it to the Rust module, where we will reverse it, then pass it back.
Rust has excellent, probably the best, support for Wasm, so this part is easy. We follow the instructions for witx-bindgen, creating the witx definition:
reverse: function(s: string) -> string
The Rust is equally straightforward : https://github.com/yowl/CSharpWitx/blob/main/rust/hellowasi/src/lib.rs and compiles to a wasi module using the instructions on the witx-bindgen repo which is just
cargo wasi build
That’s the easy bit, now we need the c# Wasi module and the equivalent code that witx-bindgen
would generate via its import!
macro. The basic c# test is simple enough https://github.com/yowl/CSharpWitx/blob/4ee165d0fd0e94d55d4a0c38a70564775bf663d6/csharp/csharpwitx/CsharpWitx/Program.cs#L12-L20 : some debug and a call to what would be the imported function, but we are doing that manually so its a call to the ABI interop function. For strings, that means converting to UTF8 and passing as a byte[]
and length pair, then the reverse for the returned string. This all happens in https://github.com/yowl/CSharpWitx/blob/4ee165d0fd0e94d55d4a0c38a70564775bf663d6/csharp/csharpwitx/CsharpWitx/Program.cs#L52 There’s a lot more to the canonical ABI (https://github.com/WebAssembly/interface-types/pull/132/file), but we are doing the bare minimum to make this example work. We also need a canonical_abi_realloc
to allocate memory on the c# side, but why? This is a “shared nothing” model, the Rust module cannot access memory in the c# module, so the canonical ABI model allocates that memory for the transfer of the UTF8 bytes and it does that via canonical_abi_realloc
. We have a minimal implementation of that in wasi.c
https://github.com/yowl/CSharpWitx/blob/main/Extras/wasi.c where we also fill in a few missing syscalls that dotnet wants that emscripten does not provide when building for Wasi. To compile this we need a couple of changes to the normal NativeAOT-LLVM wasm build. First we need a hacked NativeAOT-LLVM compiler https://github.com/yowl/runtimelab/tree/wasi. We need to modify this to get the right attributes in the LLVM to have the Wasm import appear as
(import “hellowasi” “reverse” (func $reverse (type 4)))
Normally the module name created by emscripten/wasm-ld would be “env” for symbols created via DllImport
not “hellowasi” so we hack that in at https://github.com/yowl/runtimelab/blob/509656d6aa696ce37ff55a47202533c5fc99ff49/src/coreclr/tools/aot/ILCompiler.LLVM/CodeGen/ILToLLVMImporter.cs#L3158-L3164. A proper fix might be to do that using properties on the DllImport
attribute in the c# source.
We have to add -s STANDALONE_WASM=1
to the emscripten commands, and we prefix the wasi.o
that we build using emscripten from wasi.c
to the linker inputs. Finally we link the Rust module and the c# module with the wasmlink-cli
tool https://github.com/bytecodealliance/witx-bindgen/tree/main/crates/wasmlink which is part of witx-bindgen.
cargo run — release -p wasmlink-cli — -i hellowasi=hellowasi.wasm -p wasmtime -o linked2.wasm csharpwitx.wasm
And run it with
wasmtime --enable-module-linking --enable-multi-memory linked2.wasm
Note that we are enabling 2 experimental features in wasmtime
here, module linking and multi memory. The output is:
start
sgnirts tentod esrever nac tsuR
end
Thanks to all the people in the bytecodealliance zulip chat https://bytecodealliance.zulipchat.com for help with my questions and of course for the proposals and tools. Thanks also to the community and Microsoft contributors for NativeAot-LLVM for their continued assistance getting LLVM, and by extension WebAssembly, working in the compiler.