From b5e8aa89b28a79b6fbe834498f118a757bf8fe87 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Fri, 5 Dec 2025 17:08:54 -0800 Subject: [PATCH 1/4] Modules: extracted config-time merging into separate function. This introduces ngx_js_merge_conftime_loc_conf() to handle merging of configuration-time properties. Normally ngx_js_merge_conf() does all the default value initialization for child location configurations. There is a special case for global "http" or "stream" configuration where the parent configuration needs to be initialized (so it can be reused by server configurations if no additional directives were defined in them). But parent configurations are not initialized by ngx_js_merge_conf(). Most of the ngx_js_loc_conf_t values are only used at runtime, so only configuration-time values need to be merged in the parent. The runtime values will be provided from the appropriate ngx_js_loc_conf_t during request processing. Previously, configuration-time merging was done inline. Extracting it into a dedicated function simplifies adding new configuration-time properties. --- nginx/ngx_js.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index b0b64d974..c18e97d54 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -3597,6 +3597,17 @@ ngx_js_init_preload_vm(njs_vm_t *vm, ngx_js_loc_conf_t *conf) } +/* + * Merge configuration values used at configuration time. + */ +static void +ngx_js_merge_conftime_loc_conf(ngx_js_loc_conf_t *conf, + ngx_js_loc_conf_t *prev) +{ + ngx_conf_merge_uint_value(conf->type, prev->type, NGX_ENGINE_NJS); +} + + ngx_int_t ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, ngx_js_loc_conf_t *prev, @@ -3612,6 +3623,9 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, * special handling to preserve conf->engine * in the "http" or "stream" section to inherit it to all servers */ + + ngx_js_merge_conftime_loc_conf(prev, conf); + if (init_vm(cf, (ngx_js_loc_conf_t *) prev) != NGX_OK) { return NGX_ERROR; } @@ -4316,10 +4330,7 @@ ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child, ngx_js_loc_conf_t *prev = parent; ngx_js_loc_conf_t *conf = child; - ngx_conf_merge_uint_value(conf->type, prev->type, NGX_ENGINE_NJS); - if (prev->type == NGX_CONF_UNSET_UINT) { - prev->type = NGX_ENGINE_NJS; - } + ngx_js_merge_conftime_loc_conf(conf, prev); ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000); ngx_conf_merge_size_value(conf->reuse, prev->reuse, 128); From db1725d14d475e487c211d729726045bae1bf0ab Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Tue, 2 Dec 2025 17:39:44 -0800 Subject: [PATCH 2/4] QuickJS: added native module support. Added "js_load_http_native_module" and "js_load_stream_native_module" main nginx.conf level directives. The directives load a dynamic library. For security reason it is only allowed in the main context. Later, JS code may import modules loaded with these directives with standard import syntax. example.conf: ... js_load_http_native_module /path/to/lib.so; js_load_http_native_module /path/to/lib2.so as lib2; http { js_import main.js; ... main.js: import * as lib from 'lib.so'; import * as lib2 from 'lib2'; ... See quickjs.h for the complete QuickJS API reference and nginx/t/js_native_module.t for a working example. This closes #968 feature request on Github. --- nginx/config | 4 +- nginx/ngx_http_js_module.c | 39 +++++ nginx/ngx_js.c | 209 +++++++++++++++++++++++++++ nginx/ngx_js.h | 13 ++ nginx/ngx_stream_js_module.c | 39 +++++ nginx/t/js_native_module.t | 211 +++++++++++++++++++++++++++ nginx/t/stream_js_native_module.t | 229 ++++++++++++++++++++++++++++++ 7 files changed, 742 insertions(+), 2 deletions(-) create mode 100644 nginx/t/js_native_module.t create mode 100644 nginx/t/stream_js_native_module.t diff --git a/nginx/config b/nginx/config index 5e2d9277f..f54234a83 100644 --- a/nginx/config +++ b/nginx/config @@ -157,7 +157,7 @@ fi if [ $HTTP != NO ]; then ngx_module_type=HTTP_AUX_FILTER - ngx_module_name=ngx_http_js_module + ngx_module_name="ngx_http_js_module ngx_http_js_core_module" ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build \ $NJS_QUICKJS_INC" ngx_module_deps="$NJS_ENGINE_DEP $NJS_DEPS $QJS_DEPS" @@ -174,7 +174,7 @@ fi if [ $STREAM != NO ]; then ngx_module_type=STREAM - ngx_module_name=ngx_stream_js_module + ngx_module_name="ngx_stream_js_module ngx_stream_js_core_module" ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build \ $NJS_QUICKJS_INC" ngx_module_deps="$NJS_ENGINE_DEP $NJS_DEPS $QJS_DEPS" diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 8b38dbfde..2027e6a56 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -608,6 +608,42 @@ static ngx_command_t ngx_http_js_commands[] = { }; +static ngx_command_t ngx_js_core_commands[] = { + + { ngx_string("js_load_http_native_module"), + NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE13, + ngx_js_core_load_native_module, + 0, + 0, + NULL }, + + ngx_null_command +}; + + +static ngx_core_module_t ngx_js_core_module_ctx = { + ngx_string("ngx_http_js_core"), + ngx_js_core_create_conf, + NULL +}; + + +ngx_module_t ngx_http_js_core_module = { + NGX_MODULE_V1, + &ngx_js_core_module_ctx, /* module context */ + ngx_js_core_commands, /* module directives */ + NGX_CORE_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + static ngx_http_module_t ngx_http_js_module_ctx = { NULL, /* preconfiguration */ ngx_http_js_init, /* postconfiguration */ @@ -7760,6 +7796,9 @@ ngx_http_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf) options.u.qjs.metas = ngx_http_js_uptr; options.u.qjs.addons = njs_http_qjs_addon_modules; options.clone = ngx_engine_qjs_clone; + + options.core_conf = (ngx_js_core_conf_t *) + ngx_get_conf(cf->cycle->conf_ctx, ngx_http_js_core_module); } #endif diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index c18e97d54..bd1b5f237 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -9,6 +9,7 @@ #include #include #include +#include #include "ngx_js.h" #include "ngx_js_http.h" @@ -541,6 +542,8 @@ ngx_create_engine(ngx_engine_opts_t *opts) engine->string = ngx_engine_qjs_string; engine->destroy = opts->destroy ? opts->destroy : ngx_engine_qjs_destroy; + + engine->core_conf = opts->core_conf; break; #endif @@ -1005,6 +1008,7 @@ ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external) ngx_int_t rc; JSRuntime *rt; JSContext *cx; + qjs_module_t *mod; ngx_engine_t *engine; ngx_js_code_entry_t *pc; @@ -1050,6 +1054,19 @@ ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external) JS_SetHostPromiseRejectionTracker(rt, ngx_qjs_rejection_tracker, ctx); + if (engine->native_modules != NULL) { + mod = engine->native_modules->start; + length = engine->native_modules->items; + + for (i = 0; i < length; i++) { + if (mod[i].init(cx, mod[i].name) == NULL) { + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js native module init failed: %s", mod[i].name); + goto destroy; + } + } + } + rv = JS_UNDEFINED; pc = engine->precompiled->start; length = engine->precompiled->items; @@ -2026,6 +2043,55 @@ ngx_qjs_ext_console_time_end(JSContext *cx, JSValueConst this_val, int argc, } +static JSModuleDef * +ngx_qjs_native_module_lookup(JSContext *cx, const char *module_name, + ngx_js_loc_conf_t *conf) +{ + ngx_uint_t i; + JSModuleDef *m; + qjs_module_t *mod, *modules; + ngx_js_core_conf_t *jccf; + + jccf = conf->engine->core_conf; + if (jccf == NULL || jccf->native_modules == NULL) { + return NULL; + } + + modules = jccf->native_modules->elts; + + for (i = 0; i < jccf->native_modules->nelts; i++) { + if (ngx_strcmp(modules[i].name, module_name) == 0) { + m = modules[i].init(cx, module_name); + if (m == NULL) { + return NULL; + } + + if (conf->engine->native_modules == NULL) { + conf->engine->native_modules = njs_arr_create( + conf->engine->pool, 4, + sizeof(qjs_module_t)); + if (conf->engine->native_modules == NULL) { + JS_ThrowOutOfMemory(cx); + return NULL; + } + } + + mod = njs_arr_add(conf->engine->native_modules); + if (mod == NULL) { + JS_ThrowOutOfMemory(cx); + return NULL; + } + + *mod = modules[i]; + + return m; + } + } + + return NULL; +} + + static JSModuleDef * ngx_qjs_module_loader(JSContext *cx, const char *module_name, void *opaque) { @@ -2039,6 +2105,11 @@ ngx_qjs_module_loader(JSContext *cx, const char *module_name, void *opaque) conf = opaque; + m = ngx_qjs_native_module_lookup(cx, module_name, conf); + if (m != NULL) { + return m; + } + njs_memzero(&info, sizeof(njs_module_info_t)); info.name.start = (u_char *) module_name; @@ -4391,6 +4462,144 @@ ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child, } +void * +ngx_js_core_create_conf(ngx_cycle_t *cycle) +{ + ngx_js_core_conf_t *jccf; + + jccf = ngx_pcalloc(cycle->pool, sizeof(ngx_js_core_conf_t)); + if (jccf == NULL) { + return NULL; + } + + /* + * set by ngx_pcalloc(): + * + * jccf->native_modules = NULL; + */ + + return jccf; +} + + +void +ngx_js_native_module_cleanup(void *data) +{ + void *handle = data; + + if (dlclose(handle) != 0) { + ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, + "dlclose() failed: %s", dlerror()); + } +} + + +char * +ngx_js_core_load_native_module(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ +#if (NJS_HAVE_QUICKJS) + void *handle; + u_char *p; + ngx_str_t *value, file, name; + qjs_module_t *module; + qjs_addon_init_pt init; + ngx_pool_cleanup_t *cln; + + ngx_js_core_conf_t *jccf = conf; + + if (cf->cycle->modules_used) { + return "is specified too late"; + } + + value = cf->args->elts; + file = value[1]; + + if (ngx_conf_full_name(cf->cycle, &file, 0) != NGX_OK) { + return NGX_CONF_ERROR; + } + + if (cf->args->nelts == 4) { + if (ngx_strcmp(value[2].data, "as") != 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid parameter \"%V\", expected \"as\"", + &value[2]); + return NGX_CONF_ERROR; + } + + name = value[3]; + + } else { + name = file; + + for (p = file.data + file.len - 1; p >= file.data; p--) { + if (*p == '/') { + name.data = p + 1; + name.len = file.data + file.len - name.data; + break; + } + } + } + + handle = dlopen((char *) file.data, RTLD_NOW | RTLD_LOCAL); + if (handle == NULL) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "dlopen(\"%V\") failed: %s", &file, dlerror()); + return NGX_CONF_ERROR; + } + + cln = ngx_pool_cleanup_add(cf->cycle->pool, 0); + if (cln == NULL) { + dlclose(handle); + return NGX_CONF_ERROR; + } + + cln->handler = ngx_js_native_module_cleanup; + cln->data = handle; + + init = dlsym(handle, "js_init_module"); + if (init == NULL) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "dlsym(\"%V\", \"js_init_module\") failed: %s", + &file, dlerror()); + return NGX_CONF_ERROR; + } + + if (jccf->native_modules == NULL) { + jccf->native_modules = ngx_array_create(cf->cycle->pool, 4, + sizeof(qjs_module_t)); + if (jccf->native_modules == NULL) { + return NGX_CONF_ERROR; + } + } + + module = ngx_array_push(jccf->native_modules); + if (module == NULL) { + return NGX_CONF_ERROR; + } + + p = ngx_palloc(cf->cycle->pool, name.len + 1); + if (p == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memcpy(p, name.data, name.len); + p[name.len] = '\0'; + + module->name = (const char *) p; + module->init = init; + + return NGX_CONF_OK; + +#else + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"load_js_native_module\" requires QuickJS support"); + return NGX_CONF_ERROR; + +#endif +} + + static uint64_t ngx_js_monotonic_time(void) { diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index f3c2493b8..a25dc65a0 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -85,6 +85,11 @@ typedef ngx_js_loc_conf_t *(*ngx_js_external_loc_conf_pt)(njs_external_ptr_t e); typedef ngx_js_ctx_t *(*ngx_js_external_ctx_pt)(njs_external_ptr_t e); +typedef struct { + ngx_array_t *native_modules; +} ngx_js_core_conf_t; + + typedef struct { ngx_str_t name; ngx_str_t path; @@ -245,6 +250,7 @@ typedef struct ngx_engine_opts_s { } u; njs_str_t file; + ngx_js_core_conf_t *core_conf; ngx_js_loc_conf_t *conf; ngx_engine_t *(*clone)(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, njs_int_t pr_id, @@ -292,6 +298,8 @@ struct ngx_engine_s { const char *name; njs_mp_t *pool; njs_arr_t *precompiled; + njs_arr_t *native_modules; + ngx_js_core_conf_t *core_conf; }; @@ -455,6 +463,11 @@ char * ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child, char *ngx_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf, void *tag); +void *ngx_js_core_create_conf(ngx_cycle_t *cycle); +char *ngx_js_core_load_native_module(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +void ngx_js_native_module_cleanup(void *data); + njs_int_t ngx_js_ext_string(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); njs_int_t ngx_js_ext_uint(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused, diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index cbceaf923..b21b701dd 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -440,6 +440,42 @@ static ngx_command_t ngx_stream_js_commands[] = { }; +static ngx_command_t ngx_js_core_commands[] = { + + { ngx_string("js_load_stream_native_module"), + NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE13, + ngx_js_core_load_native_module, + 0, + 0, + NULL }, + + ngx_null_command +}; + + +static ngx_core_module_t ngx_js_core_module_ctx = { + ngx_string("ngx_stream_js_core"), + ngx_js_core_create_conf, + NULL +}; + + +ngx_module_t ngx_stream_js_core_module = { + NGX_MODULE_V1, + &ngx_js_core_module_ctx, /* module context */ + ngx_js_core_commands, /* module directives */ + NGX_CORE_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + static ngx_stream_module_t ngx_stream_js_module_ctx = { NULL, /* preconfiguration */ ngx_stream_js_init, /* postconfiguration */ @@ -3036,6 +3072,9 @@ ngx_stream_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf) options.u.qjs.addons = njs_stream_qjs_addon_modules; options.clone = ngx_engine_qjs_clone; options.destroy = ngx_stream_qjs_destroy; + + options.core_conf = (ngx_js_core_conf_t *) + ngx_get_conf(cf->cycle->conf_ctx, ngx_stream_js_core_module); } #endif diff --git a/nginx/t/js_native_module.t b/nginx/t/js_native_module.t new file mode 100644 index 000000000..d8ec683ea --- /dev/null +++ b/nginx/t/js_native_module.t @@ -0,0 +1,211 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for QuickJS native module support. + +############################################################################### + +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 $cc; +for my $c ('gcc', 'clang') { + if (system("which $c >/dev/null 2>&1") == 0) { + $cc = $c; + last; + } +} + +plan(skip_all => "gcc or clang not found") unless defined $cc; + +my $configure_args = `$Test::Nginx::NGINX -V 2>&1`; +my $m32 = $configure_args =~ /-m32/ ? '-m32' : ''; +my $quickjs_inc = $configure_args =~ /(-I\S*quickjs(?:-ng)?[^\s'"]*)/ + ? $1 : undef; + +plan(skip_all => "QuickJS development files not found") unless $quickjs_inc; + +my $t = Test::Nginx->new()->has(qw/http/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +js_load_http_native_module %%TESTDIR%%/test.so; +js_load_http_native_module %%TESTDIR%%/test.so as test; + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import main from test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /add { + js_content main.test_add; + } + + location /reverse { + js_content main.test_reverse; + } + } +} + +EOF + +my $d = $t->testdir(); + +$t->write_file('test.js', <write_file('test.c', < + +#define countof(x) (sizeof(x) / sizeof((x)[0])) + +static JSValue +js_add(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +{ + int a, b; + + if (argc < 2) { + return JS_ThrowTypeError(ctx, "expected 2 arguments"); + } + + if (JS_ToInt32(ctx, &a, argv[0]) < 0) { + return JS_EXCEPTION; + } + + if (JS_ToInt32(ctx, &b, argv[1]) < 0) { + return JS_EXCEPTION; + } + + return JS_NewInt32(ctx, a + b); +} + + +static JSValue +js_reverse_string(JSContext *ctx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + char *reversed; + size_t i, len; + JSValue result; + const char *str; + + if (argc < 1) { + return JS_ThrowTypeError(ctx, "expected 1 argument"); + } + + str = JS_ToCStringLen(ctx, &len, argv[0]); + if (!str) { + return JS_EXCEPTION; + } + + reversed = js_malloc(ctx, len + 1); + if (!reversed) { + JS_FreeCString(ctx, str); + return JS_EXCEPTION; + } + + for (i = 0; i < len; i++) { + reversed[i] = str[len - 1 - i]; + } + + reversed[len] = 0; + + result = JS_NewString(ctx, reversed); + + js_free(ctx, reversed); + JS_FreeCString(ctx, str); + + return result; +} + + +static const JSCFunctionListEntry js_test_native_funcs[] = { + JS_CFUNC_DEF("add", 2, js_add), + JS_CFUNC_DEF("reverseString", 1, js_reverse_string), +}; + + +static int +js_test_native_init(JSContext *ctx, JSModuleDef *m) +{ + return JS_SetModuleExportList(ctx, m, js_test_native_funcs, + countof(js_test_native_funcs)); +} + + +JSModuleDef * +js_init_module(JSContext *ctx, const char *module_name) +{ + int rc; + JSModuleDef *m; + + m = JS_NewCModule(ctx, module_name, js_test_native_init); + if (!m) { + return NULL; + } + + rc = JS_AddModuleExportList(ctx, m, js_test_native_funcs, + countof(js_test_native_funcs)); + if (rc < 0) { + return NULL; + } + + rc = JS_AddModuleExport(ctx, m, "default"); + if (rc < 0) { + return NULL; + } + + return m; +} +EOF + +system("$cc -fPIC $m32 -O $quickjs_inc -shared -o $d/test.so $d/test.c") == 0 + or die "failed to build QuickJS native module: $!\n"; + +$t->try_run('no QuickJS native module support')->plan(2); + +############################################################################### + +like(http_get('/add?a=7&b=9'), qr/16$/, 'native module add'); +like(http_get('/reverse?str=hello'), qr/olleh$/, 'native module reverseString'); + +############################################################################### diff --git a/nginx/t/stream_js_native_module.t b/nginx/t/stream_js_native_module.t new file mode 100644 index 000000000..8d7d727cc --- /dev/null +++ b/nginx/t/stream_js_native_module.t @@ -0,0 +1,229 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for QuickJS native module support in stream. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $cc; +for my $c ('gcc', 'clang') { + if (system("which $c >/dev/null 2>&1") == 0) { + $cc = $c; + last; + } +} + +plan(skip_all => "gcc or clang not found") unless defined $cc; + +my $configure_args = `$Test::Nginx::NGINX -V 2>&1`; +my $m32 = $configure_args =~ /-m32/ ? '-m32' : ''; +my $quickjs_inc = $configure_args =~ /(-I\S*quickjs(?:-ng)?[^\s'"]*)/ + ? $1 : undef; + +plan(skip_all => "QuickJS development files not found") unless $quickjs_inc; + +my $t = Test::Nginx->new()->has(qw/stream stream_return/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +js_load_stream_native_module %%TESTDIR%%/test.so; +js_load_stream_native_module %%TESTDIR%%/test.so as test; + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_set $reverse test.reverse; + js_set $duplicate test.duplicate; + + js_import test.js; + + server { + listen 127.0.0.1:8081; + return $reverse; + } + + server { + listen 127.0.0.1:8082; + return $duplicate; + } +} + +EOF + +my $d = $t->testdir(); + +$t->write_file('test.js', <write_file('test.c', < +#include + +#define countof(x) (sizeof(x) / sizeof((x)[0])) + +static JSValue +js_reverse_string(JSContext *ctx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + char *reversed; + size_t i, len; + JSValue result; + const char *str; + + if (argc < 1) { + return JS_ThrowTypeError(ctx, "expected 1 argument"); + } + + str = JS_ToCStringLen(ctx, &len, argv[0]); + if (!str) { + return JS_EXCEPTION; + } + + reversed = js_malloc(ctx, len + 1); + if (!reversed) { + JS_FreeCString(ctx, str); + return JS_EXCEPTION; + } + + for (i = 0; i < len; i++) { + reversed[i] = str[len - 1 - i]; + } + + reversed[len] = 0; + + result = JS_NewString(ctx, reversed); + + js_free(ctx, reversed); + JS_FreeCString(ctx, str); + + return result; +} + + +static JSValue +js_duplicate(JSContext *ctx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + char *dup; + size_t len; + JSValue result; + const char *str; + + if (argc < 1) { + return JS_ThrowTypeError(ctx, "expected 1 argument"); + } + + str = JS_ToCStringLen(ctx, &len, argv[0]); + if (!str) { + return JS_EXCEPTION; + } + + dup = js_malloc(ctx, len * 2 + 1); + if (!dup) { + JS_FreeCString(ctx, str); + return JS_EXCEPTION; + } + + memcpy(dup, str, len); + memcpy(dup + len, str, len); + dup[len * 2] = 0; + + result = JS_NewString(ctx, dup); + + js_free(ctx, dup); + JS_FreeCString(ctx, str); + + return result; +} + + +static const JSCFunctionListEntry js_test_native_funcs[] = { + JS_CFUNC_DEF("reverseString", 1, js_reverse_string), + JS_CFUNC_DEF("duplicate", 1, js_duplicate), +}; + + +static int +js_test_native_init(JSContext *ctx, JSModuleDef *m) +{ + return JS_SetModuleExportList(ctx, m, js_test_native_funcs, + countof(js_test_native_funcs)); +} + + +JSModuleDef * +js_init_module(JSContext *ctx, const char *module_name) +{ + int rc; + JSModuleDef *m; + + m = JS_NewCModule(ctx, module_name, js_test_native_init); + if (!m) { + return NULL; + } + + rc = JS_AddModuleExportList(ctx, m, js_test_native_funcs, + countof(js_test_native_funcs)); + if (rc < 0) { + return NULL; + } + + rc = JS_AddModuleExport(ctx, m, "default"); + if (rc < 0) { + return NULL; + } + + return m; +} +EOF + +system("$cc -fPIC $m32 -O $quickjs_inc -shared -o $d/test.so $d/test.c") == 0 + or die "failed to build QuickJS native module: $!\n"; + +$t->try_run('no QuickJS native module support')->plan(2); + +############################################################################### + +like(stream('127.0.0.1:' . port(8081))->read(), qr/1\.0\.0\.721$/, + 'native module reverseString'); +like(stream('127.0.0.1:' . port(8082))->read(), qr/127\.0\.0\.1127\.0\.0\.1$/, + 'native module duplicate'); + +############################################################################### From ba124d6b0a2060a0887a18b1bf58a97bac4f663c Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Tue, 16 Dec 2025 22:40:08 -0800 Subject: [PATCH 3/4] CI: building with --debug=YES. --- .github/workflows/check-pr.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index d71addbd0..f54aed01c 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -68,6 +68,7 @@ jobs: - name: Configure and make njs run: | ./configure \ + --debug=YES \ --cc-opt="$CC_OPT" \ --ld-opt="$LD_OPT" \ || cat build/autoconf.err @@ -81,6 +82,7 @@ jobs: - name: Configure and make njs, 32-bit run: | ./configure \ + --debug=YES \ --cc-opt="$CC_OPT -m32" \ --ld-opt="$LD_OPT" \ || cat build/autoconf.err @@ -94,6 +96,7 @@ jobs: - name: Configure and make njs with quickjs run: | ./configure \ + --debug=YES \ --with-quickjs \ --cc-opt="$CC_OPT -Iquickjs" \ --ld-opt="$LD_OPT -Lquickjs" \ @@ -108,6 +111,7 @@ jobs: - name: Configure and make njs with quickjs-ng run: | ./configure \ + --debug=YES \ --with-quickjs \ --cc-opt="$CC_OPT" \ --ld-opt="$LD_OPT" \ From 0174ab19c4b135809efca22bede58881190f2962 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Mon, 15 Dec 2025 17:44:51 -0800 Subject: [PATCH 4/4] Added SharedArrayBuffer. Also added - ArrayBuffer.prototype.resize() - ArrayBuffer.prototype.resizeable - ArrayBuffer.prototype.maxByteLength --- src/njs.h | 19 +- src/njs_array_buffer.c | 347 ++++++++++++++++++++++++++++++---- src/njs_array_buffer.h | 14 +- src/njs_atom_defs.h | 6 + src/njs_buffer.c | 37 +++- src/njs_builtin.c | 6 + src/njs_typed_array.c | 2 +- src/njs_value.c | 18 ++ src/njs_value.h | 18 +- src/njs_vm.c | 32 ++-- src/njs_vm.h | 3 + src/njs_vmcode.c | 1 + src/test/njs_externals_test.c | 35 ++++ src/test/njs_unit_test.c | 317 ++++++++++++++++++++++++++++++- 14 files changed, 771 insertions(+), 84 deletions(-) diff --git a/src/njs.h b/src/njs.h index a2781a446..59223aadc 100644 --- a/src/njs.h +++ b/src/njs.h @@ -431,8 +431,10 @@ NJS_EXPORT njs_int_t njs_vm_value_string_create_chb(njs_vm_t *vm, NJS_EXPORT njs_int_t njs_vm_string_compare(njs_vm_t *vm, const njs_value_t *v1, const njs_value_t *v2); -NJS_EXPORT njs_int_t njs_vm_value_array_buffer_set(njs_vm_t *vm, - njs_value_t *value, const u_char *start, uint32_t size); +NJS_EXPORT njs_int_t njs_vm_value_array_buffer_set2(njs_vm_t *vm, + njs_value_t *value, u_char *start, uint32_t size, njs_bool_t shared); +#define njs_vm_value_array_buffer_set(vm, value, start, size) \ + njs_vm_value_array_buffer_set2(vm, value, start, size, 0) NJS_EXPORT njs_int_t njs_value_buffer_get(njs_vm_t *vm, njs_value_t *value, njs_str_t *dst); @@ -515,6 +517,8 @@ NJS_EXPORT njs_int_t njs_value_is_external(const njs_value_t *value, NJS_EXPORT njs_int_t njs_value_is_array(const njs_value_t *value); NJS_EXPORT njs_int_t njs_value_is_function(const njs_value_t *value); NJS_EXPORT njs_int_t njs_value_is_buffer(const njs_value_t *value); +NJS_EXPORT njs_int_t njs_value_is_array_buffer(const njs_value_t *value); +NJS_EXPORT njs_int_t njs_value_is_shared_array_buffer(const njs_value_t *value); NJS_EXPORT njs_int_t njs_value_is_data_view(const njs_value_t *value); NJS_EXPORT njs_int_t njs_value_is_promise(const njs_value_t *value); NJS_EXPORT njs_promise_type_t njs_promise_state(const njs_value_t *value); @@ -559,6 +563,17 @@ NJS_EXPORT njs_int_t njs_vm_promise_create(njs_vm_t *vm, njs_value_t *retval, njs_value_t *callbacks); +typedef struct { + void *(*sab_alloc)(void *opaque, size_t size); + void (*sab_free)(void *opaque, void *ptr); + void (*sab_dup)(void *opaque, void *ptr); + void *sab_opaque; +} njs_sab_functions_t; + +NJS_EXPORT void njs_vm_set_sab_functions(njs_vm_t *vm, + const njs_sab_functions_t *functions); + + njs_inline njs_int_t njs_value_property_val(njs_vm_t *vm, njs_value_t *value, njs_value_t *key, njs_value_t *retval) diff --git a/src/njs_array_buffer.c b/src/njs_array_buffer.c index 8a0703dc5..0fd52b7a0 100644 --- a/src/njs_array_buffer.c +++ b/src/njs_array_buffer.c @@ -7,12 +7,56 @@ #include -njs_array_buffer_t * -njs_array_buffer_alloc(njs_vm_t *vm, uint64_t size, njs_bool_t zeroing) +typedef struct { + njs_vm_t *vm; + void *data; +} njs_sab_cleanup_data_t; + + +njs_int_t +njs_vm_value_array_buffer_set2(njs_vm_t *vm, njs_value_t *value, + u_char *start, uint32_t size, njs_bool_t shared) { - njs_object_t *proto; njs_array_buffer_t *array; + array = njs_array_buffer_alloc(vm, 0, 0, shared); + if (njs_slow_path(array == NULL)) { + return NJS_ERROR; + } + + if (shared) { + if (vm->sab_funcs.sab_dup != NULL) { + vm->sab_funcs.sab_dup(vm->sab_funcs.sab_opaque, start); + } + + if (njs_shared_array_buffer_destructor_set(vm, start) != NJS_OK) { + return NJS_ERROR; + } + } + + array->u.data = (u_char *) start; + array->size = size; + + if (shared) { + njs_set_shared_array_buffer(value, array); + + } else { + njs_set_array_buffer(value, array); + } + + return NJS_OK; +} + + +njs_array_buffer_t * +njs_array_buffer_alloc(njs_vm_t *vm, uint64_t size, njs_bool_t zeroing, + njs_bool_t shared) +{ + void *data; + njs_object_t *proto; + njs_array_buffer_t *array; + njs_sab_functions_t *sab_funcs; + if (njs_slow_path(size > UINT32_MAX)) { goto overflow; } @@ -22,29 +66,53 @@ njs_array_buffer_alloc(njs_vm_t *vm, uint64_t size, njs_bool_t zeroing) goto memory_error; } - if (zeroing) { - array->u.data = njs_mp_zalloc(vm->mem_pool, size); + if (size != 0 && shared && vm->sab_funcs.sab_alloc != NULL) { + sab_funcs = &vm->sab_funcs; + + data = sab_funcs->sab_alloc(sab_funcs->sab_opaque, size); + if (njs_slow_path(data == NULL)) { + njs_internal_error(vm, "SharedArrayBuffer allocation failed"); + return NULL; + } + + if (njs_shared_array_buffer_destructor_set(vm, data) + != NJS_OK) + { + if (sab_funcs->sab_free != NULL) { + sab_funcs->sab_free(sab_funcs->sab_opaque, data); + } + + goto memory_error; + } } else { - array->u.data = njs_mp_alloc(vm->mem_pool, size); + data = njs_mp_alloc(vm->mem_pool, size); } - if (njs_slow_path(array->u.data == NULL)) { + if (njs_slow_path(data == NULL)) { goto memory_error; } - proto = njs_vm_proto(vm, NJS_OBJ_TYPE_ARRAY_BUFFER); + if (zeroing) { + memset(data, 0, size); + } + + array->u.data = data; + + proto = njs_vm_proto(vm, shared ? NJS_OBJ_TYPE_SHARED_ARRAY_BUFFER + : NJS_OBJ_TYPE_ARRAY_BUFFER); njs_flathsh_init(&array->object.hash); njs_flathsh_init(&array->object.shared_hash); array->object.__proto__ = proto; array->object.slots = NULL; - array->object.type = NJS_ARRAY_BUFFER; + array->object.type = shared ? NJS_SHARED_ARRAY_BUFFER : NJS_ARRAY_BUFFER; array->object.shared = 0; array->object.extensible = 1; array->object.error_data = 0; array->object.fast_array = 0; array->size = size; + array->shared = shared; return array; @@ -64,7 +132,7 @@ njs_array_buffer_alloc(njs_vm_t *vm, uint64_t size, njs_bool_t zeroing) static njs_int_t njs_array_buffer_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, - njs_index_t unused, njs_value_t *retval) + njs_index_t shared, njs_value_t *retval) { uint64_t size; njs_int_t ret; @@ -72,7 +140,8 @@ njs_array_buffer_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_array_buffer_t *array; if (!vm->top_frame->ctor) { - njs_type_error(vm, "Constructor ArrayBuffer requires 'new'"); + njs_type_error(vm, "Constructor %s requires 'new'", + shared ? "SharedArrayBuffer" : "ArrayBuffer"); return NJS_ERROR; } @@ -84,12 +153,18 @@ njs_array_buffer_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_ERROR; } - array = njs_array_buffer_alloc(vm, size, 1); + array = njs_array_buffer_alloc(vm, size, 1, shared); + if (njs_slow_path(array == NULL)) { return NJS_ERROR; } - njs_set_array_buffer(retval, array); + if (shared) { + njs_set_shared_array_buffer(retval, array); + + } else { + njs_set_array_buffer(retval, array); + } return NJS_OK; } @@ -164,25 +239,36 @@ const njs_object_init_t njs_array_buffer_constructor_init = { static njs_int_t njs_array_buffer_prototype_byte_length(njs_vm_t *vm, njs_value_t *args, - njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) + njs_uint_t nargs, njs_index_t shared, njs_value_t *retval) { njs_value_t *value; njs_array_buffer_t *array; value = njs_argument(args, 0); - if (!njs_is_array_buffer(value)) { - njs_type_error(vm, "Method ArrayBuffer.prototype.byteLength called " - "on incompatible receiver"); - return NJS_ERROR; - } + if (shared) { + if (value->type != NJS_SHARED_ARRAY_BUFFER) { + njs_type_error(vm, + "Method SharedArrayBuffer.prototype.byteLength " + "called on incompatible receiver"); + return NJS_ERROR; + } - array = njs_array_buffer(value); - if (njs_slow_path(njs_is_detached_buffer(array))) { - njs_set_number(retval, 0); - return NJS_OK; + } else { + if (value->type != NJS_ARRAY_BUFFER) { + njs_type_error(vm, "Method ArrayBuffer.prototype.byteLength " + "called on incompatible receiver"); + return NJS_ERROR; + } + + array = njs_array_buffer(value); + if (njs_slow_path(njs_is_detached_buffer(array))) { + njs_set_number(retval, 0); + return NJS_OK; + } } + array = njs_array_buffer(value); njs_set_number(retval, array->size); return NJS_OK; @@ -191,7 +277,7 @@ njs_array_buffer_prototype_byte_length(njs_vm_t *vm, njs_value_t *args, static njs_int_t njs_array_buffer_prototype_slice(njs_vm_t *vm, njs_value_t *args, - njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) + njs_uint_t nargs, njs_index_t shared, njs_value_t *retval) { int64_t len, start, end; njs_int_t ret; @@ -200,17 +286,28 @@ njs_array_buffer_prototype_slice(njs_vm_t *vm, njs_value_t *args, value = njs_argument(args, 0); - if (!njs_is_array_buffer(value)) { - njs_type_error(vm, "Method ArrayBuffer.prototype.slice called " - "on incompatible receiver"); - return NJS_ERROR; + if (shared) { + if (value->type != NJS_SHARED_ARRAY_BUFFER) { + njs_type_error(vm, "Method SharedArrayBuffer.prototype.slice " + "called on incompatible receiver"); + return NJS_ERROR; + } + + } else { + if (value->type != NJS_ARRAY_BUFFER) { + njs_type_error(vm, "Method ArrayBuffer.prototype.slice called " + "on incompatible receiver"); + return NJS_ERROR; + } + + this = njs_array_buffer(value); + if (njs_slow_path(njs_is_detached_buffer(this))) { + njs_type_error(vm, "detached buffer"); + return NJS_ERROR; + } } this = njs_array_buffer(value); - if (njs_slow_path(njs_is_detached_buffer(this))) { - njs_type_error(vm, "detached buffer"); - return NJS_ERROR; - } len = njs_array_buffer_size(this); end = len; @@ -236,7 +333,12 @@ njs_array_buffer_prototype_slice(njs_vm_t *vm, njs_value_t *args, return NJS_ERROR; } - njs_set_array_buffer(retval, buffer); + if (shared) { + njs_set_shared_array_buffer(retval, buffer); + + } else { + njs_set_array_buffer(retval, buffer); + } return NJS_OK; } @@ -250,8 +352,10 @@ njs_array_buffer_detach(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_array_buffer_t *buffer; value = njs_arg(args, nargs, 1); - if (njs_slow_path(!njs_is_array_buffer(value))) { - njs_type_error(vm, "\"this\" is not an ArrayBuffer"); + + if (njs_slow_path(value->type != NJS_ARRAY_BUFFER)) { + njs_type_error(vm, "detaching of %s is not supported", + njs_type_string(value->type)); return NJS_ERROR; } @@ -265,6 +369,171 @@ njs_array_buffer_detach(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } +static njs_int_t +njs_array_buffer_resize(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t shared, njs_value_t *retval) +{ + njs_value_t *value; + + value = njs_argument(args, 0); + + if (shared) { + if (value->type != NJS_SHARED_ARRAY_BUFFER) { + njs_type_error(vm, "Method SharedArrayBuffer.prototype.grow " + "called on incompatible receiver"); + return NJS_ERROR; + } + + njs_type_error(vm, "SharedArrayBuffer is not growable"); + return NJS_ERROR; + + } else { + if (value->type != NJS_ARRAY_BUFFER) { + njs_type_error(vm, "Method ArrayBuffer.prototype.resize " + "called on incompatible receiver"); + return NJS_ERROR; + } + + njs_type_error(vm, "ArrayBuffer is not resizable"); + return NJS_ERROR; + } +} + + +static njs_int_t +njs_array_buffer_get_resizable(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t shared, njs_value_t *retval) +{ + njs_value_t *value; + + value = njs_argument(args, 0); + + if (shared) { + if (value->type != NJS_SHARED_ARRAY_BUFFER) { + njs_type_error(vm, + "Method SharedArrayBuffer.prototype.growable " + "called on incompatible receiver"); + return NJS_ERROR; + } + + } else { + if (value->type != NJS_ARRAY_BUFFER) { + njs_type_error(vm, "Method ArrayBuffer.prototype.resizable " + "called on incompatible receiver"); + return NJS_ERROR; + } + } + + njs_set_boolean(retval, 0); + + return NJS_OK; +} + + +static void +njs_shared_array_buffer_cleanup(void *data) +{ + njs_vm_t *vm; + njs_sab_cleanup_data_t *cleanup_data; + + cleanup_data = data; + vm = cleanup_data->vm; + + if (vm->sab_funcs.sab_free != NULL) { + vm->sab_funcs.sab_free(vm->sab_funcs.sab_opaque, cleanup_data->data); + } +} + + +njs_int_t +njs_shared_array_buffer_destructor_set(njs_vm_t *vm, void *data) +{ + njs_mp_cleanup_t *cleanup; + njs_sab_cleanup_data_t *cleanup_data; + + if (vm->sab_funcs.sab_free == NULL) { + return NJS_OK; + } + + cleanup = njs_mp_cleanup_add(vm->mem_pool, + sizeof(njs_sab_cleanup_data_t)); + if (njs_slow_path(cleanup == NULL)) { + return NJS_ERROR; + } + + cleanup_data = cleanup->data; + cleanup_data->vm = vm; + cleanup_data->data = data; + cleanup->handler = njs_shared_array_buffer_cleanup; + + return NJS_OK; +} + + +static const njs_object_prop_init_t +njs_shared_array_buffer_constructor_properties[] = +{ + NJS_DECLARE_PROP_LENGTH(1), + + NJS_DECLARE_PROP_NAME("SharedArrayBuffer"), + + NJS_DECLARE_PROP_HANDLER(STRING_prototype, njs_object_prototype_create, + 0, 0), + + NJS_DECLARE_PROP_GETTER(SYMBOL_species, + njs_array_buffer_get_this, + 0), +}; + + +const njs_object_init_t njs_shared_array_buffer_constructor_init = { + njs_shared_array_buffer_constructor_properties, + njs_nitems(njs_shared_array_buffer_constructor_properties), +}; + + +static const njs_object_prop_init_t +njs_shared_array_buffer_prototype_properties[] = +{ + NJS_DECLARE_PROP_HANDLER(STRING_constructor, + njs_object_prototype_create_constructor, 0, + NJS_OBJECT_PROP_VALUE_CW), + + NJS_DECLARE_PROP_GETTER(STRING_byteLength, + njs_array_buffer_prototype_byte_length, 1), + + NJS_DECLARE_PROP_NATIVE(STRING_grow, njs_array_buffer_resize, 1, 1), + + NJS_DECLARE_PROP_GETTER(STRING_growable, + njs_array_buffer_get_resizable, 1), + + NJS_DECLARE_PROP_GETTER(STRING_maxByteLength, + njs_array_buffer_prototype_byte_length, 1), + + NJS_DECLARE_PROP_NATIVE(STRING_slice, + njs_array_buffer_prototype_slice, + 2, 1), + + NJS_DECLARE_PROP_VALUE(SYMBOL_toStringTag, + njs_ascii_strval("SharedArrayBuffer"), + NJS_OBJECT_PROP_VALUE_C), +}; + + +const njs_object_init_t njs_shared_array_buffer_prototype_init = { + njs_shared_array_buffer_prototype_properties, + njs_nitems(njs_shared_array_buffer_prototype_properties), +}; + + +const njs_object_type_init_t njs_shared_array_buffer_type_init = { + .constructor = njs_native_ctor(njs_array_buffer_constructor, 1, 1), + .prototype_props = &njs_shared_array_buffer_prototype_init, + .constructor_props = &njs_shared_array_buffer_constructor_init, + .prototype_value = { .object = { .type = NJS_OBJECT } }, +}; + + static const njs_object_prop_init_t njs_array_buffer_prototype_properties[] = @@ -276,6 +545,14 @@ static const njs_object_prop_init_t njs_array_buffer_prototype_properties[] = NJS_DECLARE_PROP_GETTER(STRING_byteLength, njs_array_buffer_prototype_byte_length, 0), + NJS_DECLARE_PROP_GETTER(STRING_maxByteLength, + njs_array_buffer_prototype_byte_length, 0), + + NJS_DECLARE_PROP_GETTER(STRING_resizable, + njs_array_buffer_get_resizable, 0), + + NJS_DECLARE_PROP_NATIVE(STRING_resize, njs_array_buffer_resize, 1, 0), + NJS_DECLARE_PROP_NATIVE(STRING_slice, njs_array_buffer_prototype_slice, 2, 0), diff --git a/src/njs_array_buffer.h b/src/njs_array_buffer.h index 850db9a57..f9c44a9b2 100644 --- a/src/njs_array_buffer.h +++ b/src/njs_array_buffer.h @@ -13,37 +13,39 @@ njs_array_buffer_t *njs_array_buffer_alloc(njs_vm_t *vm, uint64_t size, - njs_bool_t zeroing); + njs_bool_t zeroing, njs_bool_t shared); +njs_int_t njs_shared_array_buffer_destructor_set(njs_vm_t *vm, void *data); njs_int_t njs_array_buffer_writable(njs_vm_t *vm, njs_array_buffer_t *buffer); NJS_EXPORT njs_int_t njs_array_buffer_detach(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); njs_inline njs_array_buffer_t * -njs_array_buffer_slice(njs_vm_t *vm, njs_array_buffer_t *this, int64_t start, - int64_t end) +njs_array_buffer_slice(njs_vm_t *vm, njs_array_buffer_t *this_value, + int64_t start, int64_t end) { int64_t len, new_len, first, final; njs_array_buffer_t *new_buffer; - len = njs_array_buffer_size(this); + len = njs_array_buffer_size(this_value); first = (start < 0) ? njs_max(len + start, 0) : njs_min(start, len); final = (end < 0) ? njs_max(len + end, 0) : njs_min(end, len); new_len = njs_max(final - first, 0); - new_buffer = njs_array_buffer_alloc(vm, new_len, 1); + new_buffer = njs_array_buffer_alloc(vm, new_len, 1, this_value->shared); if (new_buffer == NULL) { return NULL; } - memcpy(new_buffer->u.u8, &this->u.u8[first], new_len); + memcpy(new_buffer->u.u8, &this_value->u.u8[first], new_len); return new_buffer; } extern const njs_object_type_init_t njs_array_buffer_type_init; +extern const njs_object_type_init_t njs_shared_array_buffer_type_init; #endif /* _NJS_ARRAY_BUFFER_H_INCLUDED_ */ diff --git a/src/njs_atom_defs.h b/src/njs_atom_defs.h index 35dcbe124..546c90d1a 100644 --- a/src/njs_atom_defs.h +++ b/src/njs_atom_defs.h @@ -135,6 +135,7 @@ NJS_DEF_STRING(Promise, "Promise", 0, 0) NJS_DEF_STRING(RangeError, "RangeError", 0, 0) NJS_DEF_STRING(ReferenceError, "ReferenceError", 0, 0) NJS_DEF_STRING(RegExp, "RegExp", 0, 0) +NJS_DEF_STRING(SharedArrayBuffer, "SharedArrayBuffer", 0, 0) NJS_DEF_STRING(SQRT1_2, "SQRT1_2", 0, 0) NJS_DEF_STRING(SQRT2, "SQRT2", 0, 0) NJS_DEF_STRING(String, "String", 0, 0) @@ -283,6 +284,8 @@ NJS_DEF_STRING(getUint16, "getUint16", 0, 0) NJS_DEF_STRING(getUint32, "getUint32", 0, 0) NJS_DEF_STRING(getUint8, "getUint8", 0, 0) NJS_DEF_STRING(getMonth, "getMonth", 0, 0) +NJS_DEF_STRING(grow, "grow", 0, 0) +NJS_DEF_STRING(growable, "growable", 0, 0) NJS_DEF_STRING(global, "global", 0, 0) NJS_DEF_STRING(globalThis, "globalThis", 0, 0) NJS_DEF_STRING(groups, "groups", 0, 0) @@ -328,6 +331,7 @@ NJS_DEF_STRING(map, "map", 0, 0) NJS_DEF_STRING(matchAll, "matchAll", 0, 0) NJS_DEF_STRING(match, "match", 0, 0) NJS_DEF_STRING(max, "max", 0, 0) +NJS_DEF_STRING(maxByteLength, "maxByteLength", 0, 0) NJS_DEF_STRING(min, "min", 0, 0) NJS_DEF_STRING(memoryStats, "memoryStats", 0, 0) NJS_DEF_STRING(message, "message", 0, 0) @@ -386,6 +390,8 @@ NJS_DEF_STRING(replace, "replace", 0, 0) NJS_DEF_STRING(replaceAll, "replaceAll", 0, 0) NJS_DEF_STRING(require, "require", 0, 0) NJS_DEF_STRING(resolve, "resolve", 0, 0) +NJS_DEF_STRING(resizable, "resizable", 0, 0) +NJS_DEF_STRING(resize, "resize", 0, 0) NJS_DEF_STRING(reverse, "reverse", 0, 0) NJS_DEF_STRING(round, "round", 0, 0) NJS_DEF_STRING(seal, "seal", 0, 0) diff --git a/src/njs_buffer.c b/src/njs_buffer.c index bd987c5f2..3c3fbd892 100644 --- a/src/njs_buffer.c +++ b/src/njs_buffer.c @@ -167,6 +167,7 @@ njs_buffer_set(njs_vm_t *vm, njs_value_t *value, const u_char *start, buffer->object.fast_array = 0; buffer->u.data = (void *) start; buffer->size = size; + buffer->shared = 0; proto = njs_vm_proto(vm, NJS_OBJ_TYPE_BUFFER); @@ -297,6 +298,7 @@ njs_buffer_from(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return njs_buffer_from_typed_array(vm, value, retval); case NJS_ARRAY_BUFFER: + case NJS_SHARED_ARRAY_BUFFER: return njs_buffer_from_array_buffer(vm, value, njs_arg(args, nargs, 2), njs_arg(args, nargs, 3), retval); @@ -586,6 +588,7 @@ njs_buffer_byte_length(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_OK; case NJS_ARRAY_BUFFER: + case NJS_SHARED_ARRAY_BUFFER: njs_set_number(retval, njs_array_buffer(value)->size); return NJS_OK; @@ -647,28 +650,44 @@ njs_buffer_slot(njs_vm_t *vm, njs_value_t *value, const char *name) njs_int_t njs_value_buffer_get(njs_vm_t *vm, njs_value_t *value, njs_str_t *dst) { + size_t offset, length; njs_typed_array_t *array; njs_array_buffer_t *array_buffer; if (njs_slow_path(!(njs_is_typed_array(value) + || njs_is_array_buffer(value) || njs_is_data_view(value)))) { njs_type_error(vm, "first argument must be a Buffer or DataView"); return NJS_ERROR; } - array = njs_typed_array(value); - if (njs_slow_path(array == NULL)) { - return NJS_ERROR; - } + if (njs_is_typed_array(value) || njs_is_data_view(value)) { + array = njs_typed_array(value); + if (njs_slow_path(array == NULL)) { + return NJS_ERROR; + } - array_buffer = njs_typed_array_writable(vm, array); - if (njs_slow_path(array_buffer == NULL)) { - return NJS_ERROR; + array_buffer = njs_typed_array_writable(vm, array); + if (njs_slow_path(array_buffer == NULL)) { + return NJS_ERROR; + } + + offset = array->offset; + length = array->byte_length; + + } else { + array_buffer = njs_array_buffer(value); + if (njs_array_buffer_writable(vm, array_buffer) != NJS_OK) { + return NJS_ERROR; + } + + offset = 0; + length = array_buffer->size; } - dst->length = array->byte_length; - dst->start = &array_buffer->u.u8[array->offset]; + dst->length = length; + dst->start = &array_buffer->u.u8[offset]; return NJS_OK; } diff --git a/src/njs_builtin.c b/src/njs_builtin.c index 1b486ba90..51af26606 100644 --- a/src/njs_builtin.c +++ b/src/njs_builtin.c @@ -69,6 +69,7 @@ static const njs_object_type_init_t *const &njs_date_type_init, &njs_promise_type_init, &njs_array_buffer_type_init, + &njs_shared_array_buffer_type_init, &njs_data_view_type_init, &njs_text_decoder_type_init, &njs_text_encoder_type_init, @@ -1028,6 +1029,11 @@ static const njs_object_prop_init_t njs_global_this_object_properties[] = NJS_OBJ_TYPE_ARRAY_BUFFER, NJS_OBJECT_PROP_VALUE_CW), + NJS_DECLARE_PROP_HANDLER(STRING_SharedArrayBuffer, + njs_top_level_constructor, + NJS_OBJ_TYPE_SHARED_ARRAY_BUFFER, + NJS_OBJECT_PROP_VALUE_CW), + NJS_DECLARE_PROP_HANDLER(STRING_DataView, njs_top_level_constructor, NJS_OBJ_TYPE_DATA_VIEW, NJS_OBJECT_PROP_VALUE_CW), diff --git a/src/njs_typed_array.c b/src/njs_typed_array.c index f74f175fa..e797eb8b8 100644 --- a/src/njs_typed_array.c +++ b/src/njs_typed_array.c @@ -130,7 +130,7 @@ njs_typed_array_alloc(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } if (buffer == NULL) { - buffer = njs_array_buffer_alloc(vm, size, zeroing); + buffer = njs_array_buffer_alloc(vm, size, zeroing, 0); if (njs_slow_path(buffer == NULL)) { return NULL; } diff --git a/src/njs_value.c b/src/njs_value.c index 7d4c0a39d..629130757 100644 --- a/src/njs_value.c +++ b/src/njs_value.c @@ -350,6 +350,9 @@ njs_type_string(njs_value_type_t type) case NJS_ARRAY_BUFFER: return "array buffer"; + case NJS_SHARED_ARRAY_BUFFER: + return "shared array buffer"; + case NJS_TYPED_ARRAY: return "typed array"; @@ -576,6 +579,20 @@ njs_value_is_buffer(const njs_value_t *value) } +njs_int_t +njs_value_is_array_buffer(const njs_value_t *value) +{ + return njs_is_array_buffer(value); +} + + +njs_int_t +njs_value_is_shared_array_buffer(const njs_value_t *value) +{ + return njs_is_shared_array_buffer(value); +} + + njs_int_t njs_value_is_data_view(const njs_value_t *value) { @@ -638,6 +655,7 @@ njs_property_query(njs_vm_t *vm, njs_property_query_t *pq, njs_value_t *value, case NJS_ARRAY: case NJS_FUNCTION: case NJS_ARRAY_BUFFER: + case NJS_SHARED_ARRAY_BUFFER: case NJS_DATA_VIEW: case NJS_TYPED_ARRAY: case NJS_REGEXP: diff --git a/src/njs_value.h b/src/njs_value.h index 1422f02cc..fea00043a 100644 --- a/src/njs_value.h +++ b/src/njs_value.h @@ -59,6 +59,7 @@ typedef enum { NJS_PROMISE, NJS_OBJECT_VALUE, NJS_ARRAY_BUFFER, + NJS_SHARED_ARRAY_BUFFER, NJS_DATA_VIEW, NJS_VALUE_TYPE_MAX } njs_value_type_t; @@ -195,6 +196,7 @@ struct njs_array_s { struct njs_array_buffer_s { njs_object_t object; size_t size; + uint8_t shared; union { uint8_t *u8; uint16_t *u16; @@ -604,7 +606,12 @@ typedef struct { #define njs_is_array_buffer(value) \ - ((value)->type == NJS_ARRAY_BUFFER) + (((value)->type == NJS_ARRAY_BUFFER) \ + || ((value)->type == NJS_SHARED_ARRAY_BUFFER)) + + +#define njs_is_shared_array_buffer(value) \ + ((value)->type == NJS_SHARED_ARRAY_BUFFER) #define njs_is_typed_array(value) \ @@ -875,6 +882,15 @@ njs_set_array_buffer(njs_value_t *value, njs_array_buffer_t *array) } +njs_inline void +njs_set_shared_array_buffer(njs_value_t *value, njs_array_buffer_t *array) +{ + value->data.u.array_buffer = array; + value->type = NJS_SHARED_ARRAY_BUFFER; + value->data.truth = 1; +} + + njs_inline void njs_set_typed_array(njs_value_t *value, njs_typed_array_t *array) { diff --git a/src/njs_vm.c b/src/njs_vm.c index 348316a14..8514155be 100644 --- a/src/njs_vm.c +++ b/src/njs_vm.c @@ -28,6 +28,13 @@ njs_vm_opt_init(njs_vm_opt_t *options) } +void +njs_vm_set_sab_functions(njs_vm_t *vm, const njs_sab_functions_t *functions) +{ + vm->sab_funcs = *functions; +} + + njs_vm_t * njs_vm_create(njs_vm_opt_t *options) { @@ -986,26 +993,6 @@ njs_value_string_get(njs_vm_t *vm, njs_value_t *value, njs_str_t *dst) } -njs_int_t -njs_vm_value_array_buffer_set(njs_vm_t *vm, njs_value_t *value, - const u_char *start, uint32_t size) -{ - njs_array_buffer_t *array; - - array = njs_array_buffer_alloc(vm, 0, 0); - if (njs_slow_path(array == NULL)) { - return NJS_ERROR; - } - - array->u.data = (u_char *) start; - array->size = size; - - njs_set_array_buffer(value, array); - - return NJS_OK; -} - - njs_int_t njs_vm_value_buffer_set(njs_vm_t *vm, njs_value_t *value, const u_char *start, uint32_t size) @@ -1575,8 +1562,11 @@ njs_vm_value_to_bytes(njs_vm_t *vm, njs_str_t *dst, njs_value_t *src) case NJS_TYPED_ARRAY: case NJS_DATA_VIEW: case NJS_ARRAY_BUFFER: + case NJS_SHARED_ARRAY_BUFFER: - if (value.type != NJS_ARRAY_BUFFER) { + if (value.type != NJS_ARRAY_BUFFER + && value.type != NJS_SHARED_ARRAY_BUFFER) + { array = njs_typed_array(&value); buffer = njs_typed_array_buffer(array); offset = array->offset; diff --git a/src/njs_vm.h b/src/njs_vm.h index 46f566e3c..252e4f793 100644 --- a/src/njs_vm.h +++ b/src/njs_vm.h @@ -39,6 +39,7 @@ typedef enum { NJS_OBJ_TYPE_DATE, NJS_OBJ_TYPE_PROMISE, NJS_OBJ_TYPE_ARRAY_BUFFER, + NJS_OBJ_TYPE_SHARED_ARRAY_BUFFER, NJS_OBJ_TYPE_DATA_VIEW, NJS_OBJ_TYPE_TEXT_DECODER, NJS_OBJ_TYPE_TEXT_ENCODER, @@ -125,6 +126,8 @@ struct njs_vm_s { njs_external_ptr_t external; + njs_sab_functions_t sab_funcs; + njs_native_frame_t *top_frame; njs_frame_t *active_frame; diff --git a/src/njs_vmcode.c b/src/njs_vmcode.c index 626f85cff..e23eaf508 100644 --- a/src/njs_vmcode.c +++ b/src/njs_vmcode.c @@ -2362,6 +2362,7 @@ njs_vmcode_typeof(njs_vm_t *vm, njs_value_t *value, njs_value_t *retval) NJS_ATOM_STRING_object, NJS_ATOM_STRING_object, NJS_ATOM_STRING_object, + NJS_ATOM_STRING_object, }; njs_atom_to_value(vm, retval, types[value->type]); diff --git a/src/test/njs_externals_test.c b/src/test/njs_externals_test.c index d34ec1f20..f064e99a0 100644 --- a/src/test/njs_externals_test.c +++ b/src/test/njs_externals_test.c @@ -553,6 +553,30 @@ njs_unit_test_r_bind(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } +static njs_int_t +njs_unit_test_r_wrap_sab(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused, njs_value_t *retval) +{ + njs_str_t buffer; + njs_value_t *sab; + + sab = njs_arg(args, nargs, 1); + + if (!njs_value_is_shared_array_buffer(sab)) { + njs_vm_type_error(vm, "argument is not a SharedArrayBuffer"); + return NJS_ERROR; + } + + if (njs_value_buffer_get(vm, sab, &buffer) != NJS_OK) { + njs_vm_type_error(vm, "cannot get SharedArrayBuffer data"); + return NJS_ERROR; + } + + return njs_vm_value_array_buffer_set2(vm, retval, buffer.start, + buffer.length, 1); +} + + static njs_int_t njs_unit_test_null_get(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) @@ -783,6 +807,17 @@ static njs_external_t njs_unit_test_r_external[] = { } }, + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("wrapSAB"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_unit_test_r_wrap_sab, + } + }, + { .flags = NJS_EXTERN_OBJECT, .name.string = njs_str("consts"), diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index c39523ac0..12b341b85 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -7,10 +7,12 @@ #include #include #include +#include #include #include #include +#include #ifndef NJS_HAVE_PCRE2 #include @@ -6290,6 +6292,41 @@ static njs_unit_test_t njs_test[] = { njs_str("var a = new ArrayBuffer(10); a.slice(0,-1).byteLength"), njs_str("9") }, + { njs_str("var ab = new ArrayBuffer(16); ab.resizable"), + njs_str("false") }, + + { njs_str("var ab = new ArrayBuffer(16); ab.maxByteLength"), + njs_str("16") }, + + { njs_str("var ab = new ArrayBuffer(16); ab.maxByteLength === ab.byteLength"), + njs_str("true") }, + + { njs_str("var ab = new ArrayBuffer(0); ab.resizable"), + njs_str("false") }, + + { njs_str("var ab = new ArrayBuffer(0); ab.maxByteLength"), + njs_str("0") }, + + { njs_str("var ab = new ArrayBuffer(16); var msg; " + "try { ab.resize(32); } catch(e) { msg = e.message } msg"), + njs_str("ArrayBuffer is not resizable") }, + + { njs_str("var ab = new ArrayBuffer(16); var msg; " + "try { ab.resize(8); } catch(e) { msg = e.message } msg"), + njs_str("ArrayBuffer is not resizable") }, + + { njs_str("var ab = new ArrayBuffer(16); var sab = new SharedArrayBuffer(16); var msg; " + "try { ArrayBuffer.prototype.resize.call(sab, 32); } catch(e) { msg = e.message } msg"), + njs_str("Method ArrayBuffer.prototype.resize called on incompatible receiver") }, + + { njs_str("var get = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'resizable').get; " + "get.call([])"), + njs_str("TypeError: Method ArrayBuffer.prototype.resizable called on incompatible receiver") }, + + { njs_str("var get = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'maxByteLength').get; " + "get.call([])"), + njs_str("TypeError: Method ArrayBuffer.prototype.byteLength called on incompatible receiver") }, + { njs_str(NJS_TYPED_ARRAY_LIST ".map(v=>{var buffer = new ArrayBuffer(8); var view = new v(buffer);" " view[0] = 511; return new Uint8Array(buffer.slice(0,4))})"), @@ -21721,6 +21758,140 @@ static njs_unit_test_t njs_backtraces_test[] = }; +static njs_unit_test_t njs_shared_array_buffer_test[] = +{ + { njs_str("SharedArrayBuffer()"), + njs_str("TypeError: Constructor SharedArrayBuffer requires 'new'") }, + + { njs_str("new SharedArrayBuffer()"), + njs_str("[object SharedArrayBuffer]") }, + + { njs_str("SharedArrayBuffer.prototype.constructor.name === 'SharedArrayBuffer'"), + njs_str("true") }, + + { njs_str("SharedArrayBuffer.name"), + njs_str("SharedArrayBuffer") }, + + { njs_str("SharedArrayBuffer.prototype[Symbol.toStringTag]"), + njs_str("SharedArrayBuffer") }, + + { njs_str("var sab = new SharedArrayBuffer(); sab.byteLength"), + njs_str("0") }, + + { njs_str("var sab = new SharedArrayBuffer(16); sab.byteLength"), + njs_str("16") }, + + { njs_str("var sab = new SharedArrayBuffer(100); sab.byteLength"), + njs_str("100") }, + + { njs_str("var sab = new SharedArrayBuffer(16); " + "var view = new Uint8Array(sab); " + "view[0] = 42; view[0]"), + njs_str("42") }, + + { njs_str("var sab = new SharedArrayBuffer(16); " + "var view = new Uint8Array(sab, 4, 8); " + "view.byteLength"), + njs_str("8") }, + + { njs_str("var sab = new SharedArrayBuffer(100); " + "var slice = sab.slice(10, 20); " + "slice.byteLength"), + njs_str("10") }, + + { njs_str("var sab = new SharedArrayBuffer(100); " + "var slice = sab.slice(10, 20); " + "slice.constructor.name"), + njs_str("SharedArrayBuffer") }, + + { njs_str("var sab = new SharedArrayBuffer(100); " + "var dv = new DataView(sab); " + "var sab2 = sab.slice(0, 16); " + "dv.setUint8(10, 99); " + "var dv2 = new DataView(sab2); " + "dv2.getUint8(10)"), + njs_str("0") }, + + { njs_str("var sab = new SharedArrayBuffer(100); " + "var dv = new DataView(sab); " + "dv.setUint8(10, 99); " + "var sab2 = sab.slice(0, 16); " + "var dv2 = new DataView(sab2); " + "dv2.getUint8(10)"), + njs_str("99") }, + + { njs_str("var sab = new SharedArrayBuffer(16); " + "var dv = new DataView(sab); " + "dv.setUint8(0, 99); " + "dv.getUint8(0)"), + njs_str("99") }, + + { njs_str("var sab = new SharedArrayBuffer(16); " + "var view1 = new Uint8Array(sab); " + "var view2 = new Uint8Array(sab); " + "view1[0] = 123; " + "view2[0]"), + njs_str("123") }, + + { njs_str("var sab = new SharedArrayBuffer(16); sab.growable"), + njs_str("false") }, + + { njs_str("var sab = new SharedArrayBuffer(16); sab.maxByteLength"), + njs_str("16") }, + + { njs_str("var sab = new SharedArrayBuffer(16); sab.maxByteLength === sab.byteLength"), + njs_str("true") }, + + { njs_str("var sab = new SharedArrayBuffer(0); sab.growable"), + njs_str("false") }, + + { njs_str("var sab = new SharedArrayBuffer(0); sab.maxByteLength"), + njs_str("0") }, + + { njs_str("var sab = new SharedArrayBuffer(16); var msg; " + "try { sab.grow(32); } catch(e) { msg = e.message } msg"), + njs_str("SharedArrayBuffer is not growable") }, + + { njs_str("var sab = new SharedArrayBuffer(16); var msg; " + "try { sab.grow(8); } catch(e) { msg = e.message } msg"), + njs_str("SharedArrayBuffer is not growable") }, + + { njs_str("var ab = new ArrayBuffer(16); var sab = new SharedArrayBuffer(16); var msg; " + "try { SharedArrayBuffer.prototype.grow.call(ab, 32); } catch(e) { msg = e.message } msg"), + njs_str("Method SharedArrayBuffer.prototype.grow called on incompatible receiver") }, + + { njs_str("var get = Object.getOwnPropertyDescriptor(SharedArrayBuffer.prototype, 'growable').get; " + "get.call([])"), + njs_str("TypeError: Method SharedArrayBuffer.prototype.growable called on incompatible receiver") }, + + { njs_str("var get = Object.getOwnPropertyDescriptor(SharedArrayBuffer.prototype, 'maxByteLength').get; " + "get.call([])"), + njs_str("TypeError: Method SharedArrayBuffer.prototype.byteLength called on incompatible receiver") }, + + { njs_str("typeof $262"), + njs_str("object") }, + + { njs_str("var sab1 = new SharedArrayBuffer(16); " + "var view1 = new Uint8Array(sab1); " + "view1[0] = 42; " + "var r = $r.create('/test'); " + "var sab2 = r.wrapSAB(sab1); " + "var view2 = new Uint8Array(sab2); " + "view2[0]"), + njs_str("42") }, + + { njs_str("var sab1 = new SharedArrayBuffer(16); " + "var view1 = new Uint8Array(sab1); " + "var r = $r.create('/test'); " + "var sab2 = r.wrapSAB(sab1); " + "var view2 = new Uint8Array(sab2); " + "view2[5] = 99; " + "view1[5]"), + njs_str("99") }, + +}; + + typedef struct { njs_bool_t disassemble; njs_str_t filter; @@ -21734,6 +21905,7 @@ typedef struct { njs_bool_t handler; njs_bool_t async; njs_bool_t preload; + njs_bool_t sab_funcs; unsigned seed; } njs_opts_t; @@ -21764,9 +21936,18 @@ typedef struct { njs_external_state_t *states; njs_uint_t size; njs_uint_t current; + njs_queue_t sab_allocs; } njs_runtime_t; +typedef struct { + njs_queue_link_t link; + void *ptr; + size_t size; + uint32_t refcount; +} njs_sab_alloc_entry_t; + + static void njs_unit_test_report(njs_str_t *name, njs_stat_t *prev, njs_stat_t *current) { @@ -21835,16 +22016,12 @@ njs_external_retval(njs_external_state_t *state, njs_int_t ret, njs_str_t *s) static njs_runtime_t * -njs_runtime_init(njs_vm_t *vm, njs_opts_t *opts) +njs_runtime_init(njs_runtime_t *rt, njs_vm_t *vm, njs_opts_t *opts) { - njs_int_t ret; - njs_uint_t i; - njs_runtime_t *rt; + njs_int_t ret; + njs_uint_t i; - rt = njs_mp_alloc(njs_vm_memory_pool(vm), sizeof(njs_runtime_t)); - if (rt == NULL) { - return NULL; - } + njs_queue_init(&rt->sab_allocs); rt->size = opts->repeat; rt->states = njs_mp_alloc(njs_vm_memory_pool(vm), @@ -21896,6 +22073,8 @@ njs_runtime_destroy(njs_runtime_t *rt) { njs_uint_t i; + njs_assert(njs_queue_is_empty(&rt->sab_allocs)); + for (i = 0; i < rt->size; i++) { if (rt->states[i].vm != NULL) { njs_vm_destroy(rt->states[i].vm); @@ -22024,6 +22203,98 @@ njs_module_t *njs_unit_test_addon_external_modules[] = { }; +static void * +njs_sab_mmap_alloc(void *opaque, size_t size) +{ + void *ptr; + njs_runtime_t *rt; + njs_sab_alloc_entry_t *entry; + + rt = opaque; + + if (size == 0) { + size = 1; + } + + ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, + -1, 0); + if (ptr == MAP_FAILED) { + return NULL; + } + + entry = malloc(sizeof(njs_sab_alloc_entry_t)); + if (entry == NULL) { + munmap(ptr, size); + return NULL; + } + + entry->ptr = ptr; + entry->size = size; + entry->refcount = 1; + + njs_queue_insert_tail(&rt->sab_allocs, &entry->link); + + return ptr; +} + + +static void +njs_sab_mmap_free(void *opaque, void *ptr) +{ + njs_runtime_t *rt; + njs_queue_link_t *lnk; + njs_sab_alloc_entry_t *entry; + + rt = opaque; + + for (lnk = njs_queue_first(&rt->sab_allocs); + lnk != njs_queue_tail(&rt->sab_allocs); + lnk = njs_queue_next(lnk)) + { + entry = njs_queue_link_data(lnk, njs_sab_alloc_entry_t, link); + + if (entry->ptr == ptr) { + entry->refcount--; + + if (entry->refcount == 0) { + munmap(entry->ptr, entry->size); + njs_queue_remove(&entry->link); + free(entry); + } + + return; + } + } + + njs_assert_msg(0, "SharedArrayBuffer free of unknown pointer"); +} + + +static void +njs_sab_mmap_dup(void *opaque, void *ptr) +{ + njs_runtime_t *rt; + njs_queue_link_t *lnk; + njs_sab_alloc_entry_t *entry; + + rt = opaque; + + for (lnk = njs_queue_first(&rt->sab_allocs); + lnk != njs_queue_tail(&rt->sab_allocs); + lnk = njs_queue_next(lnk)) + { + entry = njs_queue_link_data(lnk, njs_sab_alloc_entry_t, link); + + if (entry->ptr == ptr) { + entry->refcount++; + return; + } + } + + njs_assert_msg(0, "SharedArrayBuffer dup of unknown pointer"); +} + + static njs_int_t njs_unit_test(njs_unit_test_t tests[], size_t num, njs_str_t *name, njs_opts_t *opts, njs_stat_t *stat) @@ -22039,6 +22310,7 @@ njs_unit_test(njs_unit_test_t tests[], size_t num, njs_str_t *name, njs_runtime_t *rt; njs_opaque_value_t retval; njs_external_state_t *state; + njs_sab_functions_t sab_funcs; njs_str_t preload = njs_str( "globalThis.preload = JSON.parse(" @@ -22114,7 +22386,22 @@ njs_unit_test(njs_unit_test_t tests[], size_t num, njs_str_t *name, njs_disassembler(vm); } - rt = njs_runtime_init(vm, opts); + rt = njs_mp_alloc(njs_vm_memory_pool(vm), sizeof(njs_runtime_t)); + if (rt == NULL) { + njs_stderror("njs_mp_alloc() failed\n"); + goto done; + } + + if (opts->sab_funcs) { + sab_funcs.sab_alloc = njs_sab_mmap_alloc; + sab_funcs.sab_free = njs_sab_mmap_free; + sab_funcs.sab_dup = njs_sab_mmap_dup; + sab_funcs.sab_opaque = rt; + + njs_vm_set_sab_functions(vm, &sab_funcs); + } + + rt = njs_runtime_init(rt, vm, opts); if (rt == NULL) { njs_stderror("njs_runtime_init() failed\n"); goto done; @@ -23371,6 +23658,18 @@ static njs_test_suite_t njs_suites[] = njs_nitems(njs_backtraces_test), njs_unit_test }, + { njs_str("sab_funcs"), + { .repeat = 1, .unsafe = 1, .externals = 1, .sab_funcs = 1 }, + njs_shared_array_buffer_test, + njs_nitems(njs_shared_array_buffer_test), + njs_unit_test }, + + { njs_str("no sab_funcs"), + { .repeat = 1, .unsafe = 1, .externals = 1 }, + njs_shared_array_buffer_test, + njs_nitems(njs_shared_array_buffer_test), + njs_unit_test }, + { njs_str("timezone"), { .repeat = 1, .unsafe = 1 }, njs_tz_test,