diff --git a/.gitignore b/.gitignore index ae9cdf7..5f0510e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ src/ExtismLib.php example/php_errors.log php_errors.log +.phpunit.cache +.phpunit.result.cache +.php-cs-fixer.cache +.vscode \ No newline at end of file diff --git a/Makefile b/Makefile index 49234c2..cd6f17b 100644 --- a/Makefile +++ b/Makefile @@ -5,3 +5,9 @@ prepare: test: prepare php vendor/bin/phpunit ./tests + +cscheck: + vendor/bin/phpcs . + +csfix: + vendor/bin/php-cs-fixer fix . \ No newline at end of file diff --git a/README.md b/README.md index ef757fc..9893399 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ First you should add a using statement for Extism: ```php use Extism\Plugin; use Extism\Manifest; -use Extism\UrlWasmSource; +use Extism\Manifest\UrlWasmSource; ``` ## Creating A Plug-in diff --git a/composer.json b/composer.json index 1e6ccd2..6f9c2f5 100644 --- a/composer.json +++ b/composer.json @@ -33,14 +33,14 @@ "psr-4": { "Extism\\": "src/" }, - "files": [ - "src/Manifest.php", - "src/Plugin.php", - "src/CurrentPlugin.php" - ] + "psr-0": { + "LibExtism": "src" + } }, "autoload-dev": { - "psr-4": {} + "psr-4": { + "Extism\\Tests\\": "tests/" + } }, "config": { "sort-packages": true @@ -49,6 +49,8 @@ "scripts": {}, "scripts-descriptions": {}, "require-dev": { - "phpunit/phpunit": "^9" + "friendsofphp/php-cs-fixer": "^3.59", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.10" } } diff --git a/example/index.php b/example/index.php index 243664c..d637982 100644 --- a/example/index.php +++ b/example/index.php @@ -1,9 +1,10 @@ call("count_vowels", "Yellow, World!"); -var_dump($output); \ No newline at end of file +var_dump($output); diff --git a/example/memory_test.php b/example/memory_test.php index 6164dc4..144d940 100644 --- a/example/memory_test.php +++ b/example/memory_test.php @@ -1,19 +1,18 @@ + + + + + + */vendor/* + \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..c76e12c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,13 @@ + + + + + tests/ + + + \ No newline at end of file diff --git a/src/CurrentPlugin.php b/src/CurrentPlugin.php index 138fb08..e931a55 100644 --- a/src/CurrentPlugin.php +++ b/src/CurrentPlugin.php @@ -1,8 +1,8 @@ handle = $handle; $this->lib = $lib; @@ -26,10 +26,10 @@ function __construct($lib, \FFI\CData $handle) /** * Reads a string from the plugin's memory at the given offset. - * + * * @param int $offset Offset of the block to read. */ - function read_block(int $offset) : string + public function read_block(int $offset): string { $ptr = $this->lib->extism_current_plugin_memory($this->handle); $ptr = $this->lib->ffi->cast("char *", $ptr); @@ -42,20 +42,20 @@ function read_block(int $offset) : string /** * Allocates a block of memory in the plugin's memory and returns the offset. - * + * * @param int $size Size of the block to allocate in bytes. */ - function allocate_block(int $size) : int + private function allocate_block(int $size): int { return $this->lib->extism_current_plugin_memory_alloc($this->handle, $size); } /** * Writes a string to the plugin's memory, returning the offset of the block. - * + * * @param string $data Buffer to write to the plugin's memory. */ - function write_block(string $data) : int + public function write_block(string $data): int { $offset = $this->allocate_block(strlen($data)); $this->fill_block($offset, $data); @@ -64,11 +64,11 @@ function write_block(string $data) : int /** * Fills a block of memory in the plugin's memory. - * + * * @param int $offset Offset of the block to fill. * @param string $data Buffer to fill the block with. */ - function fill_block(int $offset, string $data) : void + private function fill_block(int $offset, string $data): void { $ptr = $this->lib->extism_current_plugin_memory($this->handle); $ptr = $this->lib->ffi->cast("char *", $ptr); @@ -79,11 +79,11 @@ function fill_block(int $offset, string $data) : void /** * Frees a block of memory in the plugin's memory. - * + * * @param int $offset Offset of the block to free. */ - function free_block(int $offset) : void + private function free_block(int $offset): void { $this->lib->extism_current_plugin_memory_free($this->handle, $offset); } -} \ No newline at end of file +} diff --git a/src/ExtismValType.php b/src/ExtismValType.php new file mode 100644 index 0000000..0a78b0f --- /dev/null +++ b/src/ExtismValType.php @@ -0,0 +1,15 @@ +getParameters(); @@ -54,7 +52,7 @@ function __construct(string $name, array $inputTypes, array $outputTypes, callab global $lib; if ($lib == null) { - $lib = new \LibExtism(); + $lib = new \Extism\Internal\LibExtism(); } $this->lib = $lib; @@ -80,9 +78,9 @@ function __construct(string $name, array $inputTypes, array $outputTypes, callab $r = $callback(...$params); - if ($r == NULL) { + if ($r == null) { $r = 0; - } else if (gettype($r) == "string") { + } elseif (gettype($r) == "string") { $r = $currentPlugin->write_block($r); } @@ -106,7 +104,6 @@ function __construct(string $name, array $inputTypes, array $outputTypes, callab throw new \Exception("Unsupported type for output: " . $output->t); } } - // Throwing an exception in FFI callback is not supported and // causes a fatal error without a stack trace. // So we catch it and print the exception manually @@ -123,12 +120,12 @@ function __construct(string $name, array $inputTypes, array $outputTypes, callab $this->set_namespace("extism:host/user"); } - function __destruct() + public function __destruct() { $this->lib->extism_function_free($this->handle); } - function set_namespace(string $namespace) + public function set_namespace(string $namespace) { $this->lib->extism_function_set_namespace($this->handle, $namespace); } @@ -156,12 +153,12 @@ private static function get_type_name(\ReflectionParameter $param) } private static function get_parameters( - CurrentPlugin $currentPlugin, + CurrentPlugin $currentPlugin, \FFI\CData $inputs, int $n_inputs, - array $arguments, - int $offset) : array - { + array $arguments, + int $offset + ): array { $params = []; if ($offset == 1) { @@ -219,7 +216,7 @@ private static function validate_arguments(array $arguments, array $inputTypes) if ($argType == null) { continue; - } else if ($argType == "string") { + } elseif ($argType == "string") { // string is represented as a pointer to a block of memory $argType = "int"; } diff --git a/src/LibExtism.php b/src/Internal/LibExtism.php similarity index 62% rename from src/LibExtism.php rename to src/Internal/LibExtism.php index 3937218..c145469 100644 --- a/src/LibExtism.php +++ b/src/Internal/LibExtism.php @@ -1,22 +1,25 @@ ffi = LibExtism::findSo($name); } - function findSo(string $name): \FFI { + private function findSo(string $name): \FFI + { $platform = php_uname("s"); - $directories = []; - if (LibExtism::startsWith($platform, "windows")) { + if ($this->startsWith($platform, "windows")) { $path = getenv('PATH'); $directories = explode(PATH_SEPARATOR, $path); - } else { $directories = ['/usr/local/lib', '/usr/lib']; } @@ -38,94 +41,102 @@ function findSo(string $name): \FFI { throw new \RuntimeException('Failed to find shared object. Searched locations: ' . implode(', ', $searchedPaths)); } - function soname() { + private function soname() + { $platform = php_uname("s"); switch ($platform) { case "Darwin": return "libextism.dylib"; - case "Linux": + case "Linux": return "libextism.so"; case "Windows NT": return "extism.dll"; default: - throw new \Exception("Extism: unsupported platform ".$platform); + throw new \Exception("Extism: unsupported platform " . $platform); } } - function extism_current_plugin_memory(FFI\CData $plugin): \FFI\CData + public function extism_current_plugin_memory(\FFI\CData $plugin): \FFI\CData { return $this->ffi->extism_current_plugin_memory($plugin); } - function extism_current_plugin_memory_free(FFI\CData $plugin, FFI\CData $ptr): void + public function extism_current_plugin_memory_free(\FFI\CData $plugin, \FFI\CData $ptr): void { $this->ffi->extism_current_plugin_memory_free($plugin, $ptr); } - function extism_current_plugin_memory_alloc(FFI\CData $plugin, int $size): int + public function extism_current_plugin_memory_alloc(\FFI\CData $plugin, int $size): int { return $this->ffi->extism_current_plugin_memory_alloc($plugin, $size); } - function extism_current_plugin_memory_length(FFI\CData $plugin, int $offset): int + public function extism_current_plugin_memory_length(\FFI\CData $plugin, int $offset): int { return $this->ffi->extism_current_plugin_memory_length($plugin, $offset); } - function extism_plugin_new(string $wasm, int $wasm_size, FFI\CData $functions, int $n_functions, bool $with_wasi, ?FFI\CData $errmsg): ?FFI\CData + public function extism_plugin_new(string $wasm, int $wasm_size, array $functions, int $n_functions, bool $with_wasi, ?\FFI\CData $errmsg): ?\FFI\CData { + $functionHandles = array_map(function ($function) { + return $function->handle; + }, $functions); + + $functionHandles = $this->toCArray($functionHandles, "ExtismFunction*"); + $ptr = $this->owned("uint8_t", $wasm); $wasi = $with_wasi ? 1 : 0; - $pluginPtr = $this->ffi->extism_plugin_new($ptr, $wasm_size, $functions, $n_functions, $wasi, $errmsg); + $pluginPtr = $this->ffi->extism_plugin_new($ptr, $wasm_size, $functionHandles, $n_functions, $wasi, $errmsg); return $this->ffi->cast("ExtismPlugin*", $pluginPtr); } - function extism_plugin_new_error_free(FFI\CData $ptr): void { + public function extism_plugin_new_error_free(\FFI\CData $ptr): void + { $this->ffi->extism_plugin_new_error_free($ptr); } - function extism_plugin_function_exists(FFI\CData $plugin, string $func_name): bool + public function extism_plugin_function_exists(\FFI\CData $plugin, string $func_name): bool { return $this->ffi->extism_plugin_function_exists($plugin, $func_name); } - function extism_version(): string + public function extism_version(): string { return $this->ffi->extism_version(); } - function extism_plugin_call(FFI\CData $plugin, string $func_name, string $data, int $data_len): int + public function extism_plugin_call(\FFI\CData $plugin, string $func_name, string $data, int $data_len): int { $dataPtr = $this->owned("uint8_t", $data); return $this->ffi->extism_plugin_call($plugin, $func_name, $dataPtr, $data_len); } - function extism_error(FFI\CData $plugin): ?string + public function extism_error(\FFI\CData $plugin): ?string { return $this->ffi->extism_error($plugin); } - function extism_plugin_error(FFI\CData $plugin): ?string + private function extism_plugin_error(\FFI\CData $plugin): ?string { return $this->ffi->extism_plugin_error($plugin); } - function extism_plugin_output_data(FFI\CData $plugin): string + public function extism_plugin_output_data(\FFI\CData $plugin): string { $length = $this->ffi->extism_plugin_output_length($plugin); $ptr = $this->ffi->extism_plugin_output_data($plugin); - return FFI::string($ptr, $length); + return \FFI::string($ptr, $length); } - function extism_plugin_free(\FFI\CData $plugin): void + public function extism_plugin_free(\FFI\CData $plugin): void { $this->ffi->extism_plugin_free($plugin); } - function extism_log_file(string $filename, string $log_level): void + public function extism_log_file(string $filename, string $log_level): void { $filenamePtr = $this->ownedZero($filename); $log_levelPtr = $this->ownedZero($log_level); @@ -133,7 +144,7 @@ function extism_log_file(string $filename, string $log_level): void $this->ffi->extism_log_file($filenamePtr, $log_levelPtr); } - function extism_function_new(string $name, array $inputTypes, array $outputTypes, callable $callback, $userData, $freeUserData) : \FFI\CData + public function extism_function_new(string $name, array $inputTypes, array $outputTypes, callable $callback, $userData, $freeUserData): \FFI\CData { $inputs = $this->toCArray($inputTypes, "ExtismValType"); $outputs = $this->toCArray($outputTypes, "ExtismValType"); @@ -143,18 +154,18 @@ function extism_function_new(string $name, array $inputTypes, array $outputTypes return $handle; } - function extism_function_free(FFI\CData $handle): void + public function extism_function_free(\FFI\CData $handle): void { $this->ffi->extism_function_free($handle); } - function extism_function_set_namespace(FFI\CData $handle, string $name) + public function extism_function_set_namespace(\FFI\CData $handle, string $name) { $namePtr = $this->ownedZero($name); $this->ffi->extism_function_set_namespace($handle, $namePtr); } - function toCArray(array $array, string $type): ?FFI\CData + private function toCArray(array $array, string $type): ?\FFI\CData { if (count($array) == 0) { return $this->ffi->new($type . "*"); @@ -168,25 +179,24 @@ function toCArray(array $array, string $type): ?FFI\CData return $cArray; } - function owned(string $type, string $string): ?FFI\CData + private function owned(string $type, string $string): ?\FFI\CData { if (strlen($string) == 0) { return null; } $str = $this->ffi->new($type . "[" . \strlen($string) . "]", true); - FFI::memcpy($str, $string, \strlen($string)); + \FFI::memcpy($str, $string, \strlen($string)); return $str; } - function ownedZero(string $string): ?FFI\CData + private function ownedZero(string $string): ?\FFI\CData { return $this->owned("char", "$string\0"); } - function startsWith($haystack, $needle) { + private function startsWith($haystack, $needle) + { return strcasecmp(substr($haystack, 0, strlen($needle)), $needle) === 0; } } - -?> diff --git a/src/extism.h b/src/Internal/extism.h similarity index 100% rename from src/extism.h rename to src/Internal/extism.h diff --git a/src/Manifest.php b/src/Manifest.php index 9c89fc0..dbb6810 100644 --- a/src/Manifest.php +++ b/src/Manifest.php @@ -1,18 +1,22 @@ wasm = $wasm; @@ -25,7 +29,7 @@ public function __construct(...$wasm) /** * List of Wasm sources. See `UrlWasmSource`, `PathWasmSource` and `ByteArrayWasmSource`. * - * @var WasmSource[] + * @var \Extism\Manifest\WasmSource[] */ public $wasm = []; @@ -65,170 +69,3 @@ public function __construct(...$wasm) */ public $timeout_ms; } - -/** - * Configures memory for the Wasm runtime. - * Memory is described in units of pages (64KB) and represents contiguous chunks of addressable memory. - */ -class MemoryOptions -{ - /** - * Max number of pages. Each page is 64KB. - * - * @var int - */ - public $max_pages; - - /** - * Max number of bytes returned by `extism_http_request` - * - * @var int - */ - public $max_http_response_bytes; - - /** - * Max number of bytes allowed in the Extism var store - * - * @var int - */ - public $max_var_bytes; -} - -/** - * A named Wasm source. - */ -abstract class WasmSource -{ - /** - * Logical name of the Wasm source. - * - * @var string|null - */ - public $name; - - /** - * Hash of the WASM source. - * - * @var string|null - */ - public $hash; -} - -/** - * Wasm Source represented by a file referenced by a path. - */ -class PathWasmSource extends WasmSource -{ - /** - * Constructor. - * - * @param string $path path to wasm plugin. - * @param string|null $name - * @param string|null $hash - */ - public function __construct($path, $name = null, $hash = null) - { - $this->path = realpath($path); - - if (!$this->path) { - throw new \Exception("Path not found: '" . $path . "'"); - } - - $this->name = $name ?? pathinfo($path, PATHINFO_FILENAME); - $this->hash = $hash; - } - - /** - * Path to wasm plugin. - * - * @var string - */ - public $path; -} - -/** - * Wasm Source represented by a file referenced by a path. - */ -class UrlWasmSource extends WasmSource -{ - /** - * Constructor. - * - * @param string $url uri to wasm plugin. - * @param string|null $name - * @param string|null $hash - */ - public function __construct($url, $name = null, $hash = null) - { - $this->url = $url; - $this->name = $name; - $this->hash = $hash; - $this->headers = new \stdClass(); - } - - /** - * Uri to wasm plugin. - * - * @var string - */ - public $url; - - /** - * HTTP headers. Examples: `header1=value`, `Authorization=Basic 123` - * - * @var object|null - */ - public $headers; - - /** - * HTTP Method. Examples: `GET`, `POST`, `DELETE`. See `HttpMethod` for a list of options. - * - * @var string|null - */ - public $method; -} - -/** - * HTTP defines a set of request methods to indicate the desired action to be performed for a given resource. - */ -class HttpMethod -{ - const GET = 'GET'; - const HEAD = 'HEAD'; - const POST = 'POST'; - const PUT = 'PUT'; - const DELETE = 'DELETE'; - const CONNECT = 'CONNECT'; - const OPTIONS = 'OPTIONS'; - const TRACE = 'TRACE'; - const PATCH = 'PATCH'; -} - -/** - * Wasm Source represented by raw bytes. - */ -class ByteArrayWasmSource extends WasmSource -{ - /** - * Constructor. - * - * @param string $data the byte array representing the Wasm code - * @param string|null $name - * @param string|null $hash - */ - public function __construct(string $data, $name = null, $hash = null) - { - $this->data = base64_encode($data); - $this->name = $name; - $this->hash = $hash; - } - - /** - * The byte array representing the Wasm code encoded in Base64. - * - * @var string - */ - public $data; -} - -?> diff --git a/src/Manifest/ByteArrayWasmSource.php b/src/Manifest/ByteArrayWasmSource.php new file mode 100644 index 0000000..17f70ae --- /dev/null +++ b/src/Manifest/ByteArrayWasmSource.php @@ -0,0 +1,30 @@ +data = base64_encode($data); + $this->name = $name; + $this->hash = $hash; + } + + /** + * The byte array representing the Wasm code encoded in Base64. + * + * @var string + */ + public $data; +} diff --git a/src/Manifest/HttpMethod.php b/src/Manifest/HttpMethod.php new file mode 100644 index 0000000..692d886 --- /dev/null +++ b/src/Manifest/HttpMethod.php @@ -0,0 +1,19 @@ +path = realpath($path); + + if (!$this->path) { + throw new \Exception("Path not found: '" . $path . "'"); + } + + $this->name = $name ?? pathinfo($path, PATHINFO_FILENAME); + $this->hash = $hash; + } + + /** + * Path to wasm plugin. + * + * @var string + */ + public $path; +} diff --git a/src/Manifest/UrlWasmSource.php b/src/Manifest/UrlWasmSource.php new file mode 100644 index 0000000..e390032 --- /dev/null +++ b/src/Manifest/UrlWasmSource.php @@ -0,0 +1,45 @@ +url = $url; + $this->name = $name; + $this->hash = $hash; + $this->headers = new \stdClass(); + } + + /** + * Uri to wasm plugin. + * + * @var string + */ + public $url; + + /** + * HTTP headers. Examples: `header1=value`, `Authorization=Basic 123` + * + * @var object|null + */ + public $headers; + + /** + * HTTP Method. Examples: `GET`, `POST`, `DELETE`. See `HttpMethod` for a list of options. + * + * @var string|null + */ + public $method; +} diff --git a/src/Manifest/WasmSource.php b/src/Manifest/WasmSource.php new file mode 100644 index 0000000..fb42ed0 --- /dev/null +++ b/src/Manifest/WasmSource.php @@ -0,0 +1,23 @@ +lib = $lib; - - $functionHandles = array_map(function ($function) { - return $function->handle; - }, $functions); - - $functionHandles = $this->lib->toCArray($functionHandles, "ExtismFunction*"); - $data = json_encode($manifest); if (!$data) { @@ -58,7 +52,7 @@ public function __construct(Manifest $manifest, bool $with_wasi = false, array $ } $errPtr = $lib->ffi->new($lib->ffi->type("char*")); - $handle = $this->lib->extism_plugin_new($data, strlen($data), $functionHandles, count($functions), $with_wasi, \FFI::addr($errPtr)); + $handle = $this->lib->extism_plugin_new($data, strlen($data), $functions, count($functions), $with_wasi, \FFI::addr($errPtr)); if (\FFI::isNull($errPtr) == false) { $error = \FFI::string($errPtr); @@ -79,9 +73,9 @@ public function __destruct() /** * Check if the plugin contains a function. - * + * * @param string $name - * + * * @return bool `true` if the function exists, `false` otherwise */ public function functionExists(string $name) @@ -91,10 +85,10 @@ public function functionExists(string $name) /** * Call a function in the Plugin and return the result. - * + * * @param string $name Name of function. * @param string $input Input buffer - * + * * @return string Output buffer */ public function call(string $name, string $input = null): string @@ -113,14 +107,14 @@ public function call(string $name, string $input = null): string /** * Configures file logging. This applies to all Plugin instances. - * + * * @param string $filename Path of log file. The file will be created if it doesn't exist. * @param string $level Minimum log level. Valid values are: `trace`, `debug`, `info`, `warn`, `error` * or more complex filter like `extism=trace,cranelift=debug`. */ public static function setLogFile(string $filename, string $level): void { - $lib = new \LibExtism(); + $lib = new \Extism\Internal\LibExtism(); $lib->extism_log_file($filename, $level); } @@ -130,7 +124,7 @@ public static function setLogFile(string $filename, string $level): void */ public static function version() { - $lib = new \LibExtism(); + $lib = new \Extism\Internal\LibExtism(); return $lib->extism_version(); } -} \ No newline at end of file +} diff --git a/tests/Helpers.php b/tests/Helpers.php index 06f2fc8..cf8b298 100644 --- a/tests/Helpers.php +++ b/tests/Helpers.php @@ -1,21 +1,22 @@ - \ No newline at end of file diff --git a/tests/ManifestTest.php b/tests/ManifestTest.php index 345127f..ddf766d 100644 --- a/tests/ManifestTest.php +++ b/tests/ManifestTest.php @@ -1,11 +1,15 @@ -allowed_hosts = ["jsonplaceholder.*.com"]; }); @@ -89,13 +93,11 @@ public static function loadPlugin(string $name, ?callable $config = null) { $path = __DIR__ . '/../wasm/' . $name; $manifest = new Manifest(new PathWasmSource($path, 'main')); - + if ($config !== null) { $config($manifest); } - + return new Plugin($manifest, true, []); } } - -?> \ No newline at end of file diff --git a/tests/PluginTest.php b/tests/PluginTest.php index 3ac8fa8..7fbd78f 100644 --- a/tests/PluginTest.php +++ b/tests/PluginTest.php @@ -1,15 +1,16 @@ -expectExceptionMessage("Extism: unable to load plugin: Unable to create Extism plugin: unknown import: `extism:host/user::kv_read` has not been defined"); - $kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (string $key) { }); + $kvRead = new HostFunction("kv_read", [ExtismValType::I64], [ExtismValType::I64], function (string $key) { + // + }); $kvRead->set_namespace("custom"); - $kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (string $key, string $value) {}); + $kvWrite = new HostFunction("kv_write", [ExtismValType::I64, ExtismValType::I64], [], function (string $key, string $value) { + // + }); $kvRead->set_namespace("custom"); $plugin = self::loadPlugin("count_vowels_kvstore.wasm", [$kvRead, $kvWrite]);