Skip to content

Commit

Permalink
feat: bring library to feature parity with libextism v1.9.0 (#112)
Browse files Browse the repository at this point in the history
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`~~
  • Loading branch information
mhmd-azeez authored Dec 5, 2024
1 parent 237701d commit 3a6d913
Show file tree
Hide file tree
Showing 18 changed files with 952 additions and 124 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Setup .NET Core SDK
uses: actions/[email protected]
with:
dotnet-version: 8.x
dotnet-version: 9.x

- name: Test .NET Sdk
run: |
Expand Down
6 changes: 6 additions & 0 deletions Extism.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
170 changes: 166 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<HostFunction>, 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:
Expand Down Expand Up @@ -193,7 +242,7 @@ var kvStore = new Dictionary<string, byte[]>();

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))
Expand All @@ -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);
Expand All @@ -222,7 +271,7 @@ let kvStore = new Dictionary<string, byte[]>()
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
Expand All @@ -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()
Expand Down Expand Up @@ -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<ExtismVal> inputs, Span<ExtismVal> outputs) => {
var text = plugin.GetUserData<string>(); // <= 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<ExtismVal>) (outputs: Span<ExtismVal>) ->
// Retrieve the userData
let text = plugin.GetUserData<string>()
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<T>()`. If no userData was provided, `GetUserData<T>()` 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<string, object> { { "requestId", 42 } };
var result = plugin.CallWithHostContext("function_name", inputData, context);

// Access in host function
void HostFunction(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> outputs)
{
var context = plugin.GetCallHostContext<Dictionary<string, object>>();
// 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<ExtismVal>) (outputs: Span<ExtismVal>) =
match plugin.GetCallHostContext<IDictionary<string, obj>>() 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<T>()`. If no context was provided for the call, `GetHostContext<T>()` 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<int64>(1000L), // plugin can execute 1000 instructions
WithWasi = true
)
use plugin = new Plugin(manifest, Array.empty<HostFunction>, options)
```

When the fuel limit is exceeded, the plugin execution is terminated and an `ExtismException` is thrown containing "fuel" in the error message.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>

Expand Down
2 changes: 1 addition & 1 deletion samples/Extism.Sdk.Sample/Extism.Sdk.Sample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Expand Down
1 change: 0 additions & 1 deletion samples/Extism.Sdk.Sample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Extism.Sdk;
using Extism.Sdk.Native;

using System.Runtime.InteropServices;
using System.Text;
Expand Down
53 changes: 46 additions & 7 deletions src/Extism.Sdk/CurrentPlugin.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,64 @@
using System.Text;
using System.Runtime.InteropServices;
using System.Text;

using Extism.Sdk.Native;

namespace Extism.Sdk;

/// <summary>
/// Represents the current plugin. Can only be used within <see cref="HostFunction"/>s.
/// </summary>
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; }

/// <summary>
/// Returns the user data object that was passed in when a <see cref="HostFunction"/> was registered.
/// </summary>
[Obsolete("Use GetUserData<T> instead.")]
public nint UserData => _userData;

/// <summary>
/// An opaque pointer to an object from the host, passed in when a <see cref="HostFunction"/> is registered.
/// Returns the user data object that was passed in when a <see cref="HostFunction"/> was registered.
/// </summary>
public nint UserData { get; set; }
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T? GetUserData<T>()
{
if (_userData == IntPtr.Zero)
{
return default;
}

var handle1 = GCHandle.FromIntPtr(_userData);
return (T?)handle1.Target;
}

/// <summary>
/// Get the current plugin call's associated host context data. Returns null if call was made without host context.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T? GetCallHostContext<T>()
{
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;
}

/// <summary>
/// Returns a offset to the memory of the currently running plugin.
Expand Down
2 changes: 1 addition & 1 deletion src/Extism.Sdk/Extism.Sdk.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.1;net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>netstandard2.1;net7.0;net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
Expand Down
Loading

0 comments on commit 3a6d913

Please sign in to comment.