Skip to content

Commit

Permalink
feat: add convenience overloads for Plugin.Call and HostFunction.SetN…
Browse files Browse the repository at this point in the history
…amespace (#32)

- add a (string -> string) overload for Plugin.Call
- add a (string -> JSON) overload for Plugin.Call
- add a (JSON -> JSON) overload for Plugin.Call
- Add a `WithNamespace` function on HostFunction to make it easier to
work with

Fixes #26
  • Loading branch information
mhmd-azeez authored Nov 16, 2023
1 parent 582e67c commit 7e95913
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 63 deletions.
52 changes: 14 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ First you should add a using statement for Extism:
C#:
```csharp
using System;
using System.Text;

using Extism.Sdk;
using Extism.Sdk.Native;
Expand All @@ -36,7 +35,6 @@ using Extism.Sdk.Native;
F#:
```fsharp
open System
open System.Text
open Extism.Sdk
open Extism.Sdk.Native
Expand Down Expand Up @@ -72,16 +70,14 @@ This plug-in was written in Rust and it does one thing, it counts vowels in a st

C#:
```csharp
var inputBytes = Encoding.UTF8.GetBytes("Hello, World!");
var output = Encoding.UTF8.GetString(plugin.Call("count_vowels", inputBytes));
var output = plugin.Call("count_vowels", "Hello, World!");

// => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
```

F#:
```fsharp
let inputBytes = Encoding.UTF8.GetBytes("Hello, World!")
let output = Encoding.UTF8.GetString(plugin.Call("count_vowels", inputBytes))
let output = plugin.Call("count_vowels", "Hello, World!")
// => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
```
Expand All @@ -94,26 +90,19 @@ Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by

C#:
```csharp
var output = Encoding.UTF8.GetString(
plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello, World!"))
);

var output = plugin.Call("count_vowels", "Hello, World!");
// => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"}
output = Encoding.UTF8.GetString(
plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello, World!"))
);
output = plugin.Call("count_vowels", "Hello, World!");
// => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"}
```

F#:
```fsharp
let inputBytes = Encoding.UTF8.GetBytes("Hello, World!")
let output1 = Encoding.UTF8.GetString(plugin.Call("count_vowels", inputBytes))
let output1 = plugin.Call("count_vowels", "Hello, World!")
// => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"}
let output2 = Encoding.UTF8.GetString(plugin.Call("count_vowels", inputBytes))
let output2 = plugin.Call("count_vowels", "Hello, World!")
// => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"}
```

Expand All @@ -129,10 +118,7 @@ var manifest = new Manifest(new UrlWasmSource("https://github.com/extism/plugins

using var plugin = new Plugin(manifest, new HostFunction[] { }, withWasi: true);

var output = Encoding.UTF8.GetString(
plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Yellow, World!"))
);

var output = plugin.Call("count_vowels", "Yellow, World!");
// => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
var manifest = new Manifest(new UrlWasmSource("https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"))
Expand All @@ -145,10 +131,7 @@ var manifest = new Manifest(new UrlWasmSource("https://github.com/extism/plugins

using var plugin = new Plugin(manifest, new HostFunction[] { }, withWasi: true);

var output = Encoding.UTF8.GetString(
plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Yellow, World!"))
);

var output = plugin.Call("count_vowels", "Yellow, World!");
// => {"count": 4, "total": 4, "vowels": "aeiouAEIOUY"}
```

Expand All @@ -160,8 +143,7 @@ manifest.Config.Add("vowels", "aeiouyAEIOUY")
use plugin = Plugin(manifest, Array.empty<HostFunction>(), withWasi = true)
let outputBytes = plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Yellow, World!"))
let output = Encoding.UTF8.GetString(outputBytes)
let output = plugin.Call("count_vowels", "Yellow, World!")
let manifest =
Manifest(new UrlWasmSource(Uri("https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm")),
Expand All @@ -170,9 +152,7 @@ let manifest =
use plugin =
Plugin(manifest, Array.empty<HostFunction>(), withWasi = true)
let outputBytes =
plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Yellow, World!"))
let output = Encoding.UTF8.GetString(outputBytes)
let output = plugin.Call("count_vowels", "Yellow, World!")
```

### Host Functions
Expand Down Expand Up @@ -266,18 +246,14 @@ C#:
```csharp
using var plugin = new Plugin(manifest, functions, withWasi: true);

var output = Encoding.UTF8.GetString(
plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World!"))
);
var output = plugin.Call("count_vowels", "Hello World!");

Console.WriteLine(output);
// => Read from key=count-vowels"
// => Writing value=3 from key=count-vowels"
// => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
output = Encoding.UTF8.GetString(
plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World!"))
);
output = plugin.Call("count_vowels", "Hello World!");

Console.WriteLine(output);
// => Read from key=count-vowels"
Expand All @@ -289,13 +265,13 @@ F#:
```fsharp
use plugin = Plugin(manifest, functions, withWasi = true)
let output = Encoding.UTF8.GetString(plugin.Call("count_vowels", inputBytes))
let output = plugin.Call("count_vowels", "Hello World!")
printfn "%s" output
// => Read from key=count-vowels
// => Writing value=3 from key=count-vowels
// => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
let output2 = Encoding.UTF8.GetString(plugin.Call("count_vowels", inputBytes))
let output2 = plugin.Call("count_vowels", "Hello World!")
printfn "%s" output2
// => Read from key=count-vowels
// => Writing value=6 from key=count-vowels
Expand Down
3 changes: 1 addition & 2 deletions samples/Extism.Sdk.Sample/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
## Example 1

This example shows how you can use the library in the most basic way.
It loads up the sample wasm plugin and lets you to pass inputs to it and show the ouput.
**Please note that on Windows you have to manually copy the `extism.dll` file to the ouput directory.**
It loads up the sample wasm plugin and lets you to pass inputs to it and show the ouput.
31 changes: 21 additions & 10 deletions src/Extism.Sdk/HostFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,27 @@ public void SetNamespace(string ns)
}
}

private unsafe void CallbackImpl(
long plugin,
ExtismVal* inputsPtr,
uint n_inputs,
ExtismVal* outputsPtr,
uint n_outputs,
nint data)
{
var outputs = new Span<ExtismVal>(outputsPtr, (int)n_outputs);
var inputs = new Span<ExtismVal>(inputsPtr, (int)n_inputs);
/// <summary>
/// Sets the function namespace. By default it's set to `extism:host/user`.
/// </summary>
/// <param name="ns"></param>
/// <returns></returns>
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<ExtismVal>(outputsPtr, (int)n_outputs);
var inputs = new Span<ExtismVal>(inputsPtr, (int)n_inputs);

_function(new CurrentPlugin(plugin, data), inputs, outputs);
}
Expand Down
65 changes: 57 additions & 8 deletions src/Extism.Sdk/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public unsafe class Plugin : IDisposable
{
private const int DisposedMarker = 1;

private static readonly JsonSerializerOptions? _serializerOptions = new()
{
PropertyNameCaseInsensitive = true,
};

private readonly HostFunction[] _functions;
private int _disposed;
private readonly IntPtr _cancelHandle;
Expand Down Expand Up @@ -133,26 +138,24 @@ unsafe public bool FunctionExists(string name)
}

/// <summary>
/// Calls a function in the current plugin and returns a status.
/// If the status represents an error, call <see cref="GetError"/> to get the error.
/// Othewise, call <see cref="OutputData"/> to get the function's output data.
/// Calls a function in the current plugin and returns the output as a byte buffer.
/// </summary>
/// <param name="functionName">Name of the function in the plugin to invoke.</param>
/// <param name="data">A buffer to provide as input to the function.</param>
/// <param name="input">A buffer to provide as input to the function.</param>
/// <param name="cancellationToken">CancellationToken used for cancelling the Extism call.</param>
/// <returns>The exit code of the function.</returns>
/// <returns>The output of the function call</returns>
/// <exception cref="ExtismException"></exception>
unsafe public ReadOnlySpan<byte> Call(string functionName, ReadOnlySpan<byte> data, CancellationToken? cancellationToken = null)
unsafe public ReadOnlySpan<byte> Call(string functionName, ReadOnlySpan<byte> input, CancellationToken? cancellationToken = null)
{
CheckNotDisposed();

cancellationToken?.ThrowIfCancellationRequested();

using var _ = cancellationToken?.Register(() => LibExtism.extism_plugin_cancel(_cancelHandle));

fixed (byte* dataPtr = data)
fixed (byte* dataPtr = input)
{
int response = LibExtism.extism_plugin_call(NativeHandle, functionName, dataPtr, data.Length);
int response = LibExtism.extism_plugin_call(NativeHandle, functionName, dataPtr, input.Length);
var errorMsg = GetError();

if (errorMsg != null)
Expand All @@ -164,6 +167,52 @@ unsafe public ReadOnlySpan<byte> Call(string functionName, ReadOnlySpan<byte> da
}
}

/// <summary>
/// Calls a function in the current plugin and returns the output as a UTF8 encoded string.
/// </summary>
/// <param name="functionName">Name of the function in the plugin to invoke.</param>
/// <param name="input">A string that will be UTF8 encoded and passed to the plugin.</param>
/// <param name="cancellationToken">CancellationToken used for cancelling the Extism call.</param>
/// <returns>The output of the function as a UTF8 encoded string</returns>
public string Call(string functionName, string input, CancellationToken? cancellationToken = null)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var outputBytes = Call(functionName, inputBytes, cancellationToken);
return Encoding.UTF8.GetString(outputBytes);
}

/// <summary>
/// Calls a function on the plugin with a payload. The payload is serialized into JSON and encoded in UTF8.
/// </summary>
/// <typeparam name="TInput">Type of the input payload.</typeparam>
/// <typeparam name="TOutput">Type of the output payload returned by the function.</typeparam>
/// <param name="functionName">Name of the function in the plugin to invoke.</param>
/// <param name="input">An object that will be serialized into JSON and passed into the function as a UTF8 encoded string.</param>
/// <param name="serializerOptions">JSON serialization options used for serialization/derserialization</param>
/// <param name="cancellationToken">CancellationToken used for cancelling the Extism call.</param>
/// <returns></returns>
public TOutput? Call<TInput, TOutput>(string functionName, TInput input, JsonSerializerOptions? serializerOptions = null, CancellationToken? cancellationToken = null)
{
var inputJson = JsonSerializer.Serialize(input, serializerOptions ?? _serializerOptions);
var outputJson = Call(functionName, inputJson, cancellationToken);
return JsonSerializer.Deserialize<TOutput>(outputJson, serializerOptions ?? _serializerOptions);
}

/// <summary>
/// Calls a function on the plugin and deserializes the output as UTF8 encoded JSON.
/// </summary>
/// <typeparam name="TOutput">Type of the output payload returned by the function.</typeparam>
/// <param name="functionName">Name of the function in the plugin to invoke.</param>
/// <param name="input">An object that will be serialized into JSON and passed into the function as a UTF8 encoded string.</param>
/// <param name="serializerOptions">JSON serialization options used for serialization/derserialization</param>
/// <param name="cancellationToken">CancellationToken used for cancelling the Extism call.</param>
/// <returns></returns>
public TOutput? Call<TOutput>(string functionName, string input, JsonSerializerOptions? serializerOptions = null, CancellationToken? cancellationToken = null)
{
var outputJson = Call(functionName, input, cancellationToken);
return JsonSerializer.Deserialize<TOutput>(outputJson, serializerOptions ?? _serializerOptions);
}

/// <summary>
/// Get the length of a plugin's output data.
/// </summary>
Expand Down
26 changes: 21 additions & 5 deletions test/Extism.Sdk/BasicTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,19 @@ public void CountHelloWorldVowels()
{
using var plugin = Helpers.LoadPlugin("code.wasm");

var response = plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
Encoding.UTF8.GetString(response).ShouldContain("\"count\":3");
var response = plugin.Call("count_vowels", "Hello World");
response.ShouldContain("\"count\":3");
}

[Fact]
public void CountVowelsJson()
{
using var plugin = Helpers.LoadPlugin("code.wasm");

var response = plugin.Call<CountVowelsResponse>("count_vowels", "Hello World");

response.ShouldNotBeNull();
response.Count.ShouldBe(3);
}

[Fact]
Expand Down Expand Up @@ -159,13 +170,18 @@ public void HostFunctionsWithMemory()
plugin.FreeBlock(offset);
return plugin.WriteString(output);
});

helloWorld.SetNamespace("host");
}).WithNamespace("host");

using var plugin = Helpers.LoadPlugin("host_memory.wasm", config: null, helloWorld);

var response = plugin.Call("run_test", Encoding.UTF8.GetBytes("Frodo"));
Encoding.UTF8.GetString(response).ShouldBe("HELLO FRODO!");
}

public class CountVowelsResponse
{
public int Count { get; set; }
public int Total { get; set; }
public string? Vowels { get; set; }
}
}

0 comments on commit 7e95913

Please sign in to comment.