Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/build
*.o
*.wasm
*.wat
example.*.bin
*.sw?

50 changes: 37 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,30 @@ https://github.com/llvm/llvm-project/commit/b06e736982a3568fe2bcea8688550f9e393b

### Custom WASM Imports

You can call platform-specific functions from your WASM code using custom imports.
Imports are realized as wasip1 modules and centrally declared in the WIT file `platform/custom-imports.wit`:

In Go, use `//go:wasmimport`:
```wit
interface testmodule {
testfunc: func(a: u32, b: u32) -> u32;
...
}
```

The corresponding implementations are done within the platforms, e.g. in `platform/riscv-qemu/custom_imports.c`:

```c
// platform/amd64/custom_imports.c
U32 testmodule__testfunc(void* p, U32 a, U32 b) {
printf("testfunc called with %u, %u\n", a, b);
return a + b;
}
```

Identifiers match WIT naming conventions: either all lowercase or all caps with dashes as separators.

#### Go

You can call platform-specific functions in Go by using `//go:wasmimport`:

```go
// examples/go/with_import/example.go
Expand All @@ -231,27 +252,30 @@ func main() {
}
```

Implement the import in `platform/*/custom_imports.c`:

```c
// platform/amd64/custom_imports.c
U32 testmodule__testfunc(void* p, U32 a, U32 b) {
printf("testfunc called with %u, %u\n", a, b);
return a + b;
}
```

#### Dotnet

Dotnet imports are more complex than for Go, already because the build artifacts are wrapped as wasip2 components. In addition there are multiple FFI mechanisms.

The currently implemented strategy revolves around unmanaged code marked as `UnmanagedCallersOnly` because managed code is wrapped in a binary blob within the w2c2 output. In order to call the unmanaged code from managed code a function pointer ("`delegate`") is used.
The declaration is done within `examples/dotnet/custom-imports.cs`:

```csharp
[global::System.Runtime.InteropServices.DllImportAttribute("example:api/testmodule", EntryPoint = "testfunc"), global::System.Runtime.InteropServices.WasmImportLinkageAttribute]
public static extern int testfunc(int a, int b);
```

Afterwards it's possible to call `CustomImports.testfunc(a, b)`.

Alternatively a fairly popular project is `componentize-dotnet`. However it is still experimental and various not trivial to upgrade dependencies marked as alpha. The glue mechanism used by the project is from `wit-bindgen`. While it can easily map even complex interface structures, unfortunately subtleties such as `unsigned` flags (`uint`) get dropped during the process. It would still be useful to evaluate the underlying mechanism.

https://github.com/bytecodealliance/componentize-dotnet
https://github.com/bytecodealliance/wit-bindgen

In addition it's also possible to explicitly work with unmanaged code, however this involves more steps.

There is also csbindgen which works with unmanaged code and function pointers.

https://github.com/Cysharp/csbindgen

### Memory Limits

For embedded targets with limited memory, use `debug.SetMemoryLimit()`:
Expand Down
76 changes: 29 additions & 47 deletions examples/dotnet/custom-imports/custom-imports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,46 @@

namespace Example {
public unsafe class CustomImports {
// Host functions need to be wrapped within Unmanaged Code
// as this is the code which can be patched on a wasm level.
//
// The managed code on the other hand is compiled to dotnet
// byte code, and embedded as a binary blob.

private static unsafe extern void printk(uint a);

private static unsafe extern int input_data_len();

private static unsafe extern int input_data(int i);

private static unsafe extern void shutdown();

private static unsafe extern uint testfunc(uint a, uint b);
public static byte[] InputDataBytes() {
int n = inputDataLen();
byte[] result = new byte[n];

[UnmanagedCallersOnly(EntryPoint = "Example_printkWrapper")]
private static void printkWrapper(uint a) {
printk(a);
}
for (int i = 0; i < n; i++) {
result[i] = (byte) inputData(i);
}

[UnmanagedCallersOnly(EntryPoint = "Example_input_data_lenWrapper")]
private static int input_data_lenWrapper() {
return input_data_len();
return result;
}

[UnmanagedCallersOnly(EntryPoint = "Example_input_dataWrapper")]
private static int input_dataWrapper(int i) {
return input_data(i);
}
// Host function declarations as specified in custom-imports.wit.
// This allows clean modularized definitions.
//
// It should be mentioned though that unsigned declarations
// become signed at the interface level.

[UnmanagedCallersOnly(EntryPoint = "Example_shutdownWrapper")]
private static void shutdownWrapper() {
shutdown();
}
[global::System.Runtime.InteropServices.DllImportAttribute("example:api/testmodule", EntryPoint = "testfunc"), global::System.Runtime.InteropServices.WasmImportLinkageAttribute]
public static extern int testfunc(int a, int b);

[UnmanagedCallersOnly(EntryPoint = "Example_testfuncWrapper")]
private static uint testfuncWrapper(uint a, uint b) {
return testfunc(a, b);
}
[global::System.Runtime.InteropServices.DllImportAttribute("example:api/testmodule", EntryPoint = "testfunc2"), global::System.Runtime.InteropServices.WasmImportLinkageAttribute]
public static extern int testfunc2(int a, int b);

// Define function pointer to access unmanaged code from managed code.
[global::System.Runtime.InteropServices.DllImportAttribute("example:api/testmodule", EntryPoint = "printk"), global::System.Runtime.InteropServices.WasmImportLinkageAttribute]
public static extern void _printk(int val);

public static delegate* unmanaged<int> InputDataLen = &input_data_lenWrapper;
public static delegate* unmanaged<int, int> InputData = &input_dataWrapper;
public static delegate* unmanaged<uint, void> Printk = &printkWrapper;
public static delegate* unmanaged<void> Shutdown = &shutdownWrapper;
public static delegate* unmanaged<uint, uint, uint> Testfunc = &testfuncWrapper;
public static void printk(uint val) {
unchecked {
_printk((int)val);
}
}

public static byte[] InputDataBytes() {
int n = InputDataLen();
byte[] result = new byte[n];
[global::System.Runtime.InteropServices.DllImportAttribute("example:api/testmodule", EntryPoint = "input-data"), global::System.Runtime.InteropServices.WasmImportLinkageAttribute]
public static extern int inputData(int i);

for (int i = 0; i < n; i++) {
result[i] = (byte) InputData(i);
}
[global::System.Runtime.InteropServices.DllImportAttribute("example:api/testmodule", EntryPoint = "input-data-len"), global::System.Runtime.InteropServices.WasmImportLinkageAttribute]
public static extern int inputDataLen();

return result;
}
[global::System.Runtime.InteropServices.DllImportAttribute("example:api/testmodule", EntryPoint = "shutdown"), global::System.Runtime.InteropServices.WasmImportLinkageAttribute]
public static extern void shutdown();
}
}
12 changes: 9 additions & 3 deletions examples/dotnet/fuzz/Example.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,15 @@ public static void Run()
example.asciiImage();
}

uint a = 7;
uint b = 13;
Console.WriteLine($"testfunc(a={a}, b={b}) = {CustomImports.Testfunc(a, b)}");
int a = 7;
int b = 13;
Console.WriteLine($"testfunc(a={a}, b={b}) = {CustomImports.testfunc(a, b)}");

CustomImports.printk(0x11111111);
CustomImports.printk(0xc0ffee);
CustomImports.printk(0xffffffff);
CustomImports.printk(123);
CustomImports.printk(0x5);
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions examples/dotnet/fuzz/Example.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
<!-- Example packages -->
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />

<!-- Include wit file https://github.com/dotnet/runtime/issues/113868 -->
<LinkerArg Include="-Wl,--component-type=../../../platform/custom-imports.wit" />
</ItemGroup>

<ItemGroup Condition="'$(RuntimeIdentifier)' == 'browser-wasm'">
Expand Down
6 changes: 6 additions & 0 deletions examples/go/with_import/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import (
"runtime/debug"
)

//go:wasmimport testmodule input-data-len
//go:noescape
func inputDataLen() uint32

//go:wasmimport testmodule testfunc
//go:noescape
func testfunc(a, b uint32) uint32
Expand All @@ -22,6 +26,8 @@ func main() {
debug.SetMemoryLimit(400 * (1 << 20))

fmt.Println("Hello world from golang")
n := inputDataLen()
fmt.Printf("Output from inputDataLen %d\n", n)
x := testfunc(1, 2)
fmt.Printf("Output from testFunc %d\n", x)
y := testfunc2(1, 2)
Expand Down
Loading