Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.git
.cache
target
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
phpts: nts

- name: Set up Rust
uses: dtolnay/rust-toolchain@1.96
uses: dtolnay/rust-toolchain@1.96.1
with:
components: rustfmt

Expand All @@ -54,6 +54,7 @@ jobs:

- name: PHP integration suite
run: |
composer install --no-interaction --prefer-dist
EXT="$(pwd)/target/debug/libphp_quickjs.so"
php -d extension="$EXT" -r 'exit(class_exists("QuickJS") ? 0 : 1);' \
|| { echo "extension failed to load"; exit 1; }
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/target
/vendor
**/*.rs.bk
*.so
.*
34 changes: 1 addition & 33 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "php_quickjs"
version = "0.0.2"
version = "0.0.3"
edition = "2021"
rust-version = "1.96"
description = "Embed a QuickJS sandbox in PHP with typed, bidirectional communication"
Expand All @@ -24,9 +24,6 @@ rquickjs = { version = "0.12", default-features = false, features = [
"classes",
"array-buffer",
] }
serde = { version = "1", features = ["derive"] }
serde_bytes = "0.11"
rmp-serde = "1"
serde_json = "1"

# TypeScript fast path: transpile guest TS -> JS in-process, remap errors.
Expand Down
33 changes: 33 additions & 0 deletions Dockerfile-dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
FROM composer:2 AS composer

FROM php:8.5-cli-bookworm

ARG RUST_VERSION=1.96.1

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
clang \
curl \
git \
libclang-dev \
make \
pkg-config \
unzip \
&& rm -rf /var/lib/apt/lists/*

ENV CARGO_HOME=/opt/cargo \
RUSTUP_HOME=/opt/rustup \
PATH=/opt/cargo/bin:${PATH}

COPY --from=composer /usr/bin/composer /usr/local/bin/composer

RUN curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs \
| sh -s -- -y --no-modify-path --profile minimal --default-toolchain "${RUST_VERSION}" \
&& rustup component add rustfmt clippy --toolchain "${RUST_VERSION}"

WORKDIR /workspace

ENV CARGO_TARGET_DIR=/workspace/target/docker

CMD ["make", "test"]
14 changes: 10 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ else
CARGO_FLAGS :=
endif

EXT := $(CURDIR)/target/$(PROFILE)/libphp_quickjs.so
EXT_SUFFIX := $(if $(filter Darwin,$(shell uname -s)),dylib,so)
TARGET_DIR := $(if $(CARGO_TARGET_DIR),$(CARGO_TARGET_DIR),$(CURDIR)/target)
EXT := $(TARGET_DIR)/$(PROFILE)/libphp_quickjs.$(EXT_SUFFIX)
PHP := php -d extension=$(EXT)

.PHONY: all build release test test-rust test-php stubs example clean fmt
.PHONY: all build release test test-rust test-php test-docker stubs example clean fmt

all: build

Expand All @@ -26,6 +28,9 @@ release:
# Rust unit tests (marshaling, manifest, facade) + the PHP integration suite.
test: build test-rust test-php

test-docker:
docker compose run --rm --build dev

test-rust:
cargo test --lib

Expand All @@ -40,8 +45,9 @@ test-php: build
# Regenerate the IDE stub for the PHP-facing classes (requires cargo-php:
# cargo install cargo-php).
stubs:
cargo php stubs --stdout > stubs/php_quickjs.stubs.php || \
echo "cargo-php not installed; run 'cargo install cargo-php'"
@tmp=$$(mktemp stubs/php_quickjs.stubs.php.XXXXXX); \
trap 'rm -f "$$tmp"' EXIT HUP INT TERM; \
cargo php stubs --stdout > "$$tmp" && mv "$$tmp" stubs/php_quickjs.stubs.php

example: build
@for ex in examples/*.php; do \
Expand Down
55 changes: 48 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ 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.
TS source. Pass `typescript: false` to `eval()` for JavaScript that should run
directly without transpilation.

Built in Rust with [`ext-php-rs`](https://github.com/davidcole1340/ext-php-rs) and
[`rquickjs`](https://github.com/DelSkayn/rquickjs). QuickJS-NG is bundled — no system
Expand Down Expand Up @@ -90,9 +91,18 @@ Or build from source (Rust 1.96+, clang, PHP dev headers — a plain cargo `cdyl

```sh
git clone https://github.com/eddmann/php-quickjs && cd php-quickjs
make build
make release
```

To build and test with PHP 8.5 + Rust 1.96.1 in Docker:

```sh
docker compose run --rm --build dev
```

The repository is mounted at `/workspace`; Cargo output stays in
`target/docker`, and dependency caches stay under `.cache/`.

→ Full platform matrix, Docker, and AWS Lambda / Bref instructions:
**[docs/install.md](docs/install.md)**.

Expand All @@ -101,16 +111,40 @@ make build
```
PHP (trusted) ──ext-php-rs──► Rust bridge ──rquickjs──► QuickJS (untrusted)
register() dispatch table php.module.fn()
eval() __host(name, bytes) frozen php.* facade
eval() __host(name, args) 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.
Registered PHP capabilities go through one direct native entry point and a flat
dispatch table; the namespaced `php.*` tree is frozen JS built from your
registrations. The separate `quickjs.postMessage()` sink copies data into a
bounded native queue. Host calls, eval, saved callbacks and messages use native
conversion. Functions cross as registry references, and errors bridge both
ways with TS source locations.

→ **[docs/architecture.md](docs/architecture.md)** for the full design.

## Asynchronous execution

`eval()` and `Js\Callback` automatically await returned Promises. External I/O
yields through Revolt using php-tokio's Fiber model; rejections become PHP
exceptions. Manual job APIs remain available for detached work. See
[asynchronous execution](docs/async.md).

Async consumers can send data-only notifications with `quickjs.postMessage(value)`
and read them using `QuickJS::drainMessages()`. The native queue snapshots values
at send time and has a configurable aggregate byte limit. `timeoutMs` remains an
optional wall-clock limit for a complete call, including Promise waits.

Resource budgets are separate: `memoryLimit` bounds the QuickJS heap, while
`maxQueuedMessageBytes` bounds retained native messages (32 MiB by default,
including accounting overhead). Native value conversion permits nesting up to
64 levels; it has no separate 16 MiB value limit. The TypeScript LRU cache defaults to
at most 256 entries and 32 MiB of source, generated JavaScript and source-map
strings. Set constructor arguments `transpileCacheMaxBytes` and
`transpileCacheMaxEntries` to customize these limits; both are non-negative, and
`0` in either disables caching. This cache budget does not bound temporary Oxc allocations. Direct
JavaScript evaluation with `typescript: false` bypasses Oxc and this cache.

## Scope

This is an *embedder*, not a standalone defence against hostile code. The capability
Expand All @@ -130,6 +164,13 @@ attacker-controlled code, nest the extension inside an outer microVM / gVisor bo
- [Errors](docs/errors.md) — typed exceptions, both-way bridging, and TypeScript
remapping.

## Development

`make stubs` regenerates the canonical IDE declarations atomically; failed
generation leaves the previous file intact. The PHP test suite compares these
signatures against the loaded extension. Consumers such as puphpeteer copy this
file for static analysis and should keep their copy synchronized.

## License

MIT
9 changes: 9 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "xtrime/php-quickjs-dev",
"description": "Development dependencies for php-quickjs integration tests",
"license": "MIT",
"require-dev": {
"amphp/amp": "^3.1",
"revolt/event-loop": "^1.0"
}
}
Loading