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:
JSRuntime: Represents the JavaScript runtime instance, managing memory allocation, garbage collection, and global state. A single process can host multiple independent runtimes.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.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:
- Every time a
JSValueis created or returned by the API, its reference count increments. - Call
JS_FreeValue(ctx, val)on anyJSValueyou create or receive to prevent memory leaks. - For strings extracted using
JS_ToCString(), always free them usingJS_FreeCString(ctx, str).
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.