# 嵌入 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();
````