# Zig 插件与标准库命名空间
所有 Deft 标准库函数——`str`、`echo`、`http/get`、`tui/split`、`sqlite/open`——都用 Zig
编写,并在构建时被发现,无需编辑任何中心清单。本文档介绍插件架构、伪命名空间的
工作方式,以及如何添加新插件。
## 架构
```
┌──────────────────────────────────────────────────────────────┐
│ Build time │
│ │
│ build.zig ─► gen_plugin_registry.zig │
│ │
│ walks src/host/plugins/** + src/host/stdext/* │
│ looks for `pub const info` in every .zig file │
│ writes plugin_registry.gen.zig (only if changed) │
│ │
│ Each plugin file exports one public constant: │
│ pub const info: PluginInfo = ...; │
│ │
│ Optional feature gates: │
│ pub const requires_tui: bool = true; (TUI-only) │
│ pub const requires_ffi: bool = true; (FFI-only) │
└──────────────────────┬───────────────────────────────────────┘
│ @import
┌──────────────────────▼───────────────────────────────────────┐
│ VM init │
│ │
│ stdlib.registerAll(alloc, heap, &vm.globals); │
│ stdlib.registerAll(alloc, heap, &vm.core_ns.vars); │
│ │
│ Each PluginEntry becomes an ObjNativeFunc stored as a │
│ flat string key in globals and core_ns: │
│ globals["fs/read"] = <native-func> │
│ globals["fs/write"] = <native-func> │
│ globals["string/upper"] = <native-func> │
└──────────────────────────────────────────────────────────────┘
````
## PluginInfo 契约
每个插件文件导出一个 `PluginInfo` 结构体:
```zig
pub const info: PluginInfo = ...;
````
其字段如下:
| 字段 | 类型 | 用途 |
|---|---|---|
| `name` | `[]const u8` | 插件标识符,例如 `"string"`、`"fs"`、`"sqlite"` |
| `version` | `u32` | API 版本(默认 `1`) |
| `entries` | `[]const PluginEntry` | 函数表 |
每个 `PluginEntry` 描述一个标准库函数:
| 字段 | 类型 | 用途 |
|---|---|---|
| `name` | `[]const u8` | 完全限定名,例如 `"fs/read"`、`"string/trim"` |
| `arity` | `i16` | 固定参数个数,可变参数时为 `-1` |
| `func` | `NativeFn` | 函数指针 |
| `flags` | `FnFlags` | 对高阶函数、协程、I/O 使用 `.needs_vm = true` |
| `doc` | `?[]const u8` | 帮助字符串 |
| `params` | `?[]const ParamDoc` | 逐参数文档 |
| `returns` | `?[]const u8` | 返回值描述 |
| `flag_opts_npos` | `u16` | CLI 风格的 `--flag value` 支持(0 表示关闭) |
### 函数签名
支持两种签名,由条目的 `flags.needs_vm` 决定:
```zig
// Pure functions — FnFlags.needs_vm is false
pub const NativeFn = *const fn (*DeftCtx, []const Value) Value;
// VM-aware — FnFlags.needs_vm is true (HOFs, coroutines, I/O)
pub const VmNativeFn = *const fn (*anyopaque, []const Value) Value;
````
传给每个标准库函数的 `DeftCtx` 提供:
- 内存分配:`.alloc`、`.heap`
- VM/运行时访问:`.vm`、`.runtime`(通过 `.vmAs(T)`、`.runtimeAs(T)` 做类型转换)
- 参数提取:`.str(i, args)`、`.num(i, args)`、`.int(i, args)`、`.boolean(i, args)`、`.kw(i, args)`
- 值构造:`.newString(s)`、`.newNumber(n)`、`.newBool(b)`、`.newKeyword(k)`
- 结果构造:`.ok(v)` → `%Ok`,`.err(msg)` / `.errVal(v)` → `%Err`
## 编译期便利:`plugin.decl()` 与 `plugin.derive()`
大多数插件不再手写 `PluginEntry` 结构体和手动解包 `[]const Value`,而是使用两个
编译期宏:它们自动推导参数个数、对参数提取做类型检查,并构建注册表:
```zig
const plugin = @import("../../runtime/plugin.zig");
const DeftCtx = plugin.DeftCtx;
const Value = plugin.Value;
const d = plugin.decl;
pub const info = plugin.derive("math", .{
d("sqrt", &nativeSqrt, .{ .doc = "Square root", .returns = "number" }),
d("sin", &nativeSin, .{ .doc = "Sine of radians", .returns = "number" }),
d("pow", &nativePow, .{
.doc = "Raise base to exponent",
.params = &.{ .{ .name = "base" }, .{ .name = "exp" } },
.returns = "number",
}),
});
````
`decl` 识别两种编写签名:
- 类型化——`fn(*DeftCtx, s: []const u8, n: i64) Value` → 固定参数个数由参数计数推导
- 原始——`fn(*DeftCtx, []const Value) Value` → 可变参数(参数个数为 `-1`)
支持的类型化参数类型:`[]const u8`、`f64`、`i64`、`bool`、`Value`。
返回类型可以是 `Value` 或 `!Value`(错误会自动转换为 `%Err`)。
最多 4 个类型化参数;更多时请使用原始 `[]const Value` 形式。
`derive(name, entries)` 从 `PluginEntry` 值组成的元组构建 `PluginInfo`,设置 `.name`
并在编译期嵌入条目数组。
### 不使用宏(原始形式)
作为参考,下面是用手动方式编写的同一插件:
```zig
fn nativeSqrt(ctx: *DeftCtx, args: []const Value) Value {
const x = ctx.num(0, args) orelse return ctx.err("expected number");
if (x < 0) return ctx.err("sqrt: negative argument");
return ctx.newNumber(std.math.sqrt(x));
}
pub const info = plugin.PluginInfo{
.name = "math",
.version = 1,
.entries = &.{
.{ .name = "sqrt", .arity = 1, .func = &nativeSqrt,
.doc = "Square root", .returns = "number" },
.{ .name = "sin", .arity = 1, .func = &nativeSin,
.doc = "Sine of radians", .returns = "number" },
},
};
````
推荐使用宏:样板代码更少、手动 `[]const Value` 索引更少,并且自动推导参数个数。
## 伪命名空间如何工作
与 Clojure 风格的命名空间(由 `.dft` 文件中的 `defmod` 创建)不同,插件命名空间是
扁平键伪命名空间。`fs`、`tui` 或 `sqlite` 并不存在对应的 `Namespace` 结构体。
`"fs/read"` 中的 `/` 纯粹是命名约定——每个条目都以扁平字符串键存储:
```
globals["fs/read"] = <native-func> // from fs plugin
globals["fs/write"] = <native-func> // from fs plugin
globals["fs/home"] = <native-func> // from fs plugin
globals["tui/split"] = <native-func> // from workspace plugin
globals["tui/close"] = <native-func> // from workspace plugin
````
当 Deft 代码以裸名引用 `fs` 时(例如检查它或传递它),VM 会通过
`synthesizePluginMap` 惰性合成一个命名空间映射:
1. 扫描所有匹配前缀 `"fs/"` 的全局变量
2. 去掉前缀得到短名称:`read`、`write`、`home`
3. 构建一个持久化映射 `{ :read <fn>, :write <fn>, :home <fn> }`
4. 将其存储为 `globals["fs"]`,以便日后快速访问
这一切都是透明发生的——你永远不会自己调用 `synthesizePluginMap`。
VM 在首次裸名访问时完成合成。
### 限定调用的解析路径
当代码调用 `[fs/read "file.txt"]` 时:
1. 字节码编译器发出 `load "fs/read"` 指令
2. `resolveVar("fs/read")` 检查 `globals` → 找到 `ObjNativeFunc`
3. `dispatchCall` 直接调用原生函数指针
## 特性门控
插件可以通过构建期特性标志进行门控:
```zig
pub const requires_tui: bool = true; // Only included in retro/retra builds
pub const requires_ffi: bool = true; // Only included in FFI-enabled builds
````
如果两者都未设置,插件始终加载(常见情况)。注册表生成器将插件分成三个
数组——`common`、`tui_only`、`ffi_only`——并在初始化时将它们拼接起来。
在运行时检查特性可用性:
[capabilities] # → map of all feature flags [get [capabilities] :tui] # → true/false [get [capabilities] :ffi] # → true/false
## 构建期发现
添加新插件无需手动编辑任何清单。将导出了 `pub const info` 的文件放入
`src/host/plugins/` 或 `src/host/stdext/`,构建系统就会自动发现它:
1. `build.zig` 在构建图构造期间调用 `gen_plugin_registry.zig`
2. 生成器递归遍历这两个目录
3. 对每个包含 `pub const info` 的 `.zig` 文件,读取其特性门控并生成一个 `@import` 引用
4. 仅当内容变化时才写出 `plugin_registry.gen.zig`(避免不必要的重新编译)
5. 注册表使用条件编译(`if (build_options.enable_tui)`),因此不需要 TUI/FFI 模块的构建永远不会分析它们
## 添加新插件
### 示例:一个 `compress/` 插件
创建 `src/host/plugins/core/compress_plugin.zig`:
```zig
const std = @import("std");
const plugin = @import("../../runtime/plugin.zig");
const DeftCtx = plugin.DeftCtx;
const Value = plugin.Value;
const d = plugin.decl;
const value_mod = @import("../../../kernel/value/value.zig");
pub const info = plugin.derive("compress", .{
d("compress/gzip", &nativeGzip, .{
.doc = "Compress bytes with gzip",
.params = &.{.{ .name = "data", .type_hint = "string" }},
.returns = "string",
}),
d("compress/decompress-gzip", &nativeGunzip, .{
.doc = "Decompress a gzip-compressed string",
.params = &.{.{ .name = "data", .type_hint = "string" }},
.returns = "string",
}),
});
fn nativeGzip(ctx: *DeftCtx, args: []const Value) Value {
const data = ctx.str(0, args) orelse return ctx.err("expected string");
// Compress with std.compress.gzip...
return ctx.ok(ctx.newString(compressed_bytes));
}
fn nativeGunzip(ctx: *DeftCtx, args: []const Value) Value {
const data = ctx.str(0, args) orelse return ctx.err("expected string");
// Decompress with std.compress.gzip...
return ctx.ok(ctx.newString(decompressed_bytes));
}
````
重新构建。现在所有 Deft 代码都可以使用 `compress/gzip` 和
`compress/decompress-gzip` 函数。裸名 `compress` 会惰性合成映射
`{ :gzip <fn>, :decompress-gzip <fn> }`。
### 仅 TUI 的插件示例
添加 `pub const requires_tui = true`:
```zig
pub const requires_tui: bool = true;
````
该插件只会编译进 `retro`/`retra` 构建。脚本二进制 `deft` 永远看不到它的导入。
## 插件位置
| 目录 | 用途 |
|---|---|
| `src/host/plugins/core/` | 核心语言标准库函数(string、math、types、list、map、set 等) |
| `src/host/plugins/data/` | 数据结构标准库函数(array、list、map、set) |
| `src/host/plugins/system/` | 系统标准库函数(config、io、log、pubsub、rpc、runtime、task) |
| `src/host/plugins/`(根目录) | TUI、dialog、debug、capabilities、workspace、draw、editor 等 |
| `src/host/stdext/` | 标准库(fs、http、json、sqlite、os、net、crypto 等) |
这种划分只是约定,并非强制。两棵目录树下的任何文件都会被自动发现。
## 错误约定
标准库函数以 `%Err` 标签映射的形式返回错误。使用 `DeftCtx` 辅助方法:
```zig
fn nativeFn(ctx: *DeftCtx, args: []const Value) Value {
const s = ctx.str(0, args) orelse return ctx.err("expected string");
// ... do work ...
return ctx.ok(result); // → %{ :type :ok/value :value result }
}
````
对于通过 `decl` 返回 `!Value` 的原始参数个数函数,错误会自动转换:
```zig
fn nativeOpen(ctx: *DeftCtx, path: []const u8) !Value {
const f = try std.fs.cwd().openFile(path, .{});
defer f.close();
// ...
return ctx.newString(content);
}
// On error → ctx.err(@errorName(e)) automatically
````
## 命名空间命名约定
- 短名称(不含 `/`),用于频繁使用的独立函数:
`str`、`trim`、`echo`、`map`、`filter`、`nil?`
- 命名空间斜杠,用于逻辑分组的操作:
`fs/read`、`fs/write`、`http/get`、`sqlite/query`、`tui/split`
- 斜杠只是命名约定。插件前缀并不存在真正的命名空间结构体——VM 会在首次
裸名访问时惰性合成一个。
## 限制
- 通过 `plugin.decl()` 最多 4 个类型化参数。可变参数或更高参数个数的函数
请使用原始 `[]const Value` 形式。
- 插件之间的名称冲突不会在构建时检测。如果两个插件都注册 `"foo/bar"`,
第二次注册会静默覆盖第一次。
- 动态插件加载(基于 `.so`)已列入计划但尚未发布。
目前所有插件都是静态链接的。