diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17bacbe..10765a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.x + dotnet-version: 9.x - name: Run tests run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d2641e..6881f44 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v3.0.3 with: - dotnet-version: 8.x + dotnet-version: 9.x - name: Test .NET Sdk run: | diff --git a/Extism.sln b/Extism.sln index da40abb..1516bb9 100644 --- a/Extism.sln +++ b/Extism.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Extism.Sdk.Sample", "sample EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Extism.Sdk.FSharpSample", "samples\Extism.Sdk.FSharpSample\Extism.Sdk.FSharpSample.fsproj", "{FD564581-E6FA-4380-B5D0-A0423BBA05A9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extism.Sdk.Benchmarks", "test\Extism.Sdk.Benchmarks\Extism.Sdk.Benchmarks.csproj", "{8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {FD564581-E6FA-4380-B5D0-A0423BBA05A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD564581-E6FA-4380-B5D0-A0423BBA05A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD564581-E6FA-4380-B5D0-A0423BBA05A9}.Release|Any CPU.Build.0 = Release|Any CPU + {8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index a32a97c..eb111ec 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,55 @@ printfn "%s" output All exports have a simple interface of optional bytes in, and optional bytes out. This plug-in happens to take a string and return a JSON encoded string with a report of results. +## Precompiling plugins + +If you're going to create more than one instance of the same plugin, we recommend pre-compiling the plugin and instantiate them: + +C#: + +```csharp +var manifest = new Manifest(new PathWasmSource("/path/to/plugin.wasm"), "main")); + +// pre-compile the wasm file +using var compiledPlugin = new CompiledPlugin(_manifest, [], withWasi: true); + +// instantiate plugins +using var plugin = compiledPlugin.Instantiate(); +``` + +F#: + +```fsharp +// Create manifest +let manifest = Manifest(PathWasmSource("/path/to/plugin.wasm")) + +// Pre-compile the wasm file +use compiledPlugin = new CompiledPlugin(manifest, Array.empty, withWasi = true) + +// Instantiate plugins +use plugin = compiledPlugin.Instantiate() +``` + +This can have a dramatic effect on performance*: + +``` +// * Summary * + +BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4460/23H2/2023Update/SunValley3) +13th Gen Intel Core i7-1365U, 1 CPU, 12 logical and 10 physical cores +.NET SDK 9.0.100 + [Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 + DefaultJob : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 + + +| Method | Mean | Error | StdDev | +|-------------------------- |------------:|----------:|------------:| +| CompiledPluginInstantiate | 266.2 ms | 6.66 ms | 19.11 ms | +| PluginInstantiate | 27,592.4 ms | 635.90 ms | 1,783.12 ms | +``` + +*: See [the complete benchmark](./test/Extism.Sdk.Benchmarks/Program.cs) + ### Plug-in State Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by the use of variables. Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result. You can see this by making subsequent calls to the export: @@ -193,7 +242,7 @@ var kvStore = new Dictionary(); var functions = new[] { - HostFunction.FromMethod("kv_read", IntPtr.Zero, (CurrentPlugin plugin, long keyOffset) => + HostFunction.FromMethod("kv_read", null, (CurrentPlugin plugin, long keyOffset) => { var key = plugin.ReadString(keyOffset); if (!kvStore.TryGetValue(key, out var value)) @@ -205,7 +254,7 @@ var functions = new[] return plugin.WriteBytes(value); }), - HostFunction.FromMethod("kv_write", IntPtr.Zero, (CurrentPlugin plugin, long keyOffset, long valueOffset) => + HostFunction.FromMethod("kv_write", null, (CurrentPlugin plugin, long keyOffset, long valueOffset) => { var key = plugin.ReadString(keyOffset); var value = plugin.ReadBytes(valueOffset); @@ -222,7 +271,7 @@ let kvStore = new Dictionary() let functions = [| - HostFunction.FromMethod("kv_read", IntPtr.Zero, fun (plugin: CurrentPlugin) (offs: int64) -> + HostFunction.FromMethod("kv_read", null, fun (plugin: CurrentPlugin) (offs: int64) -> let key = plugin.ReadString(offs) let value = match kvStore.TryGetValue(key) with @@ -233,7 +282,7 @@ let functions = plugin.WriteBytes(value) ) - HostFunction.FromMethod("kv_write", IntPtr.Zero, fun (plugin: CurrentPlugin) (kOffs: int64) (vOffs: int64) -> + HostFunction.FromMethod("kv_write", null, fun (plugin: CurrentPlugin) (kOffs: int64) (vOffs: int64) -> let key = plugin.ReadString(kOffs) let value = plugin.ReadBytes(vOffs).ToArray() @@ -282,3 +331,116 @@ printfn "%s" output2 // => Writing value=6 from key=count-vowels // => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"} ``` + +## Passing context to host functions + +Extism provides two ways to pass context to host functions: + +### UserData +UserData allows you to associate persistent state with a host function that remains available across all calls to that function. This is useful for maintaining configuration or state that should be available throughout the lifetime of the host function. + +C#: + +```csharp +var hostFunc = new HostFunction( + "hello_world", + new[] { ExtismValType.PTR }, + new[] { ExtismValType.PTR }, + "Hello again!", // <= userData, this can be any .NET object + (CurrentPlugin plugin, Span inputs, Span outputs) => { + var text = plugin.GetUserData(); // <= We're retrieving the data back + // Use text... + }); +``` + +F#: + +```fsharp +// Create host function with userData +let hostFunc = new HostFunction( + "hello_world", + [| ExtismValType.PTR |], + [| ExtismValType.PTR |], + "Hello again!", // userData can be any .NET object + (fun (plugin: CurrentPlugin) (inputs: Span) (outputs: Span) -> + // Retrieve the userData + let text = plugin.GetUserData() + printfn "%s" text // Prints: "Hello again!" + // Rest of function implementation... + )) +``` + +The userData object is preserved for the lifetime of the host function and can be retrieved in any call using `CurrentPlugin.GetUserData()`. If no userData was provided, `GetUserData()` will return the default value for type `T`. + +### Call Host Context + +Call Host Context provides a way to pass per-call context data when invoking a plugin function. This is useful when you need to provide data specific to a particular function call rather than data that persists across all calls. + +C#: + +```csharp +// Pass context for specific call +var context = new Dictionary { { "requestId", 42 } }; +var result = plugin.CallWithHostContext("function_name", inputData, context); + +// Access in host function +void HostFunction(CurrentPlugin plugin, Span inputs, Span outputs) +{ + var context = plugin.GetCallHostContext>(); + // Use context... +} +``` + +F#: + +```fsharp +// Create context for specific call +let context = dict [ "requestId", box 42 ] + +// Call plugin with context +let result = plugin.CallWithHostContext("function_name", inputData, context) + +// Access context in host function +let hostFunction (plugin: CurrentPlugin) (inputs: Span) (outputs: Span) = + match plugin.GetCallHostContext>() with + | null -> printfn "No context available" + | context -> + let requestId = context.["requestId"] :?> int + printfn "Request ID: %d" requestId +``` + +Host context is only available for the duration of the specific function call and can be retrieved using `CurrentPlugin.GetHostContext()`. If no context was provided for the call, `GetHostContext()` will return the default value for type `T`. + +## Fuel limit + +The fuel limit feature allows you to constrain plugin execution by limiting the number of instructions it can execute. This provides a safeguard against infinite loops or excessive resource consumption. + +### Setting a fuel limit + +Set the fuel limit when initializing a plugin: + +C#: + +```csharp +var manifest = new Manifest(...); +var options = new PluginIntializationOptions { + FuelLimit = 1000, // plugin can execute 1000 instructions + WithWasi = true +}; + +var plugin = new Plugin(manifest, functions, options); +``` + +F#: + +```fsharp +let manifest = Manifest(PathWasmSource("/path/to/plugin.wasm")) +let options = PluginIntializationOptions( + FuelLimit = Nullable(1000L), // plugin can execute 1000 instructions + WithWasi = true +) + +use plugin = new Plugin(manifest, Array.empty, options) +``` + +When the fuel limit is exceeded, the plugin execution is terminated and an `ExtismException` is thrown containing "fuel" in the error message. diff --git a/samples/Extism.Sdk.FSharpSample/Extism.Sdk.FSharpSample.fsproj b/samples/Extism.Sdk.FSharpSample/Extism.Sdk.FSharpSample.fsproj index 6c4f601..733cba8 100644 --- a/samples/Extism.Sdk.FSharpSample/Extism.Sdk.FSharpSample.fsproj +++ b/samples/Extism.Sdk.FSharpSample/Extism.Sdk.FSharpSample.fsproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 True diff --git a/samples/Extism.Sdk.Sample/Extism.Sdk.Sample.csproj b/samples/Extism.Sdk.Sample/Extism.Sdk.Sample.csproj index 0884dbc..a501572 100644 --- a/samples/Extism.Sdk.Sample/Extism.Sdk.Sample.csproj +++ b/samples/Extism.Sdk.Sample/Extism.Sdk.Sample.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable true diff --git a/samples/Extism.Sdk.Sample/Program.cs b/samples/Extism.Sdk.Sample/Program.cs index e7c489e..594b337 100644 --- a/samples/Extism.Sdk.Sample/Program.cs +++ b/samples/Extism.Sdk.Sample/Program.cs @@ -1,5 +1,4 @@ using Extism.Sdk; -using Extism.Sdk.Native; using System.Runtime.InteropServices; using System.Text; diff --git a/src/Extism.Sdk/CurrentPlugin.cs b/src/Extism.Sdk/CurrentPlugin.cs index 553106d..48226c6 100644 --- a/src/Extism.Sdk/CurrentPlugin.cs +++ b/src/Extism.Sdk/CurrentPlugin.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Runtime.InteropServices; +using System.Text; + using Extism.Sdk.Native; namespace Extism.Sdk; @@ -6,20 +8,57 @@ namespace Extism.Sdk; /// /// Represents the current plugin. Can only be used within s. /// -public class CurrentPlugin +public unsafe class CurrentPlugin { - internal CurrentPlugin(long nativeHandle, nint userData) + private readonly nint _userData; + internal CurrentPlugin(LibExtism.ExtismCurrentPlugin* nativeHandle, nint userData) { NativeHandle = nativeHandle; - UserData = userData; + + + _userData = userData; } - internal long NativeHandle { get; } + internal LibExtism.ExtismCurrentPlugin* NativeHandle { get; } + + /// + /// Returns the user data object that was passed in when a was registered. + /// + [Obsolete("Use GetUserData instead.")] + public nint UserData => _userData; /// - /// An opaque pointer to an object from the host, passed in when a is registered. + /// Returns the user data object that was passed in when a was registered. /// - public nint UserData { get; set; } + /// + /// + public T? GetUserData() + { + if (_userData == IntPtr.Zero) + { + return default; + } + + var handle1 = GCHandle.FromIntPtr(_userData); + return (T?)handle1.Target; + } + + /// + /// Get the current plugin call's associated host context data. Returns null if call was made without host context. + /// + /// + /// + public T? GetCallHostContext() + { + var ptr = LibExtism.extism_current_plugin_host_context(NativeHandle); + if (ptr == null) + { + return default; + } + + var handle = GCHandle.FromIntPtr(new IntPtr(ptr)); + return (T?)handle.Target; + } /// /// Returns a offset to the memory of the currently running plugin. diff --git a/src/Extism.Sdk/Extism.Sdk.csproj b/src/Extism.Sdk/Extism.Sdk.csproj index d97a3f5..2df3c27 100644 --- a/src/Extism.Sdk/Extism.Sdk.csproj +++ b/src/Extism.Sdk/Extism.Sdk.csproj @@ -1,7 +1,7 @@  - netstandard2.1;net7.0;net8.0 + netstandard2.1;net7.0;net8.0;net9.0 enable enable True diff --git a/src/Extism.Sdk/HostFunction.cs b/src/Extism.Sdk/HostFunction.cs index 12d48e6..f68cd4a 100644 --- a/src/Extism.Sdk/HostFunction.cs +++ b/src/Extism.Sdk/HostFunction.cs @@ -1,5 +1,6 @@ using Extism.Sdk.Native; using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; namespace Extism.Sdk; @@ -20,6 +21,7 @@ public class HostFunction : IDisposable private int _disposed; private readonly ExtismFunction _function; private readonly LibExtism.InternalExtismFunction _callback; + private readonly GCHandle? _userDataHandle; /// /// Registers a Host Function. @@ -27,24 +29,34 @@ public class HostFunction : IDisposable /// The literal name of the function, how it would be called from a . /// The types of the input arguments/parameters the caller will provide. /// The types of the output returned from the host function to the . - /// An opaque pointer to an object from the host, accessible on . - /// NOTE: it is the shared responsibility of the host and to cast/dereference this value properly. + /// + /// A state object that will be preserved and can be retrieved during function execution using . + /// This allows you to maintain context between function calls. /// unsafe public HostFunction( string functionName, Span inputTypes, Span outputTypes, - nint userData, + object? userData, ExtismFunction hostFunction) { - // Make sure we store the delegate referene in a field so that it doesn't get garbage collected + // Make sure we store the delegate reference in a field so that it doesn't get garbage collected _function = hostFunction; _callback = CallbackImpl; + _userDataHandle = userData is null ? null : GCHandle.Alloc(userData); fixed (ExtismValType* inputs = inputTypes) fixed (ExtismValType* outputs = outputTypes) { - NativeHandle = LibExtism.extism_function_new(functionName, inputs, inputTypes.Length, outputs, outputTypes.Length, _callback, userData, IntPtr.Zero); + NativeHandle = LibExtism.extism_function_new( + functionName, + inputs, + inputTypes.Length, + outputs, + outputTypes.Length, + _callback, + _userDataHandle is null ? IntPtr.Zero : GCHandle.ToIntPtr(_userDataHandle.Value), + IntPtr.Zero); } } @@ -62,27 +74,27 @@ public void SetNamespace(string ns) } } - /// - /// Sets the function namespace. By default it's set to `extism:host/user`. - /// - /// - /// - public HostFunction WithNamespace(string ns) - { - this.SetNamespace(ns); - return this; - } + /// + /// Sets the function namespace. By default it's set to `extism:host/user`. + /// + /// + /// + public HostFunction WithNamespace(string ns) + { + this.SetNamespace(ns); + return this; + } - private unsafe void CallbackImpl( - long plugin, - ExtismVal* inputsPtr, - uint n_inputs, - ExtismVal* outputsPtr, - uint n_outputs, - nint data) - { - var outputs = new Span(outputsPtr, (int)n_outputs); - var inputs = new Span(inputsPtr, (int)n_inputs); + private unsafe void CallbackImpl( + LibExtism.ExtismCurrentPlugin* plugin, + ExtismVal* inputsPtr, + uint n_inputs, + ExtismVal* outputsPtr, + uint n_outputs, + nint data) + { + var outputs = new Span(outputsPtr, (int)n_outputs); + var inputs = new Span(inputsPtr, (int)n_inputs); _function(new CurrentPlugin(plugin, data), inputs, outputs); } @@ -91,19 +103,20 @@ private unsafe void CallbackImpl( /// Registers a from a method that takes no parameters an returns no values. /// /// The literal name of the function, how it would be called from a . - /// An opaque pointer to an object from the host, accessible on . - /// NOTE: it is the shared responsibility of the host and to cast/dereference this value properly. + /// + /// A state object that will be preserved and can be retrieved during function execution using . + /// This allows you to maintain context between function calls. /// The host function implementation. /// public static HostFunction FromMethod( string functionName, - nint userdata, + object userData, Action callback) { var inputTypes = new ExtismValType[] { }; var returnType = new ExtismValType[] { }; - return new HostFunction(functionName, inputTypes, returnType, userdata, + return new HostFunction(functionName, inputTypes, returnType, userData, (CurrentPlugin plugin, Span inputs, Span outputs) => { callback(plugin); @@ -116,20 +129,21 @@ public static HostFunction FromMethod( /// /// Type of first parameter. Supported parameter types: , , , , , /// The literal name of the function, how it would be called from a . - /// An opaque pointer to an object from the host, accessible on . - /// NOTE: it is the shared responsibility of the host and to cast/dereference this value properly. + /// + /// A state object that will be preserved and can be retrieved during function execution using . + /// This allows you to maintain context between function calls. /// The host function implementation. /// public static HostFunction FromMethod( string functionName, - nint userdata, + object userData, Action callback) where I1 : struct { var inputTypes = new ExtismValType[] { ToExtismType() }; var returnType = new ExtismValType[] { }; - return new HostFunction(functionName, inputTypes, returnType, userdata, + return new HostFunction(functionName, inputTypes, returnType, userData, (CurrentPlugin plugin, Span inputs, Span outputs) => { callback(plugin, GetValue(inputs[0])); @@ -143,13 +157,14 @@ public static HostFunction FromMethod( /// Type of the first parameter. Supported parameter types: , , , , , /// Type of the second parameter. Supported parameter types: , , , , , /// The literal name of the function, how it would be called from a . - /// An opaque pointer to an object from the host, accessible on . - /// NOTE: it is the shared responsibility of the host and to cast/dereference this value properly. + /// + /// A state object that will be preserved and can be retrieved during function execution using . + /// This allows you to maintain context between function calls. /// The host function implementation. /// public static HostFunction FromMethod( string functionName, - nint userdata, + object userData, Action callback) where I1 : struct where I2 : struct @@ -158,37 +173,38 @@ public static HostFunction FromMethod( var returnType = new ExtismValType[] { }; - return new HostFunction(functionName, inputTypes, returnType, userdata, + return new HostFunction(functionName, inputTypes, returnType, userData, (CurrentPlugin plugin, Span inputs, Span outputs) => { callback(plugin, GetValue(inputs[0]), GetValue(inputs[1])); }); } - /// - /// Registers a from a method that takes 3 parameters an returns no values. Supported parameter types: - /// , , , , , - /// - /// Type of the first parameter. Supported parameter types: , , , , , - /// Type of the second parameter. Supported parameter types: , , , , , - /// Type of the third parameter. Supported parameter types: , , , , , - /// The literal name of the function, how it would be called from a . - /// An opaque pointer to an object from the host, accessible on . - /// NOTE: it is the shared responsibility of the host and to cast/dereference this value properly. - /// The host function implementation. - /// - public static HostFunction FromMethod( - string functionName, - nint userdata, - Action callback) - where I1 : struct - where I2 : struct - where I3 : struct - { - var inputTypes = new ExtismValType[] { ToExtismType(), ToExtismType(), ToExtismType() }; - var returnType = new ExtismValType[] { }; + /// + /// Registers a from a method that takes 3 parameters an returns no values. Supported parameter types: + /// , , , , , + /// + /// Type of the first parameter. Supported parameter types: , , , , , + /// Type of the second parameter. Supported parameter types: , , , , , + /// Type of the third parameter. Supported parameter types: , , , , , + /// The literal name of the function, how it would be called from a . + /// + /// A state object that will be preserved and can be retrieved during function execution using . + /// This allows you to maintain context between function calls. + /// The host function implementation. + /// + public static HostFunction FromMethod( + string functionName, + object userData, + Action callback) + where I1 : struct + where I2 : struct + where I3 : struct + { + var inputTypes = new ExtismValType[] { ToExtismType(), ToExtismType(), ToExtismType() }; + var returnType = new ExtismValType[] { }; - return new HostFunction(functionName, inputTypes, returnType, userdata, + return new HostFunction(functionName, inputTypes, returnType, userData, (CurrentPlugin plugin, Span inputs, Span outputs) => { callback(plugin, GetValue(inputs[0]), GetValue(inputs[1]), GetValue(inputs[2])); @@ -201,20 +217,21 @@ public static HostFunction FromMethod( /// /// Type of the first parameter. Supported parameter types: , , , , , /// The literal name of the function, how it would be called from a . - /// An opaque pointer to an object from the host, accessible on . - /// NOTE: it is the shared responsibility of the host and to cast/dereference this value properly. + /// + /// A state object that will be preserved and can be retrieved during function execution using . + /// This allows you to maintain context between function calls. /// The host function implementation. /// public static HostFunction FromMethod( string functionName, - nint userdata, + object userData, Func callback) where R : struct { var inputTypes = new ExtismValType[] { }; var returnType = new ExtismValType[] { ToExtismType() }; - return new HostFunction(functionName, inputTypes, returnType, userdata, + return new HostFunction(functionName, inputTypes, returnType, userData, (CurrentPlugin plugin, Span inputs, Span outputs) => { var value = callback(plugin); @@ -229,13 +246,14 @@ public static HostFunction FromMethod( /// Type of the first parameter. Supported parameter types: , , , , , /// Type of the first parameter. Supported parameter types: , , , , , /// The literal name of the function, how it would be called from a . - /// An opaque pointer to an object from the host, accessible on . - /// NOTE: it is the shared responsibility of the host and to cast/dereference this value properly. + /// + /// A state object that will be preserved and can be retrieved during function execution using . + /// This allows you to maintain context between function calls. /// The host function implementation. /// public static HostFunction FromMethod( string functionName, - nint userdata, + object userData, Func callback) where I1 : struct where R : struct @@ -243,7 +261,7 @@ public static HostFunction FromMethod( var inputTypes = new ExtismValType[] { ToExtismType() }; var returnType = new ExtismValType[] { ToExtismType() }; - return new HostFunction(functionName, inputTypes, returnType, userdata, + return new HostFunction(functionName, inputTypes, returnType, userData, (CurrentPlugin plugin, Span inputs, Span outputs) => { var value = callback(plugin, GetValue(inputs[0])); @@ -259,13 +277,14 @@ public static HostFunction FromMethod( /// Type of the second parameter. Supported parameter types: , , , , , /// Type of the first parameter. Supported parameter types: , , , , , /// The literal name of the function, how it would be called from a . - /// An opaque pointer to an object from the host, accessible on . - /// NOTE: it is the shared responsibility of the host and to cast/dereference this value properly. + /// + /// A state object that will be preserved and can be retrieved during function execution using . + /// This allows you to maintain context between function calls. /// The host function implementation. /// public static HostFunction FromMethod( string functionName, - nint userdata, + object userData, Func callback) where I1 : struct where I2 : struct @@ -274,7 +293,7 @@ public static HostFunction FromMethod( var inputTypes = new ExtismValType[] { ToExtismType(), ToExtismType() }; var returnType = new ExtismValType[] { ToExtismType() }; - return new HostFunction(functionName, inputTypes, returnType, userdata, + return new HostFunction(functionName, inputTypes, returnType, userData, (CurrentPlugin plugin, Span inputs, Span outputs) => { var value = callback(plugin, GetValue(inputs[0]), GetValue(inputs[1])); @@ -291,13 +310,14 @@ public static HostFunction FromMethod( /// Type of the third parameter. Supported parameter types: , , , , , /// Type of the first parameter. Supported parameter types: , , , , , /// The literal name of the function, how it would be called from a . - /// An opaque pointer to an object from the host, accessible on . - /// NOTE: it is the shared responsibility of the host and to cast/dereference this value properly. + /// + /// A state object that will be preserved and can be retrieved during function execution using . + /// This allows you to maintain context between function calls. /// The host function implementation. /// public static HostFunction FromMethod( string functionName, - nint userdata, + object userData, Func callback) where I1 : struct where I2 : struct @@ -307,7 +327,7 @@ public static HostFunction FromMethod( var inputTypes = new ExtismValType[] { ToExtismType(), ToExtismType(), ToExtismType() }; var returnType = new ExtismValType[] { ToExtismType() }; - return new HostFunction(functionName, inputTypes, returnType, userdata, + return new HostFunction(functionName, inputTypes, returnType, userData, (CurrentPlugin plugin, Span inputs, Span outputs) => { var value = callback(plugin, GetValue(inputs[0]), GetValue(inputs[1]), GetValue(inputs[2])); @@ -364,12 +384,12 @@ private static void SetValue(ref ExtismVal val, T t) else if (t is float f32) { val.t = ExtismValType.F32; - val.v.f32 = BitConverter.SingleToInt32Bits(f32); + val.v.f32 = f32; } else if (t is double f64) { val.t = ExtismValType.F64; - val.v.f64 = BitConverter.DoubleToInt64Bits(f64); + val.v.f64 = f64; } else { @@ -418,7 +438,7 @@ unsafe protected virtual void Dispose(bool disposing) { if (disposing) { - // Free up any managed resources here + _userDataHandle?.Free(); } // Free up unmanaged resources diff --git a/src/Extism.Sdk/LibExtism.cs b/src/Extism.Sdk/LibExtism.cs index 795f3d4..52dfc5c 100644 --- a/src/Extism.Sdk/LibExtism.cs +++ b/src/Extism.Sdk/LibExtism.cs @@ -113,6 +113,12 @@ internal static class LibExtism [StructLayout(LayoutKind.Sequential)] internal struct ExtismPlugin { } + [StructLayout(LayoutKind.Sequential)] + internal struct ExtismCompiledPlugin { } + + [StructLayout(LayoutKind.Sequential)] + internal struct ExtismCurrentPlugin { } + /// /// Host function signature /// @@ -122,7 +128,7 @@ internal struct ExtismPlugin { } /// /// /// - unsafe internal delegate void InternalExtismFunction(long plugin, ExtismVal* inputs, uint n_inputs, ExtismVal* outputs, uint n_outputs, IntPtr data); + unsafe internal delegate void InternalExtismFunction(ExtismCurrentPlugin* plugin, ExtismVal* inputs, uint n_inputs, ExtismVal* outputs, uint n_outputs, IntPtr data); /// /// Returns a pointer to the memory of the currently running plugin. @@ -131,7 +137,7 @@ internal struct ExtismPlugin { } /// /// [DllImport("extism", EntryPoint = "extism_current_plugin_memory")] - internal static extern long extism_current_plugin_memory(long plugin); + unsafe internal static extern long extism_current_plugin_memory(ExtismCurrentPlugin* plugin); /// /// Allocate a memory block in the currently running plugin @@ -140,7 +146,7 @@ internal struct ExtismPlugin { } /// /// [DllImport("extism", EntryPoint = "extism_current_plugin_memory_alloc")] - internal static extern long extism_current_plugin_memory_alloc(long plugin, long n); + unsafe internal static extern long extism_current_plugin_memory_alloc(ExtismCurrentPlugin* plugin, long n); /// /// Get the length of an allocated block. @@ -150,7 +156,7 @@ internal struct ExtismPlugin { } /// /// [DllImport("extism", EntryPoint = "extism_current_plugin_memory_length")] - internal static extern long extism_current_plugin_memory_length(long plugin, long n); + unsafe internal static extern long extism_current_plugin_memory_length(ExtismCurrentPlugin* plugin, long n); /// /// Get the length of an allocated block. @@ -159,7 +165,7 @@ internal struct ExtismPlugin { } /// /// [DllImport("extism", EntryPoint = "extism_current_plugin_memory_free")] - internal static extern void extism_current_plugin_memory_free(long plugin, long ptr); + unsafe internal static extern void extism_current_plugin_memory_free(ExtismCurrentPlugin* plugin, long ptr); /// /// Create a new host function. @@ -204,6 +210,20 @@ internal struct ExtismPlugin { } [DllImport("extism")] unsafe internal static extern ExtismPlugin* extism_plugin_new(byte* wasm, long wasmSize, IntPtr* functions, long nFunctions, [MarshalAs(UnmanagedType.I1)] bool withWasi, out char** errmsg); + /// + /// Load a WASM plugin with fuel limit. + /// + /// A WASM module (wat or wasm) or a JSON encoded manifest. + /// The length of the `wasm` parameter. + /// Array of host function pointers. + /// Number of host functions. + /// Enables/disables WASI. + /// Max number of instructions that can be executed by the plugin. + /// + /// + [DllImport("extism")] + unsafe internal static extern ExtismPlugin* extism_plugin_new_with_fuel_limit(byte* wasm, long wasmSize, IntPtr* functions, long nFunctions, [MarshalAs(UnmanagedType.I1)] bool withWasi, long fuelLimit, out char** errmsg); + /// /// Frees a plugin error message. /// @@ -217,6 +237,40 @@ internal struct ExtismPlugin { } /// Pointer to the plugin you want to free. [DllImport("extism")] unsafe internal static extern void extism_plugin_free(ExtismPlugin* plugin); + /// + /// Pre-compile an Extism plugin + /// + /// A WASM module (wat or wasm) or a JSON encoded manifest. + /// The length of the `wasm` parameter. + /// Array of host function pointers. + /// Number of host functions. + /// Enables/disables WASI. + /// + /// + [DllImport("extism")] + unsafe internal static extern ExtismCompiledPlugin* extism_compiled_plugin_new(byte* wasm, long wasmSize, IntPtr* functions, long nFunctions, [MarshalAs(UnmanagedType.I1)] bool withWasi, out char** errmsg); + + /// + /// Free `ExtismCompiledPlugin` + /// + /// + [DllImport("extism")] + unsafe internal static extern void extism_compiled_plugin_free(ExtismCompiledPlugin* plugin); + + /// + /// Create a new plugin from an `ExtismCompiledPlugin` + /// + /// + [DllImport("extism")] + unsafe internal static extern ExtismPlugin* extism_plugin_new_from_compiled(ExtismCompiledPlugin* compiled, out char** errmsg); + + /// + /// Enable HTTP response headers in plugins using `extism:host/env::http_request` + /// + /// + /// + [DllImport("extism")] + unsafe internal static extern ExtismPlugin* extism_plugin_allow_http_response_headers(ExtismPlugin* plugin); /// /// Get handle for plugin cancellation @@ -264,6 +318,26 @@ internal struct ExtismPlugin { } [DllImport("extism")] unsafe internal static extern int extism_plugin_call(ExtismPlugin* plugin, string funcName, byte* data, int dataLen); + /// + /// Call a function with host context. + /// + /// + /// The function to call. + /// Input data. + /// The length of the `data` parameter. + /// a pointer to context data that will be available in host functions + /// + [DllImport("extism")] + unsafe internal static extern int extism_plugin_call_with_host_context(ExtismPlugin* plugin, string funcName, byte* data, long dataLen, IntPtr hostContext); + + /// + /// Get the current plugin's associated host context data. Returns null if call was made without host context. + /// + /// + /// + [DllImport("extism")] + unsafe internal static extern void* extism_current_plugin_host_context(ExtismCurrentPlugin* plugin); + /// /// Get the error associated with a Plugin /// @@ -288,6 +362,22 @@ internal struct ExtismPlugin { } [DllImport("extism")] unsafe internal static extern IntPtr extism_plugin_output_data(ExtismPlugin* plugin); + /// + /// Reset the Extism runtime, this will invalidate all allocated memory + /// + /// + /// + [DllImport("extism")] + unsafe internal static extern bool extism_plugin_reset(ExtismPlugin* plugin); + + /// + /// Get a plugin's ID, the returned bytes are a 16 byte buffer that represent a UUIDv4 + /// + /// + /// + [DllImport("extism")] + unsafe internal static extern byte* extism_plugin_id(ExtismPlugin* plugin); + /// /// Set log file and level for file logger. /// diff --git a/src/Extism.Sdk/Plugin.cs b/src/Extism.Sdk/Plugin.cs index 96bd3aa..0ecb169 100644 --- a/src/Extism.Sdk/Plugin.cs +++ b/src/Extism.Sdk/Plugin.cs @@ -31,25 +31,51 @@ public unsafe class Plugin : IDisposable internal LibExtism.ExtismPlugin* NativeHandle { get; } /// - /// Create a plugin from a Manifest. + /// Instantiate a plugin from a compiled plugin. + /// + /// + internal Plugin(CompiledPlugin plugin) + { + char** errorMsgPtr; + + var handle = LibExtism.extism_plugin_new_from_compiled(plugin.NativeHandle, out errorMsgPtr); + if (handle == null) + { + var msg = "Unable to intialize a plugin from compiled plugin"; + + if (errorMsgPtr is not null) + { + msg = Marshal.PtrToStringAnsi(new IntPtr(errorMsgPtr)); + } + + throw new ExtismException(msg ?? "Unknown error"); + } + + NativeHandle = handle; + _functions = plugin.Functions; + _cancelHandle = LibExtism.extism_plugin_cancel_handle(NativeHandle); + } + + /// + /// Initialize a plugin from a Manifest. /// /// /// - /// - public Plugin(Manifest manifest, HostFunction[] functions, bool withWasi) + /// + public Plugin(Manifest manifest, HostFunction[] functions, PluginIntializationOptions options) { _functions = functions; - var options = new JsonSerializerOptions + var jsonOptions = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; - options.Converters.Add(new WasmSourceConverter()); - options.Converters.Add(new JsonStringEnumConverter()); + jsonOptions.Converters.Add(new WasmSourceConverter()); + jsonOptions.Converters.Add(new JsonStringEnumConverter()); - var jsonContext = new ManifestJsonContext(options); + var jsonContext = new ManifestJsonContext(jsonOptions); var json = JsonSerializer.Serialize(manifest, jsonContext.Manifest); var bytes = Encoding.UTF8.GetBytes(json); @@ -58,12 +84,23 @@ public Plugin(Manifest manifest, HostFunction[] functions, bool withWasi) fixed (byte* wasmPtr = bytes) fixed (IntPtr* functionsPtr = functionHandles) { - NativeHandle = Initialize(wasmPtr, bytes.Length, functions, withWasi, functionsPtr); + NativeHandle = Initialize(wasmPtr, bytes.Length, functions, functionsPtr, options); } _cancelHandle = LibExtism.extism_plugin_cancel_handle(NativeHandle); } + /// + /// Create a plugin from a Manifest. + /// + /// + /// + /// + public Plugin(Manifest manifest, HostFunction[] functions, bool withWasi) : this(manifest, functions, new PluginIntializationOptions { WithWasi = withWasi }) + { + + } + /// /// Create and load a plugin from a byte array. /// @@ -78,17 +115,20 @@ public Plugin(ReadOnlySpan wasm, HostFunction[] functions, bool withWasi) fixed (byte* wasmPtr = wasm) fixed (IntPtr* functionsPtr = functionHandles) { - NativeHandle = Initialize(wasmPtr, wasm.Length, functions, withWasi, functionsPtr); + NativeHandle = Initialize(wasmPtr, wasm.Length, functions, functionsPtr, new PluginIntializationOptions { WithWasi = withWasi }); } _cancelHandle = LibExtism.extism_plugin_cancel_handle(NativeHandle); } - private unsafe LibExtism.ExtismPlugin* Initialize(byte* wasmPtr, int wasmLength, HostFunction[] functions, bool withWasi, IntPtr* functionsPtr) + private unsafe LibExtism.ExtismPlugin* Initialize(byte* wasmPtr, int wasmLength, HostFunction[] functions, IntPtr* functionsPtr, PluginIntializationOptions options) { char** errorMsgPtr; - var handle = LibExtism.extism_plugin_new(wasmPtr, wasmLength, functionsPtr, functions.Length, withWasi, out errorMsgPtr); + var handle = options.FuelLimit is null ? + LibExtism.extism_plugin_new(wasmPtr, wasmLength, functionsPtr, functions.Length, options.WithWasi, out errorMsgPtr) : + LibExtism.extism_plugin_new_with_fuel_limit(wasmPtr, wasmLength, functionsPtr, functions.Length, options.WithWasi, options.FuelLimit.Value, out errorMsgPtr); + if (handle == null) { var msg = "Unable to create plugin"; @@ -104,6 +144,36 @@ public Plugin(ReadOnlySpan wasm, HostFunction[] functions, bool withWasi) return handle; } + /// + /// Get the plugin's ID. + /// + public Guid Id + { + get + { + var bytes = new Span(LibExtism.extism_plugin_id(NativeHandle), 16); + return new Guid(bytes); + } + } + + /// + /// Reset the Extism runtime, this will invalidate all allocated memory + /// + /// + public bool Reset() + { + CheckNotDisposed(); + return LibExtism.extism_plugin_reset(NativeHandle); + } + + /// + /// Enable HTTP response headers in plugins using `extism:host/env::http_request` + /// + public void AllowHttpResponseHeaders() + { + LibExtism.extism_plugin_allow_http_response_headers(NativeHandle); + } + /// /// Update plugin config values, this will merge with the existing values. /// @@ -153,22 +223,49 @@ unsafe public bool FunctionExists(string name) /// unsafe public ReadOnlySpan Call(string functionName, ReadOnlySpan input, CancellationToken? cancellationToken = null) { - CheckNotDisposed(); + return CallImpl(functionName, input, hostContext: null, cancellationToken); + } - cancellationToken?.ThrowIfCancellationRequested(); + /// + /// Calls a function in the current plugin and returns the output as a byte buffer. + /// + /// + /// Name of the function in the plugin to invoke. + /// A buffer to provide as input to the function. + /// An object that will be passed back to HostFunctions + /// CancellationToken used for cancelling the Extism call. + /// The output of the function call + /// + unsafe public ReadOnlySpan CallWithHostContext(string functionName, ReadOnlySpan input, T hostContext, CancellationToken? cancellationToken = null) + { + GCHandle handle = GCHandle.Alloc(hostContext); + try + { + return CallImpl(functionName, input, GCHandle.ToIntPtr(handle), cancellationToken); + } + finally + { + handle.Free(); + } + } + private ReadOnlySpan CallImpl(string functionName, ReadOnlySpan input, IntPtr? hostContext, CancellationToken? cancellationToken = null) + { + CheckNotDisposed(); + cancellationToken?.ThrowIfCancellationRequested(); using var _ = cancellationToken?.Register(() => LibExtism.extism_plugin_cancel(_cancelHandle)); fixed (byte* dataPtr = input) { - int response = LibExtism.extism_plugin_call(NativeHandle, functionName, dataPtr, input.Length); - var errorMsg = GetError(); + int response = hostContext.HasValue ? + LibExtism.extism_plugin_call_with_host_context(NativeHandle, functionName, dataPtr, input.Length, hostContext.Value) : + LibExtism.extism_plugin_call(NativeHandle, functionName, dataPtr, input.Length); + var errorMsg = GetError(); if (errorMsg != null) { throw new ExtismException($"{errorMsg}. Exit Code: {response}"); } - return OutputData(); } } @@ -373,7 +470,7 @@ public static string ExtismVersion() /// Minimum log level public static void ConfigureFileLogging(string path, LogLevel level) { - var logLevel = Enum.GetName(typeof(LogLevel), level)?.ToLowerInvariant() + var logLevel = Enum.GetName(typeof(LogLevel), level)?.ToLowerInvariant() ?? throw new ArgumentOutOfRangeException(nameof(level)); LibExtism.extism_log_file(path, logLevel); @@ -405,8 +502,155 @@ public static void DrainCustomLogs(LoggingSink callback) } } +/// +/// Options for initializing a plugin. +/// +public class PluginIntializationOptions +{ + /// + /// Enable WASI support. + /// + public bool WithWasi { get; set; } + + /// + /// Limits number of instructions that can be executed by the plugin. + /// + public long? FuelLimit { get; set; } +} + /// /// Custom logging callback. /// /// -public delegate void LoggingSink(string line); \ No newline at end of file +public delegate void LoggingSink(string line); + +/// +/// A pre-compiled plugin ready to be instantiated. +/// +public unsafe class CompiledPlugin : IDisposable +{ + private const int DisposedMarker = 1; + private int _disposed; + + internal LibExtism.ExtismCompiledPlugin* NativeHandle { get; } + internal HostFunction[] Functions { get; } + + /// + /// Compile a plugin from a Manifest. + /// + /// + /// + /// + public CompiledPlugin(Manifest manifest, HostFunction[] functions, bool withWasi) + { + Functions = functions; + + var options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + options.Converters.Add(new WasmSourceConverter()); + options.Converters.Add(new JsonStringEnumConverter()); + + var jsonContext = new ManifestJsonContext(options); + var json = JsonSerializer.Serialize(manifest, jsonContext.Manifest); + + var bytes = Encoding.UTF8.GetBytes(json); + + var functionHandles = functions.Select(f => f.NativeHandle).ToArray(); + fixed (byte* wasmPtr = bytes) + fixed (IntPtr* functionsPtr = functionHandles) + { + NativeHandle = Initialize(wasmPtr, bytes.Length, functions, withWasi, functionsPtr); + } + } + + /// + /// Instantiate a plugin from this compiled plugin. + /// + /// + public Plugin Instantiate() + { + CheckNotDisposed(); + return new Plugin(this); + } + + private unsafe LibExtism.ExtismCompiledPlugin* Initialize(byte* wasmPtr, int wasmLength, HostFunction[] functions, bool withWasi, IntPtr* functionsPtr) + { + char** errorMsgPtr; + + var handle = LibExtism.extism_compiled_plugin_new(wasmPtr, wasmLength, functionsPtr, functions.Length, withWasi, out errorMsgPtr); + if (handle == null) + { + var msg = "Unable to compile plugin"; + + if (errorMsgPtr is not null) + { + msg = Marshal.PtrToStringAnsi(new IntPtr(errorMsgPtr)); + } + + throw new ExtismException(msg ?? "Unknown error"); + } + + return handle; + } + + + /// + /// Frees all resources held by this Plugin. + /// + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, DisposedMarker) == DisposedMarker) + { + // Already disposed. + return; + } + + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Throw an appropriate exception if the plugin has been disposed. + /// + /// + protected void CheckNotDisposed() + { + Interlocked.MemoryBarrier(); + if (_disposed == DisposedMarker) + { + ThrowDisposedException(); + } + } + + [DoesNotReturn] + private static void ThrowDisposedException() + { + throw new ObjectDisposedException(nameof(Plugin)); + } + + /// + /// Frees all resources held by this Plugin. + /// + unsafe protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Free up any managed resources here + } + + // Free up unmanaged resources + LibExtism.extism_compiled_plugin_free(NativeHandle); + } + + /// + /// Destructs the current Plugin and frees all resources used by it. + /// + ~CompiledPlugin() + { + Dispose(false); + } +} \ No newline at end of file diff --git a/test/Extism.Sdk.Benchmarks/Extism.Sdk.Benchmarks.csproj b/test/Extism.Sdk.Benchmarks/Extism.Sdk.Benchmarks.csproj new file mode 100644 index 0000000..db37117 --- /dev/null +++ b/test/Extism.Sdk.Benchmarks/Extism.Sdk.Benchmarks.csproj @@ -0,0 +1,29 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + PreserveNewest + + + + + + + + + + + + + diff --git a/test/Extism.Sdk.Benchmarks/Program.cs b/test/Extism.Sdk.Benchmarks/Program.cs new file mode 100644 index 0000000..792440e --- /dev/null +++ b/test/Extism.Sdk.Benchmarks/Program.cs @@ -0,0 +1,51 @@ +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Attributes; + +using Extism.Sdk; + +using System.Reflection; + +var summary = BenchmarkRunner.Run(); + +public class CompiledPluginBenchmarks +{ + private const int N = 1000; + private const string _input = "Hello, World!"; + private const string _function = "count_vowels"; + private readonly Manifest _manifest; + + public CompiledPluginBenchmarks() + { + var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + _manifest = new Manifest(new PathWasmSource(Path.Combine(binDirectory, "wasm", "code.wasm"), "main")); + } + + [Benchmark] + public void CompiledPluginInstantiate() + { + using var compiledPlugin = new CompiledPlugin(_manifest, [], withWasi: true); + + for (var i = 0; i < N; i++) + { + using var plugin = compiledPlugin.Instantiate(); + var response = plugin.Call(_function, _input); + } + } + + [Benchmark] + public void PluginInstantiate() + { + for (var i = 0; i < N; i++) + { + using var plugin = new Plugin(_manifest, [], withWasi: true); + var response = plugin.Call(_function, _input); + } + } +} + +public class CountVowelsResponse +{ + public int Count { get; set; } + public int Total { get; set; } + public string? Vowels { get; set; } +} \ No newline at end of file diff --git a/test/Extism.Sdk/BasicTests.cs b/test/Extism.Sdk/BasicTests.cs index e485ad6..61c7f6b 100644 --- a/test/Extism.Sdk/BasicTests.cs +++ b/test/Extism.Sdk/BasicTests.cs @@ -18,6 +18,14 @@ public void Alloc() _ = plugin.Call("run_test", Array.Empty()); } + [Fact] + public void GetId() + { + using var plugin = Helpers.LoadPlugin("alloc.wasm"); + var id = plugin.Id; + Assert.NotEqual(Guid.Empty, id); + } + [Fact] public void Fail() { @@ -125,7 +133,7 @@ public void CountVowelsJson() } [Fact] - public void CountVowelsHostFunctions() + public void CountVowelsHostFunctionsBackCompat() { for (int i = 0; i < 100; i++) { @@ -148,7 +156,10 @@ void HelloWorld(CurrentPlugin plugin, Span inputs, Span ou { Console.WriteLine("Hello from .NET!"); + +#pragma warning disable CS0618 // Type or member is obsolete var text = Marshal.PtrToStringAnsi(plugin.UserData); +#pragma warning restore CS0618 // Type or member is obsolete Console.WriteLine(text); var input = plugin.ReadString(new nint(inputs[0].v.ptr)); @@ -159,6 +170,92 @@ void HelloWorld(CurrentPlugin plugin, Span inputs, Span ou } } + [Fact] + public void CountVowelsHostFunctions() + { + for (int i = 0; i < 100; i++) + { + var userData = "Hello again!"; + + using var helloWorld = new HostFunction( + "hello_world", + new[] { ExtismValType.PTR }, + new[] { ExtismValType.PTR }, + userData, + HelloWorld); + + using var plugin = Helpers.LoadPlugin("code-functions.wasm", config: null, helloWorld); + + var dict = new Dictionary + { + { "answer", 42 } + }; + + var response = plugin.CallWithHostContext("count_vowels", Encoding.UTF8.GetBytes("Hello World"), dict); + Encoding.UTF8.GetString(response).ShouldBe("{\"count\": 3}"); + } + + void HelloWorld(CurrentPlugin plugin, Span inputs, Span outputs) + { + Console.WriteLine("Hello from .NET!"); + + var text = plugin.GetUserData(); + Assert.Equal("Hello again!", text); + + var context = plugin.GetCallHostContext>(); + if (context is null || !context.ContainsKey("answer")) + { + throw new InvalidOperationException("Context not found"); + } + + Assert.Equal(42, context["answer"]); + + var input = plugin.ReadString(new nint(inputs[0].v.ptr)); + Console.WriteLine($"Input: {input}"); + + var output = new string(input); // clone the string + outputs[0].v.ptr = plugin.WriteString(output); + } + } + + [Fact] + public void CountVowelsHostFunctionsNoUserData() + { + for (int i = 0; i < 100; i++) + { + using var helloWorld = new HostFunction( + "hello_world", + new[] { ExtismValType.PTR }, + new[] { ExtismValType.PTR }, + null, + HelloWorld); + + using var plugin = Helpers.LoadPlugin("code-functions.wasm", config: null, helloWorld); + + var dict = new Dictionary + { + { "answer", 42 } + }; + + var response = plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World")); + Encoding.UTF8.GetString(response).ShouldBe("{\"count\": 3}"); + } + + void HelloWorld(CurrentPlugin plugin, Span inputs, Span outputs) + { + Console.WriteLine("Hello from .NET!"); + + var text = plugin.GetUserData(); + Assert.Null(text); + + var input = plugin.ReadString(new nint(inputs[0].v.ptr)); + Console.WriteLine($"Input: {input}"); + + var output = new string(input); // clone the string + outputs[0].v.ptr = plugin.WriteString(output); + } + } + [Fact] public void HostFunctionsWithMemory() { @@ -180,6 +277,18 @@ public void HostFunctionsWithMemory() Encoding.UTF8.GetString(response).ShouldBe("HELLO FRODO!"); } + [Fact] + public void FuelLimit() + { + using var plugin = Helpers.LoadPlugin("loop.wasm", options: new PluginIntializationOptions + { + FuelLimit = 1000, + WithWasi = true + }); + + Should.Throw(() => plugin.Call("loop_forever", Array.Empty())) + .Message.ShouldContain("fuel"); + } //[Fact] // flakey diff --git a/test/Extism.Sdk/CompiledPluginTests.cs b/test/Extism.Sdk/CompiledPluginTests.cs new file mode 100644 index 0000000..627626c --- /dev/null +++ b/test/Extism.Sdk/CompiledPluginTests.cs @@ -0,0 +1,59 @@ +using Shouldly; + +using System.Runtime.InteropServices; +using System.Text; + +using Xunit; + +using static Extism.Sdk.Tests.BasicTests; + +namespace Extism.Sdk.Tests; + +public class CompiledPluginTests +{ + [Fact] + public void CountVowels() + { + using var compiledPlugin = Helpers.CompilePlugin("code.wasm"); + + for (var i = 0; i < 3; i++) + { + using var plugin = compiledPlugin.Instantiate(); + + var response = plugin.Call("count_vowels", "Hello World"); + + response.ShouldNotBeNull(); + response.Count.ShouldBe(3); + } + } + + [Fact] + public void CountVowelsHostFunctions() + { + var userData = "Hello again!"; + using var helloWorld = HostFunction.FromMethod("hello_world", userData, HelloWorld); + + using var compiledPlugin = Helpers.CompilePlugin("code-functions.wasm", null, helloWorld); + for (int i = 0; i < 3; i++) + { + using var plugin = compiledPlugin.Instantiate(); + + var response = plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World")); + Encoding.UTF8.GetString(response).ShouldBe("{\"count\": 3}"); + } + + long HelloWorld(CurrentPlugin plugin, long ptr) + { + Console.WriteLine("Hello from .NET!"); + + var text = plugin.GetUserData(); + Console.WriteLine(text); + + var input = plugin.ReadString(ptr); + Console.WriteLine($"Input: {input}"); + + return plugin.WriteString(new string(input)); // clone the string + } + } + +} diff --git a/test/Extism.Sdk/Extism.Sdk.Tests.csproj b/test/Extism.Sdk/Extism.Sdk.Tests.csproj index 235e937..e4c92c9 100644 --- a/test/Extism.Sdk/Extism.Sdk.Tests.csproj +++ b/test/Extism.Sdk/Extism.Sdk.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable false @@ -9,7 +9,7 @@ - + diff --git a/test/Extism.Sdk/Helpers.cs b/test/Extism.Sdk/Helpers.cs index 6c0c4ca..49e368c 100644 --- a/test/Extism.Sdk/Helpers.cs +++ b/test/Extism.Sdk/Helpers.cs @@ -4,7 +4,7 @@ namespace Extism.Sdk.Tests; public static class Helpers { - public static Plugin LoadPlugin(string name, Action? config = null, params HostFunction[] hostFunctions) + public static Plugin LoadPlugin(string name, PluginIntializationOptions options, Action? config = null, params HostFunction[] hostFunctions) { var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; var manifest = new Manifest(new PathWasmSource(Path.Combine(binDirectory, "wasm", name), "main")); @@ -14,6 +14,26 @@ public static Plugin LoadPlugin(string name, Action? config = null, pa config(manifest); } - return new Plugin(manifest, hostFunctions, withWasi: true); + return new Plugin(manifest, hostFunctions, options); + } + + public static Plugin LoadPlugin(string name, Action? config = null, params HostFunction[] hostFunctions) + { + return LoadPlugin(name, new PluginIntializationOptions + { + WithWasi = true, + }, config, hostFunctions); + } + + public static CompiledPlugin CompilePlugin(string name, Action? config = null, params HostFunction[] hostFunctions) + { + var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var manifest = new Manifest(new PathWasmSource(Path.Combine(binDirectory, "wasm", name), "main")); + if (config is not null) + { + config(manifest); + } + + return new CompiledPlugin(manifest, hostFunctions, withWasi: true); } } \ No newline at end of file