Skip to main content

Overview

QuickJS supports both ES6 JavaScript modules and native C modules. The module system follows the ECMAScript specification and allows you to organize code into reusable components.

ES6 JavaScript modules

Module syntax

QuickJS supports standard ES6 module syntax:
// math.js - exporting module
export function add(a, b) {
    return a + b;
}

export function multiply(a, b) {
    return a * b;
}

export const PI = 3.14159;
// app.js - importing module
import { add, multiply, PI } from './math.js';

console.log(add(2, 3));        // 5
console.log(multiply(4, 5));   // 20
console.log(PI);               // 3.14159

Loading ES6 modules

From the command line:
# Explicitly load as module
qjs --module app.js

# Auto-detect from import/export
qjs app.js
From C code:
// Evaluate as a module
JSValue result = JS_Eval(ctx, module_code, code_len, 
                         "app.js", JS_EVAL_TYPE_MODULE);

if (JS_IsException(result)) {
    // Handle error
    JSValue ex = JS_GetException(ctx);
    JS_FreeValue(ctx, ex);
}

JS_FreeValue(ctx, result);

Module detection

QuickJS can automatically detect if code is a module:
int is_module = JS_DetectModule(source_code, code_length);

int eval_flags = is_module ? JS_EVAL_TYPE_MODULE : JS_EVAL_TYPE_GLOBAL;
JSValue result = JS_Eval(ctx, source_code, code_length, 
                         filename, eval_flags);

Native C modules

Native modules allow you to expose C functionality to JavaScript.

Module structure

Every native module must have an initialization function:
#include <quickjs.h>

// Module initialization function
JS_MODULE_EXTERN JSModuleDef *js_init_module(JSContext *ctx, 
                                             const char *module_name) {
    JSModuleDef *m;
    
    // Create module definition
    m = JS_NewCModule(ctx, module_name, js_module_init);
    if (!m)
        return NULL;
    
    // Add exports
    JS_AddModuleExport(ctx, m, "myFunction");
    JS_AddModuleExport(ctx, m, "myConstant");
    
    return m;
}

Implementing exports

// C function to expose
static JSValue js_my_function(JSContext *ctx, JSValueConst this_val,
                             int argc, JSValueConst *argv) {
    if (argc < 1) {
        return JS_ThrowTypeError(ctx, "expected 1 argument");
    }
    
    int32_t value;
    if (JS_ToInt32(ctx, &value, argv[0]))
        return JS_EXCEPTION;
    
    // Do something with value
    return JS_NewInt32(ctx, value * 2);
}

// Module initialization callback
static int js_module_init(JSContext *ctx, JSModuleDef *m) {
    // Set function export
    JS_SetModuleExport(ctx, m, "myFunction", 
                       JS_NewCFunction(ctx, js_my_function, "myFunction", 1));
    
    // Set constant export
    JS_SetModuleExport(ctx, m, "myConstant", JS_NewInt32(ctx, 42));
    
    return 0;
}

Complete example

Here’s a complete native module from the QuickJS examples:
// fib.c - Fibonacci module
#include <quickjs.h>

// Compute fibonacci number
static int fib(int n) {
    if (n <= 1)
        return n;
    return fib(n - 1) + fib(n - 2);
}

// JavaScript function wrapper
static JSValue js_fib(JSContext *ctx, JSValueConst this_val,
                     int argc, JSValueConst *argv) {
    int32_t n;
    if (JS_ToInt32(ctx, &n, argv[0]))
        return JS_EXCEPTION;
    return JS_NewInt32(ctx, fib(n));
}

// Module initialization
static int js_fib_init(JSContext *ctx, JSModuleDef *m) {
    JS_SetModuleExport(ctx, m, "fib", 
                       JS_NewCFunction(ctx, js_fib, "fib", 1));
    return 0;
}

// Entry point
JS_MODULE_EXTERN JSModuleDef *js_init_module(JSContext *ctx, 
                                             const char *module_name) {
    JSModuleDef *m = JS_NewCModule(ctx, module_name, js_fib_init);
    if (!m)
        return NULL;
    JS_AddModuleExport(ctx, m, "fib");
    return m;
}
Usage from JavaScript:
import { fib } from 'fib';

console.log(fib(10));  // 55

Built-in modules

QuickJS includes several built-in modules:

qjs:std module

import * as std from 'qjs:std';

std.loadFile('data.txt');  // Read file
std.exit(0);               // Exit process

qjs:os module

import * as os from 'qjs:os';

os.getcwd();               // Get current directory
os.chdir('/tmp');          // Change directory

qjs:bjson module

import * as bjson from 'qjs:bjson';

const data = { x: 42 };
const binary = bjson.write(data);
const restored = bjson.read(binary);

Initializing built-in modules

From C code:
#include <quickjs-libc.h>

JSContext *ctx = JS_NewContext(rt);

// Add standard library modules
js_init_module_std(ctx, "qjs:std");
js_init_module_os(ctx, "qjs:os");
js_init_module_bjson(ctx, "qjs:bjson");

// Or use helper to add globals + modules
js_std_add_helpers(ctx, argc, argv);

Module loading

Custom module loader

You can implement a custom module loader:
JSModuleDef *my_module_loader(JSContext *ctx, const char *module_name,
                             void *opaque, JSValueConst attributes) {
    // Check if it's a custom module
    if (strcmp(module_name, "mymodule") == 0) {
        return js_init_module_mymodule(ctx, module_name);
    }
    
    // Fall back to default loader
    return js_module_loader(ctx, module_name, opaque, attributes);
}

// Set the custom loader
JS_SetModuleLoaderFunc(rt, NULL, my_module_loader, NULL);

Default module loader

The default loader provided by quickjs-libc:
#include <quickjs-libc.h>

// Set up default module loader (loads from filesystem)
JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL);

// Set import.meta handler
JS_SetHostDefinedFunc(ctx, js_module_set_import_meta, NULL);

Dynamic imports

QuickJS supports dynamic import() expressions:
// Static import
import { helper } from './utils.js';

// Dynamic import (returns a Promise)
const moduleName = './plugins/' + pluginName + '.js';
const module = await import(moduleName);
module.init();
From C:
// Dynamic imports work through JS_Eval
JSValue result = JS_Eval(ctx, 
    "import('./module.js').then(m => m.doSomething())",
    strlen("import('./module.js').then(m => m.doSomething())"),
    "<input>", JS_EVAL_TYPE_MODULE);

Module namespaces

Import entire module as namespace:
import * as math from './math.js';

console.log(math.add(1, 2));
console.log(math.PI);
Default exports:
// lib.js
export default function() {
    return 'default export';
}

export const helper = () => 'named export';

// app.js
import myLib, { helper } from './lib.js';

console.log(myLib());     // 'default export'
console.log(helper());    // 'named export'

Module caching

QuickJS caches loaded modules:
// math.js loaded once
import { add } from './math.js';
import { multiply } from './math.js';  // Uses cached version

Best practices

Use ES6 modules for JavaScript - They provide better tooling support and are standard JavaScript.
Keep native modules focused - Each C module should have a single, well-defined purpose.
Always export declared names - The names passed to JS_AddModuleExport() must match those in JS_SetModuleExport().
Module paths are resolved relative to the current module’s directory, similar to Node.js.

Example: Class-based module

Native modules can export classes with methods:
// See examples/point.c for complete example
static JSClassID js_point_class_id;

static JSValue js_point_constructor(JSContext *ctx, JSValueConst new_target,
                                   int argc, JSValueConst *argv) {
    // Create instance with opaque data
    Point *p = malloc(sizeof(Point));
    JS_ToFloat64(ctx, &p->x, argv[0]);
    JS_ToFloat64(ctx, &p->y, argv[1]);
    
    JSValue obj = JS_NewObjectClass(ctx, js_point_class_id);
    JS_SetOpaque(obj, p);
    return obj;
}

JSModuleDef *js_init_module_point(JSContext *ctx, const char *name) {
    // Register class
    JS_NewClassID(&js_point_class_id);
    JS_NewClass(rt, js_point_class_id, &js_point_class);
    
    // Create module with Point constructor
    JSModuleDef *m = JS_NewCModule(ctx, name, js_point_init);
    JS_AddModuleExport(ctx, m, "Point");
    return m;
}

See also

Build docs developers (and LLMs) love