
# 宿主函数

通过 `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。

转换遵循 [值转换规则](https://phasis.inline0.dev/zh/docs/interop/values)。参数是副本：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](https://github.com/inline0/phasis/issues/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`（参见[值转换](https://phasis.inline0.dev/zh/docs/interop/values)）。

## 异步宿主函数

宿主函数是同步的。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 传输](https://phasis.inline0.dev/zh/docs/interop/fetch-transport)，路由到你自己的 PHP 客户端。

## 性能说明

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

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