diff --git a/.gitignore b/.gitignore index 62216bc..9781178 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ +/build *.o *.wasm *.wat example.*.bin +*.sw? diff --git a/README.md b/README.md index f7f3963..df1afbf 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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()`: diff --git a/examples/dotnet/custom-imports/custom-imports.cs b/examples/dotnet/custom-imports/custom-imports.cs index 86a90e3..6bd9806 100644 --- a/examples/dotnet/custom-imports/custom-imports.cs +++ b/examples/dotnet/custom-imports/custom-imports.cs @@ -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 InputDataLen = &input_data_lenWrapper; - public static delegate* unmanaged InputData = &input_dataWrapper; - public static delegate* unmanaged Printk = &printkWrapper; - public static delegate* unmanaged Shutdown = &shutdownWrapper; - public static delegate* unmanaged 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(); } } diff --git a/examples/dotnet/fuzz/Example.cs b/examples/dotnet/fuzz/Example.cs index 2c3588e..6cd4d0f 100644 --- a/examples/dotnet/fuzz/Example.cs +++ b/examples/dotnet/fuzz/Example.cs @@ -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); } } } diff --git a/examples/dotnet/fuzz/Example.csproj b/examples/dotnet/fuzz/Example.csproj index 5b92b9f..5dc3217 100644 --- a/examples/dotnet/fuzz/Example.csproj +++ b/examples/dotnet/fuzz/Example.csproj @@ -22,6 +22,9 @@ + + + diff --git a/examples/go/with_import/example.go b/examples/go/with_import/example.go index c203fae..ef53235 100644 --- a/examples/go/with_import/example.go +++ b/examples/go/with_import/example.go @@ -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 @@ -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) diff --git a/examples/scripts/dotnet2wasm/dotnet2wasm.go b/examples/scripts/dotnet2wasm/dotnet2wasm.go index 20e5446..03f1ac1 100644 --- a/examples/scripts/dotnet2wasm/dotnet2wasm.go +++ b/examples/scripts/dotnet2wasm/dotnet2wasm.go @@ -1,3 +1,5 @@ +// Rudimentary tool to parse and patch wasip2 style import function +// declarations such they are compatible with wasip1. package main import ( @@ -13,183 +15,68 @@ const ( parOpen = "(" parClose = ")" - space = " " + quote = `"` - memoryStr = `"memory"` + importStr = "import" - keywordFunc = "func" - keywordParam = "param" - keywordType = "type" - keywordResult = "result" - - funcStartMatch = parOpen + keywordFunc - typeStartMatch = parOpen + keywordType - paramStartMatch = parOpen + keywordParam - resultStartMatch = parOpen + keywordResult - exportMemoryMatch = parOpen + space + memoryStr - - maxLineParseLen = 1000 + importPackageName = "example:api" + importModuleName = "testmodule" + fullImportModuleName = importPackageName + "/" + importModuleName ) -type Params struct { - Args []string - Res string -} - -func extractParams(t string) (p Params) { - i := strings.Index(t, paramStartMatch) - j := strings.Index(t[i:], parClose)+i - args := t[i+len(paramStartMatch):j] - args = strings.TrimSuffix(args, parClose) - args = strings.TrimSpace(args) - p.Args = strings.Split(args, " ") - - if k := strings.Index(t[j:], resultStartMatch); k >= 0 { - k += j + len(resultStartMatch) - res := t[k:] - res = strings.TrimSuffix(res, parClose) - res = strings.TrimSpace(res) - p.Res = res - } - - return p -} - -func Main(in, out string, funs []string) (err error) { +func Main(in, out string) (err error) { buf := make([]byte, 30*1024*1024) fin, err := os.Open(in) if err != nil { return } - // 1st pass: gather function signatures - params := make(map[string]Params) - - scanner := bufio.NewScanner(fin) - scanner.Buffer(buf, len(buf)) - for scanner.Scan() { - for _, fun := range funs { - t := scanner.Text() - if isFuncDecl(t, fun) { - //log.Printf("found: %+v", t) - log.Printf("import %s", fun) - params[fun] = extractParams(t) - } - } - } - if err := scanner.Err(); err != nil { - return fmt.Errorf("scanner error: %w", err) - } - - //log.Printf("params: %+v", params) - fin.Close() - - // 2nd pass: write exports - fin, err = os.Open(in) - if err != nil { - return fmt.Errorf("2nd pass: open %s: %w", in, err) - } - defer fin.Close() - fout, err := os.Create(out) if err != nil { return fmt.Errorf("2nd pass: create %s: %w", out, err) } defer fout.Close() - // state - i := 0 - exportMemoryMatched := false - // prepare scanner - scanner = bufio.NewScanner(fin) + scanner := bufio.NewScanner(fin) scanner.Buffer(buf, len(buf)) - w := bufio.NewWriter(fout) for scanner.Scan() { t := scanner.Text() - // Output with re-written function calls - if err := writeText(w, t, funs, params); err != nil { - return fmt.Errorf("write text: %w", err) - } - - if i == 0 { - for fun, param := range params { - fmt.Fprintf(w, "(import \"testmodule\" \"%s\" (func $Example_Example_CustomImports__%sOverwrite (param %s)", fun, fun, strings.Join(param.Args, " ")) - if param.Res != "" { - fmt.Fprintf(w, space) - fmt.Fprintf(w, parOpen) - fmt.Fprintf(w, keywordResult) - fmt.Fprintf(w, space) - fmt.Fprintf(w, param.Res) - fmt.Fprintf(w, parClose) - } - fmt.Fprintf(w, "))\n") - } - } else if !exportMemoryMatched && strings.Contains(t, exportMemoryMatch) { - for fun := range params { - fmt.Fprintf(w, "(export \"Example_%s\" (func $Example_Example_CustomImports__%s))\n", fun, fun) - } + if funcName, updated, ok := importWasip1Compat(t); ok { + log.Printf("import %s", funcName) + fout.WriteString(updated+"\n") + } else { + fout.WriteString(t+"\n") } - - i++ } if err := scanner.Err(); err != nil { return fmt.Errorf("scanner error: %w", err) } - w.Flush() - - return -} - -func isFuncDecl(txt, fun string) bool { - if !strings.HasPrefix(strings.TrimSpace(txt), funcStartMatch) { - return false - } - - i := strings.Index(txt, fun) - if i < 0 { - return false - } - t := txt[i+len(fun):] - - t = strings.TrimSpace(t) - matched := len(t) == 0 || strings.HasPrefix(t, paramStartMatch) || strings.HasPrefix(t, typeStartMatch) + fin.Close() - return matched + return } -func writeText(w *bufio.Writer, t string, funs []string, params map[string]Params) (err error) { - for _, fun := range funs { - if strings.TrimSpace(t) == "call $Example_Example_CustomImports__"+fun { - p := params[fun] - // Arguments should be passed sequentially starting with the guest instance. - // dotnet seems to use stdcall calling convention which cannot be changed - // for functions annotated with UnmanagedCallersOnly. Thus overwrite - // arguments by dropping them from the WebAssembly stack and adding the - // correct ones. - code := "" - for i := 0; i < len(p.Args); i++ { - code += " drop\n" - } - for i := 0; i < len(p.Args); i++ { - code += fmt.Sprintf(" local.get %d\n", i) - } - t = strings.Replace(t, "call $Example_Example_CustomImports__"+fun, code+"call $Example_Example_CustomImports__"+fun+"Overwrite", 1) +func importWasip1Compat(line string) (funcName, updated string, ok bool) { + line = strings.TrimSpace(line) + line = strings.TrimPrefix(line, parOpen) + line = strings.TrimSuffix(line, parClose) + tokens := strings.Fields(line) + if len(tokens) >= 3 && tokens[0] == importStr { + if tokens[1] == quote + fullImportModuleName + quote { + funcName = tokens[2] + tokens[1] = quote + importModuleName + quote + updated = parOpen + strings.Join(tokens, " ") + parClose + ok = true } } - if _, err = w.WriteString(t+"\n"); err != nil { - return fmt.Errorf("write: %w", err) - } - return err + return } func main() { in := flag.String("in", "input.wat", "input file") out := flag.String("out", "output.wat", "output file") - funsStr := flag.String("import", "printk,shutdown,input_data_len,input_data,testfunc", "import C functions") flag.Parse() - funs := strings.Split(*funsStr, ",") - - if err := Main(*in, *out, funs); err != nil { + if err := Main(*in, *out); err != nil { log.Printf("main: %v", err) } } diff --git a/platform/custom-imports.wit b/platform/custom-imports.wit new file mode 100644 index 0000000..ed821a8 --- /dev/null +++ b/platform/custom-imports.wit @@ -0,0 +1,14 @@ +package example:api; + +interface testmodule { + testfunc: func(a: u32, b: u32) -> u32; + testfunc2: func(a: u32, b: u32) -> u32; + printk: func(val: u32); + input-data: func(index: u32) -> u32; + input-data-len: func() -> u32; + shutdown: func(); +} + +world hostapp { + import testmodule; +} diff --git a/platform/riscv-qemu/custom_imports.c b/platform/riscv-qemu/custom_imports.c index f22e93b..209e835 100644 --- a/platform/riscv-qemu/custom_imports.c +++ b/platform/riscv-qemu/custom_imports.c @@ -1,6 +1,6 @@ /* Custom WASM imports for zkvm target * - * This file implements custom functions for Go programs using //go:wasmimport. + * This file implements custom functions for Dotnet and Go programs. See README.md for more details. * * This will be used to implement zkvm precompiles and zkvm specific functions that the guest * program needs. @@ -38,10 +38,10 @@ void testmodule__shutdown(void* instance) { exit(0); } -U32 testmodule__input_data_len(void* instance) { +U32 testmodule__inputX2DdataX2Dlen(void* instance) { return 0; } -U32 testmodule__input_data(void* instance, U32 index) { +U32 testmodule__inputX2Ddata(void* instance, U32 index) { return 0; -} \ No newline at end of file +} diff --git a/platform/zkvm/custom_imports.c b/platform/zkvm/custom_imports.c index e6412ae..0da3a0c 100644 --- a/platform/zkvm/custom_imports.c +++ b/platform/zkvm/custom_imports.c @@ -1,6 +1,6 @@ /* Custom WASM imports for zkvm target * - * This file implements custom functions for Go programs using //go:wasmimport. + * This file implements custom functions for Dotnet and Go programs. See README.md for more details. * * This will be used to implement zkvm precompiles and zkvm specific functions that the guest * program needs. @@ -27,11 +27,11 @@ void testmodule__shutdown(void* instance) { shutdown(); } -U32 testmodule__input_data_len(void* instance) { +U32 testmodule__inputX2DdataX2Dlen(void* instance) { uint32_t *ptr_val = (uint32_t *)(INPUT_ADDR + 4 * 2); return *ptr_val; } -U32 testmodule__input_data(void* instance, U32 index) { +U32 testmodule__inputX2Ddata(void* instance, U32 index) { return *((char *)(INPUT_ADDR + 4 * 4 + index)); } \ No newline at end of file