├── .github ├── CODEOWNERS ├── actions │ └── libextism │ │ └── action.yaml └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── example ├── composer.json ├── composer.lock ├── index.php └── memory_test.php ├── phpcs.xml.dist ├── phpunit.xml.dist ├── src ├── CompiledPlugin.php ├── CurrentPlugin.php ├── ExtismValType.php ├── FunctionCallException.php ├── HostFunction.php ├── Internal │ ├── LibExtism.php │ ├── PluginHandle.php │ └── extism.h ├── Manifest.php ├── Manifest │ ├── ByteArrayWasmSource.php │ ├── HttpMethod.php │ ├── MemoryOptions.php │ ├── PathWasmSource.php │ ├── UrlWasmSource.php │ └── WasmSource.php ├── Plugin.php ├── PluginLoadException.php └── PluginOptions.php ├── tests ├── CompiledPluginTest.php ├── Helpers.php ├── ManifestTest.php ├── PluginTest.php └── data │ └── test.txt └── wasm ├── alloc.wasm ├── config.wasm ├── count_vowels.wasm ├── count_vowels_kvstore.wasm ├── exit.wasm ├── fail.wasm ├── fs.wasm ├── hello.wasm ├── http.wasm └── sleep.wasm /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mhmd-azeez @nilslice 2 | -------------------------------------------------------------------------------- /.github/actions/libextism/action.yaml: -------------------------------------------------------------------------------- 1 | on: [workflow_call] 2 | 3 | name: libextism 4 | 5 | inputs: 6 | gh-token: 7 | description: "A GitHub PAT" 8 | default: ${{ github.token }} 9 | 10 | runs: 11 | using: composite 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | repository: extism/cli 16 | path: .extism-cli 17 | - uses: ./.extism-cli/.github/actions/extism-cli 18 | - name: Install 19 | shell: bash 20 | run: sudo extism lib install --version git --github-token ${{ inputs.gh-token }} 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: PHP CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | php: 11 | name: PHP Test 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest] 16 | php: ['8.3', '7.4'] 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v3 20 | - uses: ./.github/actions/libextism 21 | - name: Setup PHP env 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | extensions: ffi 26 | tools: composer 27 | env: 28 | fail-fast: true 29 | - name: Test PHP SDK 30 | run: | 31 | make test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .phpunit.result.cache 2 | /composer.lock 3 | **/vendor/ 4 | src/ExtismLib.php 5 | example/php_errors.log 6 | php_errors.log 7 | .phpunit.cache 8 | .phpunit.result.cache 9 | .php-cs-fixer.cache 10 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Dylibso, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | prepare: 4 | composer install 5 | 6 | test: prepare 7 | php vendor/bin/phpunit ./tests 8 | 9 | cscheck: 10 | vendor/bin/phpcs . 11 | 12 | csfix: 13 | vendor/bin/php-cs-fixer fix . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extism PHP Host SDK 2 | 3 | This repo houses the PHP SDK for integrating with the [Extism](https://extism.org/) runtime. Install this library into your host PHP applications to run Extism plugins. 4 | 5 | ## Installation 6 | 7 | ### Install the Extism Runtime Dependency 8 | 9 | For this library, you first need to install the Extism Runtime. You can [download the shared object directly from a release](https://github.com/extism/extism/releases) or use the [Extism CLI](https://github.com/extism/cli) to install it: 10 | 11 | ```bash 12 | sudo extism lib install latest 13 | 14 | #=> Fetching https://github.com/extism/extism/releases/download/v0.5.2/libextism-aarch64-apple-darwin-v0.5.2.tar.gz 15 | #=> Copying libextism.dylib to /usr/local/lib/libextism.dylib 16 | #=> Copying extism.h to /usr/local/include/extism.h 17 | ``` 18 | 19 | > **Note**: This library has breaking changes and targets 1.0 of the runtime. For the time being, install the runtime from our nightly development builds on git: `sudo extism lib install --version git`. 20 | 21 | ### Install the Package 22 | 23 | Install via [Packagist](https://packagist.org/): 24 | ```sh 25 | composer require extism/extism 26 | ``` 27 | 28 | *Note*: For the time being you may need to add a minimum-stability of "dev" to your composer.json 29 | ```json 30 | { 31 | "minimum-stability": "dev", 32 | } 33 | ``` 34 | 35 | ## Getting Started 36 | 37 | This guide should walk you through some of the concepts in Extism and this PHP library. 38 | 39 | First you should add a using statement for Extism: 40 | 41 | ```php 42 | use Extism\Plugin; 43 | use Extism\Manifest; 44 | use Extism\Manifest\UrlWasmSource; 45 | ``` 46 | 47 | ## Creating A Plug-in 48 | 49 | The primary concept in Extism is the [plug-in](https://extism.org/docs/concepts/plug-in). You can think of a plug-in as a code module stored in a `.wasm` file. 50 | 51 | Since you may not have an Extism plug-in on hand to test, let's load a demo plug-in from the web: 52 | 53 | ```php 54 | $wasm = new UrlWasmSource("https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"); 55 | $manifest = new Manifest($wasm); 56 | 57 | $plugin = new Plugin($manifest, true); 58 | ``` 59 | 60 | > **Note**: The schema for this manifest can be found here: https://extism.org/docs/concepts/manifest/ 61 | 62 | ### Calling A Plug-in's Exports 63 | 64 | This plug-in was written in Rust and it does one thing, it counts vowels in a string. As such, it exposes one "export" function: `count_vowels`. We can call exports using `Plugin.call`: 65 | 66 | ```php 67 | $output = $plugin->call("count_vowels", "Hello, World!"); 68 | 69 | // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 70 | ``` 71 | 72 | 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. 73 | 74 | ### Plug-in State 75 | 76 | 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: 77 | 78 | ```php 79 | $output = $plugin->call("count_vowels", "Hello, World!"); 80 | // => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"} 81 | 82 | $output = $plugin->call("count_vowels", "Hello, World!"); 83 | // => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"} 84 | ``` 85 | 86 | These variables will persist until this plug-in is freed or you initialize a new one. 87 | 88 | ### Configuration 89 | 90 | Plug-ins may optionally take a configuration object. This is a static way to configure the plug-in. Our count-vowels plugin takes an optional configuration to change out which characters are considered vowels. Example: 91 | 92 | ```php 93 | $wasm = new UrlWasmSource("https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"); 94 | 95 | $manifest = new Manifest($wasm); 96 | 97 | $plugin = new Plugin($manifest, true); 98 | $output = $plugin->call("count_vowels", "Yellow, World!"); 99 | // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 100 | 101 | $manifest = new Manifest($wasm); 102 | $manifest->config->vowels = "aeiouyAEIOUY"; 103 | 104 | $plugin = new Plugin($manifest, true); 105 | $output = $plugin->call("count_vowels", "Yellow, World!"); 106 | // => {"count": 4, "total": 4, "vowels": "aeiouAEIOUY"} 107 | ``` 108 | 109 | ### Host Functions 110 | 111 | > **Note** 112 | > 113 | > Host Functions support is experimental. Due to usage of callbacks with FFI, It may leak memory. 114 | 115 | 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! 116 | 117 | Wasm can't use our KV store on it's own. This is where `Host Functions` come in. 118 | 119 | [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 PHP functions you write which can be passed down and invoked from any language inside the plug-in. 120 | 121 | Let's load the manifest like usual but load up this `count_vowels_kvstore` plug-in: 122 | 123 | ```php 124 | $manifest = new Manifest(new UrlWasmSource("https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm")); 125 | ``` 126 | 127 | > *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. 128 | 129 | Unlike our previous plug-in, this plug-in expects you to provide host functions that satisfy our import interface for a KV store. 130 | 131 | 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`. 132 | 133 | ```php 134 | // pretend this is Redis or something :) 135 | $kvstore = []; 136 | $kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (string $key) use (&$kvstore) { 137 | $value = $kvstore[$key] ?? "\0\0\0\0"; 138 | 139 | echo "Read " . bytesToInt($value) . " from key=$key" . PHP_EOL; 140 | return $value; 141 | }); 142 | 143 | $kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (string $key, string $value) use (&$kvstore) { 144 | echo "Writing value=" . bytesToInt($value) . " from key=$key" . PHP_EOL; 145 | $kvstore[$key] = $value; 146 | }); 147 | 148 | function bytesToInt(string $bytes): int { 149 | $result = unpack("L", $bytes); 150 | return $result[1]; 151 | } 152 | ``` 153 | 154 | > *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: 155 | > 156 | > ```php 157 | > $kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (CurrentPlugin $p, int $keyPtr) use ($kvstore) { 158 | > $key = $p->read_block($keyPtr); 159 | > 160 | > $value = $kvstore[$key] ?? "\0\0\0\0"; 161 | > 162 | > return $p->write_block($value); 163 | > }); 164 | > ``` 165 | 166 | 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: 167 | 168 | ```php 169 | $plugin = new Plugin($manifest, true, [$kvRead, $kvWrite]); 170 | 171 | $output = $plugin->call("count_vowels", "Hello World!"); 172 | 173 | echo($output . PHP_EOL); 174 | // => Read 0 from key=count-vowels" 175 | // => Writing value=3 from key=count-vowels" 176 | // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 177 | 178 | $output = $plugin->call("count_vowels", "Hello World!"); 179 | 180 | echo($output . PHP_EOL); 181 | // => Read 3 from key=count-vowels" 182 | // => Writing value=6 from key=count-vowels" 183 | // => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"} 184 | ``` 185 | 186 | For host function callbacks, these are the valid parameter types: 187 | - `CurrentPlugin`: Only if its the first parameter. Allows you to manually manage memory. Optional. 188 | - `string`: If the parameter represents a memory offset (an `i64`), then the SDK can automatically load the buffer into a `string` for you. 189 | - `int`: For `i32` and `i64` parameters. 190 | - `float`: For `f32` and `f64` parameters. 191 | 192 | Valid return types: 193 | - `void` 194 | - `int`: For `i32` and `i64` parameters. 195 | - `float`: For `f32` and `f64` parameters. 196 | - `string`: the content of the string will be allocated in the wasm plugin memory and the offset (`i64`) will be returned. 197 | 198 | ### Fuel Limits 199 | 200 | 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. 201 | 202 | ```php 203 | // Create plugin with fuel limit of 1000 instructions 204 | $plugin = new Plugin($manifest, true, [], new PluginOptions(true, 1000)); 205 | 206 | try { 207 | $output = $plugin->call("run_test", ""); 208 | } catch (\Exception $e) { 209 | // Plugin ran out of fuel 210 | // The exception message will contain "fuel" 211 | } 212 | ``` 213 | 214 | ### Call Host Context 215 | 216 | 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. 217 | 218 | 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: 219 | 220 | ```php 221 | $multiUserKvStore = [[]]; 222 | 223 | $kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (CurrentPlugin $p, string $key) use (&$multiUserKvStore) { 224 | $userId = $p->getCallHostContext(); // get a copy of the host context data 225 | $kvStore = $multiUserKvStore[$userId] ?? []; 226 | 227 | return $kvStore[$key] ?? "\0\0\0\0"; 228 | }); 229 | 230 | $kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (CurrentPlugin $p, string $key, string $value) use (&$multiUserKvStore) { 231 | $userId = $p->getCallHostContext(); // get a copy of the host context data 232 | $kvStore = $multiUserKvStore[$userId] ?? []; 233 | 234 | $kvStore[$key] = $value; 235 | $multiUserKvStore[$userId] = $kvStore; 236 | }); 237 | 238 | $plugin = self::loadPlugin("count_vowels_kvstore.wasm", [$kvRead, $kvWrite]); 239 | 240 | $userId = 1; 241 | 242 | $response = $plugin->callWithContext("count_vowels", "Hello World!", $userId); 243 | $this->assertEquals('{"count":3,"total":3,"vowels":"aeiouAEIOU"}', $response); 244 | 245 | $response = $plugin->callWithContext("count_vowels", "Hello World!", $userId); 246 | $this->assertEquals('{"count":3,"total":6,"vowels":"aeiouAEIOU"}', $response); 247 | ``` 248 | 249 | 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()`. 250 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extism/extism", 3 | "description": "Make your software programmable. Run WebAssembly extensions in your app using the first off-the-shelf, universal plug-in system.", 4 | "license": "BSD-3-Clause", 5 | "type": "library", 6 | "keywords": [ 7 | "WebAssembly", 8 | "plugin-system", 9 | "runtime", 10 | "plug-in", 11 | "wasm", 12 | "framework" 13 | ], 14 | "authors": [ 15 | { 16 | "name": "The Extism Authors", 17 | "email": "oss@extism.org", 18 | "homepage": "https://extism.org" 19 | }, 20 | { 21 | "name": "Dylibso, Inc.", 22 | "email": "oss@dylibso.com", 23 | "homepage": "https://dylibso.com" 24 | } 25 | ], 26 | "require": { 27 | "php": "^7.4 || ^8" 28 | }, 29 | "suggest": {}, 30 | "minimum-stability": "dev", 31 | "prefer-stable": true, 32 | "autoload": { 33 | "psr-4": { 34 | "Extism\\": "src/" 35 | }, 36 | "psr-0": { 37 | "LibExtism": "src" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Extism\\Tests\\": "tests/" 43 | } 44 | }, 45 | "config": { 46 | "sort-packages": true 47 | }, 48 | "extra": {}, 49 | "scripts": {}, 50 | "scripts-descriptions": {}, 51 | "require-dev": { 52 | "friendsofphp/php-cs-fixer": "^3.59", 53 | "phpunit/phpunit": "^9", 54 | "squizlabs/php_codesniffer": "^3.10" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /example/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extism/example", 3 | "description": "Example running the Extism PHP Host SDK", 4 | "license": "BSD-3-Clause", 5 | "type": "project", 6 | "authors": [ 7 | { 8 | "name": "The Extism Authors", 9 | "email": "oss@extism.org" 10 | } 11 | ], 12 | "require": { 13 | "extism/extism": "dev-main" 14 | }, 15 | "repositories": [ 16 | { 17 | "type": "path", 18 | "url": "../" 19 | } 20 | ], 21 | "minimum-stability": "dev" 22 | } 23 | -------------------------------------------------------------------------------- /example/composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "5a29b9200e3cb080913581f7f76c429d", 8 | "packages": [ 9 | { 10 | "name": "extism/extism", 11 | "version": "dev-feat/update-ffi", 12 | "dist": { 13 | "type": "path", 14 | "url": "..", 15 | "reference": "1648699bab5ebf14d5388579b172b1ff41ad7f84" 16 | }, 17 | "require": { 18 | "php": "^7.4 || ^8" 19 | }, 20 | "type": "library", 21 | "autoload": { 22 | "psr-4": { 23 | "Extism\\": "src/" 24 | }, 25 | "files": [ 26 | "src/Manifest.php", 27 | "src/Plugin.php" 28 | ] 29 | }, 30 | "autoload-dev": { 31 | "psr-4": [] 32 | }, 33 | "license": [ 34 | "BSD-3-Clause" 35 | ], 36 | "authors": [ 37 | { 38 | "name": "The Extism Authors", 39 | "email": "oss@extism.org", 40 | "homepage": "https://extism.org" 41 | }, 42 | { 43 | "name": "Dylibso, Inc.", 44 | "email": "oss@dylib.so", 45 | "homepage": "https://dylib.so" 46 | } 47 | ], 48 | "description": "Make your software programmable. Run WebAssembly extensions in your app using the first off-the-shelf, universal plug-in system.", 49 | "keywords": [ 50 | "WebAssembly", 51 | "plug-in", 52 | "plugin-system", 53 | "runtime" 54 | ], 55 | "transport-options": { 56 | "relative": true 57 | } 58 | } 59 | ], 60 | "packages-dev": [], 61 | "aliases": [], 62 | "minimum-stability": "dev", 63 | "stability-flags": [], 64 | "prefer-stable": false, 65 | "prefer-lowest": false, 66 | "platform": [], 67 | "platform-dev": [], 68 | "plugin-api-version": "2.6.0" 69 | } 70 | -------------------------------------------------------------------------------- /example/index.php: -------------------------------------------------------------------------------- 1 | call("count_vowels", "Yellow, World!"); 14 | var_dump($output); 15 | 16 | $manifest = new Manifest($wasm); 17 | $manifest->config->vowels = "aeiouyAEIOUY"; 18 | 19 | $plugin = new Plugin($manifest, true); 20 | $output = $plugin->call("count_vowels", "Yellow, World!"); 21 | var_dump($output); 22 | -------------------------------------------------------------------------------- /example/memory_test.php: -------------------------------------------------------------------------------- 1 | call("count_vowels", "Hello World!"); 28 | 29 | if ($i % 100 === 0) { 30 | echo "Iteration: $i\n"; 31 | } 32 | } 33 | 34 | readline(); 35 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | */vendor/* 8 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | tests/ 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/CompiledPlugin.php: -------------------------------------------------------------------------------- 1 | lib = $lib; 32 | $this->functions = $functions; 33 | 34 | $data = json_encode($manifest); 35 | if (!$data) { 36 | throw new \Extism\PluginLoadException("Failed to encode manifest"); 37 | } 38 | 39 | $errPtr = $lib->ffi->new($lib->ffi->type("char*")); 40 | 41 | $handle = $this->lib->extism_compiled_plugin_new( 42 | $data, 43 | strlen($data), 44 | $functions, 45 | count($functions), 46 | $withWasi, 47 | \FFI::addr($errPtr) 48 | ); 49 | 50 | if (\FFI::isNull($errPtr) === false) { 51 | $error = \FFI::string($errPtr); 52 | $this->lib->extism_plugin_new_error_free($errPtr); 53 | throw new \Extism\PluginLoadException("Extism: unable to compile plugin: " . $error); 54 | } 55 | 56 | $this->handle = $handle; 57 | } 58 | 59 | /** 60 | * Instantiate a plugin from this compiled plugin. 61 | * 62 | * @return Plugin 63 | */ 64 | public function instantiate(): Plugin 65 | { 66 | $errPtr = $this->lib->ffi->new($this->lib->ffi->type("char*")); 67 | $nativeHandle = $this->lib->extism_plugin_new_from_compiled($this->handle, \FFI::addr($errPtr)); 68 | 69 | if (\FFI::isNull($errPtr) === false) { 70 | $error = \FFI::string($errPtr); 71 | $this->lib->extism_plugin_new_error_free($errPtr); 72 | throw new \Extism\PluginLoadException("Extism: unable to load plugin from compiled: " . $error); 73 | } 74 | 75 | $handle = new \Extism\Internal\PluginHandle($this->lib, $nativeHandle); 76 | 77 | return new Plugin($handle); 78 | } 79 | 80 | /** 81 | * Destructor to clean up resources 82 | */ 83 | public function __destruct() 84 | { 85 | $this->lib->extism_compiled_plugin_free($this->handle); 86 | } 87 | } -------------------------------------------------------------------------------- /src/CurrentPlugin.php: -------------------------------------------------------------------------------- 1 | handle = $handle; 24 | $this->lib = $lib; 25 | } 26 | 27 | /** 28 | * Get *a copy* of the current plugin call's associated host context data. Returns null if call was made without host context. 29 | * 30 | * @return mixed|null Returns a copy of the host context data or null if none was provided 31 | */ 32 | public function getCallHostContext() 33 | { 34 | $serialized = $this->lib->extism_current_plugin_host_context($this->handle); 35 | if ($serialized === null) { 36 | return null; 37 | } 38 | 39 | return unserialize($serialized); 40 | } 41 | 42 | /** 43 | * Reads a string from the plugin's memory at the given offset. 44 | * 45 | * @param int $offset Offset of the block to read. 46 | */ 47 | public function read_block(int $offset): string 48 | { 49 | $ptr = $this->lib->extism_current_plugin_memory($this->handle); 50 | $ptr = $this->lib->ffi->cast("char *", $ptr); 51 | $blockStart = $ptr + $offset; 52 | $ptr = $this->lib->ffi->cast("char *", $blockStart); 53 | 54 | $length = $this->lib->extism_current_plugin_memory_length($this->handle, $offset); 55 | 56 | return \FFI::string($ptr, $length); 57 | } 58 | 59 | /** 60 | * Allocates a block of memory in the plugin's memory and returns the offset. 61 | * 62 | * @param int $size Size of the block to allocate in bytes. 63 | */ 64 | private function allocate_block(int $size): int 65 | { 66 | return $this->lib->extism_current_plugin_memory_alloc($this->handle, $size); 67 | } 68 | 69 | /** 70 | * Writes a string to the plugin's memory, returning the offset of the block. 71 | * 72 | * @param string $data Buffer to write to the plugin's memory. 73 | */ 74 | public function write_block(string $data): int 75 | { 76 | $offset = $this->allocate_block(strlen($data)); 77 | $this->fill_block($offset, $data); 78 | return $offset; 79 | } 80 | 81 | /** 82 | * Fills a block of memory in the plugin's memory. 83 | * 84 | * @param int $offset Offset of the block to fill. 85 | * @param string $data Buffer to fill the block with. 86 | */ 87 | private function fill_block(int $offset, string $data): void 88 | { 89 | $ptr = $this->lib->extism_current_plugin_memory($this->handle); 90 | $ptr = $this->lib->ffi->cast("char *", $ptr); 91 | $blockStart = $ptr + $offset; 92 | $ptr = $this->lib->ffi->cast("char *", $blockStart); 93 | 94 | \FFI::memcpy($ptr, $data, strlen($data)); 95 | } 96 | 97 | /** 98 | * Frees a block of memory in the plugin's memory. 99 | * 100 | * @param int $offset Offset of the block to free. 101 | */ 102 | private function free_block(int $offset): void 103 | { 104 | $this->lib->extism_current_plugin_memory_free($this->handle, $offset); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/ExtismValType.php: -------------------------------------------------------------------------------- 1 | error = $error; 15 | $this->functionName = $functionName; 16 | } 17 | } -------------------------------------------------------------------------------- /src/HostFunction.php: -------------------------------------------------------------------------------- 1 | 'ExtismValType_I32', 19 | 1 => 'ExtismValType_I64', 20 | 2 => 'ExtismValType_F32', 21 | 3 => 'ExtismValType_F64', 22 | 4 => 'ExtismValType_V128', 23 | 5 => 'ExtismValType_Func_Ref', 24 | 6 => 'ExtismValType_Extern_Ref', 25 | ]; 26 | } 27 | 28 | class HostFunction 29 | { 30 | private \Extism\Internal\LibExtism $lib; 31 | private $callback; 32 | 33 | public \FFI\CData $handle; 34 | 35 | /** 36 | * Constructor 37 | * 38 | * @param string $name Name of the function 39 | * @param array $inputTypes Array of input types. @see ExtismValType 40 | * @param array $outputTypes Array of output types 41 | * @param callable $callback Callback to invoke when the function is called 42 | * 43 | * @example ../tests/PluginTest.php 82 84 Simple Example 44 | * @example ../tests/PluginTest.php 100 104 Manually read memory using CurrentPlugin 45 | */ 46 | public function __construct(string $name, array $inputTypes, array $outputTypes, callable $callback) 47 | { 48 | $reflection = new \ReflectionFunction($callback); 49 | $arguments = $reflection->getParameters(); 50 | $offset = HostFunction::validate_arguments($arguments, $inputTypes); 51 | 52 | global $lib; 53 | 54 | if ($lib == null) { 55 | $lib = new \Extism\Internal\LibExtism(); 56 | } 57 | 58 | $this->lib = $lib; 59 | 60 | $inputs = []; 61 | 62 | for ($i = 0; $i < count($inputTypes); $i++) { 63 | $enum = ExtismValType::VAL_TYPE_MAP[$inputTypes[$i]]; 64 | $inputs[$i] = $this->lib->ffi->$enum; 65 | } 66 | 67 | $outputs = []; 68 | 69 | for ($i = 0; $i < count($outputTypes); $i++) { 70 | $enum = ExtismValType::VAL_TYPE_MAP[$outputTypes[$i]]; 71 | $outputs[$i] = $this->lib->ffi->$enum; 72 | } 73 | 74 | $func = function ($handle, $inputs, $n_inputs, $outputs, $n_outputs, $data) use ($callback, $lib, $arguments, $offset) { 75 | try { 76 | $currentPlugin = new CurrentPlugin($lib, $handle); 77 | $params = HostFunction::get_parameters($currentPlugin, $inputs, $n_inputs, $arguments, $offset); 78 | 79 | $r = $callback(...$params); 80 | 81 | if ($r == null) { 82 | $r = 0; 83 | } elseif (gettype($r) == "string") { 84 | $r = $currentPlugin->write_block($r); 85 | } 86 | 87 | if ($n_outputs == 1) { 88 | $output = $outputs[0]; 89 | 90 | switch ($output->t) { 91 | case ExtismValType::I32: 92 | $output->v->i32 = $r; 93 | break; 94 | case ExtismValType::I64: 95 | $output->v->i64 = $r; 96 | break; 97 | case ExtismValType::F32: 98 | $output->v->f32 = $r; 99 | break; 100 | case ExtismValType::F64: 101 | $output->v->f64 = $r; 102 | break; 103 | default: 104 | throw new \Exception("Unsupported type for output: " . $output->t); 105 | } 106 | } 107 | // Throwing an exception in FFI callback is not supported and 108 | // causes a fatal error without a stack trace. 109 | // So we catch it and print the exception manually 110 | } catch (\Throwable $e) { // PHP 7+ 111 | HostFunction::print_exception($e); 112 | } catch (\Exception $e) { // PHP 5+ 113 | HostFunction::print_exception($e); 114 | } 115 | }; 116 | 117 | $this->callback = $func; 118 | 119 | $this->handle = $this->lib->extism_function_new($name, $inputs, $outputs, $func, null, null); 120 | $this->set_namespace("extism:host/user"); 121 | } 122 | 123 | public function __destruct() 124 | { 125 | $this->lib->extism_function_free($this->handle); 126 | } 127 | 128 | public function set_namespace(string $namespace) 129 | { 130 | $this->lib->extism_function_set_namespace($this->handle, $namespace); 131 | } 132 | 133 | private static function print_exception(\Throwable $e) 134 | { 135 | echo "Exception thrown in host function: " . $e->getMessage() . PHP_EOL; 136 | echo $e->getTraceAsString() . PHP_EOL; 137 | throw $e; 138 | } 139 | 140 | private static function get_type_name(\ReflectionParameter $param) 141 | { 142 | $type = $param->getType(); 143 | 144 | if ($type == null) { 145 | return null; 146 | } 147 | 148 | if ($type instanceof \ReflectionNamedType) { 149 | return $type->getName(); 150 | } 151 | 152 | return null; 153 | } 154 | 155 | private static function get_parameters( 156 | CurrentPlugin $currentPlugin, 157 | \FFI\CData $inputs, 158 | int $n_inputs, 159 | array $arguments, 160 | int $offset 161 | ): array { 162 | $params = []; 163 | 164 | if ($offset == 1) { 165 | array_push($params, $currentPlugin); 166 | } 167 | 168 | for ($i = 0; $i < $n_inputs; $i++) { 169 | $input = $inputs[$i]; 170 | 171 | switch ($input->t) { 172 | case ExtismValType::I32: 173 | array_push($params, $input->v->i32); 174 | break; 175 | case ExtismValType::I64: 176 | $type = HostFunction::get_type_name($arguments[$i + $offset]); 177 | 178 | if ($type != null && $type == "string") { 179 | $ptr = $input->v->i64; 180 | $str = $currentPlugin->read_block($ptr); 181 | array_push($params, $str); 182 | } else { 183 | array_push($params, $input->v->i64); 184 | } 185 | 186 | break; 187 | case ExtismValType::F32: 188 | array_push($params, $input->v->f32); 189 | break; 190 | case ExtismValType::F64: 191 | array_push($params, $input->v->f64); 192 | break; 193 | default: 194 | throw new \Exception("Unsupported type for parametr #$i: " . $input->t); 195 | } 196 | } 197 | 198 | return $params; 199 | } 200 | 201 | private static function validate_arguments(array $arguments, array $inputTypes) 202 | { 203 | $offset = 0; 204 | $n_arguments = count($arguments); 205 | 206 | if ($n_arguments > 0 && HostFunction::get_type_name($arguments[0]) == "Extism\CurrentPlugin") { 207 | $offset = 1; 208 | } 209 | 210 | if ($n_arguments - $offset != count($inputTypes)) { 211 | throw new \Exception("Number of arguments does not match number of input types"); 212 | } 213 | 214 | for ($i = $offset; $i < $n_arguments; $i++) { 215 | $argType = HostFunction::get_type_name($arguments[$i]); 216 | 217 | if ($argType == null) { 218 | continue; 219 | } elseif ($argType == "string") { 220 | // string is represented as a pointer to a block of memory 221 | $argType = "int"; 222 | } 223 | 224 | $inputType = $inputTypes[$i - $offset]; 225 | 226 | switch ($inputType) { 227 | case ExtismValType::I32: 228 | case ExtismValType::I64: 229 | if ($argType != "int") { 230 | throw new \Exception("Argument #$i is not an int"); 231 | } 232 | break; 233 | case ExtismValType::F32: 234 | case ExtismValType::F64: 235 | if ($argType != "float") { 236 | throw new \Exception("Argument #$i is not a float"); 237 | } 238 | break; 239 | 240 | default: 241 | throw new \Exception("Unsupported type for argument #$i: " . $inputType); 242 | } 243 | } 244 | 245 | return $offset; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/Internal/LibExtism.php: -------------------------------------------------------------------------------- 1 | ffi = LibExtism::findSo($name); 14 | } 15 | 16 | private function findSo(string $name): \FFI 17 | { 18 | $platform = php_uname("s"); 19 | $directories = []; 20 | if ($this->startsWith($platform, "windows")) { 21 | $path = getenv('PATH'); 22 | $directories = explode(PATH_SEPARATOR, $path); 23 | } else { 24 | $directories = ['/usr/local/lib', '/usr/lib']; 25 | } 26 | 27 | $searchedPaths = []; 28 | foreach ($directories as $directory) { 29 | $fullPath = $directory . DIRECTORY_SEPARATOR . $name; 30 | 31 | if (file_exists($fullPath)) { 32 | return \FFI::cdef( 33 | file_get_contents(__DIR__ . "/extism.h"), 34 | $fullPath 35 | ); 36 | } 37 | 38 | array_push($searchedPaths, $fullPath); 39 | } 40 | 41 | throw new \RuntimeException('Failed to find shared object. Searched locations: ' . implode(', ', $searchedPaths)); 42 | } 43 | 44 | private function soname() 45 | { 46 | $platform = php_uname("s"); 47 | switch ($platform) { 48 | case "Darwin": 49 | return "libextism.dylib"; 50 | case "Linux": 51 | return "libextism.so"; 52 | case "Windows NT": 53 | return "extism.dll"; 54 | default: 55 | throw new \RuntimeException("Extism: unsupported platform " . $platform); 56 | } 57 | } 58 | 59 | public function extism_current_plugin_memory(\FFI\CData $plugin): \FFI\CData 60 | { 61 | return $this->ffi->extism_current_plugin_memory($plugin); 62 | } 63 | 64 | public function extism_current_plugin_memory_free(\FFI\CData $plugin, int $ptr): void 65 | { 66 | $this->ffi->extism_current_plugin_memory_free($plugin, $ptr); 67 | } 68 | 69 | public function extism_current_plugin_memory_alloc(\FFI\CData $plugin, int $size): int 70 | { 71 | return $this->ffi->extism_current_plugin_memory_alloc($plugin, $size); 72 | } 73 | 74 | public function extism_current_plugin_memory_length(\FFI\CData $plugin, int $offset): int 75 | { 76 | return $this->ffi->extism_current_plugin_memory_length($plugin, $offset); 77 | } 78 | 79 | public function extism_plugin_new(string $wasm, int $wasm_size, array $functions, int $n_functions, bool $with_wasi, ?\FFI\CData $errmsg): ?\FFI\CData 80 | { 81 | $functionHandles = array_map(function ($function) { 82 | return $function->handle; 83 | }, $functions); 84 | 85 | $functionHandles = $this->toCArray($functionHandles, "ExtismFunction*"); 86 | 87 | $ptr = $this->owned("uint8_t", $wasm); 88 | $wasi = $with_wasi ? 1 : 0; 89 | $pluginPtr = $this->ffi->extism_plugin_new($ptr, $wasm_size, $functionHandles, $n_functions, $wasi, $errmsg); 90 | 91 | return $this->ffi->cast("ExtismPlugin*", $pluginPtr); 92 | } 93 | 94 | public function extism_plugin_new_error_free(\FFI\CData $ptr): void 95 | { 96 | $this->ffi->extism_plugin_new_error_free($ptr); 97 | } 98 | 99 | public function extism_plugin_function_exists(\FFI\CData $plugin, string $func_name): bool 100 | { 101 | return $this->ffi->extism_plugin_function_exists($plugin, $func_name); 102 | } 103 | 104 | /** 105 | * Create a new plugin from an ExtismCompiledPlugin 106 | */ 107 | public function extism_plugin_new_from_compiled(\FFI\CData $compiled, \FFI\CData $errPtr): ?\FFI\CData 108 | { 109 | return $this->ffi->extism_plugin_new_from_compiled($compiled, $errPtr); 110 | } 111 | 112 | /** 113 | * Create a new plugin with a fuel limit 114 | */ 115 | 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 116 | { 117 | $functionHandles = array_map(function ($function) { 118 | return $function->handle; 119 | }, $functions); 120 | 121 | $functionHandles = $this->toCArray($functionHandles, "ExtismFunction*"); 122 | 123 | $ptr = $this->owned("uint8_t", $wasm); 124 | $pluginPtr = $this->ffi->extism_plugin_new_with_fuel_limit($ptr, $wasm_size, $functionHandles, $n_functions, $with_wasi ? 1 : 0, $fuel_limit, $errPtr); 125 | return $this->ffi->cast("ExtismPlugin*", $pluginPtr); 126 | } 127 | 128 | /** 129 | * Get handle for plugin cancellation 130 | */ 131 | public function extism_plugin_cancel_handle(\FFI\CData $plugin): \FFI\CData 132 | { 133 | return $this->ffi->extism_plugin_cancel_handle($plugin); 134 | } 135 | 136 | /** 137 | * Cancel a running plugin 138 | */ 139 | public function extism_plugin_cancel(\FFI\CData $handle): bool 140 | { 141 | return $this->ffi->extism_plugin_cancel($handle); 142 | } 143 | 144 | /** 145 | * Pre-compile an Extism plugin 146 | */ 147 | public function extism_compiled_plugin_new(string $wasm, int $wasm_size, array $functions, int $n_functions, bool $with_wasi, \FFI\CData $errPtr): ?\FFI\CData 148 | { 149 | $functionHandles = array_map(function ($function) { 150 | return $function->handle; 151 | }, $functions); 152 | 153 | $functionHandles = $this->toCArray($functionHandles, "ExtismFunction*"); 154 | 155 | 156 | $ptr = $this->owned("uint8_t", $wasm); 157 | $pluginPtr = $this->ffi->extism_compiled_plugin_new($ptr, $wasm_size, $functionHandles, $n_functions, $with_wasi ? 1 : 0, $errPtr); 158 | return $this->ffi->cast("ExtismCompiledPlugin*", $pluginPtr); 159 | } 160 | 161 | /** 162 | * Free ExtismCompiledPlugin 163 | */ 164 | public function extism_compiled_plugin_free(\FFI\CData $plugin): void 165 | { 166 | $this->ffi->extism_compiled_plugin_free($plugin); 167 | } 168 | 169 | /** 170 | * Enable HTTP response headers in plugins 171 | */ 172 | public function extism_plugin_allow_http_response_headers(\FFI\CData $plugin): void 173 | { 174 | $this->ffi->extism_plugin_allow_http_response_headers($plugin); 175 | } 176 | 177 | /** 178 | * Get plugin's ID 179 | */ 180 | public function extism_plugin_id(\FFI\CData $plugin): \FFI\CData 181 | { 182 | return $this->ffi->extism_plugin_id($plugin); 183 | } 184 | 185 | /** 186 | * Update plugin config 187 | */ 188 | public function extism_plugin_config(\FFI\CData $plugin, string $json, int $json_size): bool 189 | { 190 | $ptr = $this->owned("uint8_t", $json); 191 | return $this->ffi->extism_plugin_config($plugin, $ptr, $json_size); 192 | } 193 | 194 | /** 195 | * Call a function with host context 196 | */ 197 | public function extism_plugin_call_with_host_context(\FFI\CData $plugin, string $func_name, string $data, int $data_len, $host_context): int 198 | { 199 | $dataPtr = $this->owned("uint8_t", $data); 200 | 201 | if ($host_context === null) { 202 | return $this->ffi->extism_plugin_call_with_host_context($plugin, $func_name, $dataPtr, $data_len, null); 203 | } 204 | 205 | $serialized = serialize($host_context); 206 | $contextPtr = $this->ffi->new("char*"); 207 | $contextArray = $this->ownedZero($serialized); 208 | $contextPtr = \FFI::addr($contextArray); 209 | 210 | return $this->ffi->extism_plugin_call_with_host_context( 211 | $plugin, 212 | $func_name, 213 | $dataPtr, 214 | $data_len, 215 | $contextPtr 216 | ); 217 | } 218 | 219 | /** 220 | * Reset plugin 221 | */ 222 | public function extism_plugin_reset(\FFI\CData $plugin): bool 223 | { 224 | return $this->ffi->extism_plugin_reset($plugin); 225 | } 226 | 227 | public function extism_version(): string 228 | { 229 | return $this->ffi->extism_version(); 230 | } 231 | 232 | public function extism_plugin_call(\FFI\CData $plugin, string $func_name, string $data, int $data_len): int 233 | { 234 | $dataPtr = $this->owned("uint8_t", $data); 235 | return $this->ffi->extism_plugin_call($plugin, $func_name, $dataPtr, $data_len); 236 | } 237 | 238 | /** 239 | * Get the current plugin's associated host context data 240 | */ 241 | public function extism_current_plugin_host_context(\FFI\CData $plugin): ?string 242 | { 243 | $ptr = $this->ffi->extism_current_plugin_host_context($plugin); 244 | if ($ptr === null || \FFI::isNull($ptr)) { 245 | return null; 246 | } 247 | 248 | return \FFI::string($this->ffi->cast("char *", $ptr)); 249 | } 250 | 251 | public function extism_error(\FFI\CData $plugin): ?string 252 | { 253 | return $this->ffi->extism_error($plugin); 254 | } 255 | 256 | private function extism_plugin_error(\FFI\CData $plugin): ?string 257 | { 258 | return $this->ffi->extism_plugin_error($plugin); 259 | } 260 | 261 | public function extism_plugin_output_data(\FFI\CData $plugin): string 262 | { 263 | $length = $this->ffi->extism_plugin_output_length($plugin); 264 | 265 | $ptr = $this->ffi->extism_plugin_output_data($plugin); 266 | 267 | return \FFI::string($ptr, $length); 268 | } 269 | 270 | public function extism_plugin_free(\FFI\CData $plugin): void 271 | { 272 | $this->ffi->extism_plugin_free($plugin); 273 | } 274 | 275 | public function extism_log_file(string $filename, string $log_level): void 276 | { 277 | $filenamePtr = $this->ownedZero($filename); 278 | $log_levelPtr = $this->ownedZero($log_level); 279 | 280 | $this->ffi->extism_log_file($filenamePtr, $log_levelPtr); 281 | } 282 | 283 | public function extism_function_new(string $name, array $inputTypes, array $outputTypes, callable $callback, $userData, $freeUserData): \FFI\CData 284 | { 285 | $inputs = $this->toCArray($inputTypes, "ExtismValType"); 286 | $outputs = $this->toCArray($outputTypes, "ExtismValType"); 287 | 288 | $handle = $this->ffi->extism_function_new($name, $inputs, count($inputTypes), $outputs, count($outputTypes), $callback, $userData, $freeUserData); 289 | 290 | return $handle; 291 | } 292 | 293 | public function extism_function_free(\FFI\CData $handle): void 294 | { 295 | $this->ffi->extism_function_free($handle); 296 | } 297 | 298 | public function extism_function_set_namespace(\FFI\CData $handle, string $name) 299 | { 300 | $namePtr = $this->ownedZero($name); 301 | $this->ffi->extism_function_set_namespace($handle, $namePtr); 302 | } 303 | 304 | private function toCArray(array $array, string $type): ?\FFI\CData 305 | { 306 | if (count($array) == 0) { 307 | return $this->ffi->new($type . "*"); 308 | } 309 | 310 | $cArray = $this->ffi->new($type . "[" . count($array) . "]"); 311 | for ($i = 0; $i < count($array); $i++) { 312 | $cArray[$i] = $array[$i]; 313 | } 314 | 315 | return $cArray; 316 | } 317 | 318 | private function owned(string $type, string $string): ?\FFI\CData 319 | { 320 | if (strlen($string) == 0) { 321 | return null; 322 | } 323 | 324 | $str = $this->ffi->new($type . "[" . \strlen($string) . "]", true); 325 | \FFI::memcpy($str, $string, \strlen($string)); 326 | return $str; 327 | } 328 | 329 | private function ownedZero(string $string): ?\FFI\CData 330 | { 331 | return $this->owned("char", "$string\0"); 332 | } 333 | 334 | private function startsWith($haystack, $needle) 335 | { 336 | return strcasecmp(substr($haystack, 0, strlen($needle)), $needle) === 0; 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/Internal/PluginHandle.php: -------------------------------------------------------------------------------- 1 | native = $handle; 16 | $this->lib = $lib; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Internal/extism.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define EXTISM_FUNCTION(N) extern void N(ExtismCurrentPlugin*, const ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, void*) 7 | #define EXTISM_GO_FUNCTION(N) extern void N(void*, ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, uintptr_t) 8 | 9 | /** The return code from extism_plugin_call used to signal a successful call with no errors */ 10 | #define EXTISM_SUCCESS 0 11 | 12 | /** An alias for I64 to signify an Extism pointer */ 13 | #define EXTISM_PTR ExtismValType_I64 14 | 15 | 16 | /** 17 | * An enumeration of all possible value types in WebAssembly. 18 | */ 19 | typedef enum { 20 | /** 21 | * Signed 32 bit integer. 22 | */ 23 | ExtismValType_I32, 24 | /** 25 | * Signed 64 bit integer. 26 | */ 27 | ExtismValType_I64, 28 | /** 29 | * Floating point 32 bit integer. 30 | */ 31 | ExtismValType_F32, 32 | /** 33 | * Floating point 64 bit integer. 34 | */ 35 | ExtismValType_F64, 36 | /** 37 | * A 128 bit number. 38 | */ 39 | ExtismValType_V128, 40 | /** 41 | * A reference to a Wasm function. 42 | */ 43 | ExtismValType_FuncRef, 44 | /** 45 | * A reference to opaque data in the Wasm instance. 46 | */ 47 | ExtismValType_ExternRef, 48 | } ExtismValType; 49 | 50 | /** 51 | * A `CancelHandle` can be used to cancel a running plugin from another thread 52 | */ 53 | typedef struct ExtismCancelHandle ExtismCancelHandle; 54 | 55 | typedef struct ExtismCompiledPlugin ExtismCompiledPlugin; 56 | 57 | /** 58 | * CurrentPlugin stores data that is available to the caller in PDK functions, this should 59 | * only be accessed from inside a host function 60 | */ 61 | typedef struct ExtismCurrentPlugin ExtismCurrentPlugin; 62 | 63 | typedef struct ExtismFunction ExtismFunction; 64 | 65 | /** 66 | * Plugin contains everything needed to execute a WASM function 67 | */ 68 | typedef struct ExtismPlugin ExtismPlugin; 69 | 70 | typedef uint64_t ExtismMemoryHandle; 71 | 72 | typedef uint64_t ExtismSize; 73 | 74 | /** 75 | * A union type for host function argument/return values 76 | */ 77 | typedef union { 78 | int32_t i32; 79 | int64_t i64; 80 | float f32; 81 | double f64; 82 | } ExtismValUnion; 83 | 84 | /** 85 | * `ExtismVal` holds the type and value of a function argument/return 86 | */ 87 | typedef struct { 88 | ExtismValType t; 89 | ExtismValUnion v; 90 | } ExtismVal; 91 | 92 | /** 93 | * Host function signature 94 | */ 95 | typedef void (*ExtismFunctionType)(ExtismCurrentPlugin *plugin, 96 | const ExtismVal *inputs, 97 | ExtismSize n_inputs, 98 | ExtismVal *outputs, 99 | ExtismSize n_outputs, 100 | void *data); 101 | 102 | /** 103 | * Log drain callback 104 | */ 105 | typedef void (*ExtismLogDrainFunctionType)(const char *data, ExtismSize size); 106 | 107 | 108 | 109 | 110 | /** 111 | * Get a plugin's ID, the returned bytes are a 16 byte buffer that represent a UUIDv4 112 | */ 113 | const uint8_t *extism_plugin_id(ExtismPlugin *plugin); 114 | 115 | /** 116 | * Get the current plugin's associated host context data. Returns null if call was made without 117 | * host context. 118 | */ 119 | void *extism_current_plugin_host_context(ExtismCurrentPlugin *plugin); 120 | 121 | /** 122 | * Returns a pointer to the memory of the currently running plugin 123 | * NOTE: this should only be called from host functions. 124 | */ 125 | uint8_t *extism_current_plugin_memory(ExtismCurrentPlugin *plugin); 126 | 127 | /** 128 | * Allocate a memory block in the currently running plugin 129 | * NOTE: this should only be called from host functions. 130 | */ 131 | ExtismMemoryHandle extism_current_plugin_memory_alloc(ExtismCurrentPlugin *plugin, ExtismSize n); 132 | 133 | /** 134 | * Get the length of an allocated block 135 | * NOTE: this should only be called from host functions. 136 | */ 137 | ExtismSize extism_current_plugin_memory_length(ExtismCurrentPlugin *plugin, ExtismMemoryHandle n); 138 | 139 | /** 140 | * Free an allocated memory block 141 | * NOTE: this should only be called from host functions. 142 | */ 143 | void extism_current_plugin_memory_free(ExtismCurrentPlugin *plugin, ExtismMemoryHandle ptr); 144 | 145 | /** 146 | * Create a new host function 147 | * 148 | * Arguments 149 | * - `name`: function name, this should be valid UTF-8 150 | * - `inputs`: argument types 151 | * - `n_inputs`: number of argument types 152 | * - `outputs`: return types 153 | * - `n_outputs`: number of return types 154 | * - `func`: the function to call 155 | * - `user_data`: a pointer that will be passed to the function when it's called 156 | * this value should live as long as the function exists 157 | * - `free_user_data`: a callback to release the `user_data` value when the resulting 158 | * `ExtismFunction` is freed. 159 | * 160 | * Returns a new `ExtismFunction` or `null` if the `name` argument is invalid. 161 | */ 162 | ExtismFunction *extism_function_new(const char *name, 163 | const ExtismValType *inputs, 164 | ExtismSize n_inputs, 165 | const ExtismValType *outputs, 166 | ExtismSize n_outputs, 167 | ExtismFunctionType func, 168 | void *user_data, 169 | void (*free_user_data)(void *_)); 170 | 171 | /** 172 | * Free `ExtismFunction` 173 | */ 174 | void extism_function_free(ExtismFunction *f); 175 | 176 | /** 177 | * Set the namespace of an `ExtismFunction` 178 | */ 179 | void extism_function_set_namespace(ExtismFunction *ptr, const char *namespace_); 180 | 181 | /** 182 | * Pre-compile an Extism plugin 183 | */ 184 | ExtismCompiledPlugin *extism_compiled_plugin_new(const uint8_t *wasm, 185 | ExtismSize wasm_size, 186 | const ExtismFunction **functions, 187 | ExtismSize n_functions, 188 | bool with_wasi, 189 | char **errmsg); 190 | 191 | /** 192 | * Free `ExtismCompiledPlugin` 193 | */ 194 | void extism_compiled_plugin_free(ExtismCompiledPlugin *plugin); 195 | 196 | /** 197 | * Create a new plugin with host functions, the functions passed to this function no longer need to be manually freed using 198 | * 199 | * `wasm`: is a WASM module (wat or wasm) or a JSON encoded manifest 200 | * `wasm_size`: the length of the `wasm` parameter 201 | * `functions`: an array of `ExtismFunction*` 202 | * `n_functions`: the number of functions provided 203 | * `with_wasi`: enables/disables WASI 204 | */ 205 | ExtismPlugin *extism_plugin_new(const uint8_t *wasm, 206 | ExtismSize wasm_size, 207 | const ExtismFunction **functions, 208 | ExtismSize n_functions, 209 | bool with_wasi, 210 | char **errmsg); 211 | 212 | /** 213 | * Create a new plugin from an `ExtismCompiledPlugin` 214 | */ 215 | ExtismPlugin *extism_plugin_new_from_compiled(const ExtismCompiledPlugin *compiled, char **errmsg); 216 | 217 | /** 218 | * Create a new plugin and set the number of instructions a plugin is allowed to execute 219 | */ 220 | ExtismPlugin *extism_plugin_new_with_fuel_limit(const uint8_t *wasm, 221 | ExtismSize wasm_size, 222 | const ExtismFunction **functions, 223 | ExtismSize n_functions, 224 | bool with_wasi, 225 | uint64_t fuel_limit, 226 | char **errmsg); 227 | 228 | /** 229 | * Enable HTTP response headers in plugins using `extism:host/env::http_request` 230 | */ 231 | void extism_plugin_allow_http_response_headers(ExtismPlugin *plugin); 232 | 233 | /** 234 | * Free the error returned by `extism_plugin_new`, errors returned from `extism_plugin_error` don't need to be freed 235 | */ 236 | void extism_plugin_new_error_free(char *err); 237 | 238 | /** 239 | * Free `ExtismPlugin` 240 | */ 241 | void extism_plugin_free(ExtismPlugin *plugin); 242 | 243 | /** 244 | * Get handle for plugin cancellation 245 | */ 246 | const ExtismCancelHandle *extism_plugin_cancel_handle(const ExtismPlugin *plugin); 247 | 248 | /** 249 | * Cancel a running plugin 250 | */ 251 | bool extism_plugin_cancel(const ExtismCancelHandle *handle); 252 | 253 | /** 254 | * Update plugin config values. 255 | */ 256 | bool extism_plugin_config(ExtismPlugin *plugin, const uint8_t *json, ExtismSize json_size); 257 | 258 | /** 259 | * Returns true if `func_name` exists 260 | */ 261 | bool extism_plugin_function_exists(ExtismPlugin *plugin, const char *func_name); 262 | 263 | /** 264 | * Call a function 265 | * 266 | * `func_name`: is the function to call 267 | * `data`: is the input data 268 | * `data_len`: is the length of `data` 269 | */ 270 | int32_t extism_plugin_call(ExtismPlugin *plugin, 271 | const char *func_name, 272 | const uint8_t *data, 273 | ExtismSize data_len); 274 | 275 | /** 276 | * Call a function with host context. 277 | * 278 | * `func_name`: is the function to call 279 | * `data`: is the input data 280 | * `data_len`: is the length of `data` 281 | * `host_context`: a pointer to context data that will be available in host functions 282 | */ 283 | int32_t extism_plugin_call_with_host_context(ExtismPlugin *plugin, 284 | const char *func_name, 285 | const uint8_t *data, 286 | ExtismSize data_len, 287 | void *host_context); 288 | 289 | /** 290 | * Get the error associated with a `Plugin` 291 | */ 292 | const char *extism_error(ExtismPlugin *plugin); 293 | 294 | /** 295 | * Get the error associated with a `Plugin` 296 | */ 297 | const char *extism_plugin_error(ExtismPlugin *plugin); 298 | 299 | /** 300 | * Get the length of a plugin's output data 301 | */ 302 | ExtismSize extism_plugin_output_length(ExtismPlugin *plugin); 303 | 304 | /** 305 | * Get a pointer to the output data 306 | */ 307 | const uint8_t *extism_plugin_output_data(ExtismPlugin *plugin); 308 | 309 | /** 310 | * Set log file and level. 311 | * The log level can be either one of: info, error, trace, debug, warn or a more 312 | * complex filter like `extism=trace,cranelift=debug` 313 | * The file will be created if it doesn't exist. 314 | */ 315 | bool extism_log_file(const char *filename, const char *log_level); 316 | 317 | /** 318 | * Enable a custom log handler, this will buffer logs until `extism_log_drain` is called 319 | * Log level should be one of: info, error, trace, debug, warn 320 | */ 321 | bool extism_log_custom(const char *log_level); 322 | 323 | /** 324 | * Calls the provided callback function for each buffered log line. 325 | * This is only needed when `extism_log_custom` is used. 326 | */ 327 | void extism_log_drain(ExtismLogDrainFunctionType handler); 328 | 329 | /** 330 | * Reset the Extism runtime, this will invalidate all allocated memory 331 | */ 332 | bool extism_plugin_reset(ExtismPlugin *plugin); 333 | 334 | /** 335 | * Get the Extism version string 336 | */ 337 | const char *extism_version(void); 338 | -------------------------------------------------------------------------------- /src/Manifest.php: -------------------------------------------------------------------------------- 1 | wasm = $wasm; 23 | $this->allowed_hosts = []; 24 | $this->allowed_paths = new \stdClass(); 25 | $this->config = new \stdClass(); 26 | $this->memory = new MemoryOptions(); 27 | } 28 | 29 | /** 30 | * List of Wasm sources. See `UrlWasmSource`, `PathWasmSource` and `ByteArrayWasmSource`. 31 | * 32 | * @var \Extism\Manifest\WasmSource[] 33 | */ 34 | public $wasm = []; 35 | 36 | /** 37 | * Configures memory for the Wasm runtime. 38 | * Memory is described in units of pages (64KB) and represents contiguous chunks of addressable memory. 39 | * 40 | * @var MemoryOptions|null 41 | */ 42 | public $memory; 43 | 44 | /** 45 | * List of host names the plugins can access. Examples: `*.example.com`, `www.something.com`, `www.*.com` 46 | * 47 | * @var array|null 48 | */ 49 | public $allowed_hosts; 50 | 51 | /** 52 | * Map of directories that can be accessed by the plugins. Examples: `src=dest`, `\var\apps\123=\home` 53 | * 54 | * @var \object|null 55 | */ 56 | public $allowed_paths; 57 | 58 | /** 59 | * Configurations available to the plugins. Examples: `key=value`, `secret=1234` 60 | * 61 | * @var \object|null 62 | */ 63 | public $config; 64 | 65 | /** 66 | * Plugin call timeout in milliseconds. 67 | * 68 | * @var int|null 69 | */ 70 | public $timeout_ms; 71 | } 72 | -------------------------------------------------------------------------------- /src/Manifest/ByteArrayWasmSource.php: -------------------------------------------------------------------------------- 1 | data = base64_encode($data); 20 | $this->name = $name; 21 | $this->hash = $hash; 22 | } 23 | 24 | /** 25 | * The byte array representing the Wasm code encoded in Base64. 26 | * 27 | * @var string 28 | */ 29 | public $data; 30 | } 31 | -------------------------------------------------------------------------------- /src/Manifest/HttpMethod.php: -------------------------------------------------------------------------------- 1 | path = realpath($path); 20 | 21 | if (!$this->path) { 22 | throw new \Extism\PluginLoadException("Path not found: '" . $path . "'"); 23 | } 24 | 25 | $this->name = $name ?? pathinfo($path, PATHINFO_FILENAME); 26 | $this->hash = $hash; 27 | } 28 | 29 | /** 30 | * Path to wasm plugin. 31 | * 32 | * @var string 33 | */ 34 | public $path; 35 | } 36 | -------------------------------------------------------------------------------- /src/Manifest/UrlWasmSource.php: -------------------------------------------------------------------------------- 1 | url = $url; 20 | $this->name = $name; 21 | $this->hash = $hash; 22 | $this->headers = new \stdClass(); 23 | } 24 | 25 | /** 26 | * Uri to wasm plugin. 27 | * 28 | * @var string 29 | */ 30 | public $url; 31 | 32 | /** 33 | * HTTP headers. Examples: `header1=value`, `Authorization=Basic 123` 34 | * 35 | * @var object|null 36 | */ 37 | public $headers; 38 | 39 | /** 40 | * HTTP Method. Examples: `GET`, `POST`, `DELETE`. See `HttpMethod` for a list of options. 41 | * 42 | * @var string|null 43 | */ 44 | public $method; 45 | } 46 | -------------------------------------------------------------------------------- /src/Manifest/WasmSource.php: -------------------------------------------------------------------------------- 1 | handle = $manifest; 41 | return; 42 | } 43 | 44 | global $lib; 45 | 46 | if ($lib === null) { 47 | $lib = new \Extism\Internal\LibExtism(); 48 | } 49 | 50 | // Handle backwards compatibility 51 | $options = $withWasiOrOptions; 52 | if (is_bool($withWasiOrOptions)) { 53 | $options = new PluginOptions($withWasiOrOptions); 54 | } 55 | 56 | $data = json_encode($manifest); 57 | if (!$data) { 58 | throw new \Extism\PluginLoadException("Failed to encode manifest"); 59 | } 60 | 61 | $errPtr = $lib->ffi->new($lib->ffi->type("char*")); 62 | 63 | if ($options->getFuelLimit() !== null) { 64 | $handle = $lib->extism_plugin_new_with_fuel_limit( 65 | $data, 66 | strlen($data), 67 | $functions, 68 | count($functions), 69 | $options->getWithWasi(), 70 | $options->getFuelLimit(), 71 | \FFI::addr($errPtr) 72 | ); 73 | } else { 74 | $handle = $lib->extism_plugin_new( 75 | $data, 76 | strlen($data), 77 | $functions, 78 | count($functions), 79 | $options->getWithWasi(), 80 | \FFI::addr($errPtr) 81 | ); 82 | } 83 | 84 | if (\FFI::isNull($errPtr) === false) { 85 | $error = \FFI::string($errPtr); 86 | $lib->extism_plugin_new_error_free($errPtr); 87 | throw new \Extism\PluginLoadException("Extism: unable to load plugin: " . $error); 88 | } 89 | 90 | $this->handle = new PluginHandle($lib, $handle); 91 | } 92 | 93 | /** 94 | * Enable HTTP response headers in plugins using `extism:host/env::http_request` 95 | */ 96 | public function allowHttpResponseHeaders(): void 97 | { 98 | $this->handle->lib->extism_plugin_allow_http_response_headers($this->handle->native); 99 | } 100 | 101 | /** 102 | * Reset the Extism runtime, this will invalidate all allocated memory 103 | * 104 | * @return bool 105 | */ 106 | public function reset(): bool 107 | { 108 | return $this->handle->lib->extism_plugin_reset($this->handle->native); 109 | } 110 | 111 | /** 112 | * Update plugin config values. 113 | * 114 | * @param array $config New configuration values 115 | * @return bool 116 | */ 117 | public function updateConfig(array $config): bool 118 | { 119 | $json = json_encode($config); 120 | if (!$json) { 121 | return false; 122 | } 123 | 124 | return $this->handle->lib->extism_plugin_config($this->handle->native, $json, strlen($json)); 125 | } 126 | 127 | /** 128 | * Get the plugin's ID. 129 | * 130 | * @return string UUID string 131 | */ 132 | public function getId(): string 133 | { 134 | $bytes = $this->handle->lib->extism_plugin_id($this->handle->native); 135 | return bin2hex(\FFI::string($bytes, 16)); 136 | } 137 | 138 | /** 139 | * Check if the plugin contains a function. 140 | * 141 | * @param string $name 142 | * 143 | * @return bool `true` if the function exists, `false` otherwise 144 | */ 145 | public function functionExists(string $name): bool 146 | { 147 | return $this->handle->lib->extism_plugin_function_exists($this->handle->native, $name); 148 | } 149 | 150 | /** 151 | * Call a function in the Plugin and return the result. 152 | * 153 | * @param string $name Name of function. 154 | * @param string $input Input buffer 155 | * 156 | * @return string Output buffer 157 | */ 158 | public function call(string $name, string $input = ""): string 159 | { 160 | $rc = $this->handle->lib->extism_plugin_call($this->handle->native, $name, $input, strlen($input)); 161 | 162 | $msg = "code = " . $rc; 163 | $err = $this->handle->lib->extism_error($this->handle->native); 164 | if ($err) { 165 | $msg = $msg . ", error = " . $err; 166 | throw new \Extism\FunctionCallException("Extism: call to '" . $name . "' failed with " . $msg, $err, $name); 167 | } 168 | 169 | return $this->handle->lib->extism_plugin_output_data($this->handle->native); 170 | } 171 | 172 | /** 173 | * Call a function with host context. 174 | * 175 | * @param string $name Name of function 176 | * @param string $input Input buffer 177 | * @param mixed $context Host context data 178 | * @return string Output buffer 179 | */ 180 | public function callWithContext(string $name, string $input = "", $context = null): string 181 | { 182 | $rc = $this->handle->lib->extism_plugin_call_with_host_context($this->handle->native, $name, $input, strlen($input), $context); 183 | 184 | $msg = "code = " . $rc; 185 | $err = $this->handle->lib->extism_error($this->handle->native); 186 | if ($err) { 187 | $msg = $msg . ", error = " . $err; 188 | throw new \Extism\FunctionCallException("Extism: call to '" . $name . "' failed with " . $msg, $err, $name); 189 | } 190 | 191 | return $this->handle->lib->extism_plugin_output_data($this->handle->native); 192 | } 193 | 194 | /** 195 | * Configures file logging. This applies to all Plugin instances. 196 | * 197 | * @param string $filename Path of log file. The file will be created if it doesn't exist. 198 | * @param string $level Minimum log level. Valid values are: `trace`, `debug`, `info`, `warn`, `error` 199 | * or more complex filter like `extism=trace,cranelift=debug`. 200 | */ 201 | public static function setLogFile(string $filename, string $level): void 202 | { 203 | $lib = new \Extism\Internal\LibExtism(); 204 | $lib->extism_log_file($filename, $level); 205 | } 206 | 207 | /** 208 | * Get the Extism version string 209 | * @return string 210 | */ 211 | public static function version(): string 212 | { 213 | $lib = new \Extism\Internal\LibExtism(); 214 | return $lib->extism_version(); 215 | } 216 | 217 | /** 218 | * Destructor 219 | */ 220 | public function __destruct() 221 | { 222 | $this->handle->lib->extism_plugin_free($this->handle->native); 223 | } 224 | } -------------------------------------------------------------------------------- /src/PluginLoadException.php: -------------------------------------------------------------------------------- 1 | withWasi = $withWasi; 35 | $this->fuelLimit = $fuelLimit; 36 | } 37 | 38 | /** 39 | * @return bool 40 | */ 41 | public function getWithWasi(): bool 42 | { 43 | return $this->withWasi; 44 | } 45 | 46 | /** 47 | * @param bool $withWasi 48 | * @return self 49 | */ 50 | public function setWithWasi(bool $withWasi): self 51 | { 52 | $this->withWasi = $withWasi; 53 | return $this; 54 | } 55 | 56 | /** 57 | * @return int|null 58 | */ 59 | public function getFuelLimit(): ?int 60 | { 61 | return $this->fuelLimit; 62 | } 63 | 64 | /** 65 | * @param int|null $fuelLimit 66 | * @return self 67 | */ 68 | public function setFuelLimit(?int $fuelLimit): self 69 | { 70 | $this->fuelLimit = $fuelLimit; 71 | return $this; 72 | } 73 | } -------------------------------------------------------------------------------- /tests/CompiledPluginTest.php: -------------------------------------------------------------------------------- 1 | instantiate(); 22 | 23 | $response = $plugin->call("count_vowels", "Hello World!"); 24 | $actual = json_decode($response); 25 | 26 | $this->assertEquals(3, $actual->count); 27 | } 28 | 29 | public function testCompiledHostFunctions(): void 30 | { 31 | $kvstore = []; 32 | 33 | $kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (CurrentPlugin $p, string $key) use (&$kvstore) { 34 | return $kvstore[$key] ?? "\0\0\0\0"; 35 | }); 36 | 37 | $kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (string $key, string $value) use (&$kvstore) { 38 | $kvstore[$key] = $value; 39 | }); 40 | 41 | $compiledPlugin = self::compilePlugin("count_vowels_kvstore.wasm", [$kvRead, $kvWrite]); 42 | 43 | $plugin = $compiledPlugin->instantiate(); 44 | 45 | $response = $plugin->call("count_vowels", "Hello World!"); 46 | $this->assertEquals('{"count":3,"total":3,"vowels":"aeiouAEIOU"}', $response); 47 | 48 | $response = $plugin->call("count_vowels", "Hello World!"); 49 | $this->assertEquals('{"count":3,"total":6,"vowels":"aeiouAEIOU"}', $response); 50 | } 51 | 52 | public static function compilePlugin(string $name, array $functions, ?callable $config = null) 53 | { 54 | $path = __DIR__ . '/../wasm/' . $name; 55 | $manifest = new Manifest(new PathWasmSource($path, 'main')); 56 | 57 | if ($config !== null) { 58 | $config($manifest); 59 | } 60 | 61 | return new CompiledPlugin($manifest, $functions, true); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Helpers.php: -------------------------------------------------------------------------------- 1 | call("run_test", ""); 25 | $this->assertEquals("Hello, world!", $actual); 26 | } 27 | 28 | public function testCanLoadPluginFromPath(): void 29 | { 30 | $wasm = new PathWasmSource(__DIR__ . "/../wasm/hello.wasm"); 31 | $manifest = new Manifest($wasm); 32 | 33 | $plugin = new Plugin($manifest, true, []); 34 | 35 | $actual = $plugin->call("run_test", ""); 36 | $this->assertEquals("Hello, world!", $actual); 37 | } 38 | 39 | public function testCanLoadPluginFromUrl(): void 40 | { 41 | $wasm = new UrlWasmSource("https://github.com/extism/plugins/releases/download/v0.5.0/count_vowels.wasm"); 42 | $manifest = new Manifest($wasm); 43 | 44 | $plugin = new Plugin($manifest, true, []); 45 | 46 | $response = $plugin->call("count_vowels", "Hello World!"); 47 | $actual = json_decode($response); 48 | 49 | $this->assertEquals(3, $actual->count); 50 | } 51 | 52 | public function testCanSetConfig(): void 53 | { 54 | $plugin = self::loadPlugin("config.wasm", function ($manifest) { 55 | $manifest->config->thing = "hello"; 56 | }); 57 | 58 | $actual = $plugin->call("run_test", ""); 59 | $this->assertEquals("{\"config\": \"hello\"}", $actual); 60 | } 61 | 62 | public function testCanLeaveConfigUnset(): void 63 | { 64 | $plugin = self::loadPlugin("config.wasm"); 65 | 66 | $actual = $plugin->call("run_test", ""); 67 | $this->assertEquals("{\"config\": \"\"}", $actual); 68 | } 69 | 70 | public function testCanMakeHttpCallsWhenAllowed(): void 71 | { 72 | $plugin = self::loadPlugin("http.wasm", function ($manifest) { 73 | $manifest->allowed_hosts = ["jsonplaceholder.*.com"]; 74 | }); 75 | 76 | $plugin->allowHttpResponseHeaders(); 77 | 78 | $req = json_encode(["url" => "https://jsonplaceholder.typicode.com/todos/1"]); 79 | 80 | $response = $plugin->call("http_get", $req); 81 | $actual = json_decode($response); 82 | $this->assertEquals(1, $actual->userId); 83 | } 84 | 85 | public function testCantMakeHttpCallsWhenDenied(): void 86 | { 87 | $this->expectException(\Exception::class); 88 | 89 | $plugin = self::loadPlugin("http.wasm", function ($manifest) { 90 | $manifest->allowed_hosts = []; 91 | }); 92 | 93 | $req = json_encode(["url" => "https://jsonplaceholder.typicode.com/todos/1"]); 94 | 95 | $plugin->call("http_get", $req); 96 | } 97 | 98 | public static function loadPlugin(string $name, ?callable $config = null) 99 | { 100 | $path = __DIR__ . '/../wasm/' . $name; 101 | $manifest = new Manifest(new PathWasmSource($path, 'main')); 102 | 103 | if ($config !== null) { 104 | $config($manifest); 105 | } 106 | 107 | return new Plugin($manifest, true, []); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/PluginTest.php: -------------------------------------------------------------------------------- 1 | call("run_test", ""); 23 | $this->assertEquals("", $response); 24 | } 25 | 26 | public function testFail(): void 27 | { 28 | $this->expectException(\Exception::class); 29 | 30 | $plugin = self::loadPlugin("fail.wasm", []); 31 | 32 | $plugin->call("run_test", ""); 33 | } 34 | 35 | public function testExit(): void 36 | { 37 | $plugin = self::loadPlugin("exit.wasm", [], function ($manifest) { 38 | $manifest->config->code = "2"; 39 | }); 40 | 41 | try { 42 | $plugin->call("_start", ""); 43 | } catch (\Exception $e) { 44 | $this->assertStringContainsString("2", $e->getMessage()); 45 | } 46 | } 47 | 48 | public function testTimeout(): void 49 | { 50 | $plugin = self::loadPlugin("sleep.wasm", [], function ($manifest) { 51 | $manifest->timeout_ms = 50; 52 | $manifest->config->duration = "3"; // sleep for 3 seconds 53 | }); 54 | 55 | try { 56 | $plugin->call("run_test", ""); 57 | } catch (\Exception $e) { 58 | $this->assertStringContainsString("timeout", $e->getMessage()); 59 | } 60 | } 61 | 62 | public function testFileSystem(): void 63 | { 64 | $plugin = self::loadPlugin("fs.wasm", [], function ($manifest) { 65 | $manifest->allowed_paths = ["tests/data" => "/mnt"]; 66 | }); 67 | 68 | $response = $plugin->call("_start", ""); 69 | $this->assertEquals("hello world!", $response); 70 | } 71 | 72 | public function testFunctionExists(): void 73 | { 74 | $plugin = self::loadPlugin("alloc.wasm", []); 75 | 76 | $this->assertTrue($plugin->functionExists("run_test")); 77 | $this->assertFalse($plugin->functionExists("i_dont_exist")); 78 | } 79 | 80 | public function testHostFunctions(): void 81 | { 82 | $kvstore = []; 83 | 84 | $kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (CurrentPlugin $p, string $key) use (&$kvstore) { 85 | return $kvstore[$key] ?? "\0\0\0\0"; 86 | }); 87 | 88 | $kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (string $key, string $value) use (&$kvstore) { 89 | $kvstore[$key] = $value; 90 | }); 91 | 92 | $plugin = self::loadPlugin("count_vowels_kvstore.wasm", [$kvRead, $kvWrite]); 93 | 94 | $response = $plugin->call("count_vowels", "Hello World!"); 95 | $this->assertEquals('{"count":3,"total":3,"vowels":"aeiouAEIOU"}', $response); 96 | 97 | $response = $plugin->call("count_vowels", "Hello World!"); 98 | $this->assertEquals('{"count":3,"total":6,"vowels":"aeiouAEIOU"}', $response); 99 | } 100 | 101 | public function testCallWithHostContext(): void 102 | { 103 | $multiUserKvStore = [[]]; 104 | 105 | $kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (CurrentPlugin $p, string $key) use (&$multiUserKvStore) { 106 | $ctx = $p->getCallHostContext(); // get a copy of the host context data 107 | $userId = $ctx["userId"]; 108 | $kvStore = $multiUserKvStore[$userId] ?? []; 109 | 110 | return $kvStore[$key] ?? "\0\0\0\0"; 111 | }); 112 | 113 | $kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (CurrentPlugin $p, string $key, string $value) use (&$multiUserKvStore) { 114 | $ctx = $p->getCallHostContext(); // get a copy of the host context data 115 | $userId = $ctx["userId"]; 116 | $kvStore = $multiUserKvStore[$userId] ?? []; 117 | 118 | $kvStore[$key] = $value; 119 | $multiUserKvStore[$userId] = $kvStore; 120 | }); 121 | 122 | $plugin = self::loadPlugin("count_vowels_kvstore.wasm", [$kvRead, $kvWrite]); 123 | 124 | $ctx = ["userId" => 1]; 125 | 126 | $response = $plugin->callWithContext("count_vowels", "Hello World!", $ctx); 127 | $this->assertEquals('{"count":3,"total":3,"vowels":"aeiouAEIOU"}', $response); 128 | 129 | $response = $plugin->callWithContext("count_vowels", "Hello World!", $ctx); 130 | $this->assertEquals('{"count":3,"total":6,"vowels":"aeiouAEIOU"}', $response); 131 | } 132 | 133 | public function testHostFunctionManual(): void 134 | { 135 | $kvstore = []; 136 | 137 | $kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (CurrentPlugin $p, int $keyPtr) use (&$kvstore) { 138 | $key = $p->read_block($keyPtr); 139 | $value = $kvstore[$key] ?? "\0\0\0\0"; 140 | return $p->write_block($value); 141 | }); 142 | 143 | $kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (CurrentPlugin $p, int $keyPtr, int $valuePtr) use (&$kvstore) { 144 | $key = $p->read_block($keyPtr); 145 | $value = $p->read_block($valuePtr); 146 | 147 | $kvstore[$key] = $value; 148 | }); 149 | 150 | $plugin = self::loadPlugin("count_vowels_kvstore.wasm", [$kvRead, $kvWrite]); 151 | 152 | $response = $plugin->call("count_vowels", "Hello World!"); 153 | $this->assertEquals('{"count":3,"total":3,"vowels":"aeiouAEIOU"}', $response); 154 | 155 | $response = $plugin->call("count_vowels", "Hello World!"); 156 | $this->assertEquals('{"count":3,"total":6,"vowels":"aeiouAEIOU"}', $response); 157 | } 158 | 159 | public function testHostFunctionNamespace(): void 160 | { 161 | $this->expectExceptionMessage("Extism: unable to load plugin: Unable to compile Extism plugin: unknown import: `extism:host/user::kv_read` has not been defined"); 162 | 163 | $kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (string $key) { 164 | // 165 | }); 166 | $kvRead->set_namespace("custom"); 167 | 168 | $kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (string $key, string $value) { 169 | // 170 | }); 171 | $kvWrite->set_namespace("custom"); 172 | 173 | $plugin = self::loadPlugin("count_vowels_kvstore.wasm", [$kvRead, $kvWrite]); 174 | } 175 | 176 | public function testFuelLimit(): void 177 | { 178 | $plugin = self::loadPlugin("sleep.wasm", [], null, new PluginOptions(true, 10)); 179 | 180 | try { 181 | $plugin->call("run_test", ""); 182 | } catch (\Exception $e) { 183 | $this->assertStringContainsString("fuel", $e->getMessage()); 184 | } 185 | } 186 | 187 | public static function loadPlugin(string $name, array $functions, ?callable $config = null, $withWasi = true) 188 | { 189 | $path = __DIR__ . '/../wasm/' . $name; 190 | $manifest = new Manifest(new PathWasmSource($path, 'main')); 191 | 192 | if ($config !== null) { 193 | $config($manifest); 194 | } 195 | 196 | return new Plugin($manifest, $withWasi, $functions); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /tests/data/test.txt: -------------------------------------------------------------------------------- 1 | hello world! -------------------------------------------------------------------------------- /wasm/alloc.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/php-sdk/43432a7d0e06b4764c9b5558a7b44628d4aace66/wasm/alloc.wasm -------------------------------------------------------------------------------- /wasm/config.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/php-sdk/43432a7d0e06b4764c9b5558a7b44628d4aace66/wasm/config.wasm -------------------------------------------------------------------------------- /wasm/count_vowels.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/php-sdk/43432a7d0e06b4764c9b5558a7b44628d4aace66/wasm/count_vowels.wasm -------------------------------------------------------------------------------- /wasm/count_vowels_kvstore.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/php-sdk/43432a7d0e06b4764c9b5558a7b44628d4aace66/wasm/count_vowels_kvstore.wasm -------------------------------------------------------------------------------- /wasm/exit.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/php-sdk/43432a7d0e06b4764c9b5558a7b44628d4aace66/wasm/exit.wasm -------------------------------------------------------------------------------- /wasm/fail.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/php-sdk/43432a7d0e06b4764c9b5558a7b44628d4aace66/wasm/fail.wasm -------------------------------------------------------------------------------- /wasm/fs.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/php-sdk/43432a7d0e06b4764c9b5558a7b44628d4aace66/wasm/fs.wasm -------------------------------------------------------------------------------- /wasm/hello.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/php-sdk/43432a7d0e06b4764c9b5558a7b44628d4aace66/wasm/hello.wasm -------------------------------------------------------------------------------- /wasm/http.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/php-sdk/43432a7d0e06b4764c9b5558a7b44628d4aace66/wasm/http.wasm -------------------------------------------------------------------------------- /wasm/sleep.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/php-sdk/43432a7d0e06b4764c9b5558a7b44628d4aace66/wasm/sleep.wasm --------------------------------------------------------------------------------