跳转到正文

宿主函数

通过 setGlobal() 交给引擎的任何 PHP 闭包或可调用对象,都会成为可调用的 JS 函数。无需手动注册、定义架构或编组——只需传入可调用对象。

基础知识

PHP
$engine->setGlobal('greet', function (string $name): string {
    return "Hello, $name!";
});

echo $engine->eval('greet("world")');
// Hello, world!

当 greet("world") 在 JS 中运行时:

  1. 字符串 "world" 会从 JS 转换为 PHP。
  2. PHP 闭包会使用转换后的参数调用。
  3. 返回值会从 PHP 转回 JS。

转换遵循 值转换规则。参数是副本:JS 数组或对象会作为 PHP 数组传入,在 PHP 中修改它不会更改 JS 中的值。

参数数量与参数

引擎会按位置将 JS 参数原样传给可调用对象;它不会检查签名。普通闭包会忽略多余参数,可变参数会收集这些参数。缺少的参数不会自动补齐:如果 JS 传入的参数少于可调用对象所需数量,PHP 会抛出 ArgumentCountError。请为可选参数提供默认值。

PHP
$engine->setGlobal('format', function (string $template, ...$args): string {
    return vsprintf($template, $args);
});

echo $engine->eval('format("%s is %d years old", "Ada", 36)');
// Ada is 36 years old

类型转换

参数会按照 JS→PHP 规则转换为 PHP。引擎从声明了 strict_types=1 的代码调用你的可调用对象,因此 PHP 会严格检查标量参数类型,不会进行强制转换:将 JS 数值传给 string 参数会导致 TypeError。与宿主函数抛出的任何 PHP 异常一样,它会传递给 eval() 的调用方,而不是 JS(见下文)。

PHP
$engine->setGlobal('len', fn(string $s): int => strlen($s));

$engine->eval('len(42)');  // PHP TypeError: Argument #1 ($s) must be of type string, int given

可以在闭包中手动转换((string) $value),也可以使用联合类型(int|string)。

将异常传回 JS

如今,在宿主函数中抛出的普通 PHP 异常不会转换为 JS 错误。它们会经过所有 JS try / catch 和 .catch() 处理程序,保持原有类别传到调用 eval() 或 call() 的 PHP 代码(问题 #20)。其中包括 \Exception、\RuntimeException、\DivisionByZeroError,以及 \TypeError 和 \ArgumentCountError 对错误参数抛出的 PHP。

PHP
$engine->setGlobal('divide', function (int $a, int $b): int {
    if ($b === 0) {
        throw new \DivisionByZeroError('cannot divide by zero');
    }
    return intdiv($a, $b);
});

try {
    $engine->eval('
        try { divide(10, 0); } catch (e) { /* never runs */ }
    ');
} catch (\DivisionByZeroError $e) {
    echo $e->getMessage();  // cannot divide by zero
}

要抛出可由 JS 代码捕获的错误,请抛出引擎自带的错误类之一:Phasis\Exceptions\TypeError、RangeError 或 ReferenceError 会在 JS 中作为对应的 JS 错误类型出现;其他任何 Phasis\Exceptions\RuntimeError 都会作为普通的 Error 出现,并保留你提供的消息。

PHP
use Phasis\Exceptions\RangeError;

$engine->setGlobal('divide', function (int $a, int $b): int {
    if ($b === 0) {
        throw new RangeError('cannot divide by zero');
    }
    return intdiv($a, $b);
});

$engine->eval('
    try {
        divide(10, 0);
    } catch (e) {
        console.log(e.name, e.message);  // RangeError cannot divide by zero
    }
');

返回复杂值

返回值按照 PHP→JS 规则转换。若要返回 JS 对象字面量,请返回 PHP 关联数组:

PHP
$engine->setGlobal('lookup', function (string $key): array {
    return ['key' => $key, 'value' => 42, 'meta' => ['ts' => time()]];
});

$engine->eval('
    const r = lookup("answer");
    console.log(r.value, r.meta.ts);
');

若要返回 JS 数组,请返回顺序排列的 PHP 列表:

PHP
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));

$engine->eval('range(1, 5).reduce((a, b) => a + b)');  // 15

返回 PHP 对象(包括来自 ReactPHP、Amp 或 Guzzle 的 PHP promise)会产生 JS undefined(参见值转换)。

异步宿主函数

宿主函数是同步的。PHP promise 或其他 thenable 对象不会被等待:它会转换为 undefined,因此 await hostFn() 会解析为 undefined。请同步完成工作并返回结果;JS 代码仍可对该调用执行 await,从而将值包装在一个已解析的 promise 中:

PHP
$engine->setGlobal('loadBody', function (string $url): string {
    return file_get_contents($url);
});

$engine->eval('
    async function main() {
        const body = await loadBody("https://example.com");
        console.log(body.length);
    }
    main();
');

对于 HTTP,内置的 fetch() 已经会返回真正的 JS promise,并且可以通过 自定义 fetch 传输,路由到你自己的 PHP 客户端。

性能说明

每次调用宿主函数时,参数和返回值各跨越一次 PHP↔JS 边界。两次转换都会复制数据,因此复杂度为 O(size of value)——基本值的耗时是常数,数组和对象的成本则与其大小成正比。

对于频繁执行的内层循环,最好一次调用完成更多工作,而不是多次进行小调用;如果只更改少数几个字段,也应避免来回传递大型数组。