- Add wasm_mode environment variable with command/library options - Implement WIT interface generation with manifest, adapter, and binding files - Create WasmExport attribute for marking functions as component exports - Generate C bindings and integrate wit-bindgen for component compilation - Add support for library mode with reactor execution model - Update build script to handle component vs command compilation - Add JavaScript integration for calling exported Wasm functions - Implement example extension info retrieval via WasmExport functions - Add CSS styling for extension detail UI components - Update documentation for WASI component library workflowpull/46/head
parent
ac16f5fbac
commit
3d36928e8b
29 changed files with 1307 additions and 73 deletions
@ -1,8 +1,10 @@ |
|||||||
name: wasm-hello |
name: wasm-hello |
||||||
mode: bin |
mode: library |
||||||
build-dir: build |
build-dir: build |
||||||
output: component/wasm-hello.wasm |
output: component/wasm-hello.wasm |
||||||
sources: |
sources: |
||||||
- src |
- src |
||||||
wasm: browser |
wasm: browser |
||||||
wasm-browser-dir: generated |
wasm-browser-dir: generated |
||||||
|
wasm-package: typephp:wasm-hello@1.0.0 |
||||||
|
wasm-world: wasm-hello |
||||||
|
|||||||
@ -0,0 +1,9 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
class WasmExportService |
||||||
|
{ |
||||||
|
#[WasmExport] |
||||||
|
public function execute(): void |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[WasmExport(42)] |
||||||
|
function invalidWasmExportName(): void |
||||||
|
{ |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[WasmExport(name: 'greet-user')] |
||||||
|
function greetUser(string $name): string |
||||||
|
{ |
||||||
|
return "Hello, $name"; |
||||||
|
} |
||||||
@ -0,0 +1,89 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace TypePhpTest\Build; |
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase; |
||||||
|
use RuntimeException; |
||||||
|
use TypePhp\Build\WasmInterfaceGenerator; |
||||||
|
use TypePhp\Entity\ArgInfo; |
||||||
|
use TypePhp\Entity\FunctionDef; |
||||||
|
use TypePhp\Type; |
||||||
|
|
||||||
|
final class WasmInterfaceGeneratorTest extends TestCase |
||||||
|
{ |
||||||
|
public function testGeneratesTypedWitAndManifest(): void |
||||||
|
{ |
||||||
|
$function = new FunctionDef('greetUser', Type::STR, 'App'); |
||||||
|
$function->wasmExport = true; |
||||||
|
$function->displayName = 'App\\greetUser'; |
||||||
|
$function->returnTypeStr = 'string'; |
||||||
|
$argument = new ArgInfo(); |
||||||
|
$argument->name = 'name'; |
||||||
|
$argument->phpName = 'name'; |
||||||
|
$argument->type = Type::STR; |
||||||
|
$function->argInfoList = [$argument]; |
||||||
|
|
||||||
|
$generator = new WasmInterfaceGenerator(); |
||||||
|
$manifest = $generator->buildManifest( |
||||||
|
[$function], |
||||||
|
'acme:demo@1.0.0', |
||||||
|
'demo', |
||||||
|
static fn (): string => 'php_app__greetuser', |
||||||
|
); |
||||||
|
|
||||||
|
self::assertSame('greet-user', $manifest['functions'][0]['name']); |
||||||
|
self::assertSame('php_app__greetuser', $manifest['functions'][0]['cpp-symbol']); |
||||||
|
self::assertSame('string', $manifest['functions'][0]['parameters'][0]['wit-type']); |
||||||
|
self::assertStringContainsString( |
||||||
|
'greet-user: func(name: string) -> result<string, typephp-error>;', |
||||||
|
$generator->renderWit($manifest), |
||||||
|
); |
||||||
|
$adapter = $generator->renderCppAdapter($manifest); |
||||||
|
self::assertStringContainsString( |
||||||
|
'extern "C" bool exports_acme_demo_api_method_runtime_greet_user(', |
||||||
|
$adapter, |
||||||
|
); |
||||||
|
self::assertStringContainsString('auto result = php_app__greetuser(', $adapter); |
||||||
|
self::assertStringContainsString('catch (zend_object *exception)', $adapter); |
||||||
|
self::assertSame( |
||||||
|
"acme:demo/api@1.0.0#create-runtime\n" |
||||||
|
. "acme:demo/api@1.0.0#[method]runtime.greet-user\n", |
||||||
|
$generator->renderJcoAsyncExports($manifest), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsExportNameCollisions(): void |
||||||
|
{ |
||||||
|
$first = new FunctionDef('first', Type::VOID, ''); |
||||||
|
$first->wasmExport = true; |
||||||
|
$first->wasmExportName = 'same-name'; |
||||||
|
$second = new FunctionDef('second', Type::VOID, ''); |
||||||
|
$second->wasmExport = true; |
||||||
|
$second->wasmExportName = 'same-name'; |
||||||
|
|
||||||
|
$this->expectException(RuntimeException::class); |
||||||
|
$this->expectExceptionMessage('WasmExport name collision'); |
||||||
|
(new WasmInterfaceGenerator())->buildManifest( |
||||||
|
[$first, $second], |
||||||
|
'acme:demo@1.0.0', |
||||||
|
'demo', |
||||||
|
static fn (FunctionDef $function): string => $function->name, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsUntypedAbi(): void |
||||||
|
{ |
||||||
|
$function = new FunctionDef('dynamicValue', Type::VAR, ''); |
||||||
|
$function->wasmExport = true; |
||||||
|
$function->returnTypeUndeclared = true; |
||||||
|
|
||||||
|
$this->expectException(RuntimeException::class); |
||||||
|
$this->expectExceptionMessage('must declare a return type'); |
||||||
|
(new WasmInterfaceGenerator())->buildManifest( |
||||||
|
[$function], |
||||||
|
'acme:demo@1.0.0', |
||||||
|
'demo', |
||||||
|
static fn (): string => 'php_dynamicvalue', |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
final class WasmExportAttributeTest extends BaseTest |
||||||
|
{ |
||||||
|
public function testAcceptsNamedFunctionWithConstantExportName(): void |
||||||
|
{ |
||||||
|
$this->compile('wasm-export-valid.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsMethods(): void |
||||||
|
{ |
||||||
|
$this->expectException(\TypePhp\Exception\SyntaxError::class); |
||||||
|
$this->expectExceptionMessage('WasmExport can only be applied to named functions'); |
||||||
|
$this->compile('wasm-export-invalid-method.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNonStringName(): void |
||||||
|
{ |
||||||
|
$this->exec('WasmExport name must be a constant string', 'wasm-export-invalid-name.php'); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,470 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace TypePhp\Build; |
||||||
|
|
||||||
|
use RuntimeException; |
||||||
|
use TypePhp\Entity\ArgInfo; |
||||||
|
use TypePhp\Entity\FunctionDef; |
||||||
|
use TypePhp\Type; |
||||||
|
|
||||||
|
/** Generates the stable, tool-neutral contract consumed by PHPX's host bindgen. */ |
||||||
|
final class WasmInterfaceGenerator |
||||||
|
{ |
||||||
|
/** |
||||||
|
* @param iterable<FunctionDef> $functions |
||||||
|
* @return array{package:string, world:string, interface:string, functions:list<array<string, mixed>>} |
||||||
|
*/ |
||||||
|
public function buildManifest( |
||||||
|
iterable $functions, |
||||||
|
string $package, |
||||||
|
string $world, |
||||||
|
callable $nativeName, |
||||||
|
): array { |
||||||
|
$exports = []; |
||||||
|
$names = []; |
||||||
|
foreach ($functions as $function) { |
||||||
|
if (!$function->wasmExport) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$displayName = $function->displayName ?: $function->getNamespacedName(); |
||||||
|
if ($function->method || $function->stub) { |
||||||
|
throw new RuntimeException("WasmExport function {$displayName}() must have a TypePHP function body"); |
||||||
|
} |
||||||
|
if ($function->generator || $function->returnsByRef || $function->hasVariadicArg()) { |
||||||
|
throw new RuntimeException("WasmExport function {$displayName}() cannot be a generator, return by reference, or be variadic"); |
||||||
|
} |
||||||
|
if ($function->returnTypeUndeclared) { |
||||||
|
throw new RuntimeException("WasmExport function {$displayName}() must declare a return type"); |
||||||
|
} |
||||||
|
|
||||||
|
$exportName = $function->wasmExportName !== '' |
||||||
|
? $function->wasmExportName |
||||||
|
: $this->toWitName($function->name); |
||||||
|
$this->assertWitIdentifier($exportName, "WasmExport name for {$displayName}()"); |
||||||
|
$key = strtolower($exportName); |
||||||
|
if (isset($names[$key])) { |
||||||
|
throw new RuntimeException( |
||||||
|
"WasmExport name collision: {$names[$key]}() and {$displayName}() both export `{$exportName}`" |
||||||
|
); |
||||||
|
} |
||||||
|
$names[$key] = $displayName; |
||||||
|
|
||||||
|
$parameters = []; |
||||||
|
foreach ($function->argInfoList as $argument) { |
||||||
|
if ($argument->byRef || $argument->variadic || $argument->default !== '') { |
||||||
|
throw new RuntimeException( |
||||||
|
"WasmExport parameter \${$argument->phpName} of {$displayName}() cannot be by-reference, variadic, or optional" |
||||||
|
); |
||||||
|
} |
||||||
|
$parameters[] = [ |
||||||
|
'name' => $this->toWitName($argument->phpName), |
||||||
|
'php-name' => $argument->phpName, |
||||||
|
'cpp-name' => $argument->name, |
||||||
|
'cpp-type' => $argument->type, |
||||||
|
'wit-type' => $this->argumentType($argument, $displayName), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
$exports[] = [ |
||||||
|
'name' => $exportName, |
||||||
|
'php-name' => $function->getNamespacedName(), |
||||||
|
'cpp-symbol' => $nativeName($function), |
||||||
|
'parameters' => $parameters, |
||||||
|
'result' => $this->resultType($function, $displayName), |
||||||
|
'result-cpp-type' => $function->returnType, |
||||||
|
]; |
||||||
|
} |
||||||
|
if ($exports === []) { |
||||||
|
throw new RuntimeException('WASI library mode requires at least one #[WasmExport] function'); |
||||||
|
} |
||||||
|
|
||||||
|
return [ |
||||||
|
'schema' => 1, |
||||||
|
'package' => $package, |
||||||
|
'world' => $world, |
||||||
|
'interface' => 'api', |
||||||
|
'runtime' => [ |
||||||
|
'threading' => 'nts', |
||||||
|
'lifecycle' => 'wit-resource', |
||||||
|
'init-symbol' => 'typephp_runtime_init', |
||||||
|
'shutdown-symbol' => 'typephp_runtime_shutdown', |
||||||
|
'error-model' => 'result', |
||||||
|
], |
||||||
|
'functions' => $exports, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** @param array{package:string, world:string, interface:string, functions:list<array<string, mixed>>} $manifest */ |
||||||
|
public function renderWit(array $manifest): string |
||||||
|
{ |
||||||
|
$lines = [ |
||||||
|
'package ' . $manifest['package'] . ';', |
||||||
|
'', |
||||||
|
'interface ' . $manifest['interface'] . ' {', |
||||||
|
' record typephp-error {', |
||||||
|
' class: string,', |
||||||
|
' message: string,', |
||||||
|
' code: s64,', |
||||||
|
' }', |
||||||
|
'', |
||||||
|
' resource runtime {', |
||||||
|
]; |
||||||
|
foreach ($manifest['functions'] as $function) { |
||||||
|
$parameters = []; |
||||||
|
foreach ($function['parameters'] as $parameter) { |
||||||
|
$parameters[] = $parameter['name'] . ': ' . $parameter['wit-type']; |
||||||
|
} |
||||||
|
$success = $function['result'] === null ? '_' : $function['result']; |
||||||
|
$lines[] = ' ' . $function['name'] . ': func(' . implode(', ', $parameters) |
||||||
|
. ') -> result<' . $success . ', typephp-error>;'; |
||||||
|
} |
||||||
|
$lines[] = ' }'; |
||||||
|
$lines[] = ''; |
||||||
|
$lines[] = ' create-runtime: func() -> result<runtime, typephp-error>;'; |
||||||
|
$lines[] = '}'; |
||||||
|
$lines[] = ''; |
||||||
|
$lines[] = 'world ' . $manifest['world'] . ' {'; |
||||||
|
$lines[] = ' export ' . $manifest['interface'] . ';'; |
||||||
|
$lines[] = '}'; |
||||||
|
return implode(PHP_EOL, $lines) . PHP_EOL; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Every TypePHP browser export may transitively call an asynchronous WASI |
||||||
|
* import. Jco must wrap these entry points with WebAssembly.promising or a |
||||||
|
* synchronous-looking PHP call such as RINIT, file I/O, or HTTP cannot |
||||||
|
* suspend through JSPI. |
||||||
|
* |
||||||
|
* @param array<string, mixed> $manifest |
||||||
|
*/ |
||||||
|
public function renderJcoAsyncExports(array $manifest): string |
||||||
|
{ |
||||||
|
[$package, $version] = explode('@', $manifest['package'], 2); |
||||||
|
$interface = $package . '/' . $manifest['interface'] . '@' . $version; |
||||||
|
$exports = [$interface . '#create-runtime']; |
||||||
|
foreach ($manifest['functions'] as $function) { |
||||||
|
$exports[] = $interface . '#[method]runtime.' . $function['name']; |
||||||
|
} |
||||||
|
return implode(PHP_EOL, $exports) . PHP_EOL; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Render the small TypePHP-specific half of the C binding. The generic |
||||||
|
* Canonical ABI half and component type object are emitted by the pinned |
||||||
|
* wit-bindgen binary shipped with PHPX. |
||||||
|
* |
||||||
|
* @param array<string, mixed> $manifest |
||||||
|
*/ |
||||||
|
public function renderCppAdapter(array $manifest): string |
||||||
|
{ |
||||||
|
[$packageNamespace, $packageTail] = explode(':', explode('@', $manifest['package'], 2)[0], 2); |
||||||
|
$packageName = $packageTail; |
||||||
|
$world = $this->cName($manifest['world']); |
||||||
|
$prefix = 'exports_' . $this->cName($packageNamespace) . '_' . $this->cName($packageName) |
||||||
|
. '_' . $this->cName($manifest['interface']); |
||||||
|
$errorType = $prefix . '_typephp_error_t'; |
||||||
|
$lines = [ |
||||||
|
'#include <cstdlib>', |
||||||
|
'#include <cstring>', |
||||||
|
'#include <exception>', |
||||||
|
'#include <new>', |
||||||
|
'#include <phpx.h>', |
||||||
|
'#include <typephp_helper.h>', |
||||||
|
'#include "' . $this->cName($manifest['world']) . '.h"', |
||||||
|
'', |
||||||
|
'extern "C" int typephp_runtime_init(int argc, char **argv);', |
||||||
|
'extern "C" void typephp_runtime_shutdown();', |
||||||
|
'', |
||||||
|
'struct ' . $prefix . '_runtime_t {', |
||||||
|
' bool call_active = false;', |
||||||
|
'};', |
||||||
|
'', |
||||||
|
'namespace {', |
||||||
|
'bool runtime_started = false;', |
||||||
|
'bool runtime_failed = false;', |
||||||
|
'', |
||||||
|
'void set_error(' . $errorType . ' *error, const char *class_name, size_t class_len,', |
||||||
|
' const char *message, size_t message_len, int64_t code = 0) {', |
||||||
|
' ' . $world . '_string_dup_n(&error->class_, class_name, class_len);', |
||||||
|
' ' . $world . '_string_dup_n(&error->message, message, message_len);', |
||||||
|
' error->code = code;', |
||||||
|
'}', |
||||||
|
'', |
||||||
|
'void set_error(' . $errorType . ' *error, const char *message) {', |
||||||
|
' set_error(error, "TypePHP\\\\WasmError", sizeof("TypePHP\\\\WasmError") - 1,', |
||||||
|
' message, std::strlen(message));', |
||||||
|
'}', |
||||||
|
'', |
||||||
|
'struct CallGuard {', |
||||||
|
' bool &active;', |
||||||
|
' explicit CallGuard(bool &value) : active(value) { active = true; }', |
||||||
|
' ~CallGuard() { active = false; }', |
||||||
|
'};', |
||||||
|
'', |
||||||
|
'void set_exception(' . $errorType . ' *error, zend_object *exception) {', |
||||||
|
' zend_class_entry *base = instanceof_function(exception->ce, zend_ce_exception)', |
||||||
|
' ? zend_ce_exception : zend_ce_error;', |
||||||
|
' zval message_rv;', |
||||||
|
' zval code_rv;', |
||||||
|
' zval *message = zend_read_property_ex(base, exception, ZSTR_KNOWN(ZEND_STR_MESSAGE), true, &message_rv);', |
||||||
|
' zval *code = zend_read_property_ex(base, exception, ZSTR_KNOWN(ZEND_STR_CODE), true, &code_rv);', |
||||||
|
' zend_string *class_name = exception->ce->name;', |
||||||
|
' const char *message_data = Z_TYPE_P(message) == IS_STRING ? Z_STRVAL_P(message) : "PHP exception";', |
||||||
|
' size_t message_len = Z_TYPE_P(message) == IS_STRING ? Z_STRLEN_P(message) : sizeof("PHP exception") - 1;', |
||||||
|
' int64_t exception_code = Z_TYPE_P(code) == IS_LONG ? Z_LVAL_P(code) : 0;', |
||||||
|
' set_error(error, ZSTR_VAL(class_name), ZSTR_LEN(class_name), message_data, message_len, exception_code);', |
||||||
|
' zend_clear_exception();', |
||||||
|
'}', |
||||||
|
'} // namespace', |
||||||
|
'', |
||||||
|
'extern "C" bool ' . $prefix . '_create_runtime(', |
||||||
|
' ' . $prefix . '_own_runtime_t *ret, ' . $errorType . ' *error) {', |
||||||
|
' if (runtime_started) {', |
||||||
|
' set_error(error, "Only one TypePHP runtime may be active in an NTS component instance");', |
||||||
|
' return false;', |
||||||
|
' }', |
||||||
|
' if (runtime_failed) {', |
||||||
|
' set_error(error, "The TypePHP runtime is unavailable after an earlier fatal error");', |
||||||
|
' return false;', |
||||||
|
' }', |
||||||
|
' char program[] = "typephp-component";', |
||||||
|
' char *argv[] = {program, nullptr};', |
||||||
|
' if (typephp_runtime_init(1, argv) != 0) {', |
||||||
|
' runtime_failed = true;', |
||||||
|
' set_error(error, "Unable to initialize the TypePHP runtime");', |
||||||
|
' return false;', |
||||||
|
' }', |
||||||
|
' auto *runtime = new (std::nothrow) ' . $prefix . '_runtime_t();', |
||||||
|
' if (runtime == nullptr) {', |
||||||
|
' typephp_runtime_shutdown();', |
||||||
|
' set_error(error, "Unable to allocate the TypePHP runtime resource");', |
||||||
|
' return false;', |
||||||
|
' }', |
||||||
|
' runtime_started = true;', |
||||||
|
' *ret = ' . $prefix . '_runtime_new(runtime);', |
||||||
|
' return true;', |
||||||
|
'}', |
||||||
|
'', |
||||||
|
'extern "C" void ' . $prefix . '_runtime_destructor(' . $prefix . '_runtime_t *runtime) {', |
||||||
|
' delete runtime;', |
||||||
|
' if (runtime_started) {', |
||||||
|
' typephp_runtime_shutdown();', |
||||||
|
' }', |
||||||
|
' runtime_started = false;', |
||||||
|
' runtime_failed = false;', |
||||||
|
'}', |
||||||
|
'', |
||||||
|
]; |
||||||
|
|
||||||
|
foreach ($manifest['functions'] as $function) { |
||||||
|
$returnType = $function['result']; |
||||||
|
$declaration = [$prefix . '_borrow_runtime_t self']; |
||||||
|
$callArguments = []; |
||||||
|
foreach ($function['parameters'] as $parameter) { |
||||||
|
[$base, $nullable] = $this->splitWitType($parameter['wit-type']); |
||||||
|
$cType = $this->cAbiType($base, $world); |
||||||
|
$cName = $this->cName($parameter['name']); |
||||||
|
if ($base === 'string' || $nullable) { |
||||||
|
$declaration[] = $cType . ' *' . ($nullable ? 'maybe_' : '') . $cName; |
||||||
|
} else { |
||||||
|
$declaration[] = $cType . ' ' . $cName; |
||||||
|
} |
||||||
|
$callArguments[] = $this->cppArgument($parameter, $cName, $base, $nullable); |
||||||
|
} |
||||||
|
if ($returnType !== null) { |
||||||
|
[$returnBase, $returnNullable] = $this->splitWitType($returnType); |
||||||
|
$declaration[] = ($returnNullable |
||||||
|
? $world . '_option_' . $this->cName($returnBase) . '_t' |
||||||
|
: $this->cAbiType($returnBase, $world)) . ' *ret'; |
||||||
|
} else { |
||||||
|
$returnBase = ''; |
||||||
|
$returnNullable = false; |
||||||
|
} |
||||||
|
$declaration[] = $errorType . ' *error'; |
||||||
|
|
||||||
|
$lines[] = 'extern ' . $function['result-cpp-type'] . ' ' . $function['cpp-symbol'] |
||||||
|
. '(' . implode(', ', array_map( |
||||||
|
static fn (array $parameter): string => $parameter['cpp-type'] . ' ' . $parameter['cpp-name'], |
||||||
|
$function['parameters'], |
||||||
|
)) . ');'; |
||||||
|
$lines[] = ''; |
||||||
|
$lines[] = 'extern "C" bool ' . $prefix . '_method_runtime_' . $this->cName($function['name']) |
||||||
|
. '(' . implode(', ', $declaration) . ') {'; |
||||||
|
$lines[] = ' if (self == nullptr || !runtime_started || runtime_failed) {'; |
||||||
|
$lines[] = ' set_error(error, "The TypePHP runtime resource is closed or unavailable");'; |
||||||
|
$lines[] = ' return false;'; |
||||||
|
$lines[] = ' }'; |
||||||
|
$lines[] = ' if (self->call_active) {'; |
||||||
|
$lines[] = ' set_error(error, "Concurrent or reentrant calls on one NTS TypePHP component are not supported");'; |
||||||
|
$lines[] = ' return false;'; |
||||||
|
$lines[] = ' }'; |
||||||
|
$lines[] = ' CallGuard call_guard(self->call_active);'; |
||||||
|
$lines[] = ' bool success = false;'; |
||||||
|
$lines[] = ' zend_try {'; |
||||||
|
$lines[] = ' try {'; |
||||||
|
$call = $function['cpp-symbol'] . '(' . implode(', ', $callArguments) . ')'; |
||||||
|
if ($returnType === null) { |
||||||
|
$lines[] = ' ' . $call . ';'; |
||||||
|
} else { |
||||||
|
$lines[] = ' auto result = ' . $call . ';'; |
||||||
|
foreach ($this->cppResult($returnBase, $returnNullable, $world) as $resultLine) { |
||||||
|
$lines[] = ' ' . $resultLine; |
||||||
|
} |
||||||
|
} |
||||||
|
$lines[] = ' success = true;'; |
||||||
|
$lines[] = ' } catch (zend_object *exception) {'; |
||||||
|
$lines[] = ' set_exception(error, exception);'; |
||||||
|
$lines[] = ' } catch (const std::exception &exception) {'; |
||||||
|
$lines[] = ' set_error(error, exception.what());'; |
||||||
|
$lines[] = ' } catch (...) {'; |
||||||
|
$lines[] = ' set_error(error, "Unknown C++ exception");'; |
||||||
|
$lines[] = ' }'; |
||||||
|
$lines[] = ' } zend_catch {'; |
||||||
|
$lines[] = ' runtime_failed = true;'; |
||||||
|
$lines[] = ' set_error(error, "Zend bailout while executing the exported function");'; |
||||||
|
$lines[] = ' } zend_end_try();'; |
||||||
|
$lines[] = ' return success;'; |
||||||
|
$lines[] = '}'; |
||||||
|
$lines[] = ''; |
||||||
|
} |
||||||
|
|
||||||
|
return implode(PHP_EOL, $lines) . PHP_EOL; |
||||||
|
} |
||||||
|
|
||||||
|
/** @return array{string, bool} */ |
||||||
|
private function splitWitType(string $type): array |
||||||
|
{ |
||||||
|
if (str_starts_with($type, 'option<') && str_ends_with($type, '>')) { |
||||||
|
return [substr($type, 7, -1), true]; |
||||||
|
} |
||||||
|
return [$type, false]; |
||||||
|
} |
||||||
|
|
||||||
|
private function cAbiType(string $type, string $world): string |
||||||
|
{ |
||||||
|
return match ($type) { |
||||||
|
'bool' => 'bool', |
||||||
|
's64' => 'int64_t', |
||||||
|
'f64' => 'double', |
||||||
|
'string' => $world . '_string_t', |
||||||
|
default => throw new RuntimeException("Unsupported WIT C ABI type `{$type}`"), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** @param array<string, mixed> $parameter */ |
||||||
|
private function cppArgument(array $parameter, string $name, string $base, bool $nullable): string |
||||||
|
{ |
||||||
|
if (!$nullable) { |
||||||
|
return match ($base) { |
||||||
|
'string' => 'php::Str(reinterpret_cast<const char *>(' . $name . '->ptr), ' . $name . '->len)', |
||||||
|
default => $name, |
||||||
|
}; |
||||||
|
} |
||||||
|
$pointer = 'maybe_' . $name; |
||||||
|
$value = match ($base) { |
||||||
|
'string' => 'php::Str(reinterpret_cast<const char *>(' . $pointer . '->ptr), ' . $pointer . '->len)', |
||||||
|
default => '*' . $pointer, |
||||||
|
}; |
||||||
|
return '(' . $pointer . ' == nullptr ? php::Var(php::null) : php::Var(' . $value . '))'; |
||||||
|
} |
||||||
|
|
||||||
|
/** @return list<string> */ |
||||||
|
private function cppResult(string $base, bool $nullable, string $world): array |
||||||
|
{ |
||||||
|
if ($nullable) { |
||||||
|
$lines = [ |
||||||
|
'ret->is_some = !result.isNull();', |
||||||
|
'if (ret->is_some) {', |
||||||
|
]; |
||||||
|
if ($base === 'string') { |
||||||
|
$lines[] = ' php::Str string_result = php::toString(result);'; |
||||||
|
$lines[] = ' ' . $world . '_string_dup_n(&ret->val, string_result.data(), string_result.length());'; |
||||||
|
$lines[] = '}'; |
||||||
|
return $lines; |
||||||
|
} |
||||||
|
$assignment = match ($base) { |
||||||
|
'bool' => 'ret->val = php::toBool(result);', |
||||||
|
's64' => 'ret->val = php::toInt(result);', |
||||||
|
'f64' => 'ret->val = php::toFloat(result);', |
||||||
|
default => throw new RuntimeException("Unsupported nullable WIT result `{$base}`"), |
||||||
|
}; |
||||||
|
$lines[] = ' ' . $assignment; |
||||||
|
$lines[] = '}'; |
||||||
|
return $lines; |
||||||
|
} |
||||||
|
return [match ($base) { |
||||||
|
'bool' => '*ret = php::toBool(result);', |
||||||
|
's64' => '*ret = php::toInt(result);', |
||||||
|
'f64' => '*ret = php::toFloat(result);', |
||||||
|
'string' => $world . '_string_dup_n(ret, result.data(), result.length());', |
||||||
|
default => throw new RuntimeException("Unsupported WIT result `{$base}`"), |
||||||
|
}]; |
||||||
|
} |
||||||
|
|
||||||
|
private function cName(string $name): string |
||||||
|
{ |
||||||
|
return strtolower(str_replace('-', '_', $name)); |
||||||
|
} |
||||||
|
|
||||||
|
private function argumentType(ArgInfo $argument, string $function): string |
||||||
|
{ |
||||||
|
$type = $this->scalarType( |
||||||
|
$argument->type, |
||||||
|
"parameter \${$argument->phpName} of {$function}()", |
||||||
|
$argument->typeStr, |
||||||
|
); |
||||||
|
return $argument->nullable ? "option<{$type}>" : $type; |
||||||
|
} |
||||||
|
|
||||||
|
private function resultType(FunctionDef $function, string $displayName): ?string |
||||||
|
{ |
||||||
|
if ($function->returnType === Type::VOID) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
$nullable = str_starts_with($function->returnTypeStr, '?') |
||||||
|
|| str_contains(strtolower($function->returnTypeStr), 'null'); |
||||||
|
$type = $this->scalarType($function->returnType, "return type of {$displayName}()", $function->returnTypeStr); |
||||||
|
return $nullable ? "option<{$type}>" : $type; |
||||||
|
} |
||||||
|
|
||||||
|
private function scalarType(string $type, string $location, string $declaredType = ''): string |
||||||
|
{ |
||||||
|
$mapped = match ($type) { |
||||||
|
Type::BOOL => 'bool', |
||||||
|
Type::INT => 's64', |
||||||
|
Type::FLOAT => 'f64', |
||||||
|
Type::STR => 'string', |
||||||
|
default => null, |
||||||
|
}; |
||||||
|
if ($mapped !== null) { |
||||||
|
return $mapped; |
||||||
|
} |
||||||
|
$normalized = strtolower(str_replace(['?', '|null', 'null|'], '', $declaredType)); |
||||||
|
$mapped = match ($normalized) { |
||||||
|
'bool' => 'bool', |
||||||
|
'int' => 's64', |
||||||
|
'float' => 'f64', |
||||||
|
'string' => 'string', |
||||||
|
default => null, |
||||||
|
}; |
||||||
|
if ($mapped !== null) { |
||||||
|
return $mapped; |
||||||
|
} |
||||||
|
throw new RuntimeException( |
||||||
|
"Unsupported WasmExport {$location}; the first release supports bool, int, float, string, nullable scalars, and void" |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
private function toWitName(string $name): string |
||||||
|
{ |
||||||
|
$name = preg_replace('/(?<=[a-z0-9])(?=[A-Z])/', '-', $name) ?? $name; |
||||||
|
return strtolower(str_replace('_', '-', $name)); |
||||||
|
} |
||||||
|
|
||||||
|
private function assertWitIdentifier(string $name, string $location): void |
||||||
|
{ |
||||||
|
if (preg_match('/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/', $name) !== 1) { |
||||||
|
throw new RuntimeException("{$location} must be a lowercase WIT identifier (for example `greet-user`)"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue