|
| 1 | +# Engine and module development (C) |
| 2 | + |
| 3 | +This document covers C development inside the njs repository: the engine |
| 4 | +core (`src/`), the external module wrappers (`external/`), the NGINX |
| 5 | +modules (`nginx/`), and the build system (`auto/`, `configure`). |
| 6 | + |
| 7 | +For per-task orientation see the top-level [AGENTS.md](../../AGENTS.md). |
| 8 | + |
| 9 | +## Building |
| 10 | + |
| 11 | +njs has no autotools/cmake. The shell-based `configure` script generates |
| 12 | +`build/Makefile`. Always run `make clean` before reconfiguring with |
| 13 | +different options — the Makefile is not regenerated in place. |
| 14 | + |
| 15 | +### Standalone CLI |
| 16 | + |
| 17 | +```bash |
| 18 | +./configure |
| 19 | +make -j$(nproc) njs # build/njs |
| 20 | +``` |
| 21 | + |
| 22 | +### njs with QuickJS backend |
| 23 | + |
| 24 | +QuickJS is built separately and linked into njs. |
| 25 | + |
| 26 | +```bash |
| 27 | +# Build libquickjs.a in the QuickJS source tree |
| 28 | +( cd <QUICKJS_SRC> && CFLAGS=-fPIC make libquickjs.a ) |
| 29 | + |
| 30 | +# Configure njs to use it |
| 31 | +make clean |
| 32 | +./configure \ |
| 33 | + --cc-opt='-I<QUICKJS_SRC>' \ |
| 34 | + --ld-opt='-L<QUICKJS_SRC>' |
| 35 | +make -j$(nproc) njs |
| 36 | +``` |
| 37 | + |
| 38 | +### NGINX module (static / dynamic) |
| 39 | + |
| 40 | +njs builds as an NGINX module from a separate NGINX source tree. |
| 41 | + |
| 42 | +```bash |
| 43 | +# Static |
| 44 | +cd <NGINX_SRC> |
| 45 | +./auto/configure --add-module=<NJS_SRC>/nginx --with-stream --with-debug |
| 46 | +make -j$(nproc) |
| 47 | + |
| 48 | +# Dynamic |
| 49 | +./auto/configure --add-dynamic-module=<NJS_SRC>/nginx --with-stream |
| 50 | +make -j$(nproc) modules |
| 51 | +``` |
| 52 | + |
| 53 | +Adding `--with-cc-opt='-I<QUICKJS_SRC>'` and |
| 54 | +`--with-ld-opt='-L<QUICKJS_SRC>'` enables QuickJS in the NGINX module. |
| 55 | + |
| 56 | +### AddressSanitizer build |
| 57 | + |
| 58 | +```bash |
| 59 | +./auto/configure --add-module=<NJS_SRC>/nginx --with-stream --with-debug \ |
| 60 | + --with-cc=clang \ |
| 61 | + --with-cc-opt='-O0 -fsanitize=address' \ |
| 62 | + --with-ld-opt='-fsanitize=address' |
| 63 | +make -j$(nproc) |
| 64 | +``` |
| 65 | + |
| 66 | +The njs `configure` exposes `--address-sanitizer=YES` directly when |
| 67 | +building the CLI; prefer `clang` on arm64 (gcc ASan is slow there). |
| 68 | + |
| 69 | +### Configure options (njs) |
| 70 | + |
| 71 | +| Option | Purpose | |
| 72 | +|---|---| |
| 73 | +| `--cc=FILE` | C compiler (default: gcc) | |
| 74 | +| `--cc-opt=OPTIONS` | Additional CFLAGS | |
| 75 | +| `--ld-opt=OPTIONS` | Additional LDFLAGS | |
| 76 | +| `--debug=YES` | Runtime checks | |
| 77 | +| `--debug-memory=YES` | Memory allocation tracing | |
| 78 | +| `--debug-opcode=YES` | Per-instruction execution trace | |
| 79 | +| `--debug-generator=YES` | Bytecode generator trace | |
| 80 | +| `--address-sanitizer=YES` | AddressSanitizer (use with `clang`) | |
| 81 | +| `--with-quickjs` | Require QuickJS to be present | |
| 82 | +| `--no-openssl` / `--no-libxml2` / `--no-zlib` | Drop optional deps | |
| 83 | + |
| 84 | +Run `./configure --help` for the complete list. |
| 85 | + |
| 86 | +## Testing |
| 87 | + |
| 88 | +```bash |
| 89 | +make unit_test # 5800+ language and API tests |
| 90 | +make lib_test # internal data structures (hash, rbtree, unicode) |
| 91 | +make test262 # ECMAScript test262 compliance suite |
| 92 | +make test # shell tests + unit_test + test262 |
| 93 | +``` |
| 94 | + |
| 95 | +NGINX integration tests live under `nginx/t/` and use Perl's `prove` |
| 96 | +harness against `Test::Nginx`. |
| 97 | + |
| 98 | +```bash |
| 99 | +TMPDIR=$(mktemp -d) \ |
| 100 | +TEST_NGINX_BINARY=<NGINX_BIN> \ |
| 101 | + prove -I <TESTS_LIB> nginx/t/ |
| 102 | +``` |
| 103 | + |
| 104 | +Useful environment variables: |
| 105 | + |
| 106 | +| Variable | Effect | |
| 107 | +|---|---| |
| 108 | +| `TEST_NGINX_BINARY` | Path to the nginx binary (required) | |
| 109 | +| `TEST_NGINX_VERBOSE=1` | Verbose harness output | |
| 110 | +| `TEST_NGINX_LEAVE=1` | Keep test artifacts in `$TMPDIR/nginx-test-*` | |
| 111 | +| `TEST_NGINX_CATLOG=1` | Dump `error.log` after the run | |
| 112 | +| `TEST_NGINX_GLOBALS=<conf>` | Inject global-scope config (e.g. `load_module ...`) | |
| 113 | +| `TEST_NGINX_GLOBALS_HTTP='js_engine qjs;'` | Run http tests under QuickJS | |
| 114 | +| `TEST_NGINX_GLOBALS_STREAM='js_engine qjs;'` | Same, for stream tests | |
| 115 | + |
| 116 | +Use a per-run `TMPDIR=$(mktemp -d)` to isolate artifacts across concurrent |
| 117 | +runs and avoid destructive `rm -fr /tmp/nginx-test*`. |
| 118 | + |
| 119 | +For more on the harness see `<TESTS_LIB>/Test/Nginx.pm`. |
| 120 | + |
| 121 | +## Validation checklist |
| 122 | + |
| 123 | +Before submitting a change: |
| 124 | + |
| 125 | +1. `./configure && make -j$(nproc)` compiles without warnings (`-Werror`). |
| 126 | +2. `make unit_test` and `make lib_test` pass. |
| 127 | +3. If you touched `src/`, also run `make test262`. |
| 128 | +4. If you touched `nginx/`, run `prove -I <TESTS_LIB> nginx/t/`, |
| 129 | + once with the default engine and once with |
| 130 | + `TEST_NGINX_GLOBALS_HTTP='js_engine qjs;'`. |
| 131 | +5. New source files: update `auto/sources` (njs core), |
| 132 | + `auto/modules` (njs external modules), or |
| 133 | + `auto/qjs_modules` (QuickJS external modules). |
| 134 | +6. Dual-engine: if you added/changed behavior in an `njs_*.c` module, |
| 135 | + mirror it in the corresponding `qjs_*.c` (and vice versa). |
| 136 | + |
| 137 | +## Code style and commits |
| 138 | + |
| 139 | +NGINX coding style: |
| 140 | + |
| 141 | +- 4 spaces, no tabs. |
| 142 | +- 80-column line limit. |
| 143 | +- No trailing whitespace. |
| 144 | +- Newline after closing brace. |
| 145 | +- Comments explain *why*, not *what*; avoid em-dashes. |
| 146 | +- `-Werror` is on by default — fix all warnings. |
| 147 | + |
| 148 | +Commit messages: |
| 149 | + |
| 150 | +- Past tense subject (`Added X`, `Fixed Y`). |
| 151 | +- Subject ≤67 chars, body wrapped to ~80 chars. |
| 152 | +- Subject prefix: `HTTP:`, `Stream:`, `Core:`, `QuickJS:`, `Tests:`, |
| 153 | + `Modules:`, etc. |
| 154 | +- One logical change per commit; rebase/squash before submitting. |
| 155 | + |
| 156 | +## Project layout |
| 157 | + |
| 158 | +``` |
| 159 | +njs/ |
| 160 | +├── configure # build entry point |
| 161 | +├── auto/ # shell-based build system |
| 162 | +│ ├── sources # njs core source list |
| 163 | +│ ├── modules # njs external module list |
| 164 | +│ ├── qjs_modules # QuickJS external module list |
| 165 | +│ └── cc, options, ... # compiler/option detection |
| 166 | +├── src/ # engine core (C) |
| 167 | +│ ├── njs_vm.c / njs_vmcode.c # virtual machine |
| 168 | +│ ├── njs_lexer.c # tokenizer |
| 169 | +│ ├── njs_parser.c # parser |
| 170 | +│ ├── njs_generator.c # bytecode generator |
| 171 | +│ ├── njs_object.c / njs_array.c # built-in types |
| 172 | +│ ├── njs_promise.c / njs_async.c |
| 173 | +│ ├── njs_value.h # value representation |
| 174 | +│ ├── njs.h # public C API |
| 175 | +│ ├── qjs.c # QuickJS engine wrapper |
| 176 | +│ └── test/ # C unit tests |
| 177 | +├── external/ # extension modules |
| 178 | +│ ├── njs_shell.c # CLI entry point (main()) |
| 179 | +│ ├── njs_*_module.c # njs-engine modules (crypto, fs, ...) |
| 180 | +│ └── qjs_*_module.c # QuickJS-engine counterparts |
| 181 | +├── nginx/ # NGINX module integration |
| 182 | +│ ├── ngx_http_js_module.c |
| 183 | +│ ├── ngx_stream_js_module.c |
| 184 | +│ ├── ngx_js.c # core nginx-JS bindings |
| 185 | +│ ├── config # NGINX build glue |
| 186 | +│ └── t/ # Perl integration tests |
| 187 | +├── test/ # functional test suite |
| 188 | +│ ├── js/ # JS language feature tests |
| 189 | +│ ├── harness/ # test framework utilities |
| 190 | +│ └── shell_test.exp # interactive shell tests (Expect) |
| 191 | +└── ts/ # TypeScript type definitions |
| 192 | +``` |
| 193 | + |
| 194 | +Public C API: `src/njs.h`. VM internals: `src/njs_vm.h`, |
| 195 | +`src/njs_value.h`. CLI entry point: `external/njs_shell.c`. |
| 196 | + |
| 197 | +## VM architecture (njs engine) |
| 198 | + |
| 199 | +The QuickJS backend uses upstream QuickJS internals (see |
| 200 | +[bellard.org/quickjs](https://bellard.org/quickjs/)). What follows is the |
| 201 | +**njs engine** internals only. |
| 202 | + |
| 203 | +### Register-based VM |
| 204 | + |
| 205 | +Each instruction has operands that are immediate values or **indexes**. |
| 206 | +An index is encoded as: |
| 207 | + |
| 208 | +``` |
| 209 | +index | level_type (4 bits) | var_type (4 bits) |
| 210 | +``` |
| 211 | + |
| 212 | +### Level types (storage location) |
| 213 | + |
| 214 | +``` |
| 215 | +NJS_LEVEL_LOCAL = 0 // local variable in current frame |
| 216 | +NJS_LEVEL_CLOSURE = 1 // closure variable from parent frame |
| 217 | +NJS_LEVEL_GLOBAL = 2 // global variable |
| 218 | +NJS_LEVEL_STATIC = 3 // static / absolute scope |
| 219 | +``` |
| 220 | + |
| 221 | +Values are addressed as `vm->levels[NJS_LEVEL_*][index]`. |
| 222 | + |
| 223 | +### Variable types |
| 224 | + |
| 225 | +``` |
| 226 | +NJS_VARIABLE_CONST = 0 |
| 227 | +NJS_VARIABLE_LET = 1 |
| 228 | +NJS_VARIABLE_CATCH = 2 |
| 229 | +NJS_VARIABLE_VAR = 3 |
| 230 | +NJS_VARIABLE_FUNCTION = 4 |
| 231 | +``` |
| 232 | + |
| 233 | +### Bytecode example |
| 234 | + |
| 235 | +``` |
| 236 | +$ ./build/njs -d |
| 237 | +>> var a = 42; function f(v) { return v + 1 } |
| 238 | +
|
| 239 | +shell:main |
| 240 | + 1 | 00000 MOVE 0123 0133 |
| 241 | + 1 | 00024 STOP 0033 |
| 242 | +
|
| 243 | +shell:f |
| 244 | + 1 | 00000 ADD 0203 0103 0233 |
| 245 | + 1 | 00032 RETURN 0203 |
| 246 | +``` |
| 247 | + |
| 248 | +`MOVE 0123 0133` copies the value at index `0x0133` to `0x0123`. |
| 249 | +`ADD a b c` computes `a = b + c`. Indexes are printed in hex and encode |
| 250 | +level and variable type. |
| 251 | + |
| 252 | +## Object model (njs engine) |
| 253 | + |
| 254 | +For performance and footprint, a JS object is split into a **local mutable |
| 255 | +hash** for the current object and a **shared hash** holding inherited |
| 256 | +properties. Built-ins are lazily materialized: the shared definitions stay |
| 257 | +shared until first mutation. For functions, the first access copies the |
| 258 | +function from the shared hash into the local mutable hash so the |
| 259 | +per-object copy can be modified. |
| 260 | + |
| 261 | +Key entry points: |
| 262 | + |
| 263 | +- `njs_value_property()` — top-level property lookup. |
| 264 | +- `njs_property_query()` — lookup with descriptor result. |
| 265 | +- `njs_object_property_query()` — object-level walk including prototype. |
| 266 | +- `njs_prop_private_copy()` — promotion from shared to local on write. |
| 267 | + |
| 268 | +## Debugging |
| 269 | + |
| 270 | +### CLI |
| 271 | + |
| 272 | +```bash |
| 273 | +./build/njs -c '<code>' # one-shot |
| 274 | +./build/njs -d # interactive, with disassembly |
| 275 | +./build/njs -d script.js # dump bytecode for a script |
| 276 | +./build/njs -o script.js # opcode trace |
| 277 | +./build/njs -h # full option list |
| 278 | +``` |
| 279 | + |
| 280 | +Select the JavaScript engine with `-n <engine>` (case-insensitive; default |
| 281 | +is `njs`): |
| 282 | + |
| 283 | +```bash |
| 284 | +./build/njs -n njs -c 'console.log(typeof Map)' # built-in engine |
| 285 | +./build/njs -n QuickJS -c 'console.log(typeof Map)' # QuickJS backend |
| 286 | +``` |
| 287 | + |
| 288 | +`-n QuickJS` requires the binary to be built with QuickJS linked in (see |
| 289 | +[njs with QuickJS backend](#njs-with-quickjs-backend) above); otherwise |
| 290 | +the CLI reports `unknown engine "QuickJS"`. |
| 291 | + |
| 292 | +### Opcode trace |
| 293 | + |
| 294 | +Built with `--debug-opcode=YES`, `./build/njs -o script.js` prints each |
| 295 | +instruction as it executes — `ENTER`/`EXIT` for function boundaries, |
| 296 | +opcode mnemonics for everything else. Useful for confirming control flow |
| 297 | +through bytecode without a debugger. |
| 298 | + |
| 299 | +### Test failures (NGINX) |
| 300 | + |
| 301 | +With `TEST_NGINX_LEAVE=1`, each test leaves |
| 302 | +`$TMPDIR/nginx-test-<random>/` containing the generated `nginx.conf`, |
| 303 | +`error.log`, and any artifacts. `TEST_NGINX_CATLOG=1` dumps the log to |
| 304 | +stdout automatically. |
0 commit comments