Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Host Functions support #13

Merged
merged 22 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,19 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
php: ['8.3']
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/libextism
- name: Setup PHP env
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
php-version: ${{ matrix.php }}
extensions: ffi
tools: composer
env:
fail-fast: true
- name: Test PHP SDK
run: |
make test
make test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
/composer.lock
**/vendor/
src/ExtismLib.php
example/php_errors.log
php_errors.log
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,91 @@ $output = $plugin->call("count_vowels", "Yellow, World!");
// => {"count": 4, "total": 4, "vowels": "aeiouAEIOUY"}
```

### Host Functions

> **Note**
>
> Host Functions support is experimental. Due to usage of callbacks with FFI, It may leak memory.

Let's extend our count-vowels example a little bit: Instead of storing the `total` in an ephemeral plug-in var, let's store it in a persistent key-value store!

Wasm can't use our KV store on it's own. This is where `Host Functions` come in.

[Host functions](https://extism.org/docs/concepts/host-functions) allow us to grant new capabilities to our plug-ins from our application. They are simply some Go functions you write which can be passed down and invoked from any language inside the plug-in.

Let's load the manifest like usual but load up this `count_vowels_kvstore` plug-in:

```php
$manifest = new Manifest(new UrlWasmSource("https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm"));
```

> *Note*: The source code for this is [here](https://github.com/extism/plugins/blob/main/count_vowels_kvstore/src/lib.rs) and is written in rust, but it could be written in any of our PDK languages.

Unlike our previous plug-in, this plug-in expects you to provide host functions that satisfy our its import interface for a KV store.

We want to expose two functions to our plugin, `void kv_write(key string, value byte[])` which writes a bytes value to a key and `byte[] kv_read(key string)` which reads the bytes at the given `key`.

```php
// pretend this is Redis or something :)
$kvstore = [];
$kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (string $key) use (&$kvstore) {
$value = $kvstore[$key] ?? "\0\0\0\0";

echo "Read " . bytesToInt($value) . " from key=$key" . PHP_EOL;
return $value;
});

$kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (string $key, string $value) use (&$kvstore) {
echo "Writing value=" . bytesToInt($value) . " from key=$key" . PHP_EOL;
$kvstore[$key] = $value;
});

function bytesToInt(string $bytes): int {
$result = unpack("L", $bytes);
return $result[1];
}
```

> *Note*: The plugin provides memory pointers, which the SDK automatically converts into a `string`. Similarly, when a host function returns a `string`, the SDK allocates it in the plugin memory and provides a pointer back to the plugin. For manual memory management, request `CurrentPlugin` as the first parameter of the host function. For example:
>
> ```php
> $kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (CurrentPlugin $p, int $keyPtr) use ($kvstore) {
> $key = $p->read_block($keyPtr);
>
> $value = $kvstore[$key] ?? "\0\0\0\0";
>
> return $p->write_block($value);
> });
> ```

We need to pass these imports to the plug-in to create them. All imports of a plug-in must be satisfied for it to be initialized:

```php
$plugin = new Plugin($manifest, true, [$kvRead, $kvWrite]);

$output = $plugin->call("count_vowels", "Hello World!");

echo($output . PHP_EOL);
// => Read 0 from key=count-vowels"
// => Writing value=3 from key=count-vowels"
// => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}

$output = $plugin->call("count_vowels", "Hello World!");

echo($output . PHP_EOL);
// => Read 3 from key=count-vowels"
// => Writing value=6 from key=count-vowels"
// => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"}
```

For host function callbacks, these are the valid parameter types:
- `CurrentPlugin`: Only if its the first parameter. Allows you to manually manage memory. Optional.
- `string`: If the parameter represents a memory offset (an `i64`), then the SDK can automatically load the buffer into a `string` for you.
- `int`: For `i32` and `i64` parameters.
- `float`: For `f32` and `f64` parameters.

Valid return types:
- `void`
- `int`: For `i32` and `i64` parameters.
- `float`: For `f32` and `f64` parameters.
- `string`: the content of the string will be allocated in the wasm plugin memory and the offset (`i64`) will be returned.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
},
"files": [
"src/Manifest.php",
"src/Plugin.php"
"src/Plugin.php",
"src/CurrentPlugin.php"
]
},
"autoload-dev": {
Expand Down
35 changes: 35 additions & 0 deletions example/memory_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
use Extism\PathWasmSource;
use Extism\UrlWasmSource;
use Extism\Manifest;
use Extism\Plugin;
use Extism\HostFunction;
use Extism\ExtismValType;
use Extism\CurrentPlugin;

require_once __DIR__ . "/../src/Plugin.php";
require_once __DIR__ . "/../src/HostFunction.php";

$wasm = new PathWasmSource(__DIR__ . "/../wasm/count_vowels_kvstore.wasm");
$manifest = new Manifest($wasm);

for ($i = 0; $i < 10_000; $i++){
$kvstore = [];

$kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (CurrentPlugin $p, string $key) use (&$kvstore) {
return $kvstore[$key] ?? "\0\0\0\0";
});

$kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (string $key, string $value) use (&$kvstore) {
$kvstore[$key] = $value;
});

$plugin = new Plugin($manifest, true, [$kvRead, $kvWrite]);
$output = $plugin->call("count_vowels", "Hello World!");

if ($i % 100 === 0) {
echo "Iteration: $i\n";
}
}

readline();
89 changes: 89 additions & 0 deletions src/CurrentPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Extism;

require_once __DIR__ . "/LibExtism.php";

/**
* Represents a plugin that is calling the currently running host function.
*/
class CurrentPlugin
{
private \FFI\CData $handle;
private \LibExtism $lib;

/**
* constructor.
*
* @param \LibExtism $lib
* @param \FFI\CData $handle
*/
function __construct($lib, \FFI\CData $handle)
{
$this->handle = $handle;
$this->lib = $lib;
}

/**
* Reads a string from the plugin's memory at the given offset.
*
* @param int $offset Offset of the block to read.
*/
function read_block(int $offset) : string
{
$ptr = $this->lib->extism_current_plugin_memory($this->handle);
$ptr = $this->lib->ffi->cast("char *", $ptr);
$ptr = $this->lib->ffi->cast("char *", $ptr + $offset);

$length = $this->lib->extism_current_plugin_memory_length($this->handle, $offset);

return \FFI::string($ptr, $length);
}

/**
* Allocates a block of memory in the plugin's memory and returns the offset.
*
* @param int $size Size of the block to allocate in bytes.
*/
function allocate_block(int $size) : int
{
return $this->lib->extism_current_plugin_memory_alloc($this->handle, $size);
}

/**
* Writes a string to the plugin's memory, returning the offset of the block.
*
* @param string $data Buffer to write to the plugin's memory.
*/
function write_block(string $data) : int
{
$offset = $this->allocate_block(strlen($data));
$this->fill_block($offset, $data);
return $offset;
}

/**
* Fills a block of memory in the plugin's memory.
*
* @param int $offset Offset of the block to fill.
* @param string $data Buffer to fill the block with.
*/
function fill_block(int $offset, string $data) : void
{
$ptr = $this->lib->extism_current_plugin_memory($this->handle);
$ptr = $this->lib->ffi->cast("char *", $ptr);
$ptr = $this->lib->ffi->cast("char *", $ptr + $offset);

\FFI::memcpy($ptr, $data, strlen($data));
}

/**
* Frees a block of memory in the plugin's memory.
*
* @param int $offset Offset of the block to free.
*/
function free_block(int $offset) : void
{
$this->lib->extism_current_plugin_memory_free($this->handle, $offset);
}
}
Loading
Loading