Embedding Patterns
Common patterns for embedding qzjs in C applications.
Basic Embedding
qzjs owns its own internal thread and libuv event loop. All JS runs on that thread; the host communicates with the runtime over JSON messages.
#include <qzjs/qzjs.h>
#include <stdio.h>
static void on_message(qz_t *rt, const char *json, size_t len, void *data) {
(void)rt; (void)data;
printf("received: %.*s\n", (int)len, json);
}
int main(void) {
qz_config_t cfg = {0};
cfg.initial_script = "postMessage({hello: 'world'});";
cfg.message_cb = on_message;
qz_t *rt = qz_create(&cfg);
if (!rt) return 1;
// Your application logic: drive the runtime by posting JSON messages
qz_post_message(rt, "{\"cmd\":\"echo\",\"data\":\"hi\"}", 26);
qz_destroy(rt);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
qz_create blocks until qzjs's internal thread is ready and initial_script has been eval'd. The host sends messages via qz_post_message (thread-safe) and receives replies through message_cb, which fires on the qzjs thread (so your callback must be thread-safe). qz_destroy performs a graceful shutdown.
Calling C Functions from JS
Register C functions as JS globals:
#include <quickjs.h>
#include "qz_internal.h" // qz_get_active_jsctx (internal helper)
static JSValue greet(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv) {
QZ_UNUSED(this_val);
const char *name = "World";
if (argc > 0) name = JS_ToCString(ctx, argv[0]);
printf("Hello, %s!\n", name);
if (argc > 0) JS_FreeCString(ctx, name);
return JS_UNDEFINED;
}
// Register in a custom extension's init hook:
static int my_ext_init(qz_ext_t *ext, qz_t *rt) {
QZ_UNUSED(ext);
JSContext *ctx = qz_get_active_jsctx(rt);
JSValue global = JS_GetGlobalObject(ctx);
JS_SetPropertyStr(ctx, global, "greet",
JS_NewCFunction(ctx, greet, "greet", 1));
JS_FreeValue(ctx, global);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
The init hook runs on qzjs's internal thread during qz_create, before the host receives the runtime — so registering globals here is safe. qz_get_active_jsctx is an internal helper (declared in src/qz_internal.h), for use from extension hooks.
Calling JS from C with Structured Data
Calling JS from C means posting a JSON message and letting the JS side reply via postMessage. Install a handler in initial_script:
static void on_message(qz_t *rt, const char *json, size_t len, void *data) {
(void)rt; (void)data;
printf("JS returned: %.*s\n", (int)len, json);
}
// Bootstrap an onmessage handler that processes structured data
qz_config_t cfg = {0};
cfg.initial_script =
"globalThis.onmessage = function (e) {"
" var d = e.data;"
" if (d.cmd === 'process')"
" postMessage({ doubled: d.value * 2, ok: true });"
"};";
cfg.message_cb = on_message;
qz_t *rt = qz_create(&cfg);
// Post the input as a JSON message; the reply arrives via message_cb
qz_post_message(rt, "{\"cmd\":\"process\",\"value\":21}", 28);
// on_message prints: JS returned: {"doubled":42,"ok":true}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
The JSON is copied by qz_post_message (thread-safe, callable from any thread). There is no synchronous qz_call — results always flow back as messages.
Per-Request Context Isolation
Contexts are managed inside the runtime (src/context.c); the host does not manipulate them through the public C API. The host sees one runtime and communicates over JSON messages (qz_post_message / message_cb). For request-level isolation, either create a fresh qz_t per request (each is fully independent — own thread, loop, and JS state) or route requests into a running runtime by message, tagging them so the JS side can keep per-request state.
Testing with mock_libuv
For deterministic offline tests, replace libuv with mock_libuv (test/mock_libuv.{c,h}) — a fake uv_* API with no network or system calls. Gtest suites link qzjs + mock_libuv (with -DQZ_USE_MOCK_LIBUV) and drive the runtime through the HostCtx harness in test/test_host.h:
host_create(script)/host_destroy(h)— start/stop a runtime with a bootstrap that installsglobalThis.onmessagehandling{cmd:'eval', code}and{cmd:'echo'}host_eval(h, code, &out)/host_value(h, code, &out)— evaluate JS via the command channelhost_poll_until_value(h, expr, sub, &out)— poll until a condition holds (used for async results: promises, timers, storage)
See Testing for details.
Multiple Independent Runtimes
Since qzjs has zero global state, you can run multiple qz_t instances — each owns its own internal thread, libuv loop, and JS state:
static void on_message(qz_t *rt, const char *json, size_t len, void *data) {
(void)rt;
printf("%s: %.*s\n", (const char *)data, (int)len, json);
}
qz_config_t cfg1 = { .initial_script = "postMessage('rt1');",
.message_cb = on_message, .host_data = "rt1" };
qz_config_t cfg2 = { .initial_script = "postMessage('rt2');",
.message_cb = on_message, .host_data = "rt2" };
qz_t *rt1 = qz_create(&cfg1);
qz_t *rt2 = qz_create(&cfg2);
// Post to each independently; replies arrive on each runtime's message_cb
qz_post_message(rt1, "{\"cmd\":\"echo\",\"data\":\"a\"}", 26);
qz_post_message(rt2, "{\"cmd\":\"echo\",\"data\":\"b\"}", 26);
qz_destroy(rt1);
qz_destroy(rt2);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
No host loop to drive — each runtime runs itself.
Error Handling Patterns
There is no synchronous eval, so errors surface as messages rather than return codes:
- If
initial_scriptthrows,qz_createreturnsNULL. - At runtime, JS can report failures explicitly — e.g. an
onmessagehandler repliespostMessage({ ok: false, e: String(err) }), which the host reads inmessage_cb:
static void on_message(qz_t *rt, const char *json, size_t len, void *data) {
(void)rt; (void)data;
printf("%.*s\n", (int)len, json); // e.g. {"ok":false,"e":"TypeError: ..."}
}2
3
4
An uncaught exception inside a message handler does not take down the runtime.
Memory Management
qz_freestill exists for malloc'd blocks returned by qzjs (qz_free(NULL)is safe) — there is no longer anyqz_eval/qz_callresult to free- The runtime owns all its internal resources (thread, libuv loop, contexts) —
qz_destroyfrees everything on graceful shutdown - Per-runtime host data: set
config.host_databeforeqz_create; an extension'sinithook reads it viaqz_get_runtime_data(rt)during create (the rt is valid inside init, before the host receives it). Note:qz_ext_t.user_datais on the shared compile-time extension struct — useqz_get_runtime_data/qz_set_runtime_datafor per-instance data, notuser_data(which is shared across runtimes).