NGINX dynamic HTTP module that hosts RustScript (pd-vm) at the access
phase. Location directive access_by_rss_file <path> compiles the script
once at configuration time.
Import-free scripts run synchronously to Halted on a worker-local VM
pool. Scripts that import ngx::sleep or ngx::sleep_ms bind a
request-local VM, return NGX_AGAIN while a generic host future is
pending, and resume the same VM from the eventfd callback. Client
disconnect during a wait finalizes the request as HTTP 499. Graceful
master shutdown cancels in-flight host futures instead of waiting them
out.
| Component | Pin |
|---|---|
| ngx-rust | cda9d8372ccd2e139ef2b03e5806e019e7796187 (ngx / nginx-sys 0.5.0, nginx-src 1.30.0+1.30.4) |
| NGINX | 1.30.4 (nginx_version 1030004). Tarball SHA-256 4261dc90e9e47c1c4041276e9aaa3d48ebe2e664f728e14fa95ae6c67d57a08b |
RustScript / pd-vm |
https://github.com/rustscript-lang/rustscript at 4b749862ff1632ef1266d75709da3676de37356c (Cargo git dependency package = "pd-vm" on that immutable rev; no sibling path) |
| Rust toolchain | 1.85.0 or later (ngx-rust MSRV). Verified with rustc 1.94.1 |
Required build tools on Linux:
- C compiler, make, OpenSSL, PCRE2, zlib
- clang and libclang (official
nginx-sysbindgen) - Rust 1.85+
- GNU
nmon Linux for a minimal check that the production cdylib does not define test-only NGINX C stubs
nginx-sys generates bindings from a configured NGINX source tree. The
module .so must be built against the same source and ./configure flags as
the nginx binary that loads it. Distro packages often apply ABI-breaking
patches; do not mix a distro nginx with this module.
Official nginx-sys inputs (do not invent extra variables):
NGINX_SOURCE_DIR— absolute path to the NGINX source directory after./configureNGINX_BUILD_DIR— absolute path to the build directory (objsby default, or an official--builddir=path)
Only ./configure is required for bindgen. The fixture below also runs make
so nginx -t can load the module. An explicit --builddir= path is accepted
and validated as a path-shaped configure argument.
The selected required build path is official nginx-sys bindgen against this
pinned, configured NGINX 1.30.4 tree. Pre-generated or vendored bindings are
not used: they cannot match ngx-rust cda9d8372ccd2e139ef2b03e5806e019e7796187
and NGINX 1.30.4. Install clang and libclang so rust-bindgen can generate
bindings at cargo build.
Clear inherited CFLAGS / BINDGEN_EXTRA_CLANG_ARGS before ./configure and
cargo build. A leaked include path from another NGINX tree will change
ngx_auto_config.h and break the link.
# Run from the crate root. Capture absolute paths before changing directories.
CRATE_ROOT="$PWD"
NGINX_SOURCE_DIR="$CRATE_ROOT/nginx-1.30.4"
NGINX_BUILD_DIR="$NGINX_SOURCE_DIR/objs"
# 1. NGINX 1.30.4 with dynamic-module compat
curl -fL -o nginx-1.30.4.tar.gz https://nginx.org/download/nginx-1.30.4.tar.gz
echo '4261dc90e9e47c1c4041276e9aaa3d48ebe2e664f728e14fa95ae6c67d57a08b nginx-1.30.4.tar.gz' | sha256sum -c
tar -xf nginx-1.30.4.tar.gz
cd "$NGINX_SOURCE_DIR"
unset CFLAGS CPPFLAGS BINDGEN_EXTRA_CLANG_ARGS
./configure --prefix=/usr/local/nginx-1.30.4 --with-compat --with-http_ssl_module
make -j"$(nproc)"
# 2. return to the crate and build the cdylib against that exact tree
cd "$CRATE_ROOT"
export NGINX_SOURCE_DIR
export NGINX_BUILD_DIR
unset CFLAGS CPPFLAGS BINDGEN_EXTRA_CLANG_ARGS
# Use the git CLI when GitHub HTTPS is rewritten to SSH (libgit2 cannot auth).
export CARGO_NET_GIT_FETCH_WITH_CLI=true
# Honor a workspace CARGO_TARGET_DIR; otherwise use <crate>/target.
: "${CARGO_TARGET_DIR:=$CRATE_ROOT/target}"
export CARGO_TARGET_DIR
cargo build --lib
# Absolute module path derived from the same target dir used above.
export NGX_RUSTSCRIPT_MODULE="$(readlink -f "$CARGO_TARGET_DIR/debug/libngx_rustscript.so")"cargo build --lib is the supported standalone path. It enables the
export-modules feature so the cdylib dynamically exports ngx_modules,
ngx_module_names, and ngx_module_order for load_module. The HTTP
module object is named ngx_http_rustscript_module inside that table; it
is not a dynamic symbol.
Standalone cargo test validates the canonical $NGINX_BUILD_DIR/nginx
binary, nginx/1.30.4, --with-compat, and --with-http_ssl_module in the
module-load harness before nginx -t. Quoted configure paths, --prefix,
--builddir, --add-dynamic-module, and other valid NGINX flags are
accepted.
Tests resolve ${CARGO_TARGET_DIR:-<crate>/target}/debug/libngx_rustscript.so
(or NGX_RUSTSCRIPT_MODULE when set) and pass that absolute path to
load_module. A missing or non-regular shared library is rejected. nm -D --defined-only rejects a production cdylib that defines test-only NGINX C
stubs. nginx -t, real module load, and HTTP are the acceptance evidence.
Tests also require the selected nginx binary to be canonical
$NGINX_BUILD_DIR/nginx reporting nginx/1.30.4 with --with-compat and
--with-http_ssl_module. Fixture nginx.conf.in interpolates quoted paths;
unsafe bytes (NUL, newline, non-UTF-8) are rejected fail-closed, and
nginx -t receives -p / -c as OsStr.
config, config.make, and auto/rust follow ngx-rust
examples/{config,config.make,auto/rust} at the same git revision. That path
builds a staticlib with --no-default-features (no ngx_modules table;
NGINX generates it). Use it only when configuring NGINX with
--add-dynamic-module=/path/to/ngx-rustscript.
Full configure + parallel make + nginx -t is tests/verify_add_dynamic.sh
(a standalone script so nested cargo rustc cannot deadlock a parent
cargo test). After cargo build --lib, run:
export CARGO_TARGET_DIR
export NGINX_SOURCE_DIR
bash tests/verify_add_dynamic.shLoad the cdylib and attach a script to a location:
load_module /absolute/path/to/libngx_rustscript.so;
http {
server {
listen 127.0.0.1:8080;
location /t {
access_by_rss_file /absolute/path/to/script.rss;
}
location /plain/ {
}
}
}access_by_rss_file is location-only and takes exactly one argument.
Relative paths resolve with NGINX ngx_conf_full_name against the conf
prefix (the directory of the config file). Absolute paths are unchanged.
The file is read once at nginx -t / reload, must be a regular UTF-8 file,
and is compiled with compile_source.
Config accepts catalogued ngx hosts in the access phase, including timers,
codec, time, metadata, constants, and ngx::log. print is not a host (use ngx::log); runtime::sleep, unknown imports, and
phase-forbidden imports are rejected at configuration time. Unknown
modules fail compile or import check. Duplicate directives in the same
location are rejected. Unset nested locations inherit the parent
program; an explicit child directive overrides. Unconfigured locations
keep the original access-phase decline.
Prove load with an isolated prefix:
export NGX_RUSTSCRIPT_NGINX="$NGINX_BUILD_DIR/nginx"
: "${CARGO_TARGET_DIR:=$CRATE_ROOT/target}"
export CARGO_TARGET_DIR
cargo build --lib
export NGX_RUSTSCRIPT_MODULE="$(readlink -f "$CARGO_TARGET_DIR/debug/libngx_rustscript.so")"
cargo test --test module_load -- --nocapture
nginx -t -p /path/to/prefix -c conf/nginx.confImport-free access script (runs to Halted, then the content phase):
let x = 1 + 2;One sleep, then the content phase:
use ngx;
ngx::sleep(0.03);Millisecond sleep for integer delays:
use ngx;
ngx::sleep_ms(30);Two sequential sleeps:
use ngx;
ngx::sleep(0.01);
ngx::sleep_ms(10);ngx::sleep is a pd-host-function async host (seconds:number -> null).
seconds is an OpenResty-style integer or float; negative, NaN, infinity,
and duration overflow are rejected. ngx::sleep_ms is the millisecond
integer form (ms:int -> null). The generated wrapper submits an owned
host future; the impl validates the duration and awaits Tokio. The first
host entry returns VmStatus::Waiting. Two sequential sleeps reuse the
same generation-safe request identity and each wait once.
On each request the access handler leases a worker-thread-local VM via
Vm::new_shared(Arc<Program>). Import-free programs run to
VmStatus::Halted and return NGX_OK so the content phase proceeds. After
Halted and after a recoverable runtime error the VM is reset with
Vm::reset_for_reuse and parked only when Vm::is_reusable is true. Idle
pool size is bounded. VM errors and unexpected Waiting on import-free
programs fail the request with 500.
Scripts that imported ngx::sleep or ngx::sleep_ms bind a request-local
VM instead of the pool. Waiting returns NGX_AGAIN without incrementing
r->main->count.
The eventfd callback polls the waiting host op, resumes the same VM on the
worker main thread, and continues ngx_http_core_run_phases exactly once
so the access handler can return NGX_OK. Host-bound waiting VMs are never
returned to the pool.
The Release A ngx host surface (signatures, phases, yield behavior, limits, and OpenResty adaptations) is listed in docs/ngx-api-compatibility.md.
- Worker runtime is created only in NGINX worker
init_processafter fork, and only whenngx_processisNGX_PROCESS_WORKERorNGX_PROCESS_SINGLE. Helper processes skip initialization.nginx -t, master, and config never create Tokio threads or the eventfd.exit_processshuts the runtime down and is idempotent. - Tokio features:
rt+rt-multi-thread+timeonly (no HTTP/network client). One worker thread namedngx-rustscript-async. - Async wait capacity is
DEFAULT_MAX_IN_FLIGHT(1024). Reserve is rejected when full. Host futures and wakers own only a queue sender and request identity — no NGINX request/pool/VM pointers cross into Tokio, and Tokio never callsngx_post_event. - Completions enqueue the request identity under a mutex, then write
eventfd(2)created withEFD_NONBLOCK|EFD_CLOEXEC. The worker main thread registers the fd withngx_get_connection. The pinned Linux epoll path usesaddwithEPOLLIN. - Client close while a wait is in flight is detected with a worker-thread
wrapper around
ngx_http_test_reading. Confirmed close finalizes once withNGX_HTTP_CLIENT_CLOSED_REQUEST(access log 499), then request-pool cleanup invalidates the identity and drops the host future. - Worker
exit_processrejects new work, invalidates senders/wakers, cancels/drops host futures, unregisters/closes eventfd once, and clears worker registries without touching request pointers. Long sleeps do not delay graceful worker exit (SIGQUIT). pd-vmpin4b749862ff1632ef1266d75709da3676de37356c, packagespd-vmandpd-host-function. This pin hasVm::new_shared(Arc<Program>),HostAsyncBridge,Vm::set_async_bridge,VmStatus::Waiting,Vm::poll_waiting_host_op, andVm::resume.try_newonly wraps ownedVm::newafter frame-limit validation. Normal termination isOk(VmStatus::Halted). Drop ofVmowns cancellation and resource closure.
export NGINX_SOURCE_DIR
export NGINX_BUILD_DIR
export CARGO_TARGET_DIR
export NGX_RUSTSCRIPT_MODULE="$(readlink -f "$CARGO_TARGET_DIR/debug/libngx_rustscript.so")"
cargo test
python3 -m py_compile tests/nginx_integration.py tests/test_nginx_integration_unit.py
python3 tests/test_nginx_integration_unit.py
python3 tests/nginx_integration.py --nginx <absolute pinned nginx> --module <absolute actual cdylib>
bash tests/verify_add_dynamic.shThe standalone runner:
python3 tests/nginx_integration.py --nginx <absolute pinned nginx> --module <absolute actual cdylib>It checks that the nginx binary and module paths are absolute, that nginx
reports nginx/1.30.4 with --with-compat and --with-http_ssl_module,
and that nginx -t accepts a valid sleep config while missing,
syntax, unsupported-import, and wrong-arity fixtures fail with those
causes. Against a live isolated master (master_process on,
worker_processes 1, foreground) it checks that a 2s sleep does not block
/plain/ (plain returns HTTP 200 first), that one-sleep and two-sleep
routes return distinct 200 bodies, that 12 overlapping requests across two
routes keep per-route bodies, that a client close of a 30s sleep logs
access status 499 and leaves later routes working, and that SIGQUIT of that
master exits successfully while a long sleep is in flight.
Python stdlib only. The runner owns its prefix, config, pid, logs, and
loopback port, and reaps only its own process. Isolated prefixes go under
tempfile.gettempdir() (typically $TMPDIR). Set NGX_RUSTSCRIPT_TASK_TMP
to a directory to place them elsewhere.
dlopen() ... failed— the module path is wrong, not a regular file, or was built against a different NGINX ABI than the binary that loads it.nginx version is ..., expected nginx/1.30.4— use the pinned 1.30.4 binary from$NGINX_BUILD_DIR/nginx, not a distro package.- bindgen / include errors — unset
CFLAGS,CPPFLAGS, andBINDGEN_EXTRA_CLANG_ARGS; install clang and libclang; pointNGINX_SOURCE_DIR/NGINX_BUILD_DIRat the configured 1.30.4 tree. script imports are not supported/failed to compile script— only catalogued ngx hosts may be imported in the access phase (timers, codec, time, metadata, constants, andngx::log).printis rejected (usengx::log);runtime::sleepis rejected.- HTTP 500 from an access script — VM runtime error or unexpected Waiting on an import-free program. Check the error log.
- HTTP 499 on a sleep route — the client closed the connection before the sleep completed.
CARGO_TARGET_DIRmismatch —load_modulemust receive the cdylib built in the same target dir (NGX_RUSTSCRIPT_MODULE).
- ngx-rust — NGINX Rust SDK
- rustscript — compiler and
pd-vm