# 嵌入 Deft Deft 的设计目标之一是作为脚本引擎嵌入宿主应用。它提供两套 API 接口,二者由同一份代码支撑: - C API —— 链接 `libdeft.a`,`#include "deft.h"`。值以 `uint64_t`(NaN 装箱)传递。适用于 C/C++/Rust 以及任何支持 C FFI 的语言。 - Zig API —— `@import` `kernel` 模块并直接使用 `Kernel` 结构体。值是原生 `Value` 联合体。当宿主本身也是 Zig 项目时使用它(可获得更丰富的类型,并且能在不跨越 ABI 边界的情况下注册原生函数)。 两套 API 暴露相同的操作:创建 VM、求值源码、按名称调用函数、读写全局变量、注册宿主函数,以及实施资源限制。 ## 架构 ``` ┌─────────────────────────────────────────────┐ │ Host application (C, C++, Rust, Zig, ...) │ │ │ │ deft.h / deft_vm_t* Kernel struct │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────┐ ┌────────────────────┐ │ │ │ src/host/ffi/ │ │ src/kernel/ │ │ │ │ capi.zig │──▶│ kernel.zig │ │ │ │ (C ABI shim) │ │ (embedding API) │ │ │ └─────────────────┘ └────────┬───────────┘ │ │ │ │ │ ┌──────────────▼──────────┐ │ │ │ VM + compiler + parser │ │ │ │ + GC heap (NaN-boxing) │ │ │ └─────────────────────────┘ │ └─────────────────────────────────────────────┘ ```` C 垫片(`capi.zig`)是 `Kernel` 之上的薄封装。每个 `deft_*` 函数都与一个 `Kernel` 方法一一对应,因此两套接口的行为完全一致。 --- ## 构建库 ### C / 静态库 ``` $ zig build lib # builds + installs libdeft.a and deft.h $ zig build # installs the vendored dependency libs too ```` `zig build lib` 会生成: ``` zig-out/lib/libdeft.a zig-out/include/deft.h ```` `libdeft.a` 以位置无关方式(`-fPIC`)构建,因此可以链接进 PIE 可执行文件或共享库。它引用 —— 但不捆绑 —— 随附的 SQLite、PCRE2 和 mbedTLS 静态库。运行默认的 `zig build`(该命令构建可执行文件及其依赖产物)时,这些库会落到 `zig-out/lib/`: ``` zig-out/lib/libdeft.a libsqlite3.a libpcre2.a libmbedtls.a ```` 将你的程序与全部四个库链接,`deft` 在前,其后是它的依赖: ``` $ cc -o myapp myapp.c -Izig-out/include -Lzig-out/lib -ldeft \ -lsqlite3 -lpcre2 -lmbedtls -lm -lpthread ```` > 构建缺口: `lib` 步骤目前只安装 `libdeft.a` > 和头文件;还不会安装它链接所需的依赖归档。与 `lib` 一起 > 运行默认的 `zig build`(或任意可执行目标)才会把随附依赖 > 填充到 `zig-out/lib/`。自包含的 `lib` 安装步骤 > 是已知的待改进项。 ### 交叉编译 面向常用的 Zig 三元组: ``` $ zig build lib -Dtarget=x86_64-linux-gnu $ zig build lib -Dtarget=aarch64-linux-gnu $ zig build lib -Dtarget=x86_64-linux-musl # fully static ```` ### Zig 依赖 如果你的宿主是 Zig 项目,请在 `build.zig.zon` 中添加 `deft_zig`: ```zig .dependencies = .{ .deft_zig = .{ .url = "https://...", .hash = "...", }, }, ```` 然后在 `build.zig` 中: ```zig const deft = b.dependency("deft_zig", .{ .target = target, .optimize = optimize, }); const mod = deft.module("deft_zig"); // the root module exe_mod.addImport("deft_zig", mod); ```` 再在你的代码中: ```zig const deft = @import("deft_zig"); const Kernel = deft.kernel.Kernel; ```` --- ## C API 完整头文件是 `include/deft.h`。所有操作都基于不透明的 `deft_vm_t*` 句柄,值以 `uint64_t` 传递。 ### 生命周期 ```c deft_vm_t *vm = deft_vm_init(print_fn, import_fn, max_fuel); // ... use vm ... deft_vm_deinit(vm); ```` | 参数 | 类型 | 用途 | |---|---|---| | `print_fn` | `deft_print_fn` | 接收 `echo`/`puts` 的输出。`NULL` = stdout。 | | `import_fn` | `deft_import_fn` | 将 `import` 路径解析为源码。`NULL` = 文件系统。 | | `max_fuel` | `uint64_t` | 操作预算。`0` = 无限制。 | 分配失败时返回 `NULL`。 #### 打印回调 ```c void my_print(const char *data, size_t len) { fwrite(data, 1, len, stdout); // or your own log/buffer } deft_vm_t *vm = deft_vm_init(my_print, NULL, 0); ```` 回调接收的是原始字节 —— 不以 null 结尾。请使用 `len` 参数;`printf("%s", data)` 并不安全。 #### 导入回调 拦截 `import "path" as alias`,让嵌入式 VM 完全不接触文件系统: ```c const char *my_import(const char *path, size_t path_len, size_t *out_len) { if (memcmp(path, "mathy", path_len) == 0) { static const char src[] = "def pi 3.14159"; *out_len = sizeof(src) - 1; return src; } return NULL; // not found } deft_vm_t *vm = deft_vm_init(NULL, my_import, 0); ```` 返回的指针必须保持有效,直到下一次导入调用或 `deft_vm_deinit`。 ### 求值 ```c uint64_t deft_eval(deft_vm_t *vm, const char *source, size_t len); uint64_t deft_eval_cstr(deft_vm_t *vm, const char *source); ```` 求值一个源码字符串。`deft_eval` 接受显式长度(源码不必以 null 结尾);`deft_eval_cstr` 是面向 C 字符串的便捷封装。 返回结果为 NaN 装箱的 `uint64_t`,出错时返回 `DEFT_NIL`。 ```c uint64_t r = deft_eval_cstr(vm, "(1 + 2)"); if (deft_is_number(r)) { printf("%f\n", deft_get_number(r)); // 3.0 } ```` ### 调用函数 ```c uint64_t deft_call(deft_vm_t *vm, const char *name, const uint64_t *args, size_t nargs); ```` 按名称调用 Deft 函数,参数值可预先构造。当宿主在 VM 中持有函数定义,并希望反复调用而无需重新解析源码时,这很有用。 ```c // Define a function, then call it from C deft_eval_cstr(vm, "def greet {|name| str \"hello \" $name }"); uint64_t arg = deft_string(vm, "world", 5); uint64_t result = deft_call(vm, "greet", &arg, 1); size_t len = 0; const char *s = deft_get_string(result, &len); // s == "hello world" ```` ### 值构造器与访问器 值是 NaN 装箱的 `uint64_t`。请始终通过提供的函数构造和检查它们 —— 永远不要手工进行位转换。 构造器: | 函数 | 生成 | |---|---| | `deft_number(double)` | 数字 | | `deft_string(vm, ptr, len)` | GC 堆字符串(需要存活的 `vm`) | | `deft_bool(bool)` | 布尔值 | | `deft_nil()` | nil 哨兵 | 谓词: | 函数 | 返回 | |---|---| | `deft_is_number(v)` | 若 `v` 是数字则为 true | | `deft_is_string(v)` | 若 `v` 是字符串则为 true | | `deft_is_bool(v)` | 若 `v` 是布尔值则为 true | | `deft_is_nil(v)` | 若 `v` 是 nil 则为 true | 访问器: | 函数 | 返回 | |---|---| | `deft_get_number(v)` | `double` 值;若不是数字则为 `0.0` | | `deft_get_bool(v)` | `bool` 值;若不是布尔值则为 `false` | | `deft_get_string(v, &len)` | 指向字节的指针;若不是字符串则为 `NULL` | ```c uint64_t s = deft_string(vm, "hello", 5); size_t len = 0; const char *bytes = deft_get_string(s, &len); // valid until next GC ```` > GC 生命周期: `deft_get_string` 返回的指针只在下一个 GC 周期 > 或 `deft_vm_deinit` 之前有效。如果需要持有更久,请复制字节。 ### 全局变量 ```c void deft_set_global(deft_vm_t *vm, const char *name, uint64_t val); uint64_t deft_get_global(deft_vm_t *vm, const char *name); ```` 在宿主中读写顶层的 Deft 绑定。对于未知名称,`deft_get_global` 返回 `DEFT_NIL`。 ```c // Push a value into the VM deft_set_global(vm, "host_version", deft_string(vm, "1.2.3", 5)); // Read a value the script set deft_eval_cstr(vm, "set computed ($x * 2)"); uint64_t v = deft_get_global(vm, "computed"); ```` ### 资源限制(fuel) Fuel 是一个操作计数器。每条 VM 指令都会使其递减;当它归零时,执行中止,调用返回 `DEFT_NIL`。这是防御不可信或失控脚本的主要手段。 ```c deft_vm_t *vm = deft_vm_init(NULL, NULL, 1000000); // 1M ops budget // ... eval untrusted script ... uint64_t left = deft_remaining_fuel(vm); // check what's left deft_set_fuel(vm, 5000000); // top up for the next run ```` | 函数 | 用途 | |---|---| | `deft_set_fuel(vm, n)` | 设置预算。`0` 表示禁用 fuel(无限制)。 | | `deft_remaining_fuel(vm)` | 剩余操作数。fuel 被禁用时为 `0`。 | ### 完整的 C 示例 ```c #include <stdio.h> #include "deft.h" static void on_print(const char *data, size_t len) { fwrite(data, 1, len, stdout); } int main(void) { deft_vm_t *vm = deft_vm_init(on_print, NULL, 1000000); if (!vm) { fprintf(stderr, "init failed\n"); return 1; } // Feed in a value from the host deft_set_global(vm, "greeting", deft_string(vm, "world", 5)); // Evaluate Deft source uint64_t r = deft_eval_cstr(vm, "echo \"hello $greeting\""); // Call a defined function deft_eval_cstr(vm, "def double {|n^number| ($n * 2) }"); uint64_t arg = deft_number(21.0); uint64_t doubled = deft_call(vm, "double", &arg, 1); printf("doubled = %f\n", deft_get_number(doubled)); // 42.0 deft_vm_deinit(vm); return 0; } ```` 构建(依赖库见 构建库): ``` $ zig build lib && zig build $ cc -o demo demo.c -Izig-out/include -Lzig-out/lib -ldeft \ -lsqlite3 -lpcre2 -lmbedtls -lm -lpthread ```` --- ## Zig API 当宿主是 Zig 项目时,可以跳过 C ABI,直接使用 `Kernel`。你能获得原生 `Value` 类型(而非裸 `u64`),可以把普通的 Zig 函数注册为原生函数,并避开线程局部回调适配器那一套。 ```zig const std = @import("std"); const deft = @import("deft_zig"); const Kernel = deft.kernel.Kernel; const KernelConfig = deft.kernel.KernelConfig; const Value = deft.value_mod.Value; const DeftCtx = deft.vm.DeftCtx; ```` 注意,`DeftCtx` 位于 `deft.vm` 模块层级(它从插件 ABI 模块重新导出),而不是 `VM` 结构体的成员。 ### 配置 `KernelConfig` 控制三个宿主钩子: ```zig pub const KernelConfig = struct { print_fn: ?PrintFn = null, // stdout if null import_fn: ?ImportFn = null, // filesystem if null max_fuel: u64 = 0, // 0 = unlimited max_call_depth: u32 = 0, // 0 = default (256) }; ```` 回调签名: ```zig pub const PrintFn = *const fn ([]const u8) void; pub const ImportFn = *const fn (path: []const u8) ?[]const u8; ```` ### 生命周期与求值 ```zig var kernel = Kernel.init(allocator, .{ .max_fuel = 1_000_000, }); defer kernel.deinit(); const result = try kernel.eval("(1 + 2)"); std.debug.print("{d}\n", .{result.asF64()}); // 3.0 ```` > 稳定地址: `Kernel.init` 按值返回,但结构体内含一个捕获自身指针的 > `ArenaAllocator`。请让 `Kernel` 保持固定地址:将其赋给 `var` 绑定 > (初始化后不要按值传递),并在 `&kernel` 上调用 `eval`/`call`。 > 第一次 `eval` 会惰性地接好 arena 指针。正因如此,C 垫片会 > 在堆上分配一个 `StableKernel` 包装。 ### 调用函数 ```zig try kernel.eval("def sq {|n| ($n * $n) }"); const r = try kernel.call("sq", &.{Value.number(7.0)}); std.debug.print("{d}\n", .{r.asF64()}); // 49.0 ```` 如果名称未绑定,`call` 返回 `error.UndefinedFunction`;如果不是函数,返回 `error.NotCallable`;如果执行出错,返回 `error.RuntimeError`。 ### 注册宿主函数 这是 Zig API 的主要优势。把一个普通的 Zig 函数注册为可从脚本调用的 Deft 标准库函数: ```zig fn native_greet(ctx: *DeftCtx, args: []const Value) Value { const name = ctx.str(0, args) orelse "stranger"; return ctx.newString(name); // returns a heap string } // name, arity (-1 = variadic), function pointer kernel.registerFn("greet", 1, &native_greet); ```` 现在 Deft 代码可以调用它:
echo [greet "alice"]   # prints alice
`DeftCtx` 是每次调用的上下文。它提供带类型的参数提取器和值构造器: | 方法 | 提取 / 构造 | |---|---| | `ctx.str(i, args)` | `?[]const u8`(字符串参数) | | `ctx.num(i, args)` | `?f64`(数字参数) | | `ctx.int(i, args)` | `?i64`(整数参数,带范围检查) | | `ctx.boolean(i, args)` | `?bool` | | `ctx.kw(i, args)` | `?[]const u8`(关键字参数) | | `ctx.newString(s)` | 堆字符串 `Value` | | `ctx.newNumber(n)` | 数字 `Value` | | `ctx.newBool(b)` | 布尔 `Value` | | `ctx.newKeyword(k)` | 堆关键字 `Value` | | `ctx.ok(v)` | `%Ok{:value v}` 标签映射 | | `ctx.err(msg)` | `%Err{:message msg}` 标签映射 | 参数个数或类型不匹配时,提取器返回 `null` —— 用 `ctx.err(...)` 将其作为错误处理。 ```zig fn native_add(ctx: *DeftCtx, args: []const Value) Value { const a = ctx.num(0, args) orelse return ctx.err("add: expected number"); const b = ctx.num(1, args) orelse return ctx.err("add: expected number"); return ctx.ok(ctx.newNumber(a + b)); } ```` 内置标准库函数使用的更丰富的 `plugin.decl()` / `plugin.derive()` 注册方式(自动推导参数个数、参数文档、特性门控)见 Zig 插件。`registerFn` 是适合临时宿主函数的轻量路径;插件系统用于构建可复用的标准库函数库。 ### 全局变量 ```zig kernel.setGlobal("host_name", Value.fromF64(42.0)); const v = kernel.getGlobal("computed") orelse Value.nil; ```` `getGlobal` 返回 `?Value` —— 若名称未绑定则为 `null`。它会沿完整的 var 链解析(局部 → 当前命名空间 → 全局),因此既能找到 `def` 绑定也能找到 `set` 绑定。 ### 燃料(fuel) ```zig kernel.setFuel(5_000_000); // top up const left = kernel.remainingFuel(); // ops remaining (0 if disabled) ```` ### Value 类型 `Value` 是以 Zig 结构体暴露的 NaN 装箱 `u64`。关键操作: ```zig // Construction (no allocation needed for scalars) const n = Value.fromF64(3.14); const b = Value.fromBool(true); const nil = Value.nil; // Heap values need the GC heap const s = try deft.value_mod.makeString(&kernel.vm.heap, "hi"); const k = try deft.value_mod.makeKeyword(&kernel.vm.heap, "tag"); // Predicates if (v.isNumber()) { ... } if (v.isString()) { ... } if (v.isBool()) { ... } if (v.isNull()) { ... } // Accessors (check the predicate first) const f = v.asF64(); const bool_val = v.asBool(); const slice = v.stringSlice(); // []const u8 ```` ### 完整的 Zig 示例 ```zig const std = @import("std"); const deft = @import("deft_zig"); const Kernel = deft.kernel.Kernel; const Value = deft.value_mod.Value; const DeftCtx = deft.vm.DeftCtx; fn native_celsius(ctx: *DeftCtx, args: []const Value) Value { const f = ctx.num(0, args) orelse return ctx.err("celsius: expected number"); return ctx.newNumber((f - 32.0) * 5.0 / 9.0); } pub fn main() !void { var kernel = Kernel.init(std.heap.page_allocator, .{ .max_fuel = 1_000_000 }); defer kernel.deinit(); kernel.registerFn("celsius", 1, &native_celsius); // Call the host function from Deft source const r = try kernel.eval("[celsius 98.6]"); std.debug.print("{d}\n", .{r.asF64()}); // 37.0 } ```` --- ## NaN 装箱 Deft 的值采用 NaN 装箱(NaN-boxing):所有值 —— 数字、字符串、映射、闭包、nil、布尔 —— 都能放进一个 64 位字。C API 将其暴露为 `uint64_t`;Zig API 将其包装进只有 `.bits` 一个字段的 `Value` 结构体。 实际影响: - 对象指针不会泄漏到 ABI 之外。 你传递的是 `uint64_t`;堆归 VM 所有。 - 标量是立即值。 数字(`f64`)、布尔和 nil 无需分配 —— 它们直接编码在位中。 - 堆值是伪装的指针。 字符串 `Value` 携带指向 GC 管理的 `ObjString` 的指针。该指针在值的生命周期内保持稳定,但 GC 可能移动或回收底层对象 —— 如果需要在多次调用之间持有引用,请保留 `Value`(而不是从它提取出的裸指针)。 `DEFT_NIL` 常量(`0x7FFC000000000001`)是规范 nil。请使用 `deft_nil()` / `Value.nil`,不要书写字面量。 --- ## 已可用与进行中的功能 嵌入 API 覆盖核心循环:init、eval、call、全局变量、fuel、宿主函数以及 I/O 回调。以下功能现已可用: - 同步求值(`eval`、`eval_cstr`、`call`) - 所有标量与字符串值的编组 - 宿主函数注册(Zig API:`registerFn`) - 资源限制(fuel、调用深度) - 可插拔的打印与导入回调 - VM 上的纯/同步标准库函数 —— `str`、`math/*`、集合、`json/*`、`sqlite/*`(本地文件)、regex、crypto。这些由 `VM.init` 自动注册。 以下功能尚未通过嵌入 API 暴露: - 宿主平台层。 `Kernel` 给你的是 VM,而不是 `Runtime`。依赖 Runtime 的标准库函数 —— `pub/*`、`rpc/*`、`config/*`、`runtime/*`、HTTP 服务器、异步 `task`、`vwait` —— 虽然已注册为函数,但调用时会失败,因为其执行上下文没有绑定 Runtime。通过 C 嵌入完整的 Runtime(事件循环、发布/订阅、子运行时)尚未接通;在 Zig 中可以直接构造 `Runtime`。 - 预置文件。 `Kernel.init` 不会加载 `.deft/prelude.dft`,因此基于 `defmod` 的模块命名空间以及任何预置定义都不存在,除非宿主显式求值它们。 - C 侧宿主函数注册。 `deft.h` 中还没有 `deft_register_fn` —— 宿主函数只能从 Zig 添加(针对任意 C 函数的跳板与签名编组是缺失的部分;可跟踪 `src/host/ffi/capi.zig`)。 - 错误内省。 失败时,`eval`/`call` 返回 `DEFT_NIL`(C)或 `error{...}`(Zig)。没有结构化错误值返回给宿主 —— 错误详情会送到打印回调。 - 宿主对异步/协程的控制。 协程在 `eval` 内运行,但宿主无法跨调用单步执行或恢复它们。 - 从 C 固定对象生命周期。 Zig FFI 插件的 `ffi/pin` 存在,但 C 嵌入 API 没有对应物;长生命周期引用必须以 `Value` 持有,并在每次使用时重新提取。 如果你需要其中某项功能,底层 VM 是支持的 —— 缺口纯粹在嵌入接口层。 --- ## 快速参考 ### C ```c deft_vm_t *vm = deft_vm_init(print_fn, import_fn, fuel); deft_vm_deinit(vm); uint64_t r = deft_eval_cstr(vm, "(1 + 2)"); uint64_t r2 = deft_eval(vm, src, len); uint64_t r3 = deft_call(vm, "fn", args, nargs); uint64_t num = deft_number(3.14); uint64_t str = deft_string(vm, "hi", 2); uint64_t b = deft_bool(true); uint64_t nil = deft_nil(); deft_is_number(r); deft_get_number(r); deft_is_string(s); deft_get_string(s, &len); deft_is_bool(b); deft_get_bool(b); deft_is_nil(nil); deft_set_global(vm, "x", val); deft_get_global(vm, "x"); deft_set_fuel(vm, n); deft_remaining_fuel(vm); ```` ### Zig ```zig var kernel = Kernel.init(allocator, .{ .max_fuel = 1_000_000 }); defer kernel.deinit(); const r = try kernel.eval("(1 + 2)"); const r2 = try kernel.call("fn", &.{ Value.number(1.0) }); kernel.registerFn("greet", 1, &native_greet); kernel.setGlobal("x", Value.fromF64(42.0)); const v = kernel.getGlobal("x") orelse Value.nil; kernel.setFuel(5_000_000); _ = kernel.remainingFuel(); ````