Skip to content

主机集成 ​

在 C 应用里嵌入 qzjs 分五步。宿主和运行时只通过 JSON 消息通信。

五步 ​

┌─────────────────────────────────────────────────────────────┐
│ 1. create      qz_create(&cfg)   — 线程 + 循环 + JS 就绪    │
│ 2. script      initial_script            — JS 先跑什么        │
│ 3. communicate qz_post_message ⇄ message_cb  — JSON 契约   │
│ 4. lend        暴露 C 函数、serve/fs/worker/crypto 给 JS      │
│ 5. destroy     qz_destroy(rt)    — 优雅销毁                 │
└─────────────────────────────────────────────────────────────┘

1. Create ​

qz_create 启动 qzjs 内部线程、拉起 libuv 循环、执行 cfg.initial_script。它阻塞到就绪才返回,这时运行时已活、initial_script 已跑完。

c
qz_config_t cfg = {0};
cfg.initial_script = "postMessage({ready: true});";
cfg.message_cb = on_message;      // 出站 JS→host
qz_t *rt = qz_create(&cfg);   // 阻塞直到就绪

2. 选择 JS 先跑什么 ​

喂给运行时初始脚本有三种方式:

  • initial_script —— 小字符串,适合引导逻辑。qzjs 在内部把自身的 WinterTC polyfill 编译为字节码。预编译字节码也可叠加:设置 initial_bytecode 在脚本之后运行 qz_compile() 产物(字节码与构建绑定, 见 字节码)
  • initial_script_path —— 指向磁盘上 JS 文件的路径;qz_create 时读取并求值。适合脚本以文件形式部署的场景。两者都设时 initial_script_path 优先;文件不存在则 qz_create 返回 NULL
  • qz_post_message —— 创建后一切由消息驱动

3. 消息契约 ​

宿主和 JS 双向都以 JSON 字符串交换数据:不传指针,不共享内存对象。

qzjs 自己管线程和循环。宿主不调用 JS 让它运行,运行时也不阻塞宿主线程。

方向机制线程
主机 → JSqz_post_message(rt, json, len)线程安全,任意线程可调
JS → 主机cfg.message_cb(rt, json, len, data)在 qzjs 线程上触发

规则:

  • 两个方向都是 JSON 字符串。 不传指针,不共享内存对象,只传可序列化的数据。
  • qz_post_message 线程安全。 可从任意主机线程调用;它入队到 qzjs 的入站队列。
  • message_cb 在 qzjs 线程上运行。 保持快速且线程安全,它和事件循环、所有 JS 共享这个线程。
  • 有界队列。 运行时忙(或者 JS 一直不读)时,入站消息会在队列边界积压。 你的主机代码要能接受 qz_post_message 不会马上排空。
c
static void on_message(qz_t *rt, const char *json, size_t len, void *data) {
    (void)rt; (void)data;
    // json 是完整 JSON 字符串;在主机侧解析并分发
    handle_json(json, len);
}

// 任意主机线程:
qz_post_message(rt, "{\"cmd\":\"start\",\"n\":42}", 22);

反方向(JS 调 C)也一样:JS 里 postMessage 会落到 message_cb,或者把 C 函数 注册成 JS 全局。见 扩展 和 嵌入模式。

发送代码执行 ​

边界承载 JSON,但往 JSON 里放什么由你决定。常见的做法是发一条 { cmd: 'eval', code: ... } 消息,让 JS 侧执行——REPL、动态规则引擎就是这么做的:

js
// initial_script
globalThis.onmessage = function (e) {
  if (e.data && e.data.cmd === 'eval') {
    let out;
    try { out = eval(e.data.code); }
    catch (err) { out = { error: String(err) }; }
    postMessage({ result: out });
  }
};
c
// 宿主侧——发送要执行的代码
qz_post_message(rt, "{\"cmd\":\"eval\",\"code\":\"2 + 2\"}", 26);
// message_cb 收到:{"result":4}

代码片段由运行时的 JS eval 执行,结果像任何其他回复一样经 message_cb 流回。

双端事件分发 ​

两端都按事件类型分发。约定一个形状——{"type": ..., "payload": ...}——并给每一端 各自的转发器:JS 侧在 onmessage 里路由入站宿主消息,C 侧在 message_cb 里路由 入站 JS 回复。

JS 侧——一个处理事件表并回复的转发器:

js
// initial_script — JS 事件转发器
const handlers = {
  ping(d)  { return { ok: true, at: Date.now() }; },
  add(d)   { return d.a + d.b; },
};
globalThis.onmessage = function (e) {
  const { type, payload } = e.data || {};
  const h = handlers[type];
  postMessage({ type: type + ':reply', ok: !!h, payload: h ? h(payload) : undefined });
};

宿主侧——在 message_cb 里镜像同样的分发,把每条入站事件(一个 {type, payload} JSON 字符串)路由到对应 C 处理器:

c
#include <qzjs/qzjs.h>
#include <stdio.h>
#include <string.h>

static void on_ping(const char *json)  { puts("[host] ping:reply"); }
static void on_add(const char *json)   { puts("[host] add:reply"); }

static void on_message(qz_t *rt, const char *json, size_t len, void *data) {
    (void)rt; (void)data;
    /* 用宿主语言的 JSON 库解析 type 再分发;此处为简洁用子串匹配 */
    if (strstr(json, "\"type\":\"ping:reply\"")) on_ping(json);
    else if (strstr(json, "\"type\":\"add:reply\"")) on_add(json);
}

int main(void) {
    qz_config_t cfg = {0};
    cfg.message_cb = on_message;
    cfg.initial_script = "/* 上面的 JS 转发器 */";
    qz_t *rt = qz_create(&cfg);

    const char *ping = "{\"type\":\"ping\",\"payload\":{}}";
    qz_post_message(rt, ping, strlen(ping));            // → on_ping
    const char *add  = "{\"type\":\"add\",\"payload\":{\"a\":2,\"b\":3}}";
    qz_post_message(rt, add, strlen(add));              // → on_add

    qz_destroy(rt);
    return 0;
}

每端一个转发器让事件契约对称、可读:JS 的事件表与 C 的 if/else 链命名同一组事件, 两端对 type 的含义保持一致。

4. 向 JS 出借能力 ​

运行时里的 JS 以全局对象的形式拿到 WinterTC 接口,不用 import:fetch、 crypto.subtle、ReadableStream、timers、fs、WebSocket、Worker、 BroadcastChannel、serve()(HTTP/WS/gRPC 服务器)。见 JS API 参考。

也可以把自己的 C 函数注册成 JS 全局。见 扩展。

5. Destroy ​

qz_destroy 执行优雅关闭:通知内部线程、排空待处理工作、 释放运行时。运行时不再需要时从宿主调用。完整生命周期与内存模型见 运行时生命周期。


相关页面 ​

主题页面
线程所有权、就绪、关闭运行时生命周期
谁驱动循环、背压事件循环
单运行时内多个隔离上下文多上下文
注册 C 函数 / 结构化数据嵌入模式
C API 参考C API

MIT 许可证