From b5e8aa89b28a79b6fbe834498f118a757bf8fe87 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Fri, 5 Dec 2025 17:08:54 -0800 Subject: [PATCH 1/2] 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/2] 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'); + +###############################################################################