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 b0b64d974..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; @@ -3597,6 +3668,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 +3694,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 +4401,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); @@ -4380,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'); + +###############################################################################