├── .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
--------------------------------------------------------------------------------