# 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`)已列入计划但尚未发布。 目前所有插件都是静态链接的。