From 7bbc1b559551cf3fe7194a8e4696ed1d67c4a7b9 Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Sat, 28 Oct 2023 17:05:42 -0600 Subject: [PATCH] Runtime: Add WASI Preview 1 proc_exit function This function will normally call Environment.Exit(code), but an integration point was added to WasmRuntime in order to facilitate unit testing (or allow a user of the runtime to choose to do something else upon this call). --- WasmNet.Core/HostFunctionInstance.cs | 12 ++--- WasmNet.Core/Wasi/Preview1.ProcExit.cs | 6 +++ WasmNet.Core/Wasi/Preview1.cs | 20 ++++++++ WasmNet.Core/WasmRuntime.cs | 12 ++++- WasmNet.Tests/IntegrationTests.cs | 51 ++++++++++++++++++- .../0150-WasiPreview1ProcExit.wat | 12 +++++ WasmNet.sln.DotSettings | 3 +- 7 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 WasmNet.Core/Wasi/Preview1.ProcExit.cs create mode 100644 WasmNet.Core/Wasi/Preview1.cs create mode 100644 WasmNet.Tests/IntegrationTests/0150-WasiPreview1ProcExit.wat diff --git a/WasmNet.Core/HostFunctionInstance.cs b/WasmNet.Core/HostFunctionInstance.cs index 044c3bd..4501a39 100644 --- a/WasmNet.Core/HostFunctionInstance.cs +++ b/WasmNet.Core/HostFunctionInstance.cs @@ -1,16 +1,10 @@ namespace WasmNet.Core; -public class HostFunctionInstance : IFunctionInstance +public class HostFunctionInstance(WasmType type, Delegate hostCode) : IFunctionInstance { - public HostFunctionInstance(WasmType type, Delegate hostCode) - { - Type = type; - HostCode = hostCode; - } + public WasmType Type { get; } = type; - public WasmType Type { get; } - - public Delegate HostCode { get; } + public Delegate HostCode { get; } = hostCode; public Type ReturnType => HostCode.Method.ReturnType; diff --git a/WasmNet.Core/Wasi/Preview1.ProcExit.cs b/WasmNet.Core/Wasi/Preview1.ProcExit.cs new file mode 100644 index 0000000..5107537 --- /dev/null +++ b/WasmNet.Core/Wasi/Preview1.ProcExit.cs @@ -0,0 +1,6 @@ +namespace WasmNet.Core.Wasi; + +public static partial class Preview1 +{ + public static void ProcExit(WasmRuntime runtime, int exitCode) => runtime.ExitHandler(exitCode); +} \ No newline at end of file diff --git a/WasmNet.Core/Wasi/Preview1.cs b/WasmNet.Core/Wasi/Preview1.cs new file mode 100644 index 0000000..81f508d --- /dev/null +++ b/WasmNet.Core/Wasi/Preview1.cs @@ -0,0 +1,20 @@ +namespace WasmNet.Core.Wasi; + +public static partial class Preview1 +{ + public const string Namespace = "wasi_snapshot_preview1"; + + public static void RegisterWasiPreview1(this WasmRuntime runtime) + { + var actionIntType = new WasmType + { + Kind = WasmTypeKind.Function, + Parameters = new List + { + WasmNumberType.I32 + } + }; + + runtime.RegisterImportable(Namespace, "proc_exit", (int exitCode) => ProcExit(runtime, exitCode)); + } +} \ No newline at end of file diff --git a/WasmNet.Core/WasmRuntime.cs b/WasmNet.Core/WasmRuntime.cs index 44ed2dd..fe022f5 100644 --- a/WasmNet.Core/WasmRuntime.cs +++ b/WasmNet.Core/WasmRuntime.cs @@ -1,14 +1,22 @@ using System.Reflection; using System.Reflection.Emit; +using WasmNet.Core.Wasi; namespace WasmNet.Core; public class WasmRuntime { - public Store Store { get; } = new(); - private readonly IDictionary> _importables = new Dictionary>(); + public WasmRuntime() + { + this.RegisterWasiPreview1(); + } + + public Action ExitHandler { get; set; } = Environment.Exit; + + public Store Store { get; } = new(); + public void RegisterImportable(string module, string name, object? value) { if (!_importables.TryGetValue(module, out var importables)) diff --git a/WasmNet.Tests/IntegrationTests.cs b/WasmNet.Tests/IntegrationTests.cs index ea35ec0..efea814 100644 --- a/WasmNet.Tests/IntegrationTests.cs +++ b/WasmNet.Tests/IntegrationTests.cs @@ -22,7 +22,10 @@ public async Task IntegrationTest(string file) throw new Exception($"wat2wasm failed to produce {wasmFile}."); } - WasmRuntime runtime = new(); + WasmRuntime runtime = new() + { + ExitHandler = code => throw new ExitCodeException(code), + }; var externalCalls = new Dictionary(); @@ -128,7 +131,7 @@ public async Task IntegrationTest(string file) } else if (op is ExpectTrapOperation expectTrap) { - if (exception is TargetInvocationException tie) + while (exception is TargetInvocationException tie) { exception = tie.InnerException; } @@ -137,6 +140,21 @@ public async Task IntegrationTest(string file) Assert.Equal(expectTrap.ExceptionType, exception.GetType().Name); testOutputHelper.WriteLine($"Exception is of type {exception.GetType().Name}: {exception.Message}"); } + else if (op is ExitCodeOperation exitCode) + { + while (exception is TargetInvocationException tie) + { + exception = tie.InnerException; + } + + if (exception is not ExitCodeException ece) + { + throw new Exception($"Expected exit code {exitCode.ExitCode} but the program did not exit"); + } + + Assert.Equal(exitCode.ExitCode, ece.ExitCode); + testOutputHelper.WriteLine($"Expected exit code {exitCode.ExitCode} and got {ece.ExitCode}"); + } else { throw new NotImplementedException($"Unknown operation: {op.GetType().Name}"); @@ -150,9 +168,34 @@ public static IEnumerable GetWatFiles() return files.Select(i => new object[] { Path.GetFileName(i) }); } + + private class ExitCodeException : Exception + { + public ExitCodeException(int exitCode) + { + ExitCode = exitCode; + } + + public int ExitCode { get; } + } private abstract class Operation; + private class ExitCodeOperation : Operation + { + public required int ExitCode { get; init; } + + public static ExitCodeOperation Parse(string text) + { + var code = int.Parse(text); + + return new ExitCodeOperation + { + ExitCode = code, + }; + } + } + private class ExpectTrapOperation : Operation { public required string ExceptionType { get; init; } @@ -348,6 +391,10 @@ public static Header Parse(string text) { ops.Add(ExpectTrapOperation.Parse(line[13..])); } + else if (line.StartsWith("exit_code: ")) + { + ops.Add(ExitCodeOperation.Parse(line[11..])); + } else if (line.StartsWith("source: ") || line.StartsWith("TODO: ") || line.StartsWith("NOTE:")) { // ignore diff --git a/WasmNet.Tests/IntegrationTests/0150-WasiPreview1ProcExit.wat b/WasmNet.Tests/IntegrationTests/0150-WasiPreview1ProcExit.wat new file mode 100644 index 0000000..d4561f2 --- /dev/null +++ b/WasmNet.Tests/IntegrationTests/0150-WasiPreview1ProcExit.wat @@ -0,0 +1,12 @@ +;; invoke: proc-exit +;; exit_code: 1 + +(module + (type $t0 (func (param i32))) + (import "wasi_snapshot_preview1" "proc_exit" (func $exit (type $t0))) + (func (export "proc-exit") + (call $exit (i32.const 1)) + ) + (memory (;0;) 256 256) + (export "memory" (memory 0)) +) \ No newline at end of file diff --git a/WasmNet.sln.DotSettings b/WasmNet.sln.DotSettings index 8e6bc12..8b614a1 100644 --- a/WasmNet.sln.DotSettings +++ b/WasmNet.sln.DotSettings @@ -1,3 +1,4 @@  True - True \ No newline at end of file + True + True \ No newline at end of file