|
1 | 1 | # php-quickjs |
2 | 2 |
|
3 | | -Run **untrusted JavaScript or TypeScript inside PHP**, safely and ergonomically. |
4 | | - |
5 | | -PHP applications increasingly need to execute user-supplied logic — rules, |
6 | | -formulas, templates, plugins, AI-generated snippets. Doing that in PHP itself |
7 | | -means `eval()` (no isolation) or a separate service (operational weight). |
8 | | -`php-quickjs` embeds a [QuickJS-NG](https://github.com/quickjs-ng/quickjs) engine |
9 | | -directly in the process and gives you a **typed, bidirectional bridge**: |
10 | | - |
11 | | -- The guest runs in an **isolated context** with memory, time, and stack limits. |
12 | | -- PHP exposes a **controlled allowlist** of capabilities into JS as a frozen, |
13 | | - namespaced `php.module.fn()` SDK — the guest can only reach what you grant. |
14 | | -- JS can **call back into PHP** mid-execution, pass functions both ways, and |
15 | | - hold opaque handles to live PHP objects. |
16 | | -- Guest code may be **TypeScript**: it is transpiled to JS in-process with |
17 | | - [`oxc`](https://github.com/oxc-project/oxc), and runtime errors are mapped back |
18 | | - to the original TS line/column. |
19 | | - |
20 | | -Written in Rust with [`ext-php-rs`](https://github.com/davidcole1340/ext-php-rs) |
21 | | -(the Zend side) and [`rquickjs`](https://github.com/DelSkayn/rquickjs) (QuickJS-NG |
22 | | -is bundled — no system library needed). |
23 | | - |
24 | | -> **Scope.** This is an *embedder*, not a security boundary against hostile code |
25 | | -> on its own. The capability model contains *what JS can reach*; the resource |
26 | | -> limits contain *abuse* (infinite loops, alloc bombs). QuickJS C |
27 | | -> memory-corruption bugs are **not** contained — for attacker-controlled code, |
28 | | -> nest the whole extension inside an outer microVM/gVisor boundary. |
29 | | -
|
30 | | -## Getting started |
31 | | - |
32 | | -**Requirements:** Rust 1.96+ (for oxc), clang, and PHP 8.4 dev headers |
33 | | -(`php-config`). The extension is a plain cargo `cdylib` — no `phpize` step. |
34 | | - |
35 | | -```sh |
36 | | -git clone https://github.com/eddmann/php-quickjs && cd php-quickjs |
37 | | -make build # -> target/debug/libphp_quickjs.so |
38 | | -make test # Rust unit tests + PHP integration suite |
39 | | -``` |
40 | | - |
41 | | -Load it and run your first guest: |
| 3 | +Run untrusted **JavaScript or TypeScript inside PHP** — safely, with a typed, |
| 4 | +bidirectional bridge. |
| 5 | + |
| 6 | +`php-quickjs` embeds the [QuickJS-NG](https://github.com/quickjs-ng/quickjs) engine |
| 7 | +directly in your PHP process. Guest code runs in an isolated context with memory, |
| 8 | +time, and stack limits; PHP exposes a controlled allowlist of capabilities into JS; |
| 9 | +and values, functions, and errors cross the boundary both ways. Guest code may be |
| 10 | +TypeScript — it's transpiled in-process and runtime errors map back to the original |
| 11 | +TS source. |
| 12 | + |
| 13 | +Built in Rust with [`ext-php-rs`](https://github.com/davidcole1340/ext-php-rs) and |
| 14 | +[`rquickjs`](https://github.com/DelSkayn/rquickjs). QuickJS-NG is bundled — no system |
| 15 | +library required. |
| 16 | + |
| 17 | +## Features |
| 18 | + |
| 19 | +- **Isolated guest** — memory, time, and stack limits contain runaway code. |
| 20 | +- **Capability allowlist** — JS only sees the PHP functions you expose, as a frozen |
| 21 | + `php.module.fn()` SDK. |
| 22 | +- **Bidirectional** — JS calls back into PHP, functions pass both ways, and opaque |
| 23 | + handles wrap live PHP objects. |
| 24 | +- **TypeScript built in** — transpiled with [`oxc`](https://github.com/oxc-project/oxc); |
| 25 | + errors map to the original TS line and column. |
| 26 | +- **Typed exceptions** — guest failures surface as `QuickJSEvalException` with a |
| 27 | + JS-like message and stack. |
| 28 | + |
| 29 | +## Quick example |
42 | 30 |
|
43 | 31 | ```php |
44 | 32 | <?php |
45 | | -// hello.php — run with: |
46 | | -// php -d extension=$(pwd)/target/debug/libphp_quickjs.so hello.php |
47 | | - |
48 | 33 | $js = new QuickJS(memoryLimit: 64 * 1024 * 1024, timeoutMs: 1000); |
49 | 34 |
|
50 | 35 | $js->register('log.info', fn(string $m) => error_log("[js] $m")); |
51 | 36 | $js->register('fetchUser', fn(int $id) => ['name' => 'Ada', 'orders' => [1, 2, 3]]); |
52 | 37 |
|
53 | 38 | echo $js->eval(<<<'TS' |
54 | 39 | php.log.info("starting"); |
55 | | - const u = php.fetchUser(42); // reenters PHP |
| 40 | + const u = php.fetchUser(42); // re-enters PHP |
56 | 41 | `${u.name} has ${u.orders.length} orders`; |
57 | 42 | TS); |
58 | 43 | // => "Ada has 3 orders" |
59 | 44 | ``` |
60 | 45 |
|
61 | | -More to copy from: [`examples/kitchen_sink.php`](examples/kitchen_sink.php) (every |
62 | | -feature), [`examples/modes.php`](examples/modes.php), and |
63 | | -[`examples/usage.php`](examples/usage.php). |
64 | | - |
65 | | -## API |
| 46 | +Run it with `php -d extension=/path/to/libphp_quickjs.so hello.php`. More to copy from |
| 47 | +[`examples/`](examples): `kitchen_sink.php`, `modes.php`, `usage.php`. |
66 | 48 |
|
67 | | -### `new QuickJS(?int $memoryLimit = null, ?int $timeoutMs = null, ?int $maxStack = null, bool $isolated = false)` |
68 | | -Limits default to unbounded; pass non-zero values to contain resource abuse. |
69 | | -`isolated: true` runs each `eval()` in a fresh realm (see |
70 | | -[execution modes](docs/execution-modes.md)). |
| 49 | +## Installation |
71 | 50 |
|
72 | | -### `register(string $name, callable $fn, ?string $types = null): void` |
73 | | -Expose a PHP callable to JS under a flat, dotted name — it becomes |
74 | | -`php.<dotted.name>(...)` in the guest. `$types` is an optional TypeScript |
75 | | -signature surfaced by `dts()`. This flat registry is the **entire** trust |
76 | | -boundary. |
| 51 | +Prebuilt binaries are attached to each |
| 52 | +[release](https://github.com/eddmann/php-quickjs/releases) for PHP 8.4 / 8.5 — |
| 53 | +self-hosted Linux, AWS Lambda (a ready Bref layer), and macOS (Apple Silicon). Enable |
| 54 | +the one matching your platform: |
77 | 55 |
|
78 | | -### `eval(string $code): mixed` |
79 | | -Run TypeScript or JavaScript and marshal the result back to PHP. Errors raise a |
80 | | -`QuickJSEvalException` located at the original TS line/column (see |
81 | | -[errors](docs/errors.md)). |
| 56 | +```ini |
| 57 | +; php.ini |
| 58 | +extension=/path/to/php-quickjs-...so |
| 59 | +``` |
82 | 60 |
|
83 | | -### `grant(mixed $resource): int` / `resolve(int $h): mixed` / `revoke(int $h): bool` |
84 | | -Capability handles for live, stateful objects (DB connections, file handles). |
85 | | -The object stays host-side; JS only ever sees an opaque integer it can pass back |
86 | | -to a capability. The handle **is** the capability. |
| 61 | +Or build from source (Rust 1.96+, clang, PHP dev headers — a plain cargo `cdylib`, no |
| 62 | +`phpize`): |
87 | 63 |
|
88 | | -```php |
89 | | -$pdo = new PDO('sqlite:app.db'); |
90 | | -$h = $js->grant($pdo); |
91 | | -$js->register('db.query', fn(int $handle, string $sql) => $js->resolve($handle)->query($sql)->fetchAll()); |
| 64 | +```sh |
| 65 | +git clone https://github.com/eddmann/php-quickjs && cd php-quickjs |
| 66 | +make build |
92 | 67 | ``` |
93 | 68 |
|
94 | | -### `manifest(): array` / `dts(): string` |
95 | | -The registration manifest and a generated TypeScript `.d.ts` for the `php` |
96 | | -global, both from the same source of truth. |
| 69 | +→ Full platform matrix, Docker, and AWS Lambda / Bref instructions: |
| 70 | +**[docs/install.md](docs/install.md)**. |
97 | 71 |
|
98 | | -## How it works (in brief) |
| 72 | +## How it works |
99 | 73 |
|
100 | 74 | ``` |
101 | 75 | PHP (trusted) ──ext-php-rs──► Rust bridge ──rquickjs──► QuickJS (untrusted) |
102 | 76 | register() dispatch table php.module.fn() |
103 | 77 | eval() __host(name, bytes) frozen php.* facade |
104 | 78 | ``` |
105 | 79 |
|
106 | | -Everything the guest reaches goes through a **single** `__host(name, argsBytes)` |
107 | | -import and a flat dispatch table — the namespaced `php.*` tree is cosmetic JS, |
108 | | -built from the manifest and **frozen**. Values cross as MessagePack; functions |
109 | | -cross both ways as references backed by registries; errors bridge both ways and |
110 | | -remap to TS coordinates. |
111 | | - |
112 | | -→ Full details in **[docs/architecture.md](docs/architecture.md)**. |
113 | | - |
114 | | -### TypeScript |
115 | | - |
116 | | -`eval()` accepts TS (the Bun model: transpile-and-go, no type-checking on the hot |
117 | | -path). Types/`interface`/generics erase; the transpile result is cached by |
118 | | -content hash; the source map stays host-side and is used only to remap errors. |
119 | | -→ [docs/architecture.md#the-typescript-fast-path](docs/architecture.md#the-typescript-fast-path). |
120 | | - |
121 | | -### Execution modes |
| 80 | +Everything the guest reaches goes through a single `__host` import and a flat dispatch |
| 81 | +table; the namespaced `php.*` tree is frozen JS built from your registrations. Values |
| 82 | +cross as MessagePack, functions as references backed by registries, and errors bridge |
| 83 | +both ways — remapping to TS coordinates on the way out. |
122 | 84 |
|
123 | | -By default, all `eval()` calls on an instance share one **persistent** global |
124 | | -realm (a REPL-like session; state and callbacks carry over). Pass |
125 | | -`isolated: true` to run **each `eval()` in its own fresh realm** (a stateless |
126 | | -script runner). → [docs/execution-modes.md](docs/execution-modes.md). |
| 85 | +→ **[docs/architecture.md](docs/architecture.md)** for the full design. |
127 | 86 |
|
128 | | -### Sandbox & security |
| 87 | +## Scope |
129 | 88 |
|
130 | | -| Layer | Contains | |
131 | | -|-------|----------| |
132 | | -| frozen `php.*` + flat dispatch table | what JS can *name* / reach | |
133 | | -| capability handles | which live objects JS can *use* | |
134 | | -| `memoryLimit` / `timeoutMs` / `maxStack` | resource abuse (loops, alloc bombs) | |
135 | | -| **outer microVM / gVisor** | QuickJS C memory-corruption → host RCE | |
136 | | - |
137 | | -These are resource guards; the extension is the embedder, not a memory-safety |
138 | | -boundary. For hostile code, add an outer VM. |
| 89 | +This is an *embedder*, not a standalone defence against hostile code. The capability |
| 90 | +model contains *what JS can reach*; the resource limits contain *abuse* (infinite |
| 91 | +loops, alloc bombs). QuickJS C memory-corruption bugs are **not** contained — for |
| 92 | +attacker-controlled code, nest the extension inside an outer microVM / gVisor boundary. |
139 | 93 |
|
140 | 94 | ## Documentation |
141 | 95 |
|
142 | | -- [Installation](docs/install.md) — prebuilt binaries for self-hosted PHP, AWS |
143 | | - Lambda (Bref), and macOS; or build from source. |
144 | | -- [Architecture](docs/architecture.md) — the bridge, marshaling, function |
145 | | - passing, security model. |
146 | | -- [Execution modes](docs/execution-modes.md) — realms, shared vs. isolated, |
| 96 | +- [Installation](docs/install.md) — prebuilt binaries, Docker, AWS Lambda (Bref), and |
| 97 | + building from source. |
| 98 | +- [API reference](docs/api.md) — the `QuickJS` class and every method. |
| 99 | +- [Architecture](docs/architecture.md) — the bridge, marshaling, function passing, and |
| 100 | + security model. |
| 101 | +- [Execution modes](docs/execution-modes.md) — shared vs. isolated realms and the |
147 | 102 | callback lifecycle. |
148 | | -- [Errors](docs/errors.md) — typed exceptions, both-way bridging, TS remapping. |
149 | | - |
150 | | -## Project layout |
151 | | - |
152 | | -``` |
153 | | -src/lib.rs QuickJS class + module src/handles.rs capability handle table |
154 | | -src/engine.rs runtime/realms, re-entrancy src/sandbox.rs memory/stack/timeout limits |
155 | | -src/bridge.rs __host dispatch, frozen facade src/error.rs exception bridging + TS remap |
156 | | -src/marshal.rs value <-> msgpack <-> zval src/exceptions.rs typed exception classes |
157 | | -src/callback.rs Js\Callback (JS fn -> PHP) src/manifest.rs manifest + .d.ts generation |
158 | | -src/transpile.rs oxc TS->JS + cache src/js/*.js in-sandbox codec + runtime |
159 | | -docs/ implementation guide examples/ runnable demos |
160 | | -tests/php/ integration suite |
161 | | -``` |
| 103 | +- [Errors](docs/errors.md) — typed exceptions, both-way bridging, and TypeScript |
| 104 | + remapping. |
162 | 105 |
|
163 | 106 | ## License |
164 | 107 |
|
|
0 commit comments