How to Embed QuickJS JavaScript Engine in C Programs

QuickJS is an ultra-compact, high-performance JavaScript engine that supports the full ECMAScript 2020 specification with minimal memory overhead. Created by Fabrice Bellard and Charlie Gordon, it enables developers to seamlessly embed a modern JavaScript runtime into native C and C++ applications. This article explores what QuickJS is, why it is ideal for embedded scripting, and the step-by-step process of integrating it into a C project to execute scripts and bind native functions.


What is QuickJS?

QuickJS is a lightweight, embeddable JavaScript engine written in pure C. Unlike heavy engines like Google V8 or SpiderMonkey, QuickJS is designed for simplicity, minimal resource consumption, and rapid startup times.

Key features include: - Full ES2020 Compliance: Supports modules, asynchronous generators, Proxies, BigInt, and standard library features. - Small Footprint: The compiled binary is only a few hundred kilobytes with zero external dependencies. - Fast Startup: Initializes in fractions of a millisecond, making it suitable for command-line tools and resource-constrained environments. - C Integration: Offers a straightforward C API to execute code, manipulate JavaScript objects, and expose native C functions to the runtime.


Core Architecture Concepts

To embed QuickJS, you need to understand three core structures:

  1. JSRuntime: Represents the JavaScript runtime instance, managing memory allocation, garbage collection, and global state. A single process can host multiple independent runtimes.
  2. JSContext: Represents an execution context within a runtime. It holds its own global object and built-in prototypes. Multiple contexts can share a single runtime.
  3. JSValue: The unified data type used to pass values (primitives, objects, functions, errors) between C and JavaScript.

Step-by-Step: Embedding QuickJS in C

1. Initialize the Engine

To start running JavaScript, allocate a new runtime and create an execution context.

#include "quickjs.h"

int main(void) {
    // 1. Create a runtime
    JSRuntime *rt = JS_NewRuntime();
    if (!rt) return 1;

    // 2. Create a context
    JSContext *ctx = JS_NewContext(rt);
    if (!ctx) {
        JS_FreeRuntime(rt);
        return 1;
    }

    // Engine is ready for execution...

    // 3. Clean up
    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);
    return 0;
}

2. Evaluating JavaScript Code

You can evaluate JavaScript strings using JS_Eval(). The function returns a JSValue containing the result.

const char *script = "const add = (a, b) => a + b; add(10, 25);";

JSValue result = JS_Eval(ctx, script, strlen(script), "<input>", JS_EVAL_TYPE_GLOBAL);

if (JS_IsException(result)) {
    JSValue exception = JS_GetException(ctx);
    const char *err_msg = JS_ToCString(ctx, exception);
    printf("Error: %s\n", err_msg);
    JS_FreeCString(ctx, err_msg);
    JS_FreeValue(ctx, exception);
} else {
    int32_t val;
    JS_ToInt32(ctx, &val, result);
    printf("Result: %d\n", val); // Output: Result: 35
}

JS_FreeValue(ctx, result);

3. Exposing a C Function to JavaScript

QuickJS allows binding native C functions to the JavaScript global object so they can be called from scripts.

#include <stdio.h>
#include "quickjs.h"

// Define a C function matching the JSCFunction signature
static JSValue js_native_print(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    if (argc > 0) {
        const char *str = JS_ToCString(ctx, argv[0]);
        printf("[Native C Output] %s\n", str);
        JS_FreeCString(ctx, str);
    }
    return JS_UNDEFINED;
}

int main(void) {
    JSRuntime *rt = JS_NewRuntime();
    JSContext *ctx = JS_NewContext(rt);

    // Get the global JavaScript object
    JSValue global_obj = JS_GetGlobalObject(ctx);

    // Bind the C function to globalThis.nativePrint
    JS_SetPropertyStr(ctx, global_obj, "nativePrint",
                      JS_NewCFunction(ctx, js_native_print, "nativePrint", 1));

    // Execute script that invokes the C function
    const char *code = "nativePrint('Hello from QuickJS!');";
    JSValue ret = JS_Eval(ctx, code, strlen(code), "<eval>", JS_EVAL_TYPE_GLOBAL);

    // Cleanup
    JS_FreeValue(ctx, ret);
    JS_FreeValue(ctx, global_obj);
    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);

    return 0;
}

Memory Management Best Practices

QuickJS uses reference counting for memory management:


Summary

QuickJS provides a full-featured JavaScript environment in an embeddable C library. By creating a JSRuntime and JSContext, executing code with JS_Eval, and exposing native functions with JS_NewCFunction, you can add scripting capabilities, configuration logic, and dynamic runtime features to any C or C++ application.