Run untrusted JavaScript or TypeScript inside PHP — safely, with a typed, bidirectional bridge.
php-quickjs embeds the QuickJS-NG engine
directly in your PHP process. Guest code runs in an isolated context with memory,
time, and stack limits; PHP exposes a controlled allowlist of capabilities into JS;
and values, functions, and errors cross the boundary both ways. Guest code may be
TypeScript — it's transpiled in-process and runtime errors map back to the original
TS source.
Built in Rust with ext-php-rs and
rquickjs. QuickJS-NG is bundled — no system
library required.
- Isolated guest — memory, time, and stack limits contain runaway code.
- Capability allowlist — JS only sees the PHP functions you expose, as a frozen
php.module.fn()SDK. - Bidirectional — JS calls back into PHP, functions pass both ways, and opaque handles wrap live PHP objects.
- TypeScript built in — transpiled with
oxc; errors map to the original TS line and column. - Typed exceptions — guest failures surface as
QuickJSEvalExceptionwith a JS-like message and stack.
PHP exposes a narrow capability surface; the guest — JavaScript or TypeScript — runs sandboxed and calls back in.
<?php
$js = new QuickJS(memoryLimit: 32 * 1024 * 1024, timeoutMs: 500);
$js->register('fetchUser', fn(int $id) => ['name' => 'Ada', 'roles' => ['admin', 'dev']]);
echo $js->eval(<<<'TS'
interface User { name: string; roles: string[] } // types erase in-process
const u: User = php.fetchUser(42); // re-enters PHP
`${u.name} has ${u.roles.length} roles`;
TS);
// => "Ada has 2 roles"A tour of the rest of the bridge:
// Functions cross both ways — a JS callback arrives in PHP as a Js\Callback.
$js->register('map', fn(array $xs, callable $fn) => array_map($fn, $xs));
$js->eval('php.map([1, 2, 3], (n: number) => n * n)'); // => [1, 4, 9]
// Live objects stay host-side; JS only ever holds an opaque handle.
$h = $js->grant(new ArrayObject(['hits' => 0]));
$js->register('bump', fn(int $h) => ++$js->resolve($h)['hits']);
$js->eval("php.bump($h); php.bump($h);"); // => 2
// Guest failures surface as typed exceptions, located in the original TS.
try {
$js->eval("const x: any = null;\nx.field;");
} catch (QuickJSEvalException $e) {
echo $e->getJsName(), ' @ line ', $e->getLine(); // => "TypeError @ line 2"
}
// Resource abuse is contained, and the engine recovers afterwards.
$g = new QuickJS(timeoutMs: 100);
try { $g->eval('while (true) {}'); } catch (QuickJSTimeoutException $e) {}
$g->eval('1 + 1'); // => 2Run with php -d extension=/path/to/libphp_quickjs.so script.php. Fuller programs —
typed capabilities, dts(), isolated realms — in examples/:
kitchen_sink.php, modes.php, usage.php.
Prebuilt binaries are attached to each release for PHP 8.4 / 8.5 — self-hosted Linux, AWS Lambda (a ready Bref layer), and macOS (Apple Silicon). Enable the one matching your platform:
; php.ini
extension=/path/to/php-quickjs-...soOr build from source (Rust 1.96+, clang, PHP dev headers — a plain cargo cdylib, no
phpize):
git clone https://github.com/eddmann/php-quickjs && cd php-quickjs
make build→ Full platform matrix, Docker, and AWS Lambda / Bref instructions: docs/install.md.
PHP (trusted) ──ext-php-rs──► Rust bridge ──rquickjs──► QuickJS (untrusted)
register() dispatch table php.module.fn()
eval() __host(name, bytes) frozen php.* facade
Everything the guest reaches goes through a single __host import and a flat dispatch
table; the namespaced php.* tree is frozen JS built from your registrations. Values
cross as MessagePack, functions as references backed by registries, and errors bridge
both ways — remapping to TS coordinates on the way out.
→ docs/architecture.md for the full design.
This is an embedder, not a standalone defence against hostile code. The capability model contains what JS can reach; the resource limits contain abuse (infinite loops, alloc bombs). QuickJS C memory-corruption bugs are not contained — for attacker-controlled code, nest the extension inside an outer microVM / gVisor boundary.
- Installation — prebuilt binaries, Docker, AWS Lambda (Bref), and building from source.
- API reference — the
QuickJSclass and every method. - Architecture — the bridge, marshaling, function passing, and security model.
- Execution modes — shared vs. isolated realms and the callback lifecycle.
- Errors — typed exceptions, both-way bridging, and TypeScript remapping.
MIT