Skip to content

Commit 3a6d913

Browse files
authored
feat: bring library to feature parity with libextism v1.9.0 (#112)
Fixes dylibso/xtp#955 This updates the sample apps to use the latest libextism version - [x] CompiledPlugin - [x] Fuel limit support - [x] HTTP Response headers - [x] Plugin Reset - [x] Plugin ID access - [x] Host Context in plugin calls - [x] Update docs Questions: 1. Do we need a `extism_compiled_plugin_new_error_free` function (similar to `extism_plugin_new_error_free`)? 2. Did we cahnge how we're representing/reading f32/f64 values? 3. ~~Breaking changes to `CurrentPlugin.UserData`~~
1 parent 237701d commit 3a6d913

File tree

18 files changed

+952
-124
lines changed

18 files changed

+952
-124
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Setup .NET Core SDK
2424
uses: actions/setup-dotnet@v1
2525
with:
26-
dotnet-version: 8.x
26+
dotnet-version: 9.x
2727

2828
- name: Run tests
2929
run: |

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Setup .NET Core SDK
1717
uses: actions/[email protected]
1818
with:
19-
dotnet-version: 8.x
19+
dotnet-version: 9.x
2020

2121
- name: Test .NET Sdk
2222
run: |

Extism.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Extism.Sdk.Sample", "sample
1111
EndProject
1212
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Extism.Sdk.FSharpSample", "samples\Extism.Sdk.FSharpSample\Extism.Sdk.FSharpSample.fsproj", "{FD564581-E6FA-4380-B5D0-A0423BBA05A9}"
1313
EndProject
14+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extism.Sdk.Benchmarks", "test\Extism.Sdk.Benchmarks\Extism.Sdk.Benchmarks.csproj", "{8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}"
15+
EndProject
1416
Global
1517
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1618
Debug|Any CPU = Debug|Any CPU
@@ -33,6 +35,10 @@ Global
3335
{FD564581-E6FA-4380-B5D0-A0423BBA05A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
3436
{FD564581-E6FA-4380-B5D0-A0423BBA05A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
3537
{FD564581-E6FA-4380-B5D0-A0423BBA05A9}.Release|Any CPU.Build.0 = Release|Any CPU
38+
{8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39+
{8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
40+
{8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
41+
{8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Release|Any CPU.Build.0 = Release|Any CPU
3642
EndGlobalSection
3743
GlobalSection(SolutionProperties) = preSolution
3844
HideSolutionNode = FALSE

README.md

Lines changed: 166 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,55 @@ printfn "%s" output
7979

8080
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.
8181

82+
## Precompiling plugins
83+
84+
If you're going to create more than one instance of the same plugin, we recommend pre-compiling the plugin and instantiate them:
85+
86+
C#:
87+
88+
```csharp
89+
var manifest = new Manifest(new PathWasmSource("/path/to/plugin.wasm"), "main"));
90+
91+
// pre-compile the wasm file
92+
using var compiledPlugin = new CompiledPlugin(_manifest, [], withWasi: true);
93+
94+
// instantiate plugins
95+
using var plugin = compiledPlugin.Instantiate();
96+
```
97+
98+
F#:
99+
100+
```fsharp
101+
// Create manifest
102+
let manifest = Manifest(PathWasmSource("/path/to/plugin.wasm"))
103+
104+
// Pre-compile the wasm file
105+
use compiledPlugin = new CompiledPlugin(manifest, Array.empty<HostFunction>, withWasi = true)
106+
107+
// Instantiate plugins
108+
use plugin = compiledPlugin.Instantiate()
109+
```
110+
111+
This can have a dramatic effect on performance*:
112+
113+
```
114+
// * Summary *
115+
116+
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4460/23H2/2023Update/SunValley3)
117+
13th Gen Intel Core i7-1365U, 1 CPU, 12 logical and 10 physical cores
118+
.NET SDK 9.0.100
119+
[Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2
120+
DefaultJob : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2
121+
122+
123+
| Method | Mean | Error | StdDev |
124+
|-------------------------- |------------:|----------:|------------:|
125+
| CompiledPluginInstantiate | 266.2 ms | 6.66 ms | 19.11 ms |
126+
| PluginInstantiate | 27,592.4 ms | 635.90 ms | 1,783.12 ms |
127+
```
128+
129+
*: See [the complete benchmark](./test/Extism.Sdk.Benchmarks/Program.cs)
130+
82131
### Plug-in State
83132

84133
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<string, byte[]>();
193242

194243
var functions = new[]
195244
{
196-
HostFunction.FromMethod("kv_read", IntPtr.Zero, (CurrentPlugin plugin, long keyOffset) =>
245+
HostFunction.FromMethod("kv_read", null, (CurrentPlugin plugin, long keyOffset) =>
197246
{
198247
var key = plugin.ReadString(keyOffset);
199248
if (!kvStore.TryGetValue(key, out var value))
@@ -205,7 +254,7 @@ var functions = new[]
205254
return plugin.WriteBytes(value);
206255
}),
207256

208-
HostFunction.FromMethod("kv_write", IntPtr.Zero, (CurrentPlugin plugin, long keyOffset, long valueOffset) =>
257+
HostFunction.FromMethod("kv_write", null, (CurrentPlugin plugin, long keyOffset, long valueOffset) =>
209258
{
210259
var key = plugin.ReadString(keyOffset);
211260
var value = plugin.ReadBytes(valueOffset);
@@ -222,7 +271,7 @@ let kvStore = new Dictionary<string, byte[]>()
222271
223272
let functions =
224273
[|
225-
HostFunction.FromMethod("kv_read", IntPtr.Zero, fun (plugin: CurrentPlugin) (offs: int64) ->
274+
HostFunction.FromMethod("kv_read", null, fun (plugin: CurrentPlugin) (offs: int64) ->
226275
let key = plugin.ReadString(offs)
227276
let value =
228277
match kvStore.TryGetValue(key) with
@@ -233,7 +282,7 @@ let functions =
233282
plugin.WriteBytes(value)
234283
)
235284
236-
HostFunction.FromMethod("kv_write", IntPtr.Zero, fun (plugin: CurrentPlugin) (kOffs: int64) (vOffs: int64) ->
285+
HostFunction.FromMethod("kv_write", null, fun (plugin: CurrentPlugin) (kOffs: int64) (vOffs: int64) ->
237286
let key = plugin.ReadString(kOffs)
238287
let value = plugin.ReadBytes(vOffs).ToArray()
239288
@@ -282,3 +331,116 @@ printfn "%s" output2
282331
// => Writing value=6 from key=count-vowels
283332
// => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"}
284333
```
334+
335+
## Passing context to host functions
336+
337+
Extism provides two ways to pass context to host functions:
338+
339+
### UserData
340+
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.
341+
342+
C#:
343+
344+
```csharp
345+
var hostFunc = new HostFunction(
346+
"hello_world",
347+
new[] { ExtismValType.PTR },
348+
new[] { ExtismValType.PTR },
349+
"Hello again!", // <= userData, this can be any .NET object
350+
(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> outputs) => {
351+
var text = plugin.GetUserData<string>(); // <= We're retrieving the data back
352+
// Use text...
353+
});
354+
```
355+
356+
F#:
357+
358+
```fsharp
359+
// Create host function with userData
360+
let hostFunc = new HostFunction(
361+
"hello_world",
362+
[| ExtismValType.PTR |],
363+
[| ExtismValType.PTR |],
364+
"Hello again!", // userData can be any .NET object
365+
(fun (plugin: CurrentPlugin) (inputs: Span<ExtismVal>) (outputs: Span<ExtismVal>) ->
366+
// Retrieve the userData
367+
let text = plugin.GetUserData<string>()
368+
printfn "%s" text // Prints: "Hello again!"
369+
// Rest of function implementation...
370+
))
371+
```
372+
373+
The userData object is preserved for the lifetime of the host function and can be retrieved in any call using `CurrentPlugin.GetUserData<T>()`. If no userData was provided, `GetUserData<T>()` will return the default value for type `T`.
374+
375+
### Call Host Context
376+
377+
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.
378+
379+
C#:
380+
381+
```csharp
382+
// Pass context for specific call
383+
var context = new Dictionary<string, object> { { "requestId", 42 } };
384+
var result = plugin.CallWithHostContext("function_name", inputData, context);
385+
386+
// Access in host function
387+
void HostFunction(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> outputs)
388+
{
389+
var context = plugin.GetCallHostContext<Dictionary<string, object>>();
390+
// Use context...
391+
}
392+
```
393+
394+
F#:
395+
396+
```fsharp
397+
// Create context for specific call
398+
let context = dict [ "requestId", box 42 ]
399+
400+
// Call plugin with context
401+
let result = plugin.CallWithHostContext("function_name", inputData, context)
402+
403+
// Access context in host function
404+
let hostFunction (plugin: CurrentPlugin) (inputs: Span<ExtismVal>) (outputs: Span<ExtismVal>) =
405+
match plugin.GetCallHostContext<IDictionary<string, obj>>() with
406+
| null -> printfn "No context available"
407+
| context ->
408+
let requestId = context.["requestId"] :?> int
409+
printfn "Request ID: %d" requestId
410+
```
411+
412+
Host context is only available for the duration of the specific function call and can be retrieved using `CurrentPlugin.GetHostContext<T>()`. If no context was provided for the call, `GetHostContext<T>()` will return the default value for type `T`.
413+
414+
## Fuel limit
415+
416+
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.
417+
418+
### Setting a fuel limit
419+
420+
Set the fuel limit when initializing a plugin:
421+
422+
C#:
423+
424+
```csharp
425+
var manifest = new Manifest(...);
426+
var options = new PluginIntializationOptions {
427+
FuelLimit = 1000, // plugin can execute 1000 instructions
428+
WithWasi = true
429+
};
430+
431+
var plugin = new Plugin(manifest, functions, options);
432+
```
433+
434+
F#:
435+
436+
```fsharp
437+
let manifest = Manifest(PathWasmSource("/path/to/plugin.wasm"))
438+
let options = PluginIntializationOptions(
439+
FuelLimit = Nullable<int64>(1000L), // plugin can execute 1000 instructions
440+
WithWasi = true
441+
)
442+
443+
use plugin = new Plugin(manifest, Array.empty<HostFunction>, options)
444+
```
445+
446+
When the fuel limit is exceeded, the plugin execution is terminated and an `ExtismException` is thrown containing "fuel" in the error message.

samples/Extism.Sdk.FSharpSample/Extism.Sdk.FSharpSample.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFramework>net9.0</TargetFramework>
66
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
77
</PropertyGroup>
88

samples/Extism.Sdk.Sample/Extism.Sdk.Sample.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFramework>net9.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

samples/Extism.Sdk.Sample/Program.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using Extism.Sdk;
2-
using Extism.Sdk.Native;
32

43
using System.Runtime.InteropServices;
54
using System.Text;

src/Extism.Sdk/CurrentPlugin.cs

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,64 @@
1-
using System.Text;
1+
using System.Runtime.InteropServices;
2+
using System.Text;
3+
24
using Extism.Sdk.Native;
35

46
namespace Extism.Sdk;
57

68
/// <summary>
79
/// Represents the current plugin. Can only be used within <see cref="HostFunction"/>s.
810
/// </summary>
9-
public class CurrentPlugin
11+
public unsafe class CurrentPlugin
1012
{
11-
internal CurrentPlugin(long nativeHandle, nint userData)
13+
private readonly nint _userData;
14+
internal CurrentPlugin(LibExtism.ExtismCurrentPlugin* nativeHandle, nint userData)
1215
{
1316
NativeHandle = nativeHandle;
14-
UserData = userData;
17+
18+
19+
_userData = userData;
1520
}
1621

17-
internal long NativeHandle { get; }
22+
internal LibExtism.ExtismCurrentPlugin* NativeHandle { get; }
23+
24+
/// <summary>
25+
/// Returns the user data object that was passed in when a <see cref="HostFunction"/> was registered.
26+
/// </summary>
27+
[Obsolete("Use GetUserData<T> instead.")]
28+
public nint UserData => _userData;
1829

1930
/// <summary>
20-
/// An opaque pointer to an object from the host, passed in when a <see cref="HostFunction"/> is registered.
31+
/// Returns the user data object that was passed in when a <see cref="HostFunction"/> was registered.
2132
/// </summary>
22-
public nint UserData { get; set; }
33+
/// <typeparam name="T"></typeparam>
34+
/// <returns></returns>
35+
public T? GetUserData<T>()
36+
{
37+
if (_userData == IntPtr.Zero)
38+
{
39+
return default;
40+
}
41+
42+
var handle1 = GCHandle.FromIntPtr(_userData);
43+
return (T?)handle1.Target;
44+
}
45+
46+
/// <summary>
47+
/// Get the current plugin call's associated host context data. Returns null if call was made without host context.
48+
/// </summary>
49+
/// <typeparam name="T"></typeparam>
50+
/// <returns></returns>
51+
public T? GetCallHostContext<T>()
52+
{
53+
var ptr = LibExtism.extism_current_plugin_host_context(NativeHandle);
54+
if (ptr == null)
55+
{
56+
return default;
57+
}
58+
59+
var handle = GCHandle.FromIntPtr(new IntPtr(ptr));
60+
return (T?)handle.Target;
61+
}
2362

2463
/// <summary>
2564
/// Returns a offset to the memory of the currently running plugin.

src/Extism.Sdk/Extism.Sdk.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.1;net7.0;net8.0</TargetFrameworks>
4+
<TargetFrameworks>netstandard2.1;net7.0;net8.0;net9.0</TargetFrameworks>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>

0 commit comments

Comments
 (0)