]> git.kaiwu.me - njs.git/commitdiff
QuickJS: added native module support.
authorDmitry Volyntsev <xeioex@nginx.com>
Wed, 3 Dec 2025 01:39:44 +0000 (17:39 -0800)
committerDmitry Volyntsev <xeioexception@gmail.com>
Mon, 12 Jan 2026 23:05:41 +0000 (15:05 -0800)
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
nginx/ngx_http_js_module.c
nginx/ngx_js.c
nginx/ngx_js.h
nginx/ngx_stream_js_module.c
nginx/t/js_native_module.t [new file with mode: 0644]
nginx/t/stream_js_native_module.t [new file with mode: 0644]

index 5e2d9277f53d3e7c9f26c6c91828eb55b8a7212c..f54234a83684276969ed0896da2b96cf9fe169be 100644 (file)
@@ -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"
index d7bcd78f4345df3eceddfefa5e6bdef288a921dc..20308a815432dc482be2ccc4e4ebc07ee0b5a833 100644 (file)
@@ -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 */
@@ -7755,6 +7791,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
 
index c18e97d54da92f380ab7aeb3054eb04ace954211..bd1b5f2379303834b0770b62c455834e4facbabd 100644 (file)
@@ -9,6 +9,7 @@
 #include <ngx_config.h>
 #include <ngx_core.h>
 #include <math.h>
+#include <dlfcn.h>
 #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 @@ not_found:
 }
 
 
+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)
 {
index f3c2493b804d521470607d1b75c37ee7aeba7427..a25dc65a0998820a87a793f930ac8f78a231ff7e 100644 (file)
@@ -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,
index cbceaf92366db909689a3476c2869968338820d1..b21b701dd2fa7c721e07e06f4eb01ecf12f96c4d 100644 (file)
@@ -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 (file)
index 0000000..d8ec683
--- /dev/null
@@ -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', <<EOF);
+    import * as native from 'test.so';
+    import * as native2 from 'test';
+
+    function test_add(r) {
+        let a = Number(r.args.a);
+        let b = Number(r.args.b);
+        r.return(200, native.add(a, b).toString());
+    }
+
+    function test_reverse(r) {
+        r.return(200, native2.reverseString(r.args.str));
+    }
+
+    export default { test_add, test_reverse };
+
+EOF
+
+$t->write_file('test.c', <<EOF);
+#include <quickjs.h>
+
+#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 (file)
index 0000000..8d7d727
--- /dev/null
@@ -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', <<EOF);
+    import * as native from 'test.so';
+    import * as native2 from 'test';
+
+    function reverse(s) {
+        return native.reverseString(s.remoteAddress);
+    }
+
+    function duplicate(s) {
+        return native2.duplicate(s.remoteAddress);
+    }
+
+    export default { reverse, duplicate };
+
+EOF
+
+$t->write_file('test.c', <<EOF);
+#include <quickjs.h>
+#include <string.h>
+
+#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');
+
+###############################################################################