Implementation of js_shared_array directive for nginx njs modules (HTTP and Stream) that creates shared memory zones accessible as SharedArrayBuffer objects via ngx.sharedArray.foo dynamic property lookup.
Scope: QuickJS engine only (wrapped in #if (NJS_HAVE_QUICKJS))
nginx.conf: js_shared_array zone=foo:1m;
↓
JavaScript: ngx.sharedArray ← NGX_QJS_CLASS_ID_SHARED_ARRAY (exotic class)
.foo ← returns built-in SharedArrayBuffer
↓
QuickJS: ngx_qjs_ext_ngx_shared_array() getter
↓
Exotic: NGX_QJS_CLASS_ID_SHARED_ARRAY class (for ngx.sharedArray object)
↓
Lookup: ngx_qjs_shared_array_own_property()
↓
Return: JS_NewArrayBuffer(ctx, data, size, NULL, NULL, TRUE)
↑ ↑ ↑
| | is_shared
| opaque
free_func = NULL
↓
Result: Built-in SharedArrayBuffer instance (no custom class needed)
Key Distinction:
NGX_QJS_CLASS_ID_SHARED_ARRAYis for the exotic container (ngx.sharedArray)- The SharedArrayBuffer returned by property lookup is a built-in QuickJS type
Use QuickJS's JS_NewArrayBuffer() directly instead of setting vm->sab_funcs:
JSValue JS_NewArrayBuffer(JSContext *ctx, uint8_t *buf, size_t len,
JSFreeArrayBufferDataFunc *free_func, void *opaque,
JS_BOOL is_shared);buf: Pointer to pre-allocated shared memorylen: Size in bytesfree_func: NULL (nginx owns memory, not QuickJS)opaque: Not usedis_shared: TRUE (creates SharedArrayBuffer)
Pattern: ngx.sharedArray.foo (like ngx.shared.foo)
- Uses QuickJS exotic class methods
- Property handler looks up zone by name
- Returns new SharedArrayBuffer wrapping the zone
- Allocation: Entire shared zone is the backing store
- Initialization: Zero-filled on first worker
- Cleanup: None (nginx manages lifecycle)
- Reload: Reuse existing memory (preserve data)
Create new files for cleaner separation of concerns:
nginx/ngx_js_shared_array.c- Core implementation (~250 lines)nginx/ngx_js_shared_array.h- Public API and structure definitions
Why separate files?
- Distinct features (dict vs array)
- Better maintainability
ngx_js_shared_dict.cis already 4200+ lines- Single responsibility principle
File: nginx/ngx_js_shared_array.h (new file)
/*
* Copyright (C) Dmitry Volyntsev
* Copyright (C) NGINX, Inc.
*/
#ifndef _NGX_JS_SHARED_ARRAY_H_INCLUDED_
#define _NGX_JS_SHARED_ARRAY_H_INCLUDED_
#include <ngx_config.h>
#include <ngx_core.h>
#include "ngx_js.h"
typedef struct ngx_js_shared_array_s ngx_js_shared_array_t;
struct ngx_js_shared_array_s {
ngx_shm_zone_t *shm_zone;
void *data; /* SharedArrayBuffer backing store */
size_t size; /* Zone size in bytes */
/* fd for event debug (same position as ngx_connection_t) */
ngx_socket_t fd;
ngx_js_shared_array_t *next; /* Linked list */
};
/* Directive handler */
char *ngx_js_shared_array_zone(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf, void *tag);
#endif /* _NGX_JS_SHARED_ARRAY_H_INCLUDED_ */File: nginx/ngx_js.h
Add class ID (after line 65):
#if (NJS_HAVE_QUICKJS)
enum {
// ... existing ...
NGX_QJS_CLASS_ID_SHARED_DICT_ERROR,
NGX_QJS_CLASS_ID_SHARED_ARRAY, /* ADD THIS */
NGX_QJS_CLASS_ID_FETCH_HEADERS,
};
#endifModify main config macro (around line 123):
#define NGX_JS_COMMON_MAIN_CONF \
ngx_js_dict_t *dicts; \
ngx_js_shared_array_t *shared_arrays; \ /* ADD THIS */
ngx_array_t *periodicsNote: typedef is now in ngx_js_shared_array.h
File: nginx/ngx_js_shared_array.c (new file)
Pattern: Follow ngx_js_shared_dict_zone() in ngx_js_shared_dict.c (lines 2819-2991)
/*
* Copyright (C) Dmitry Volyntsev
* Copyright (C) NGINX, Inc.
*/
#include <ngx_config.h>
#include <ngx_core.h>
#include "ngx_js.h"
#include "ngx_js_shared_array.h"
#if (NJS_HAVE_QUICKJS)
#include <quickjs.h>
/* Forward declarations */
static ngx_int_t ngx_js_shared_array_init_zone(ngx_shm_zone_t *shm_zone,
void *data);
static int ngx_qjs_shared_array_own_property(JSContext *cx,
JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
static int ngx_qjs_shared_array_own_property_names(JSContext *cx,
JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj);
static JSValue ngx_qjs_ext_ngx_shared_array(JSContext *cx,
JSValueConst this_val);
/* Exotic class definition */
static JSClassDef ngx_qjs_shared_array_class = {
"SharedArray",
.finalizer = NULL,
.exotic = &(JSClassExoticMethods) {
.get_own_property = ngx_qjs_shared_array_own_property,
.get_own_property_names = ngx_qjs_shared_array_own_property_names,
},
};
/* Module definition for registration with ngx object */
static const JSCFunctionListEntry ngx_qjs_ext_ngx_shared_array_props[] = {
JS_CGETSET_DEF("sharedArray", ngx_qjs_ext_ngx_shared_array, NULL),
};
/* Directive handler */
char *
ngx_js_shared_array_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf,
void *tag)
{
ssize_t size;
ngx_str_t *value, name, s;
ngx_uint_t i;
ngx_shm_zone_t *shm_zone;
ngx_js_shared_array_t *array;
ngx_js_main_conf_t *jmcf = conf;
value = cf->args->elts;
// Parse zone=name:size
if (ngx_strncmp(value[1].data, "zone=", 5) != 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid parameter \"%V\"", &value[1]);
return NGX_CONF_ERROR;
}
name.data = value[1].data + 5;
name.len = 0;
for (i = 5; i < value[1].len; i++) {
if (value[1].data[i] == ':') {
name.len = i - 5;
break;
}
}
if (name.len == 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid zone name in \"%V\"", &value[1]);
return NGX_CONF_ERROR;
}
s.data = value[1].data + 5 + name.len + 1;
s.len = value[1].len - (5 + name.len + 1);
size = ngx_parse_size(&s);
if (size == NGX_ERROR) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid zone size in \"%V\"", &value[1]);
return NGX_CONF_ERROR;
}
if (size < (ssize_t) (8 * ngx_pagesize)) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"zone \"%V\" is too small", &name);
return NGX_CONF_ERROR;
}
// Create shared memory zone
shm_zone = ngx_shared_memory_add(cf, &name, size, tag);
if (shm_zone == NULL) {
return NGX_CONF_ERROR;
}
if (shm_zone->data) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"duplicate zone \"%V\"", &name);
return NGX_CONF_ERROR;
}
// Allocate array structure
array = ngx_pcalloc(cf->pool, sizeof(ngx_js_shared_array_t));
if (array == NULL) {
return NGX_CONF_ERROR;
}
array->shm_zone = shm_zone;
array->size = size;
array->fd = (ngx_socket_t) -1;
// Link to main config
array->next = jmcf->shared_arrays;
jmcf->shared_arrays = array;
// Set zone callbacks
shm_zone->data = array;
shm_zone->init = ngx_js_shared_array_init_zone;
return NGX_CONF_OK;
}
#endif /* NJS_HAVE_QUICKJS */Continue in nginx/ngx_js_shared_array.c:
Pattern: Follow ngx_js_dict_init_zone() in ngx_js_shared_dict.c (lines 2745-2814)
/* Zone initialization callback */
static ngx_int_t
ngx_js_shared_array_init_zone(ngx_shm_zone_t *shm_zone, void *data)
{
ngx_js_shared_array_t *array = shm_zone->data;
ngx_js_shared_array_t *prev = data;
// Handle nginx reload - reuse existing data
if (prev) {
array->data = prev->data;
return NGX_OK;
}
// First initialization or exists from previous process
array->data = shm_zone->shm.addr;
// Zero-initialize on first creation
if (!shm_zone->shm.exists) {
ngx_memzero(array->data, array->size);
}
return NGX_OK;
}
/* Property handlers */
static int
ngx_qjs_shared_array_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
JSValueConst obj, JSAtom prop)
{
int ret;
ngx_str_t name;
ngx_js_shared_array_t *array;
ngx_shm_zone_t *shm_zone;
ngx_js_main_conf_t *conf;
name.data = (u_char *) JS_AtomToCString(cx, prop);
if (name.data == NULL) {
return -1;
}
name.len = ngx_strlen(name.data);
ret = 0;
conf = ngx_qjs_main_conf(cx);
for (array = conf->shared_arrays; array != NULL; array = array->next) {
shm_zone = array->shm_zone;
if (shm_zone->shm.name.len == name.len
&& ngx_strncmp(shm_zone->shm.name.data, name.data, name.len) == 0)
{
if (pdesc != NULL) {
pdesc->flags = JS_PROP_ENUMERABLE;
pdesc->getter = JS_UNDEFINED;
pdesc->setter = JS_UNDEFINED;
/* Create SharedArrayBuffer wrapping shared memory */
pdesc->value = JS_NewArrayBuffer(cx, array->data, array->size,
NULL, NULL, TRUE);
if (JS_IsException(pdesc->value)) {
ret = -1;
break;
}
}
ret = 1;
break;
}
}
JS_FreeCString(cx, (char *) name.data);
return ret;
}
static int
ngx_qjs_shared_array_own_property_names(JSContext *cx, JSPropertyEnum **ptab,
uint32_t *plen, JSValueConst obj)
{
int ret;
JSAtom key;
JSValue keys;
ngx_js_shared_array_t *array;
ngx_shm_zone_t *shm_zone;
ngx_js_main_conf_t *conf;
keys = JS_NewObject(cx);
if (JS_IsException(keys)) {
return -1;
}
conf = ngx_qjs_main_conf(cx);
for (array = conf->shared_arrays; array != NULL; array = array->next) {
shm_zone = array->shm_zone;
key = JS_NewAtomLen(cx, (const char *) shm_zone->shm.name.data,
shm_zone->shm.name.len);
if (key == JS_ATOM_NULL) {
return -1;
}
if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED,
JS_PROP_ENUMERABLE) < 0)
{
return -1;
}
}
ret = JS_GetOwnPropertyNames(cx, ptab, plen, keys,
JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY);
JS_FreeValue(cx, keys);
return ret;
}
/* ngx.sharedArray getter */
static JSValue
ngx_qjs_ext_ngx_shared_array(JSContext *cx, JSValueConst this_val)
{
return JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_SHARED_ARRAY);
}
/* Initialization function called from ngx_js_shared_dict module */
ngx_int_t
ngx_js_shared_array_init(JSContext *cx)
{
JSValue ngx_obj;
/* Register class */
if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_SHARED_ARRAY,
&ngx_qjs_shared_array_class) < 0)
{
return NGX_ERROR;
}
/* Add sharedArray getter to ngx object */
ngx_obj = JS_GetGlobalObject(cx);
if (JS_IsException(ngx_obj)) {
return NGX_ERROR;
}
ngx_obj = JS_GetPropertyStr(cx, ngx_obj, "ngx");
if (JS_IsException(ngx_obj)) {
return NGX_ERROR;
}
JS_SetPropertyFunctionList(cx, ngx_obj,
ngx_qjs_ext_ngx_shared_array_props,
njs_nitems(ngx_qjs_ext_ngx_shared_array_props));
JS_FreeValue(cx, ngx_obj);
return NGX_OK;
}
#endif /* NJS_HAVE_QUICKJS */File: nginx/ngx_js_shared_array.h
Add at end before #endif:
#if (NJS_HAVE_QUICKJS)
#include <quickjs.h>
/* Initialize QuickJS class and register with ngx object */
ngx_int_t ngx_js_shared_array_init(JSContext *cx);
#endif
#endif /* _NGX_JS_SHARED_ARRAY_H_INCLUDED_ */File: nginx/ngx_js_shared_dict.c
Add include at top (after line 11):
#include "ngx_js.h"
#include "ngx_js_shared_dict.h"
#include "ngx_js_shared_array.h" /* ADD THIS */In ngx_qjs_ngx_shared_dict_init() function (around line 4220), add near the end before return:
static JSModuleDef *
ngx_qjs_ngx_shared_dict_init(JSContext *cx, const char *name)
{
// ... existing initialization code ...
JS_SetClassProto(cx, NGX_QJS_CLASS_ID_SHARED_DICT_ERROR, proto);
// ADD THIS:
if (ngx_js_shared_array_init(cx) != NGX_OK) {
return NULL;
}
return ngx_js_module(cx, name, ngx_qjs_ext_ngx,
njs_nitems(ngx_qjs_ext_ngx));
}File: nginx/ngx_http_js_module.c (around line 420)
static ngx_command_t ngx_http_js_commands[] = {
// ... existing directives ...
#if (NJS_HAVE_QUICKJS)
{ ngx_string("js_shared_array"),
NGX_HTTP_MAIN_CONF|NGX_CONF_1MORE,
ngx_js_shared_array_zone,
NGX_HTTP_MAIN_CONF_OFFSET,
0,
&ngx_http_js_module },
#endif
ngx_null_command
};File: nginx/ngx_stream_js_module.c (similar location)
static ngx_command_t ngx_stream_js_commands[] = {
// ... existing directives ...
#if (NJS_HAVE_QUICKJS)
{ ngx_string("js_shared_array"),
NGX_STREAM_MAIN_CONF|NGX_CONF_1MORE,
ngx_js_shared_array_zone,
NGX_STREAM_MAIN_CONF_OFFSET,
0,
&ngx_stream_js_module },
#endif
ngx_null_command
};js_engine qjs;
js_shared_array zone=counter:1m;
js_shared_array zone=buffer:256k;
http {
js_import test.js;
server {
location /counter {
js_content test.incrementCounter;
}
location /read {
js_content test.readBuffer;
}
}
}function incrementCounter(r) {
var sab = ngx.sharedArray.counter;
var view = new Int32Array(sab);
// Atomic increment
var oldValue = Atomics.add(view, 0, 1);
r.return(200, "Counter: " + (oldValue + 1));
}
function readBuffer(r) {
var sab = ngx.sharedArray.buffer;
var view = new Uint8Array(sab);
// Read first 10 bytes
var data = Array.from(view.slice(0, 10));
r.return(200, JSON.stringify(data));
}
export default { incrementCounter, readBuffer };- Basic allocation: Verify SharedArrayBuffer is created with correct size
- Multiple zones: Test multiple
js_shared_arraydirectives - Atomics operations: Test
Atomics.add(),Atomics.load(), etc. - Multi-worker: Verify data is shared across nginx workers
- Reload: Verify data persists across
nginx -s reload - Enumeration: Test
Object.keys(ngx.sharedArray) - Error handling: Test accessing non-existent zone
File: nginx/t/js_shared_array.t (new)
#!/usr/bin/perl
use warnings;
use strict;
use Test::More;
BEGIN { use FindBin; chdir($FindBin::Bin); }
use lib 'lib';
use Test::Nginx;
###############################################################################
select STDERR; $| = 1;
select STDOUT; $| = 1;
my $t = Test::Nginx->new()->has(qw/http/)
->write_file_expand('nginx.conf', <<'EOF');
%%TEST_GLOBALS%%
daemon off;
events {
}
http {
%%TEST_GLOBALS_HTTP%%
js_engine qjs;
js_shared_array zone=test:1m;
js_import test.js;
server {
listen 127.0.0.1:8080;
server_name localhost;
location /init {
js_content test.init;
}
location /increment {
js_content test.increment;
}
}
}
EOF
$t->write_file('test.js', <<EOF);
function init(r) {
var sab = ngx.sharedArray.test;
var view = new Int32Array(sab);
Atomics.store(view, 0, 0);
r.return(200, "initialized");
}
function increment(r) {
var sab = ngx.sharedArray.test;
var view = new Int32Array(sab);
var val = Atomics.add(view, 0, 1);
r.return(200, (val + 1).toString());
}
export default { init, increment };
EOF
$t->try_run('no QuickJS or shared array support')->plan(3);
###############################################################################
like(http_get('/init'), qr/initialized/, 'initialize shared array');
like(http_get('/increment'), qr/1/, 'first increment');
like(http_get('/increment'), qr/2/, 'second increment');
###############################################################################| File | Action | Lines | Purpose |
|---|---|---|---|
nginx/ngx_js_shared_array.h |
CREATE | ~35 | Structure definitions and public API |
nginx/ngx_js_shared_array.c |
CREATE | ~200 | Core implementation (QuickJS only) |
nginx/ngx_js.h |
MODIFY | +2 | Add class ID and main config field |
nginx/ngx_js_shared_dict.c |
MODIFY | +2 | Include header and call init |
nginx/ngx_http_js_module.c |
MODIFY | +7 | HTTP directive registration |
nginx/ngx_stream_js_module.c |
MODIFY | +7 | Stream directive registration |
nginx/t/js_shared_array.t |
CREATE | ~80 | Integration tests |
Total: ~333 lines of code (235 new + 18 modified + 80 tests)
- All code wrapped in
#if (NJS_HAVE_QUICKJS): Not supported in njs engine yet - Class ID for exotic container only:
NGX_QJS_CLASS_ID_SHARED_ARRAYis for thengx.sharedArrayobject, NOT for SharedArrayBuffer (which is a built-in QuickJS type) - Separate files for modularity: New
ngx_js_shared_array.{c,h}instead of adding tongx_js_shared_dict.c - No cleanup function:
free_func = NULLinJS_NewArrayBuffer() - Entire zone is backing store: Simple 1:1 mapping
- No slab pool needed: Unlike dict, no dynamic allocation
- Zero-initialize: Required for predictable behavior
- Preserve on reload: Essential for SharedArrayBuffer semantics
- Atomic access required: JavaScript must use
Atomicsfor safety
- Support for njs engine (requires different SAB implementation)
- Multiple arrays per zone (requires metadata structure)
- Named TypedArray views (e.g.,
zone=foo:1m:Int32Array) - Integration with existing
js_shared_dictzones - State persistence across restarts
JS_NewArrayBuffer():quickjs/quickjs.h:862- Exotic class methods: QuickJS documentation
- Exotic class definition:
ngx_qjs_shared_class(line 484) - Property lookup handler:
ngx_qjs_shared_own_property()(line 3030) - Property enumeration:
ngx_qjs_shared_own_property_names()(line 3082) - Zone initialization:
ngx_js_dict_init_zone()(line 2745) - Directive handler:
ngx_js_shared_dict_zone()(line 2819) - Module initialization:
ngx_qjs_ngx_shared_dict_init()(line 4133)
- Implementation:
nginx/ngx_js_shared_array.c - Header:
nginx/ngx_js_shared_array.h