Skip to content

Commit

Permalink
feat: rollup to libextism 1.9.1 (#32)
Browse files Browse the repository at this point in the history
- [x] CompiledPlugin
 - [x] HTTP Response headers
 - [x] Call with Host Context
 - [x] Fuel limit support
 - [x] Update docs
 - [x] Add tests
 - [x] Check for backward compatibility
  • Loading branch information
mhmd-azeez authored Dec 16, 2024
1 parent d6d8bed commit 43432a7
Show file tree
Hide file tree
Showing 12 changed files with 663 additions and 38 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,56 @@ Valid return types:
- `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.

### Fuel Limits

Plugins can be initialized with a fuel limit to constrain their execution. When a plugin runs out of fuel, it will throw an exception. This is useful for preventing infinite loops or limiting resource usage.

```php
// Create plugin with fuel limit of 1000 instructions
$plugin = new Plugin($manifest, true, [], new PluginOptions(true, 1000));

try {
$output = $plugin->call("run_test", "");
} catch (\Exception $e) {
// Plugin ran out of fuel
// The exception message will contain "fuel"
}
```

### 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.

Here's an example of using call host context to implement a multi-user key-value store where each user has their own isolated storage:

```php
$multiUserKvStore = [[]];

$kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (CurrentPlugin $p, string $key) use (&$multiUserKvStore) {
$userId = $p->getCallHostContext(); // get a copy of the host context data
$kvStore = $multiUserKvStore[$userId] ?? [];

return $kvStore[$key] ?? "\0\0\0\0";
});

$kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (CurrentPlugin $p, string $key, string $value) use (&$multiUserKvStore) {
$userId = $p->getCallHostContext(); // get a copy of the host context data
$kvStore = $multiUserKvStore[$userId] ?? [];

$kvStore[$key] = $value;
$multiUserKvStore[$userId] = $kvStore;
});

$plugin = self::loadPlugin("count_vowels_kvstore.wasm", [$kvRead, $kvWrite]);

$userId = 1;

$response = $plugin->callWithContext("count_vowels", "Hello World!", $userId);
$this->assertEquals('{"count":3,"total":3,"vowels":"aeiouAEIOU"}', $response);

$response = $plugin->callWithContext("count_vowels", "Hello World!", $userId);
$this->assertEquals('{"count":3,"total":6,"vowels":"aeiouAEIOU"}', $response);
```

Note: Unlike some other language SDKS, in the Extism PHP SDK the host context is copied when accessed via `getCallHostContext()`. This means that modifications to the context object within host functions won't affect the original context object passed to `callWithContext()`.
87 changes: 87 additions & 0 deletions src/CompiledPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Extism;

/**
* A pre-compiled plugin ready to be instantiated.
*/
class CompiledPlugin
{
private \FFI\CData $handle;
private \Extism\Internal\LibExtism $lib;
private array $functions;

/**
* Compile a plugin from a Manifest.
*
* @param Manifest $manifest A manifest that describes the Wasm binaries
* @param array $functions Array of host functions
* @param bool $withWasi Enable WASI support
*/
public function __construct(Manifest $manifest, array $functions = [], bool $withWasi = false)
{
global $lib;

if ($lib === null) {
$lib = new \Extism\Internal\LibExtism();
}

$this->lib = $lib;
$this->functions = $functions;

$data = json_encode($manifest);
if (!$data) {
throw new \Extism\PluginLoadException("Failed to encode manifest");
}

$errPtr = $lib->ffi->new($lib->ffi->type("char*"));

$handle = $this->lib->extism_compiled_plugin_new(
$data,
strlen($data),
$functions,
count($functions),
$withWasi,
\FFI::addr($errPtr)
);

if (\FFI::isNull($errPtr) === false) {
$error = \FFI::string($errPtr);
$this->lib->extism_plugin_new_error_free($errPtr);
throw new \Extism\PluginLoadException("Extism: unable to compile plugin: " . $error);
}

$this->handle = $handle;
}

/**
* Instantiate a plugin from this compiled plugin.
*
* @return Plugin
*/
public function instantiate(): Plugin
{
$errPtr = $this->lib->ffi->new($this->lib->ffi->type("char*"));
$nativeHandle = $this->lib->extism_plugin_new_from_compiled($this->handle, \FFI::addr($errPtr));

if (\FFI::isNull($errPtr) === false) {
$error = \FFI::string($errPtr);
$this->lib->extism_plugin_new_error_free($errPtr);
throw new \Extism\PluginLoadException("Extism: unable to load plugin from compiled: " . $error);
}

$handle = new \Extism\Internal\PluginHandle($this->lib, $nativeHandle);

return new Plugin($handle);
}

/**
* Destructor to clean up resources
*/
public function __destruct()
{
$this->lib->extism_compiled_plugin_free($this->handle);
}
}
21 changes: 19 additions & 2 deletions src/CurrentPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ public function __construct($lib, \FFI\CData $handle)
$this->lib = $lib;
}

/**
* Get *a copy* of the current plugin call's associated host context data. Returns null if call was made without host context.
*
* @return mixed|null Returns a copy of the host context data or null if none was provided
*/
public function getCallHostContext()
{
$serialized = $this->lib->extism_current_plugin_host_context($this->handle);
if ($serialized === null) {
return null;
}

return unserialize($serialized);
}

/**
* Reads a string from the plugin's memory at the given offset.
*
Expand All @@ -33,7 +48,8 @@ public 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);
$blockStart = $ptr + $offset;
$ptr = $this->lib->ffi->cast("char *", $blockStart);

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

Expand Down Expand Up @@ -72,7 +88,8 @@ private 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);
$blockStart = $ptr + $offset;
$ptr = $this->lib->ffi->cast("char *", $blockStart);

\FFI::memcpy($ptr, $data, strlen($data));
}
Expand Down
138 changes: 137 additions & 1 deletion src/Internal/LibExtism.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public function extism_current_plugin_memory(\FFI\CData $plugin): \FFI\CData
return $this->ffi->extism_current_plugin_memory($plugin);
}

public function extism_current_plugin_memory_free(\FFI\CData $plugin, \FFI\CData $ptr): void
public function extism_current_plugin_memory_free(\FFI\CData $plugin, int $ptr): void
{
$this->ffi->extism_current_plugin_memory_free($plugin, $ptr);
}
Expand Down Expand Up @@ -101,6 +101,129 @@ public function extism_plugin_function_exists(\FFI\CData $plugin, string $func_n
return $this->ffi->extism_plugin_function_exists($plugin, $func_name);
}

/**
* Create a new plugin from an ExtismCompiledPlugin
*/
public function extism_plugin_new_from_compiled(\FFI\CData $compiled, \FFI\CData $errPtr): ?\FFI\CData
{
return $this->ffi->extism_plugin_new_from_compiled($compiled, $errPtr);
}

/**
* Create a new plugin with a fuel limit
*/
public function extism_plugin_new_with_fuel_limit(string $wasm, int $wasm_size, array $functions, int $n_functions, bool $with_wasi, int $fuel_limit, \FFI\CData $errPtr): ?\FFI\CData
{
$functionHandles = array_map(function ($function) {
return $function->handle;
}, $functions);

$functionHandles = $this->toCArray($functionHandles, "ExtismFunction*");

$ptr = $this->owned("uint8_t", $wasm);
$pluginPtr = $this->ffi->extism_plugin_new_with_fuel_limit($ptr, $wasm_size, $functionHandles, $n_functions, $with_wasi ? 1 : 0, $fuel_limit, $errPtr);
return $this->ffi->cast("ExtismPlugin*", $pluginPtr);
}

/**
* Get handle for plugin cancellation
*/
public function extism_plugin_cancel_handle(\FFI\CData $plugin): \FFI\CData
{
return $this->ffi->extism_plugin_cancel_handle($plugin);
}

/**
* Cancel a running plugin
*/
public function extism_plugin_cancel(\FFI\CData $handle): bool
{
return $this->ffi->extism_plugin_cancel($handle);
}

/**
* Pre-compile an Extism plugin
*/
public function extism_compiled_plugin_new(string $wasm, int $wasm_size, array $functions, int $n_functions, bool $with_wasi, \FFI\CData $errPtr): ?\FFI\CData
{
$functionHandles = array_map(function ($function) {
return $function->handle;
}, $functions);

$functionHandles = $this->toCArray($functionHandles, "ExtismFunction*");


$ptr = $this->owned("uint8_t", $wasm);
$pluginPtr = $this->ffi->extism_compiled_plugin_new($ptr, $wasm_size, $functionHandles, $n_functions, $with_wasi ? 1 : 0, $errPtr);
return $this->ffi->cast("ExtismCompiledPlugin*", $pluginPtr);
}

/**
* Free ExtismCompiledPlugin
*/
public function extism_compiled_plugin_free(\FFI\CData $plugin): void
{
$this->ffi->extism_compiled_plugin_free($plugin);
}

/**
* Enable HTTP response headers in plugins
*/
public function extism_plugin_allow_http_response_headers(\FFI\CData $plugin): void
{
$this->ffi->extism_plugin_allow_http_response_headers($plugin);
}

/**
* Get plugin's ID
*/
public function extism_plugin_id(\FFI\CData $plugin): \FFI\CData
{
return $this->ffi->extism_plugin_id($plugin);
}

/**
* Update plugin config
*/
public function extism_plugin_config(\FFI\CData $plugin, string $json, int $json_size): bool
{
$ptr = $this->owned("uint8_t", $json);
return $this->ffi->extism_plugin_config($plugin, $ptr, $json_size);
}

/**
* Call a function with host context
*/
public function extism_plugin_call_with_host_context(\FFI\CData $plugin, string $func_name, string $data, int $data_len, $host_context): int
{
$dataPtr = $this->owned("uint8_t", $data);

if ($host_context === null) {
return $this->ffi->extism_plugin_call_with_host_context($plugin, $func_name, $dataPtr, $data_len, null);
}

$serialized = serialize($host_context);
$contextPtr = $this->ffi->new("char*");
$contextArray = $this->ownedZero($serialized);
$contextPtr = \FFI::addr($contextArray);

return $this->ffi->extism_plugin_call_with_host_context(
$plugin,
$func_name,
$dataPtr,
$data_len,
$contextPtr
);
}

/**
* Reset plugin
*/
public function extism_plugin_reset(\FFI\CData $plugin): bool
{
return $this->ffi->extism_plugin_reset($plugin);
}

public function extism_version(): string
{
return $this->ffi->extism_version();
Expand All @@ -112,6 +235,19 @@ public function extism_plugin_call(\FFI\CData $plugin, string $func_name, string
return $this->ffi->extism_plugin_call($plugin, $func_name, $dataPtr, $data_len);
}

/**
* Get the current plugin's associated host context data
*/
public function extism_current_plugin_host_context(\FFI\CData $plugin): ?string
{
$ptr = $this->ffi->extism_current_plugin_host_context($plugin);
if ($ptr === null || \FFI::isNull($ptr)) {
return null;
}

return \FFI::string($this->ffi->cast("char *", $ptr));
}

public function extism_error(\FFI\CData $plugin): ?string
{
return $this->ffi->extism_error($plugin);
Expand Down
18 changes: 18 additions & 0 deletions src/Internal/PluginHandle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Extism\Internal;

/** @internal */
class PluginHandle
{
/** @internal */
public \Extism\Internal\LibExtism $lib;
/** @internal */
public \FFI\CData $native;

public function __construct($lib, \FFI\CData $handle)
{
$this->native = $handle;
$this->lib = $lib;
}
}
Loading

0 comments on commit 43432a7

Please sign in to comment.