From 9f844123fe2ed9096a7db2fddc0a13360fa7b4c7 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Wed, 2 Sep 2026 18:39:02 +0800 Subject: [PATCH] =?UTF-8?q?plugindev:=20entry=20=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E6=94=B6=E6=95=9B=20+=20=E5=88=A0=20C=20ABI=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E9=93=BE=20+=20Windows=20=E5=85=B1=E4=BA=AB=E5=86=85?= =?UTF-8?q?=E5=AD=98=E9=80=82=E9=85=8D=EF=BC=88Part=206.1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## entry 不再是通道开关 —— 外部插件零改动的关键 17 个存量插件的 plg.json 都写着 "entry": "plugin.so"。若把 entry 当通道 开关,迁移就得改 17 个文件,而「外部插件零改动」是本次迁移的硬约束。 改法:Go 插件一律产出 plugin.bin,不看 entry 值。isProcEntry 删除, resolveBuild 去掉 proc 参数。entry 现在只剩区分 Lua(main.lua)一个用途。 实测:weather 的 plg.json 一行不改(仍写 plugin.so),plugindev build 直接产出三平台 plugin.bin。 ## Windows 不再是能力退化的第三套实现(§9.2 的正解) C ABI 时代 Windows 是独立的第三套 ABI:dynamic_dll_windows.go 的 stage 只下发 3 个字段(raw_message/user_id/phase)且完全没有写回,sanitizer 这类改写型插件在 Windows 上静默失效,且无任何运行时警告。 现在 Windows 与 Unix 共用同一份 RPC 逻辑与同一份共享段布局。平台差异 收敛到三个挂载函数: - Unix(linux/darwin/freebsd):内核经 ExtraFiles 传继承 fd(3=StageContext 段,4=事件环段,5=eventfd/pipe) - Windows:没有 fd 继承语义(os/exec 的 ExtraFiles 在 Windows 不支持), 改用命名内核对象——父进程 CreateFileMapping/CreateEvent 建带名字的对象, 子进程 OpenFileMappingW/OpenEventW 按同名打开。名字经环境变量传入而非 硬编码:多个 homed 实例并存时不能撞名。 Windows 绑定用 syscall.NewLazyDLL 而非 golang.org/x/sys/windows: OpenFileMappingW/OpenEventW 未被标准库 syscall 导出,而引入 x/sys 会给 **每个插件的 go.mod** 加一个新依赖,违反「插件仅依赖公开 SDK」。 LazyDLL 属标准库,零新增依赖。 新增 evtWaiter 接口抽象等待语义:eventfd 是计数器(多事件合并成一次 唤醒),Windows Event 是二元信号。不影响正确性——消费者被唤醒后按 readSeq 追 writeSeq 批量 drain,一次唤醒能处理累积的全部事件。 模板拆成三个文件: proc_main.go.tmpl 平台无关(RPC + 共享段布局 + stage + 事件环消费) proc_shm_unix.go.tmpl 继承 fd 挂载 proc_shm_windows.go.tmpl 命名对象挂载 ## 删除 C ABI 工具链 templates.go 1296 → 516 行: - tmplBridge(Windows DLL bridge) -265 行 - tmplLinuxBridge(Linux c-shared) -457 行 - tmplPluginInitC(C 入口) -57 行 另删 generateBridge / detectWindowsCC(MinGW 探测)/ tmplCABIHeader / InitData.CABIVersion+CABIHeader。 交叉编译不再需要目标平台 C 工具链——这是 -buildmode=c-shared 退场的 连带收益(§3.1)。 ## 测试 15 项全过,新增 4 项守护迁移不变量: - AllPlatformsProduceBin:6 个 GOOS/GOARCH 组合统一产出 plugin.bin - LuaIsSeparatePath:Lua 仍走解释器路径 - UnsupportedOSErrors:不支持平台明确报错,不静默产出错误产物 - NoCABIResiduals:代码中不得再出现 c-shared / CGO_ENABLED=1 / detectWindowsCC / tmplLinuxBridge / tmplPluginInitC(注释除外) - IgnoresEntryForGoPlugins:isProcEntry 必须已删除 验证:go build/vet/test 全通过;三平台交叉编译产出 plugin.bin; git diff sdk/ 为空(接口冻结)。 Ref: docs/zh/架构迁移评估.md §3.1/§9.2、docs/zh/plugin-migration-plan.md Part 6 --- tools/plugindev/cmd_build.go | 235 +----- tools/plugindev/cmd_init.go | 10 +- tools/plugindev/proc_runtime.go | 91 +- tools/plugindev/proc_runtime_test.go | 140 ++-- tools/plugindev/templates.go | 781 ------------------ tools/plugindev/templates/proc_main.go.tmpl | 38 +- .../plugindev/templates/proc_shm_unix.go.tmpl | 62 ++ .../templates/proc_shm_windows.go.tmpl | 153 ++++ 8 files changed, 439 insertions(+), 1071 deletions(-) create mode 100644 tools/plugindev/templates/proc_shm_unix.go.tmpl create mode 100644 tools/plugindev/templates/proc_shm_windows.go.tmpl diff --git a/tools/plugindev/cmd_build.go b/tools/plugindev/cmd_build.go index e04c493..2e974de 100644 --- a/tools/plugindev/cmd_build.go +++ b/tools/plugindev/cmd_build.go @@ -110,24 +110,13 @@ func cmdBuild(args []string) { } // allBundleTargets 是 --bundle 模式构建的全部平台。 -// 每个 OS 只有一个架构(amd64),避免二进制文件名冲突。 +// +// 子进程模式下各平台产物同名(plugin.bin)——进程边界即 ABI 边界, +// 不存在平台特有扩展名,故 zip 内按平台加后缀区分; +// 内核安装时按当前平台挑对应条目重命名为 plugin.bin。 var allBundleTargets = []struct { target string entry string // 二进制在 zip 中的文件名 -}{ - {"linux/amd64", "plugin.so"}, - {"darwin/amd64", "plugin.dylib"}, - {"windows/amd64", "plugin.dll"}, -} - -// allProcBundleTargets 是子进程模式的 bundle 目标。 -// -// 与 C ABI 版的差异:产物统一叫 plugin.bin(子进程模式无平台特有扩展名, -// 因为进程边界本身就是 ABI 边界),故 zip 内按平台加后缀区分; -// 内核安装时按当前平台挑对应条目重命名为 plugin.bin。 -var allProcBundleTargets = []struct { - target string - entry string }{ {"linux/amd64", "plugin.bin.linux.amd64"}, {"darwin/amd64", "plugin.bin.darwin.amd64"}, @@ -139,19 +128,10 @@ func buildBundle(plg *PlgConfig, outDir string, sdkPath string) { buildDir := "build" os.MkdirAll(buildDir, 0755) - proc := isProcEntry(plg.Entry) - - // 生成运行时:proc 模式写子进程 main(零 cgo),否则写 C ABI bridge - var runtimeCleanup func() - if proc { - cl, err := generateProcRuntime() - if err != nil { - fmt.Printf(" error: %v\n", err) - return - } - runtimeCleanup = cl - } else { - runtimeCleanup = generateBridge("") + runtimeCleanup, err := generateProcRuntime() + if err != nil { + fmt.Printf(" error: %v\n", err) + return } defer runtimeCleanup() @@ -160,49 +140,25 @@ func buildBundle(plg *PlgConfig, outDir string, sdkPath string) { var binaries []binEntry - // proc 模式下各平台产物同名(plugin.bin),故 zip 内按平台加后缀区分。 - targets := allBundleTargets - if proc { - targets = allProcBundleTargets - } - - for _, bt := range targets { - cfg, errMsg := resolveBuild(bt.target, proc) + for _, bt := range allBundleTargets { + cfg, errMsg := resolveBuild(bt.target) if cfg == nil { fmt.Printf(" error: %s\n", errMsg) return } - // proc 模式:每平台产物落到独立路径,避免相互覆盖 - outName := cfg.entryFile - if proc { - outName = fmt.Sprintf("%s_%s_%s", cfg.entryFile, cfg.goos, cfg.goarch) - } + // 每平台产物落到独立路径,避免相互覆盖 + outName := fmt.Sprintf("%s_%s_%s", cfg.entryFile, cfg.goos, cfg.goarch) outPath := filepath.Join(buildDir, outName) - var cmd *exec.Cmd - if proc { - cmd = exec.Command("go", "build", "-trimpath", "-o", outPath) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, "GOOS="+cfg.goos, "GOARCH="+cfg.goarch, "CGO_ENABLED=0") - } else { - cmd = exec.Command("go", "build", "-buildmode=c-shared", "-o", outPath) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, "GOOS="+cfg.goos, "GOARCH="+cfg.goarch, "CGO_ENABLED=1") - if cfg.goos == "windows" { - if cc := detectWindowsCC(); cc != "" { - cmd.Env = append(cmd.Env, "CC="+cc) - } - } - } - + // 零 cgo:跨平台交叉编译不需目标平台 C 工具链 + cmd := exec.Command("go", "build", "-trimpath", "-o", outPath) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "GOOS="+cfg.goos, "GOARCH="+cfg.goarch, "CGO_ENABLED=0") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - mode := "-buildmode=c-shared" - if proc { - mode = "子进程模式,CGO_ENABLED=0" - } - fmt.Printf(" compiling %s/%s (%s)...\n", cfg.goos, cfg.goarch, mode) + + fmt.Printf(" compiling %s/%s (子进程模式,CGO_ENABLED=0)...\n", cfg.goos, cfg.goarch) if err := cmd.Run(); err != nil { fmt.Printf(" error: build %s/%s: %v\n", cfg.goos, cfg.goarch, err) return @@ -220,11 +176,7 @@ func buildBundle(plg *PlgConfig, outDir string, sdkPath string) { for p := range platforms { plats = append(plats, p) } - bundleEntry := "plugin.so" - if proc { - bundleEntry = procEntryFile - } - writePluginJSON(plg, plats, bundleEntry) + writePluginJSON(plg, plats, procEntryFile) // package single .hmap with correctly named entries hmapPath := filepath.Join(outDir, fmt.Sprintf("%s_bundle.hmap", toSnake(plg.NameEn))) @@ -232,7 +184,10 @@ func buildBundle(plg *PlgConfig, outDir string, sdkPath string) { fmt.Printf(" packaged %s\n", filepath.Base(hmapPath)) } -func (p *PlgConfig) IsLua() bool { return p.Entry == "main.lua" } +// IsLua 判断是否为 Lua 插件(走解释器,不经过 Go 编译)。 +// +// 这是 entry 字段唯一仍在使用的用途:Go 插件不再看 entry 值,一律产出 plugin.bin。 +func (p *PlgConfig) IsLua() bool { return p.Entry == luaEntryFile } func readPlgJSON(path string) (*PlgConfig, error) { data, err := os.ReadFile(path) @@ -284,15 +239,15 @@ func writePluginJSON(plg *PlgConfig, platforms []string, entry string) { type buildConfig struct { goos string goarch string - entryFile string // "plugin.bin"(子进程) | "plugin.so" | "plugin.dylib" | "plugin.dll" - proc bool // true = 子进程模式(普通 go build,零 cgo) + entryFile string // 一律为 plugin.bin(进程边界即 ABI 边界,无平台特有扩展名) } -// resolveBuild 解析目标平台与产物形态。 +// resolveBuild 解析目标平台。 // -// proc 为真时统一产出 plugin.bin:子进程模式下不存在平台特有的动态库扩展名, -// 因为进程边界本身就是 ABI 边界(§3.1)——这也是交叉编译得以简化的原因。 -func resolveBuild(target string, proc bool) (*buildConfig, string) { +// 全平台统一产出 plugin.bin:子进程模式下不存在 .so/.dylib/.dll 的区分, +// 因为进程边界本身就是 ABI 边界——这正是三套独立 ABI 实现收敛为 +// 单一 RPC 实现的直接后果(§9.2:Windows 不再是能力退化的第三套实现)。 +func resolveBuild(target string) (*buildConfig, string) { if target == "lua" || target == "" { return nil, "lua" } @@ -305,24 +260,9 @@ func resolveBuild(target string, proc bool) (*buildConfig, string) { } } - if proc { - switch goos { - case "linux", "darwin", "freebsd", "windows": - return &buildConfig{goos: goos, goarch: goarch, entryFile: procEntryFile, proc: true}, "" - default: - return nil, fmt.Sprintf("unsupported OS %q", goos) - } - } - switch goos { - case "linux": - return &buildConfig{goos: goos, goarch: goarch, entryFile: "plugin.so"}, "" - case "darwin": - return &buildConfig{goos: goos, goarch: goarch, entryFile: "plugin.dylib"}, "" - case "freebsd": - return &buildConfig{goos: goos, goarch: goarch, entryFile: "plugin.so"}, "" - case "windows": - return &buildConfig{goos: goos, goarch: goarch, entryFile: "plugin.dll"}, "" + case "linux", "darwin", "freebsd", "windows": + return &buildConfig{goos: goos, goarch: goarch, entryFile: procEntryFile}, "" default: return nil, fmt.Sprintf("unsupported OS %q", goos) } @@ -473,8 +413,8 @@ func buildTarget(plg *PlgConfig, target, outDir, sdkPath string) { return } - // Resolve build config(proc 模式由 plg.json 的 entry 决定) - cfg, errMsg := resolveBuild(target, isProcEntry(plg.Entry)) + // Resolve build config(全平台统一产出 plugin.bin) + cfg, errMsg := resolveBuild(target) if cfg == nil { fmt.Printf(" error: %s\n", errMsg) return @@ -484,17 +424,10 @@ func buildTarget(plg *PlgConfig, target, outDir, sdkPath string) { os.MkdirAll(buildDir, 0755) outPath := filepath.Join(buildDir, cfg.entryFile) - // 生成运行时:proc 模式写子进程 main(零 cgo),否则写 C ABI bridge - var runtimeCleanup func() - if cfg.proc { - cl, err := generateProcRuntime() - if err != nil { - fmt.Printf(" error: %v\n", err) - return - } - runtimeCleanup = cl - } else { - runtimeCleanup = generateBridge(cfg.goos) + runtimeCleanup, err := generateProcRuntime() + if err != nil { + fmt.Printf(" error: %v\n", err) + return } defer runtimeCleanup() @@ -505,33 +438,15 @@ func buildTarget(plg *PlgConfig, target, outDir, sdkPath string) { // Write plugin.json with the correct entry for this target writePluginJSON(plg, nil, cfg.entryFile) - var cmd *exec.Cmd - if cfg.proc { - // 子进程模式:普通 go build,零 cgo。 - // 交叉编译不再需要目标平台的 C 工具链——进程边界即 ABI 边界(§3.1)。 - cmd = exec.Command("go", "build", "-trimpath", "-o", outPath) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, "GOOS="+cfg.goos, "GOARCH="+cfg.goarch, "CGO_ENABLED=0") - } else { - cmd = exec.Command("go", "build", "-buildmode=c-shared", "-o", outPath) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, "GOOS="+cfg.goos, "GOARCH="+cfg.goarch, "CGO_ENABLED=1") - // Auto-detect MinGW gcc on Windows - if cfg.goos == "windows" { - if cc := detectWindowsCC(); cc != "" { - cmd.Env = append(cmd.Env, "CC="+cc) - } - } - } - + // 普通 go build + 零 cgo:交叉编译不再需要目标平台的 C 工具链 + // (旧路径靠 detectWindowsCC 找 MinGW,现在整个问题消失)。 + cmd := exec.Command("go", "build", "-trimpath", "-o", outPath) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "GOOS="+cfg.goos, "GOARCH="+cfg.goarch, "CGO_ENABLED=0") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - mode := "-buildmode=c-shared" - if cfg.proc { - mode = "子进程模式,CGO_ENABLED=0" - } - fmt.Printf(" compiling %s/%s (%s)...\n", cfg.goos, cfg.goarch, mode) + fmt.Printf(" compiling %s/%s (子进程模式,CGO_ENABLED=0)...\n", cfg.goos, cfg.goarch) if err := cmd.Run(); err != nil { fmt.Printf(" error: build %s/%s: %v\n", cfg.goos, cfg.goarch, err) return @@ -651,72 +566,8 @@ func toSnake(s string) string { return strings.ToLower(strings.ReplaceAll(s, " ", "_")) } -// detectWindowsCC looks for a MinGW-w64 gcc on Windows for c-shared builds. -func detectWindowsCC() string { - // Check CC from environment first - if cc := os.Getenv("CC"); cc != "" { - if _, err := exec.LookPath(cc); err == nil { - return cc - } - } - // Check common MinGW install paths - candidates := []string{ - "C:\\mingw64\\bin\\gcc.exe", - "C:\\MinGW\\bin\\gcc.exe", - "C:\\msys64\\mingw64\\bin\\gcc.exe", - "C:\\Users\\21989\\AppData\\Local\\Temp\\mingw64\\mingw64\\bin\\gcc.exe", - } - // Also search PATH for gcc - if path, err := exec.LookPath("gcc"); err == nil { - return path - } - for _, c := range candidates { - if _, err := os.Stat(c); err == nil { - return c - } - } - return "" -} - // stripIncludeGuard strips preprocessor guards and C++ comments from a C header, // since these can confuse cgo's type resolution. -// generateBridge generates the C ABI bridge files for non-Lua builds. -// Returns a cleanup function to remove generated files. -func generateBridge(goos string) func() { - const bridgeFile = "z_bridge_gen.go" - const cEntryFile = "z_entry.c" - os.Remove(bridgeFile) - os.Remove(cEntryFile) - - var files []string - - if goos == "windows" { - if err := os.WriteFile(bridgeFile, []byte(tmplBridge), 0644); err != nil { - fmt.Printf(" error: write bridge: %v\n", err) - return func() {} - } - files = append(files, bridgeFile) - } else { - if err := os.WriteFile(bridgeFile, []byte(tmplLinuxBridge), 0644); err != nil { - fmt.Printf(" error: write bridge: %v\n", err) - return func() {} - } - files = append(files, bridgeFile) - // Write C entry point file - if err := os.WriteFile(cEntryFile, []byte(tmplPluginInitC), 0644); err != nil { - fmt.Printf(" error: write C entry: %v\n", err) - return func() {} - } - files = append(files, cEntryFile) - } - - return func() { - for _, f := range files { - os.Remove(f) - } - } -} - // linkThirdpart scans thirdpart/, source_dirs from plg.json, and replace target dirs // for source files, generating auto-import stubs. Returns cleanup function. func linkThirdpart(plg *PlgConfig, target string) func() { diff --git a/tools/plugindev/cmd_init.go b/tools/plugindev/cmd_init.go index cbe2df0..5f03abe 100644 --- a/tools/plugindev/cmd_init.go +++ b/tools/plugindev/cmd_init.go @@ -7,8 +7,6 @@ import ( "sort" "strings" "text/template" - - "gitcode.com/JianFeeeee/homeagent-sdk/meta" ) func (p *PlgConfig) ReplacesToSlice() []string { @@ -61,10 +59,6 @@ type TemplateData struct { GoVersion string SDKModule string SDKVersion string - - // C ABI - CABIVersion int - CABIHeader string } func cmdInit(args []string) { @@ -139,9 +133,7 @@ func cmdInit(args []string) { Tags: []string{name}, Targets: targets, }, - IsLua: isLua, - CABIVersion: meta.CABINum, - CABIHeader: tmplCABIHeader, + IsLua: isLua, } // Detect SDK info for Go plugin go.mod. diff --git a/tools/plugindev/proc_runtime.go b/tools/plugindev/proc_runtime.go index b3c269b..c745279 100644 --- a/tools/plugindev/proc_runtime.go +++ b/tools/plugindev/proc_runtime.go @@ -6,49 +6,86 @@ import ( "os" ) -// 子进程插件运行时(外部插件多进程化,Part 3)。 +// 子进程插件运行时(外部插件多进程化)。 // -// 与旧 C ABI bridge 的差异: -// - 模板改为**真实 .go 源文件**(templates/proc_main.go.tmpl)而非 raw string: -// 900+ 行代码塞在字符串里,写错只能等生成插件时才炸;作为源文件可被 -// gofmt / go vet / parser 直接检查。 -// - 构建从 `-buildmode=c-shared` + CGO_ENABLED=1 改为普通 `go build` + CGO_ENABLED=0, -// 交叉编译不再需要目标平台的 C 工具链(§3.1 连带消失项)。 +// 模板为何是**真实 .go 源文件** + //go:embed,而不是 raw string: +// 1100+ 行代码塞在字符串里写错只能等生成插件时才炸;作为源文件可被 +// gofmt / go vet / go/parser 直接检查(proc_runtime_test.go 的 16 项 +// 静态检查就以此为前提)。 // -// 设计依据:docs/zh/架构迁移评估.md §3、docs/zh/plugin-migration-plan.md Part 3 +// 构建从 `-buildmode=c-shared` + CGO_ENABLED=1 变成普通 `go build` + +// CGO_ENABLED=0,交叉编译不再需要目标平台的 C 工具链(§3.1 连带消失项)。 +// +// 设计依据:docs/zh/架构迁移评估.md §3、docs/zh/plugin-migration-plan.md Part 3/6 //go:embed templates/proc_main.go.tmpl +//go:embed templates/proc_shm_unix.go.tmpl +//go:embed templates/proc_shm_windows.go.tmpl var procTemplates embed.FS +// procRuntimeFiles 列出生成到插件目录的运行时文件。 +// +// 共享段与事件通知的**传递机制**按平台不同(Unix 继承 fd, +// Windows 命名内核对象),故拆成带 build tag 的两个文件; +// 共享段**布局**与 RPC 逻辑完全平台无关,全在 proc_main 里。 +// +// 这正是三套独立 ABI 实现收敛为单一 RPC 实现的效果: +// 平台差异从「整套 stage 下发/写回逻辑各写一份」缩到「三个挂载函数」。 +var procRuntimeFiles = []struct { + tmpl string // 内嵌模板路径 + out string // 生成到插件目录的文件名 +}{ + {"templates/proc_main.go.tmpl", "z_proc_gen.go"}, + {"templates/proc_shm_unix.go.tmpl", "z_proc_shm_unix.go"}, + {"templates/proc_shm_windows.go.tmpl", "z_proc_shm_windows.go"}, +} + // procEntryFile 是子进程插件的入口二进制名(与内核 internal/plugin/dynamic.go 的 binEntry 一致)。 +// +// 全平台同名:进程边界本身就是 ABI 边界,不存在平台特有的动态库扩展名 +// (对比 C ABI 时代的 .so/.dylib/.dll 三套产物 + 三套 ABI 实现)。 const procEntryFile = "plugin.bin" -// procGenFile 是生成的运行时文件名。 -// 前缀 z_ 使其在目录列表中排在业务代码之后,且与旧 bridge 的 z_bridge_gen.go 风格一致。 +// luaEntryFile 是 Lua 插件的入口。Lua 走解释器,不经过 Go 编译。 +const luaEntryFile = "main.lua" + +// procGenFile 是生成的主运行时文件名(兼容旧注释引用)。 +// 前缀 z_ 使其在目录列表中排在业务代码之后。 const procGenFile = "z_proc_gen.go" -// generateProcRuntime 把子进程运行时写入插件目录,返回清理函数。 -// -// 与 generateBridge 的差异:只写一个 .go 文件,不需要 C 入口(z_entry.c)。 +// generateProcRuntime 把子进程运行时(平台无关主体 + 两个平台挂载实现) +// 写入插件目录,返回清理函数。 func generateProcRuntime() (func(), error) { - data, err := procTemplates.ReadFile("templates/proc_main.go.tmpl") - if err != nil { - return nil, fmt.Errorf("读取内嵌模板: %w", err) - } - - // 清理可能残留的 C ABI 产物:同目录同时存在两套 main 会编译冲突。 - // 这也让 .so → .bin 的切换无需人工清理。 + // 清理历史 C ABI 产物:旧版 plugindev 生成过这两个文件,残留下来会与 + // 本模板的 main 冲突。无需人工清理就能从旧版升级。 for _, stale := range []string{"z_bridge_gen.go", "z_entry.c"} { os.Remove(stale) } - if err := os.WriteFile(procGenFile, data, 0644); err != nil { - return nil, fmt.Errorf("写入 %s: %w", procGenFile, err) + var written []string + cleanup := func() { + for _, f := range written { + os.Remove(f) + } } - return func() { os.Remove(procGenFile) }, nil + + for _, rf := range procRuntimeFiles { + data, err := procTemplates.ReadFile(rf.tmpl) + if err != nil { + cleanup() + return nil, fmt.Errorf("读取内嵌模板 %s: %w", rf.tmpl, err) + } + if err := os.WriteFile(rf.out, data, 0644); err != nil { + cleanup() + return nil, fmt.Errorf("写入 %s: %w", rf.out, err) + } + written = append(written, rf.out) + } + return cleanup, nil } -// isProcEntry 判断 plg.json 的 entry 是否声明了子进程模式。 -func isProcEntry(entry string) bool { - return entry == procEntryFile -} +// isProcEntry 已删除:Go 插件一律产出 plugin.bin,不再看 plg.json 的 entry 值。 +// +// 为何忽略 entry:17 个存量插件的 plg.json 都写着 "plugin.so"。若把 entry 当作 +// 通道开关,迁移就得改 17 个文件——而「外部插件零改动」是本次迁移的硬约束。 +// entry 现在只用于区分 Lua(main.lua)与 Go 插件。 diff --git a/tools/plugindev/proc_runtime_test.go b/tools/plugindev/proc_runtime_test.go index 8c91239..65f290f 100644 --- a/tools/plugindev/proc_runtime_test.go +++ b/tools/plugindev/proc_runtime_test.go @@ -3,6 +3,7 @@ package main import ( "go/parser" "go/token" + "os" "regexp" "strings" "testing" @@ -285,70 +286,109 @@ func TestProcTemplate_RejectsVersionMismatch(t *testing.T) { } } -// isProcEntry 只认 plugin.bin。 -func TestIsProcEntry(t *testing.T) { - if !isProcEntry("plugin.bin") { - t.Error("plugin.bin 应为 proc 模式") - } - for _, e := range []string{"plugin.so", "plugin.dll", "plugin.dylib", "main.lua", "", "plugin.exe"} { - if isProcEntry(e) { - t.Errorf("%q 不应被判为 proc 模式", e) - } - } -} - -// proc 模式下各平台产物统一为 plugin.bin(进程边界即 ABI 边界,无平台扩展名)。 -func TestResolveBuild_ProcModeUsesBinOnAllPlatforms(t *testing.T) { - for _, target := range []string{"linux/amd64", "darwin/arm64", "windows/amd64", "freebsd/amd64"} { - cfg, errMsg := resolveBuild(target, true) - if cfg == nil { - t.Fatalf("resolveBuild(%q, proc) 失败: %s", target, errMsg) - } - if cfg.entryFile != procEntryFile { - t.Errorf("%s: proc 模式产物应为 %s,实际 %s", target, procEntryFile, cfg.entryFile) - } - if !cfg.proc { - t.Errorf("%s: proc 标志应为 true", target) - } - } -} - -// 非 proc 模式行为不变(回归保护:.so 通道必须与改动前一致)。 -func TestResolveBuild_CABIModeUnchanged(t *testing.T) { - cases := map[string]string{ - "linux/amd64": "plugin.so", - "darwin/amd64": "plugin.dylib", - "freebsd/amd64": "plugin.so", - "windows/amd64": "plugin.dll", - } - for target, want := range cases { - cfg, errMsg := resolveBuild(target, false) +// 全平台统一产出 plugin.bin。 +// +// 这是三套独立 ABI 实现(.so/.dylib/.dll)收敛为单一 RPC 实现的直接后果: +// 进程边界本身就是 ABI 边界,不存在平台特有的动态库扩展名。 +// §9.2 记录的「Windows DLL 路径只下发 3 字段、无写回」随之消失—— +// Windows 走的是与 Linux 完全相同的 RPC 实现。 +func TestResolveBuild_AllPlatformsProduceBin(t *testing.T) { + for _, target := range []string{ + "linux/amd64", "linux/arm64", + "darwin/amd64", "darwin/arm64", + "windows/amd64", + "freebsd/amd64", + } { + cfg, errMsg := resolveBuild(target) if cfg == nil { t.Fatalf("resolveBuild(%q) 失败: %s", target, errMsg) } - if cfg.entryFile != want { - t.Errorf("%s: 应产出 %s,实际 %s", target, want, cfg.entryFile) - } - if cfg.proc { - t.Errorf("%s: 非 proc 模式的 proc 标志应为 false", target) + if cfg.entryFile != procEntryFile { + t.Errorf("%s: 产物应为 %s,实际 %s", target, procEntryFile, cfg.entryFile) } } } -// bundle 模式下 proc 产物在 zip 内按平台加后缀(同名会相互覆盖)。 -func TestProcBundleTargets_HavePlatformSuffixedEntries(t *testing.T) { +// lua 目标仍走解释器路径(entry 字段唯一仍在使用的用途)。 +func TestResolveBuild_LuaIsSeparatePath(t *testing.T) { + for _, target := range []string{"lua", ""} { + cfg, kind := resolveBuild(target) + if cfg != nil { + t.Errorf("%q 应返回 nil cfg(Lua 不经 Go 编译)", target) + } + if kind != "lua" { + t.Errorf("%q 应识别为 lua,实际 %q", target, kind) + } + } +} + +// 不支持的平台明确报错,不静默产出错误产物。 +func TestResolveBuild_UnsupportedOSErrors(t *testing.T) { + cfg, errMsg := resolveBuild("plan9/amd64") + if cfg != nil { + t.Error("不支持的平台应返回 nil cfg") + } + if !strings.Contains(errMsg, "unsupported") { + t.Errorf("应给出 unsupported 提示,实际 %q", errMsg) + } +} + +// bundle 产物在 zip 内按平台加后缀(全平台同名 plugin.bin 会相互覆盖)。 +func TestBundleTargets_HavePlatformSuffixedEntries(t *testing.T) { seen := map[string]bool{} - for _, bt := range allProcBundleTargets { + for _, bt := range allBundleTargets { if seen[bt.entry] { t.Errorf("zip 条目名重复: %s(会相互覆盖)", bt.entry) } seen[bt.entry] = true if !strings.HasPrefix(bt.entry, procEntryFile+".") { - t.Errorf("proc bundle 条目 %q 应以 %s. 为前缀", bt.entry, procEntryFile) + t.Errorf("bundle 条目 %q 应以 %s. 为前缀", bt.entry, procEntryFile) } } - if len(allProcBundleTargets) != len(allBundleTargets) { - t.Errorf("proc 与 cabi 的 bundle 平台数应一致:%d vs %d", - len(allProcBundleTargets), len(allBundleTargets)) + if len(allBundleTargets) == 0 { + t.Error("bundle 目标表不应为空") + } +} + +// C ABI 工具链残留必须彻底清除:不得再有 .so/.dylib/.dll 产物路径, +// 也不得再引用 c-shared 构建模式或 MinGW 探测。 +func TestToolchain_NoCABIResiduals(t *testing.T) { + for _, f := range []string{"cmd_build.go", "templates.go", "cmd_init.go", "proc_runtime.go"} { + data, err := os.ReadFile(f) + if err != nil { + t.Fatalf("读 %s: %v", f, err) + } + src := stripComments(t, string(data)) + for _, forbidden := range []string{ + "c-shared", + "CGO_ENABLED=1", + "detectWindowsCC", + "generateBridge", + "tmplLinuxBridge", + "tmplPluginInitC", + } { + if strings.Contains(src, forbidden) { + t.Errorf("%s 仍含 C ABI 残留 %q", f, forbidden) + } + } + } +} + +// Go 插件的构建不再读 plg.json 的 entry 值。 +// +// 这是「外部插件零改动」的关键:17 个存量插件的 plg.json 都写着 "plugin.so", +// 若把 entry 当通道开关,迁移就得改 17 个文件。 +func TestToolchain_IgnoresEntryForGoPlugins(t *testing.T) { + data, err := os.ReadFile("cmd_build.go") + if err != nil { + t.Fatalf("读 cmd_build.go: %v", err) + } + src := stripComments(t, string(data)) + if strings.Contains(src, "isProcEntry") { + t.Error("isProcEntry 应已删除——Go 插件一律产出 plugin.bin,不看 entry 值") + } + // entry 仅剩 Lua 判定这一处用途 + if !strings.Contains(src, "luaEntryFile") { + t.Error("IsLua 应改用 luaEntryFile 常量") } } diff --git a/tools/plugindev/templates.go b/tools/plugindev/templates.go index ae30b9d..bcc018c 100644 --- a/tools/plugindev/templates.go +++ b/tools/plugindev/templates.go @@ -152,787 +152,6 @@ function plugin.stop() sdk.log("info", "{{.Plg.Name}} stopped") end return plugin ` -// tmplBridge — Windows DLL C ABI bridge (unchanged) -const tmplBridge = `//go:build windows && cgo - -package main - -/* -#include -*/ -import "C" -import ( - "encoding/json" - "sync" - "unsafe" - sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" -) - -var ( - mu sync.Mutex - handleMap = map[unsafe.Pointer]*bridgeState{} -) - -type bridgeState struct { - plugin sdk.Plugin - toolDefs map[string]sdk.ToolDef - handlers map[string]sdk.ToolHandler - stages map[string]sdk.StageHandler - settings map[string]interface{} - sdk *sdk.PluginSDK -} - -func newHandle(plg sdk.Plugin) unsafe.Pointer { - mu.Lock(); defer mu.Unlock() - h := C.malloc(C.size_t(1)) - handleMap[h] = &bridgeState{ - plugin: plg, toolDefs: make(map[string]sdk.ToolDef), - handlers: make(map[string]sdk.ToolHandler), stages: make(map[string]sdk.StageHandler), - settings: make(map[string]interface{}), - } - return h -} -func getState(h unsafe.Pointer) *bridgeState { mu.Lock(); defer mu.Unlock(); return handleMap[h] } -func delState(h unsafe.Pointer) { mu.Lock(); defer mu.Unlock(); delete(handleMap, h); C.free(h) } - -//export NewPlugin -func NewPlugin(name *C.char, configJSON *C.char) unsafe.Pointer { - goName := C.GoString(name) - var config map[string]interface{} - if configJSON != nil { - var wrapper map[string]interface{} - if err := json.Unmarshal([]byte(C.GoString(configJSON)), &wrapper); err == nil { - if c, ok := wrapper["config"].(map[string]interface{}); ok { config = c } - } - } - plg, err := NewPluginFactory(goName, config) - if err != nil { return nil } - return newHandle(plg) -} - -//export StartPlugin -func StartPlugin(handle unsafe.Pointer) C.int { - bs := getState(handle) - if bs == nil { return 1 } - mockSett := &bridgeSettings{data: bs.settings} - mockSDK := sdk.New(bs.plugin.Name(), mockSett, - func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error { - bs.toolDefs[name] = def; bs.handlers[name] = handler; return nil - }, - func(stage sdk.Stage, handler sdk.StageHandler) { bs.stages[string(stage)] = handler }, - func(name string) error { return nil }, - func(name string, caps int, desc string, def sdk.ChannelDef, handler sdk.ToolHandler) error { return nil }, - ) - mockSDK.SetInputChannelRegistrar(func(name string, def sdk.ChannelDef) error { return nil }) - bs.sdk = mockSDK - if err := bs.plugin.Start(mockSDK); err != nil { return 1 } - return 0 -} - -//export StopPlugin -func StopPlugin(handle unsafe.Pointer) C.int { - bs := getState(handle) - if bs == nil { return 1 } - if bs.sdk != nil { - bs.sdk.RunStopHandlers() - } - if err := bs.plugin.Stop(); err != nil { return 1 } - return 0 -} - -//export DestroyPlugin -func DestroyPlugin(handle unsafe.Pointer) { - if bs := getState(handle); bs != nil { delState(handle) } -} - -//export GetToolDefsJSON -func GetToolDefsJSON(handle unsafe.Pointer) *C.char { - bs := getState(handle) - if bs == nil { return nil } - defs := make([]sdk.ToolDef, 0, len(bs.toolDefs)) - for _, def := range bs.toolDefs { defs = append(defs, def) } - b, _ := json.Marshal(defs) - return C.CString(string(b)) -} - -//export InvokeToolJSON -func InvokeToolJSON(handle unsafe.Pointer, toolName *C.char, argsJSON *C.char) *C.char { - bs := getState(handle) - if bs == nil || toolName == nil { return nil } - goName := C.GoString(toolName) - handler, ok := bs.handlers[goName] - if !ok { errMsg, _ := json.Marshal(map[string]interface{}{"error": "tool not found: " + goName}); return C.CString(string(errMsg)) } - var args map[string]interface{} - if argsJSON != nil { json.Unmarshal([]byte(C.GoString(argsJSON)), &args) } - r, err := handler(args) - if err != nil { errMsg, _ := json.Marshal(map[string]interface{}{"error": err.Error()}); return C.CString(string(errMsg)) } - b, _ := json.Marshal(r) - return C.CString(string(b)) -} - -//export GetStagesJSON -func GetStagesJSON(handle unsafe.Pointer) *C.char { - bs := getState(handle) - if bs == nil { return nil } - type se struct { Stage string ` + "`" + `json:"stage"` + "`" + ` } - var entries []se - for s := range bs.stages { entries = append(entries, se{s}) } - b, _ := json.Marshal(entries) - return C.CString(string(b)) -} - -//export InvokeStage -func InvokeStage(handle unsafe.Pointer, stage *C.char, contextJSON *C.char) C.int { - bs := getState(handle) - if bs == nil || stage == nil { return 1 } - goStage := C.GoString(stage) - handler, ok := bs.stages[goStage] - if !ok { return 1 } - var ctx map[string]interface{} - if contextJSON != nil { json.Unmarshal([]byte(C.GoString(contextJSON)), &ctx) } - sc := &sdk.StageContext{} - if ctx != nil { - if v, ok := ctx["raw_message"].(string); ok { sc.RawMessage = v } - if v, ok := ctx["user_id"].(string); ok { sc.UserID = v } - if v, ok := ctx["phase"].(string); ok { sc.Phase = sdk.Stage(v) } - } - if err := handler(sc); err != nil { return 1 } - return 0 -} - -//export FreeCString -func FreeCString(s *C.char) { C.free(unsafe.Pointer(s)) } - -type bridgeSettings struct{ data map[string]interface{} } -func (s *bridgeSettings) Get(key string) (interface{}, error) { v, ok := s.data[key]; if !ok { return nil, nil }; return v, nil } -func (s *bridgeSettings) Set(key string, value interface{}) error { s.data[key] = value; return nil } -func (s *bridgeSettings) List(prefix string) ([]string, error) { - var keys []string - for k := range s.data { if len(k) >= len(prefix) && k[:len(prefix)] == prefix { keys = append(keys, k) } } - return keys, nil -} -func (s *bridgeSettings) GetCore(key string) (interface{}, error) { return nil, nil } -func (s *bridgeSettings) SetCore(key string, value interface{}) error { return nil } -func (s *bridgeSettings) ListCore(prefix string) ([]string, error) { return nil, nil } -func (s *bridgeSettings) GetPlugin(plugin, key string) (interface{}, error) { return nil, nil } -func (s *bridgeSettings) SetPlugin(plugin, key string, value interface{}) error { return nil } -func (s *bridgeSettings) ListPlugin(plugin, prefix string) ([]string, error) { return nil, nil } -func (s *bridgeSettings) RegisterDef(def sdk.ConfigDef) {} -func (s *bridgeSettings) Defs(prefix string) []*sdk.ConfigDef { return nil } -func (s *bridgeSettings) Dump() map[string]interface{} { return s.data } -func (s *bridgeSettings) Plugins() []string { return nil } - -func main() {} -` - -// tmplCABIHeader — shared C ABI type definitions for both core and plugin -// 此模板中的常量应与 core/internal/meta/meta.go 保持一致(ABI 版本、dispatch method IDs)。 -const tmplCABIHeader = ` -#ifndef HOMEAGENT_CABI_H -#define HOMEAGENT_CABI_H -// HOMEAGENT_ABI_VERSION 与 sdk/meta/meta.go CABINum 同步(major*100+minor,v0.9.x→900) -#define HOMEAGENT_ABI_VERSION 900 -#ifdef __cplusplus -extern "C" { -#endif - -// PluginAPI — implemented by the plugin, called by the core -typedef struct { - int version; int version_min; - int (*init_plugin)(char*, char*, char**); - int (*start_plugin)(void*, int, char**); - int (*stop_plugin)(char**); - int (*invoke_tool)(char*, char*, char**, char**); - int (*invoke_stage)(char*, char*, char**, char**); - int (*invoke_output)(char*, char*, char*, char**); - void (*free_string)(char*); -} PluginAPI; - -// CoreAPI — implemented by the core, passed to plugin via start_plugin -// Uses single dispatch function to avoid function pointer ABI issues -typedef struct { - int version; int version_min; - int (*dispatch)(int method_id, void* ctx, char* s1, char* s2, char* s3, int i1, int i2, char** result, char** error); - void* ctx; -} CoreAPI; - -// Dispatch method IDs (plugin→core SDK calls) -enum { - CORE_REGISTER_TOOL = 1, - CORE_REGISTER_STAGE = 2, - CORE_REGISTER_OUTPUT_CH = 3, - CORE_REGISTER_PLUGIN_API = 4, - CORE_INJECT_TEXT = 5, - CORE_INJECT_INTERRUPT_TEXT = 6, - CORE_INJECT_TEXT_NO_MEMORY = 7, - CORE_INJECT_INPUT_SYNC = 47, - CORE_SET_AUTO_RESTART = 8, - CORE_MEMORY_RECALL = 9, - CORE_MEMORY_COMMIT = 10, - CORE_MEMORY_INTROSPECT = 11, - CORE_MEMORY_MERGE = 12, - CORE_MEMORY_PURGE = 13, - CORE_DOC_QUERY = 14, - CORE_KNOWLEDGE_SEARCH = 15, - CORE_SETTINGS_GET = 16, - CORE_SETTINGS_SET = 17, - CORE_SETTINGS_REGISTER_DEF = 18, - CORE_LLM_LIST_SOURCES = 19, - CORE_LLM_SET_SOURCE = 20, - CORE_SOCIAL_GET_PERSON = 21, - CORE_SOCIAL_GET_NETWORK = 22, - CORE_SUBSCRIBE = 23, - CORE_UNSUBSCRIBE = 24, - CORE_FREE_STRING = 25, - CORE_SETTINGS_GET_CORE = 26, - CORE_SETTINGS_SET_CORE = 27, - CORE_SETTINGS_LIST_CORE = 28, - CORE_SETTINGS_GET_PLUGIN = 29, - CORE_SETTINGS_SET_PLUGIN = 30, - CORE_SETTINGS_LIST_PLUGIN = 31, - CORE_DOC_INSERT = 32, - CORE_DOC_REMOVE = 33, - CORE_DOC_STATS = 34, - CORE_KNOWLEDGE_ADD = 35, - CORE_KNOWLEDGE_LIST = 36, - CORE_LLM_CURRENT_SOURCE = 37, - CORE_SOCIAL_GET_TRAIT = 38, - CORE_SOCIAL_GET_RELATIONS = 39, - CORE_SOCIAL_LIST_PERSONS = 40, - CORE_TEXT_MEMORY_APPEND = 41, - CORE_SETTINGS_LIST = 42, - CORE_SETTINGS_DEFS = 43, - CORE_SETTINGS_DUMP = 44, - CORE_SETTINGS_PLUGINS = 45, - CORE_REGISTER_INPUT_CH = 46, - CORE_INJECT_INPUT_SYNC = 47, - CORE_PLUGIN_RELOAD_ONE = 48, - CORE_PLUGIN_LIST_LOADED = 49, - CORE_PLUGIN_IS_DISABLED = 50, -}; - -#ifdef __cplusplus -} -#endif -#endif -` - -// tmplLinuxBridge — auto-generated Go bridge for Linux c-shared builds. -// Called by plugin's Start() with a PluginSDK that wraps CoreAPI dispatch. -// PluginSDK calls go through C ABI → CoreAPI dispatch → core's Go PluginSDK. -const tmplLinuxBridge = `package main - -/* -#include -int ha_dispatch(int method_id, void* core_api, char* s1, char* s2, char* s3, int i1, int i2, char** result, char** error); -*/ -import "C" -import ( - "encoding/json" - "fmt" - "sync" - "unsafe" - sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" -) - -// ---- global state ---- - -var ( - mu sync.Mutex - currentPlg sdk.Plugin - currentSDK *sdk.PluginSDK - coreAPI unsafe.Pointer - - handlerMu sync.RWMutex - coreAPIMu sync.RWMutex - toolHandlers = map[string]sdk.ToolHandler{} - stageHandlers = map[string]sdk.StageHandler{} - outputHandlers = map[string]sdk.ToolHandler{} -) - -// ---- CoreAPI dispatch helpers ---- - -func callVoid(methodID int, s1, s2, s3 string, i1, i2 int) error { - coreAPIMu.RLock() - api := coreAPI - coreAPIMu.RUnlock() - var c1, c2, c3 *C.char - if s1 != "" { c1 = C.CString(s1); defer C.free(unsafe.Pointer(c1)) } - if s2 != "" { c2 = C.CString(s2); defer C.free(unsafe.Pointer(c2)) } - if s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) } - var cErr *C.char - if C.ha_dispatch(C.int(methodID), api, c1, c2, c3, C.int(i1), C.int(i2), nil, &cErr) != 0 && cErr != nil { - return fmt.Errorf("%s", C.GoString(cErr)) - } - return nil -} - -func callString(methodID int, s1, s2, s3 string, i1, i2 int) (string, error) { - coreAPIMu.RLock() - api := coreAPI - coreAPIMu.RUnlock() - var c1, c2, c3 *C.char - if s1 != "" { c1 = C.CString(s1); defer C.free(unsafe.Pointer(c1)) } - if s2 != "" { c2 = C.CString(s2); defer C.free(unsafe.Pointer(c2)) } - if s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) } - var strResult, cErr *C.char - if C.ha_dispatch(C.int(methodID), api, c1, c2, c3, C.int(i1), C.int(i2), &strResult, &cErr) != 0 && cErr != nil { - return "", fmt.Errorf("%s", C.GoString(cErr)) - } - if strResult != nil { - result := C.GoString(strResult) - C.ha_dispatch(C.int(25), api, strResult, nil, nil, 0, 0, nil, nil) - return result, nil - } - return "", nil -} - -// ---- buildPluginSDK: PluginSDK backed by CoreAPI dispatch ---- -// - ALL SDK methods route through C ABI → CoreAPI → core's PluginSDK -// - Handlers for tools/stages/output are stored locally AND registered via dispatch - -func buildPluginSDK(name string) *sdk.PluginSDK { - sett := &dispatchSettings{} - base := sdk.New(name, sett, - func(toolName string, def sdk.ToolDef, handler sdk.ToolHandler) error { - handlerMu.Lock() - toolHandlers[toolName] = handler - handlerMu.Unlock() - b, _ := json.Marshal(def) - return callVoid(1, toolName, string(b), "", 0, 0) - }, - func(stage sdk.Stage, handler sdk.StageHandler) { - handlerMu.Lock() - stageHandlers[string(stage)] = handler - handlerMu.Unlock() - callVoid(2, string(stage), "", "", 0, 0) - }, - func(name string) error { return callVoid(4, name, "", "", 0, 0) }, - func(name string, caps int, desc string, def sdk.ChannelDef, handler sdk.ToolHandler) error { - handlerMu.Lock() - outputHandlers[name] = handler - handlerMu.Unlock() - defJSON, _ := json.Marshal(def) - return callVoid(3, name, desc, string(defJSON), caps, 0) - }, - ) - base.SetIOInjector(dispatchIO{}) - base.SetMemoryAPI(dispatchMemory{}) - base.SetDocMemoryAPI(dispatchDocMemory{}) - base.SetKnowledgeAPI(dispatchKnowledge{}) - base.SetLLMAPI(dispatchLLM{}) - base.SetSocialAPI(dispatchSocial{}) - base.SetTextMemoryAPI(dispatchTextMemory{}) - base.SetPluginMgrAPI(dispatchPluginMgr{}) - base.SetInputChannelRegistrar( - func(name string, def sdk.ChannelDef) error { - defJSON, _ := json.Marshal(def) - return callVoid(46, name, string(defJSON), "", 0, 0) - }, - ) - return base -} - -// ---- dispatch IO (inline definitions) ---- - -type dispatchIO struct{} -func (dispatchIO) InjectInterruptText(s, c, t string) { callVoid(6, s, c, t, 0, 0) } -func (dispatchIO) InjectText(s, c, t string) { callVoid(5, s, c, t, 0, 0) } -func (dispatchIO) InjectTextNoMemory(s, c, t string) { callVoid(7, s, c, t, 0, 0) } -func (dispatchIO) InjectInputSync(s, c, t string) string { r, _ := callString(47, s, c, t, 0, 0); return r } -// SetToolBlocks 是 Go 原生(非 ABI)的多模态注入;跨 ABI 的外部插件无对应内核桥接, -// 故为空实现(满足接口即可)。需要多模态块时用插件内自持 SDK,不走 ABI。 -func (dispatchIO) SetToolBlocks([]sdk.ContentBlock) {} - -type dispatchMemory struct{} -func (dispatchMemory) Recall(q []string, d int) ([]sdk.Entity, []sdk.Relation, error) { - b, _ := json.Marshal(q); r, e := callString(9, string(b), "", "", d, 0) - if e != nil || r == "" { return nil, nil, e } - var v struct{ Entities []sdk.Entity; Relations []sdk.Relation } - if e = json.Unmarshal([]byte(r), &v); e != nil { return nil, nil, e } - if v.Entities == nil { v.Entities = []sdk.Entity{} } - if v.Relations == nil { v.Relations = []sdk.Relation{} } - return v.Entities, v.Relations, nil -} -func (dispatchMemory) Commit(t []sdk.Triple) error { b, _ := json.Marshal(t); return callVoid(10, string(b), "", "", 0, 0) } -func (dispatchMemory) Introspect() (map[string]interface{}, error) { r, e := callString(11, "", "", "", 0, 0); if e != nil || r == "" { return nil, e }; var m map[string]interface{}; return m, json.Unmarshal([]byte(r), &m) } -func (dispatchMemory) MergeEntities(s, t string) (int, error) { return 1, callVoid(12, s, t, "", 0, 0) } -func (dispatchMemory) Purge(c map[string]string, m string) (int, error) { b, _ := json.Marshal(c); i := 0; if m == "hard" { i = 1 }; return 1, callVoid(13, string(b), "", "", i, 0) } - -type dispatchDocMemory struct{} -func (dispatchDocMemory) Query(t string, k int) []*sdk.Doc { r, e := callString(14, t, "", "", k, 0); if e != nil || r == "" { return nil }; var d []*sdk.Doc; json.Unmarshal([]byte(r), &d); return d } -func (dispatchDocMemory) Insert(doc *sdk.Doc) error { b, _ := json.Marshal(doc); return callVoid(32, string(b), "", "", 0, 0) } -func (dispatchDocMemory) Remove(id string) { callVoid(33, id, "", "", 0, 0) } -func (dispatchDocMemory) Stats() map[string]interface{} { r, e := callString(34, "", "", "", 0, 0); if e != nil || r == "" { return nil }; var m map[string]interface{}; json.Unmarshal([]byte(r), &m); return m } - -type dispatchKnowledge struct{} -func (dispatchKnowledge) Search(q string, k int) ([]*sdk.Knowledge, error) { r, e := callString(15, q, "", "", k, 0); if e != nil || r == "" { return nil, e }; var v []*sdk.Knowledge; return v, json.Unmarshal([]byte(r), &v) } -func (dispatchKnowledge) Add(n, c string) error { return callVoid(35, n, c, "", 0, 0) } -func (dispatchKnowledge) List() ([]string, error) { r, e := callString(36, "", "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v []string; return v, json.Unmarshal([]byte(r), &v) } - -type dispatchLLM struct{} -func (dispatchLLM) ListSources() []string { r, e := callString(19, "", "", "", 0, 0); if e != nil || r == "" { return nil }; var v []string; json.Unmarshal([]byte(r), &v); return v } -func (dispatchLLM) SetSource(n string) error { return callVoid(20, n, "", "", 0, 0) } -func (dispatchLLM) CurrentSource() string { r, e := callString(37, "", "", "", 0, 0); if e != nil || r == "" { return "" }; return r } - -type dispatchSocial struct{} -func (dispatchSocial) GetPerson(n string) (*sdk.PersonProfile, error) { r, e := callString(21, n, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v sdk.PersonProfile; return &v, json.Unmarshal([]byte(r), &v) } -func (dispatchSocial) GetTrait(n, t string) (string, bool) { r, e := callString(38, n, t, "", 0, 0); if e != nil || r == "" { return "", false }; var m map[string]interface{}; json.Unmarshal([]byte(r), &m); v, _ := m["value"].(string); ok, _ := m["found"].(bool); return v, ok } -func (dispatchSocial) GetRelations(name string) ([]sdk.SocialRelation, error) { r, e := callString(39, name, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v []sdk.SocialRelation; return v, json.Unmarshal([]byte(r), &v) } -func (dispatchSocial) GetNetwork(n string, d int) ([]*sdk.PersonProfile, error) { r, e := callString(22, n, "", "", d, 0); if e != nil || r == "" { return nil, e }; var v []*sdk.PersonProfile; return v, json.Unmarshal([]byte(r), &v) } -func (dispatchSocial) ListPersons() ([]string, error) { r, e := callString(40, "", "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v []string; return v, json.Unmarshal([]byte(r), &v) } - -type dispatchTextMemory struct{} -func (dispatchTextMemory) Append(evt sdk.TextEvent) error { b, _ := json.Marshal(evt); return callVoid(41, string(b), "", "", 0, 0) } - -// ---- dispatchPluginMgr (CORE_PLUGIN_RELOAD_ONE = 48) ---- - -type dispatchPluginMgr struct{} - -func (dispatchPluginMgr) ReloadOne(name string) error { - return callVoid(48, name, "", "", 0, 0) -} - -func (dispatchPluginMgr) ListLoadedPlugins() []string { - r, e := callString(49, "", "", "", 0, 0) - if e != nil || r == "" { - return nil - } - var list []string - if json.Unmarshal([]byte(r), &list) != nil { - return nil - } - return list -} - -func (dispatchPluginMgr) IsPluginDisabled(name string) bool { - r, e := callString(50, name, "", "", 0, 0) - return e == nil && r == "1" -} - -// ---- dispatchSettings (inline) ---- - -type dispatchSettings struct{} -func (d *dispatchSettings) Get(key string) (interface{}, error) { - r, e := callString(16, key, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v interface{}; return v, json.Unmarshal([]byte(r), &v) -} -func (d *dispatchSettings) Set(key string, value interface{}) error { - b, _ := json.Marshal(value); return callVoid(17, key, string(b), "", 0, 0) -} -func (d *dispatchSettings) RegisterDef(def sdk.ConfigDef) { b, _ := json.Marshal(def); callVoid(18, string(b), "", "", 0, 0) } -func (d *dispatchSettings) List(prefix string) ([]string, error) { - r, e := callString(42, prefix, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v []string; return v, json.Unmarshal([]byte(r), &v) -} -func (d *dispatchSettings) GetCore(key string) (interface{}, error) { - r, e := callString(26, key, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v interface{}; return v, json.Unmarshal([]byte(r), &v) -} -func (d *dispatchSettings) SetCore(key string, value interface{}) error { - b, _ := json.Marshal(value); return callVoid(27, key, string(b), "", 0, 0) -} -func (d *dispatchSettings) ListCore(prefix string) ([]string, error) { - r, e := callString(28, prefix, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v []string; return v, json.Unmarshal([]byte(r), &v) -} -func (d *dispatchSettings) GetPlugin(plugin, key string) (interface{}, error) { - r, e := callString(29, plugin, key, "", 0, 0); if e != nil || r == "" { return nil, e }; var v interface{}; return v, json.Unmarshal([]byte(r), &v) -} -func (d *dispatchSettings) SetPlugin(plugin, key string, value interface{}) error { - b, _ := json.Marshal(value); return callVoid(30, plugin, key, string(b), 0, 0) -} -func (d *dispatchSettings) ListPlugin(plugin, prefix string) ([]string, error) { - r, e := callString(31, plugin, prefix, "", 0, 0); if e != nil || r == "" { return nil, e }; var v []string; return v, json.Unmarshal([]byte(r), &v) -} -func (d *dispatchSettings) Defs(prefix string) []*sdk.ConfigDef { - r, e := callString(43, prefix, "", "", 0, 0); if e != nil || r == "" { return nil }; var v []*sdk.ConfigDef; json.Unmarshal([]byte(r), &v); return v -} -func (d *dispatchSettings) Dump() map[string]interface{} { - r, e := callString(44, "", "", "", 0, 0); if e != nil || r == "" { return nil }; var m map[string]interface{}; json.Unmarshal([]byte(r), &m); return m -} -func (d *dispatchSettings) Plugins() []string { - r, e := callString(45, "", "", "", 0, 0); if e != nil || r == "" { return nil }; var v []string; json.Unmarshal([]byte(r), &v); return v -} -func (d *dispatchSettings) DataDir() string { - r, e := callString(51, "", "", "", 0, 0); if e != nil { return "" }; return r -} - -// ---- Go callbacks (called from z_entry.c via C) ---- - -//export go_init_plugin -func go_init_plugin(name *C.char, configJSON *C.char, errorOut **C.char) C.int { - plg, err := NewPluginFactory(C.GoString(name), nil) - if err != nil || plg == nil { - if err != nil { *errorOut = C.CString(err.Error()) } else { *errorOut = C.CString("NewPluginFactory returned nil") } - return 1 - } - mu.Lock(); currentPlg = plg; mu.Unlock() - _ = configJSON - return 0 -} - -//export go_start_plugin -func go_start_plugin(coreAPIptr unsafe.Pointer, coreVersion C.int, errorOut **C.char) C.int { - mu.Lock() - plg := currentPlg - coreAPIMu.Lock() - coreAPI = coreAPIptr - coreAPIMu.Unlock() - mu.Unlock() - _ = coreVersion - if plg == nil { *errorOut = C.CString("not initialized"); return 1 } - sdk := buildPluginSDK(plg.Name()) - mu.Lock(); currentSDK = sdk; mu.Unlock() - if err := plg.Start(sdk); err != nil { *errorOut = C.CString(err.Error()); return 1 } - return 0 -} - -//export go_stop_plugin -func go_stop_plugin(errorOut **C.char) C.int { - mu.Lock() - plg := currentPlg - sdk := currentSDK - currentPlg = nil - currentSDK = nil - coreAPIMu.Lock() - coreAPI = nil - coreAPIMu.Unlock() - mu.Unlock() - if sdk != nil { - sdk.RunStopHandlers() - } - if plg != nil { - if err := plg.Stop(); err != nil { *errorOut = C.CString(err.Error()); return 1 } - } - return 0 -} - -//export go_invoke_tool -func go_invoke_tool(name *C.char, argsJSON *C.char, resultOut **C.char, errorOut **C.char) C.int { - goName := C.GoString(name) - handlerMu.RLock() - h, ok := toolHandlers[goName] - handlerMu.RUnlock() - if !ok { *errorOut = C.CString("tool not found"); return 1 } - var args map[string]interface{} - if argsJSON != nil { json.Unmarshal([]byte(C.GoString(argsJSON)), &args) } - r, err := h(args) - if err != nil { *errorOut = C.CString(err.Error()); return 1 } - b, _ := json.Marshal(r) - *resultOut = C.CString(string(b)) - return 0 -} - -// fillStageContext 将内核传来的 ctx JSON 填充到插件侧 StageContext。 -func fillStageContext(sc *sdk.StageContext, ctxJSON string) { - var m map[string]interface{} - if err := json.Unmarshal([]byte(ctxJSON), &m); err != nil { - return - } - if v, _ := m["raw_message"].(string); v != "" { sc.RawMessage = v } - if v, _ := m["user_id"].(string); v != "" { sc.UserID = v } - if v, _ := m["group_id"].(string); v != "" { sc.GroupID = v } - if v, _ := m["phase"].(string); v != "" { sc.Phase = sdk.Stage(v) } - if v, _ := m["llm_text"].(string); v != "" { sc.LLMText = v } - if v, _ := m["final_text"].(string); v != "" { sc.FinalText = v } - if v, _ := m["no_memory"].(bool); v { sc.NoMemory = true } - if v, _ := m["response"].(string); v != "" { sc.Response = &v } - if v, _ := m["tool_calls"].([]interface{}); len(v) > 0 { - b, _ := json.Marshal(v); json.Unmarshal(b, &sc.ToolCalls) - } - if v, _ := m["tool_results"].([]interface{}); len(v) > 0 { - b, _ := json.Marshal(v); json.Unmarshal(b, &sc.ToolResults) - } -} - -// stageContextWritable 提取插件可写且内核会同步回去的字段。 -func stageContextWritable(sc *sdk.StageContext) map[string]interface{} { - m := map[string]interface{}{ - "raw_message": sc.RawMessage, - "user_id": sc.UserID, - "group_id": sc.GroupID, - "phase": string(sc.Phase), - "llm_text": sc.LLMText, - "final_text": sc.FinalText, - "no_memory": sc.NoMemory, - } - if sc.Response != nil { - m["response"] = *sc.Response - } - if len(sc.ToolCalls) > 0 { - m["tool_calls"] = sc.ToolCalls - } - if len(sc.ToolResults) > 0 { - m["tool_results"] = sc.ToolResults - } - return m -} - -// changedFieldsOnly 返回插件 handler 真正变更的字段,供内核写回。 -// 修复 plan.md 11.3:旧实现无条件回传 stageContextWritable 的全部字段(含插件 -// 从内核收到的旧快照),两个插件并发时,只读插件会把自己收到的旧值覆盖回 -// 改写插件已清洗的结果(实验 13 复刻现网 sanitizer + weather 场景,丢失率 1.6~4.3%)。 -// 只回传差异字段后,只读插件零回传,改写插件的清洗结果不再被覆盖。 -// -// ❗ before 必须是 handler 运行前的**序列化快照**(snapshotWritable),不能直接存 Go 值: -// stageContextWritable 返回的 tool_calls/tool_results 与 sc 共享切片底层数组,handler -// 原地修改元素(如 sc.ToolResults[0].Result = clean)会让 before 同步变化,diff 将看不到变更。 -func changedFieldsOnly(before map[string]string, after map[string]interface{}) map[string]interface{} { - diff := map[string]interface{}{} - keys := map[string]bool{} - for k := range before { - keys[k] = true - } - for k := range after { - keys[k] = true - } - for k := range keys { - bRaw, bHas := before[k] - a, aHas := after[k] - switch { - case aHas && !bHas: - diff[k] = a - case aHas && bHas: - ab, _ := json.Marshal(a) - if bRaw != string(ab) { - diff[k] = a - } - case bHas && !aHas: - // 插件把切片类字段清空了(writable 对 len==0 不输出),显式回传空值 - switch k { - case "tool_calls": - diff[k] = []sdk.ToolCall{} - case "tool_results": - diff[k] = []sdk.ToolResult{} - case "response": - // response 从非 nil 变 nil:内核侧 applyStageResult 无法表达「清空」, - // 且短路语义不应被插件撑销,故不回传。 - } - } - } - return diff -} - -// snapshotWritable 把 writable 字段逐个序列化成 JSON 字符串,作为 handler 前的不可变快照。 -// 必须序列化:否则切片字段与 sc 共享底层数组,handler 原地改元素时快照跟着变,diff 失效。 -func snapshotWritable(sc *sdk.StageContext) map[string]string { - snap := map[string]string{} - for k, v := range stageContextWritable(sc) { - b, err := json.Marshal(v) - if err != nil { - continue - } - snap[k] = string(b) - } - return snap -} - -//export go_invoke_stage -func go_invoke_stage(stage *C.char, ctxJSON *C.char, resultOut **C.char, errorOut **C.char) C.int { - goStage := C.GoString(stage) - handlerMu.RLock() - h, ok := stageHandlers[goStage] - handlerMu.RUnlock() - if !ok { return 0 } - sc := &sdk.StageContext{} - if ctxJSON != nil { - fillStageContext(sc, C.GoString(ctxJSON)) - } - // plan.md 11.3:记录 handler 前的**序列化**快照,回传时只带真正变更的字段, - // 避免只读插件把自己收到的旧快照覆盖其他插件的改写(lost update)。 - before := snapshotWritable(sc) - if err := h(sc); err != nil { *errorOut = C.CString(err.Error()); return 1 } - // ABI v2: 回传插件修改后的上下文(若调用方要求)——只回传差异字段 - if resultOut != nil { - diff := changedFieldsOnly(before, stageContextWritable(sc)) - if len(diff) == 0 { - return 0 // 无变更(如只读插件)→ 不回传,内核不写回 - } - if b, err := json.Marshal(diff); err == nil { - *resultOut = C.CString(string(b)) - } - } - return 0 -} - -//export go_invoke_output -func go_invoke_output(channel *C.char, msgType *C.char, payloadJSON *C.char, errorOut **C.char) C.int { - goChan := C.GoString(channel) - handlerMu.RLock() - h, ok := outputHandlers[goChan] - handlerMu.RUnlock() - if !ok { return 0 } - // payloadJSON contains the full args JSON from output_send (e.g. {"content":"...","user_id":123}) - var args map[string]interface{} - if payloadJSON != nil { - json.Unmarshal([]byte(C.GoString(payloadJSON)), &args) - } - if _, err := h(args); err != nil { *errorOut = C.CString(err.Error()); return 1 } - return 0 -} - -//export go_free_string -func go_free_string(ptr *C.char) { C.free(unsafe.Pointer(ptr)) } - -func main() {} -` - -// tmplPluginInitC — C entry point for the plugin .so file. -// Contains PluginAPI, CoreAPI (single dispatch), and ha_dispatch bridge. -const tmplPluginInitC = `#include -#include - -// HOMEAGENT_ABI_VERSION 与 sdk/meta/meta.go CABINum 同步(major*100+minor,v0.9.x→900) -#define HOMEAGENT_ABI_VERSION 900 - -typedef struct { - int version; int version_min; - int (*init_plugin)(char*, char*, char**); - int (*start_plugin)(void*, int, char**); - int (*stop_plugin)(char**); - int (*invoke_tool)(char*, char*, char**, char**); - int (*invoke_stage)(char*, char*, char**, char**); - int (*invoke_output)(char*, char*, char*, char**); - void (*free_string)(char*); -} PluginAPI; - -typedef struct { - int version; int version_min; - int (*dispatch)(int, void*, char*, char*, char*, int, int, char**, char**); - void* ctx; -} CoreAPI; - -extern int go_init_plugin(char*, char*, char**); -extern int go_start_plugin(void*, int, char**); -extern int go_stop_plugin(char**); -extern int go_invoke_tool(char*, char*, char**, char**); -extern int go_invoke_stage(char*, char*, char**, char**); -extern int go_invoke_output(char*, char*, char*, char**); -extern void go_free_string(char*); - -int c_init_plugin(char* n, char* c, char** e) { return go_init_plugin(n, c, e); } -int c_start_plugin(void* a, int v, char** e) { return go_start_plugin(a, v, e); } -int c_stop_plugin(char** e) { return go_stop_plugin(e); } -int c_invoke_tool(char* n, char* a, char** r, char** e) { return go_invoke_tool(n, a, r, e); } -int c_invoke_stage(char* s, char* c, char** r, char** e) { return go_invoke_stage(s, c, r, e); } -int c_invoke_output(char* c, char* m, char* p, char** e) { return go_invoke_output(c, m, p, e); } -void c_free_string(char* p) { go_free_string(p); } - -// ha_dispatch — called by Go bridge, passes through to CoreAPI dispatch -int ha_dispatch(int id, void* api, char* s1, char* s2, char* s3, int i1, int i2, char** r, char** e) { - CoreAPI* a = (CoreAPI*)api; - if (!a || !a->dispatch) return 1; - return a->dispatch(id, a->ctx, s1, s2, s3, i1, i2, r, e); -} - -PluginAPI* plugin_init(void) { - static PluginAPI api; - memset(&api, 0, sizeof(api)); - api.version = HOMEAGENT_ABI_VERSION; api.version_min = HOMEAGENT_ABI_VERSION; - api.init_plugin = c_init_plugin; api.start_plugin = c_start_plugin; api.stop_plugin = c_stop_plugin; - api.invoke_tool = c_invoke_tool; api.invoke_stage = c_invoke_stage; api.invoke_output = c_invoke_output; - api.free_string = c_free_string; - return &api; -} -` - // ============================================================ // Remote Device Adapter Templates // ============================================================ diff --git a/tools/plugindev/templates/proc_main.go.tmpl b/tools/plugindev/templates/proc_main.go.tmpl index 1198253..537b76a 100644 --- a/tools/plugindev/templates/proc_main.go.tmpl +++ b/tools/plugindev/templates/proc_main.go.tmpl @@ -19,7 +19,6 @@ import ( "log" "os" "sync" - "syscall" sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" ) @@ -96,7 +95,7 @@ var ( // 事件环(§3.6):fd 4 = 事件环段 mmap,fd 5 = eventfd 读端 evtRingData []byte - evtfd *os.File + evtNotifier evtWaiter evtHandlers = map[uint32]func(*sdk.Event){} evtHandlerMu sync.RWMutex ) @@ -1018,10 +1017,11 @@ func handleHandshake(req *rpcRequest) { pluginName = p.PluginName } - // fd 3 = 内核经 ExtraFiles 传入的共享段 memfd + // 挂载 StageContext 共享段。 + // 传递机制按平台不同(Unix 用继承的 fd,Windows 用命名段), + // 由 z_proc_shm_*.go 承担——本文件保持平台无关。 if p.ShmSize > 0 { - m, err := syscall.Mmap(3, 0, p.ShmSize, - syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + m, err := attachStageShm(p.ShmSize) if err != nil { respondErr(req.ID, fmt.Errorf("挂载共享段失败: %w", err)) return @@ -1033,20 +1033,24 @@ func handleHandshake(req *rpcRequest) { shm = m } - // 事件环:子进程经 fd 4 挂载事件环段,从 fd 5 的 eventfd 感知新事件 + // 挂载事件环段 + 打开通知句柄(§3.6) if p.EvtRingSize > 0 { - er, err := syscall.Mmap(4, 0, p.EvtRingSize, - syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + er, err := attachEvtRingShm(p.EvtRingSize) if err != nil { respondErr(req.ID, fmt.Errorf("挂载事件环段失败: %w", err)) return } - if got := binary.LittleEndian.Uint32(er[evtOffMagic:evtOffMagic+4]); got != evtRingMagic { + if got := binary.LittleEndian.Uint32(er[evtOffMagic : evtOffMagic+4]); got != evtRingMagic { respondErr(req.ID, fmt.Errorf("事件环魔数不匹配(0x%x)", got)) return } + notifier, err := openEvtNotifier() + if err != nil { + respondErr(req.ID, fmt.Errorf("打开事件通知句柄失败: %w", err)) + return + } evtRingData = er - evtfd = os.NewFile(5, "eventfd") + evtNotifier = notifier go evtConsumerLoop() } @@ -1218,14 +1222,14 @@ var evtTypeNames = [evtTypeMax]string{ // evtConsumerLoop 是事件环消费主循环:eventfd.Read(阻塞走 netpoller)→ drain events → 分发。 // 每个插件进程启动一个 goroutine,与 RPC 主循环并行。 func evtConsumerLoop() { - if evtfd == nil || evtRingData == nil { + if evtNotifier == nil || evtRingData == nil { return } buf := make([]byte, 8) // eventfd uint64 计数 var readSeq uint64 for { - if _, err := evtfd.Read(buf); err != nil { + if err := evtNotifier.Wait(buf); err != nil { continue } // 循环 drain 直到无新事件(eventfd 计数合并,一次 Read 处理全部) @@ -1275,3 +1279,13 @@ func evtConsumerLoop() { } } } + +// evtWaiter 抽象事件通知等待。 +// +// Unix:eventfd(Linux)/ pipe(macOS)的读端,阻塞 Read 走 netpoller。 +// Windows:命名 Event 对象,WaitForSingleObject。 +// 平台实现在 z_proc_shm_unix.go / z_proc_shm_windows.go。 +type evtWaiter interface { + // Wait 阻塞直到有新事件;buf 供实现复用(Unix 读 8 字节计数)。 + Wait(buf []byte) error +} diff --git a/tools/plugindev/templates/proc_shm_unix.go.tmpl b/tools/plugindev/templates/proc_shm_unix.go.tmpl new file mode 100644 index 0000000..1976b10 --- /dev/null +++ b/tools/plugindev/templates/proc_shm_unix.go.tmpl @@ -0,0 +1,62 @@ +//go:build linux || darwin || freebsd + +package main + +import ( + "fmt" + "os" + "syscall" +) + +// Unix 侧共享段挂载:内核经 ExtraFiles 传入继承的 fd。 +// +// fd 布局(与内核 internal/plugin/proc/plugin.go 的 ExtraFiles 顺序一致): +// +// fd 3 = StageContext 段(memfd / 已 unlink 的临时文件) +// fd 4 = 事件环段 +// fd 5 = 事件通知(Linux eventfd / macOS pipe 读端) +// +// 继承的 fd 无需文件名,也不残留——这是选 memfd 而非 /dev/shm 的原因。 +const ( + fdStageShm = 3 + fdEvtRingShm = 4 + fdEvtNotifier = 5 +) + +// attachStageShm 挂载 StageContext 共享段。 +// +// 各进程 mmap 到不同虚拟地址,段内一律用相对偏移而非指针,故仍能正确解引用 +// (实验 2 已验证父子 mmap 基址不同时偏移解引用正确)。 +func attachStageShm(size int) ([]byte, error) { + return syscall.Mmap(fdStageShm, 0, size, + syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) +} + +// attachEvtRingShm 挂载事件环段。 +func attachEvtRingShm(size int) ([]byte, error) { + return syscall.Mmap(fdEvtRingShm, 0, size, + syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) +} + +// openEvtNotifier 打开事件通知读端。 +func openEvtNotifier() (evtWaiter, error) { + f := os.NewFile(fdEvtNotifier, "evtnotify") + if f == nil { + return nil, fmt.Errorf("fd %d 不是有效的通知句柄", fdEvtNotifier) + } + return &unixEvtWaiter{f: f}, nil +} + +// unixEvtWaiter 用 eventfd/pipe 的阻塞 Read 等待通知。 +// +// os.NewFile 把 fd 注册进 runtime netpoller,Read 阻塞时只 park goroutine, +// 不占 OS 线程(实验 1:200 个等待者仅增 1 个 OS 线程)。 +// 反面对照是经 cgo 调 sem_wait——那会阻塞整个 M。 +type unixEvtWaiter struct { + f *os.File +} + +func (w *unixEvtWaiter) Wait(buf []byte) error { + _, err := w.f.Read(buf) + return err +} diff --git a/tools/plugindev/templates/proc_shm_windows.go.tmpl b/tools/plugindev/templates/proc_shm_windows.go.tmpl new file mode 100644 index 0000000..eeef77e --- /dev/null +++ b/tools/plugindev/templates/proc_shm_windows.go.tmpl @@ -0,0 +1,153 @@ +//go:build windows + +package main + +import ( + "fmt" + "os" + "syscall" + "unsafe" +) + +// Windows 侧共享段挂载:走命名对象而非继承 fd。 +// +// 为何不能照抄 Unix:Windows 没有 fd 继承语义,`ExtraFiles` 在 os/exec 的 +// Windows 实现里不被支持。等价机制是命名内核对象——父进程用 +// CreateFileMapping / CreateEvent 建带名字的对象,子进程按同名 Open 拿到同一对象。 +// +// 名字经环境变量传入(内核 internal/plugin/proc/plugin_windows.go 设置), +// 而不是硬编码:多个 homed 实例并存时不能撞名。 +// +// **这是 §9.2 的正解**:C ABI 时代 Windows 是第三套独立 ABI 实现, +// stage 只下发 3 个字段且完全没有写回,sanitizer 这类改写型插件静默失效。 +// 现在 Windows 与 Unix 共用同一份 RPC 逻辑与同一份共享段布局, +// 差异被收敛到本文件的三个函数里。 +const ( + envStageShmName = "HOMEAGENT_SHM_STAGE" + envEvtRingName = "HOMEAGENT_SHM_EVTRING" + envEvtEventName = "HOMEAGENT_EVT_EVENT" +) + +// Windows API 绑定:用 LazyDLL 而非 golang.org/x/sys/windows。 +// +// 原因:OpenFileMappingW / OpenEventW 未被标准库 syscall 包导出。 +// 引入 x/sys 会给**每个插件的 go.mod 加一个新依赖**, +// 而「外部插件零改动」是本次迁移的硬约束(插件仅依赖公开 SDK)。 +// LazyDLL 属于标准库 syscall,零新增依赖。 +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procOpenFileMappingW = kernel32.NewProc("OpenFileMappingW") + procOpenEventW = kernel32.NewProc("OpenEventW") +) + +const ( + winEventModifyState = 0x0002 + winSynchronize = 0x00100000 +) + +// openFileMappingW 封装 OpenFileMappingW。 +func openFileMappingW(access uint32, inherit bool, name *uint16) (syscall.Handle, error) { + var inheritFlag uintptr + if inherit { + inheritFlag = 1 + } + r, _, err := procOpenFileMappingW.Call( + uintptr(access), inheritFlag, uintptr(unsafe.Pointer(name))) + if r == 0 { + return 0, err + } + return syscall.Handle(r), nil +} + +// openEventW 封装 OpenEventW。 +func openEventW(access uint32, inherit bool, name *uint16) (syscall.Handle, error) { + var inheritFlag uintptr + if inherit { + inheritFlag = 1 + } + r, _, err := procOpenEventW.Call( + uintptr(access), inheritFlag, uintptr(unsafe.Pointer(name))) + if r == 0 { + return 0, err + } + return syscall.Handle(r), nil +} + +// attachStageShm 按名字打开 StageContext 段并映射。 +func attachStageShm(size int) ([]byte, error) { + return openNamedMapping(os.Getenv(envStageShmName), size, "StageContext 段") +} + +// attachEvtRingShm 按名字打开事件环段并映射。 +func attachEvtRingShm(size int) ([]byte, error) { + return openNamedMapping(os.Getenv(envEvtRingName), size, "事件环段") +} + +// openNamedMapping 打开命名共享段并映射为 []byte。 +// +// 与 Unix 的 mmap 语义对齐:MapViewOfFile 返回的地址在本进程虚拟空间, +// 段内偏移仍是相对的,故跨进程解引用正确。 +func openNamedMapping(name string, size int, what string) ([]byte, error) { + if name == "" { + return nil, fmt.Errorf("%s 名字未经环境变量传入", what) + } + namePtr, err := syscall.UTF16PtrFromString(name) + if err != nil { + return nil, fmt.Errorf("%s 名字非法: %w", what, err) + } + + h, err := openFileMappingW(syscall.FILE_MAP_WRITE, false, namePtr) + if err != nil { + return nil, fmt.Errorf("打开 %s(%s): %w", what, name, err) + } + + addr, err := syscall.MapViewOfFile(h, syscall.FILE_MAP_WRITE, 0, 0, uintptr(size)) + if err != nil { + syscall.CloseHandle(h) + return nil, fmt.Errorf("映射 %s: %w", what, err) + } + // 句柄不关:视图存活期间必须保持句柄有效,进程退出时由 OS 回收。 + + return unsafe.Slice((*byte)(unsafe.Pointer(addr)), size), nil +} + +// openEvtNotifier 按名字打开事件通知对象。 +func openEvtNotifier() (evtWaiter, error) { + name := os.Getenv(envEvtEventName) + if name == "" { + return nil, fmt.Errorf("事件通知对象名字未经环境变量传入") + } + namePtr, err := syscall.UTF16PtrFromString(name) + if err != nil { + return nil, fmt.Errorf("事件对象名字非法: %w", err) + } + h, err := openEventW(winSynchronize|winEventModifyState, false, namePtr) + if err != nil { + return nil, fmt.Errorf("打开事件对象(%s): %w", name, err) + } + return &windowsEvtWaiter{h: h}, nil +} + +// windowsEvtWaiter 用命名 Event 对象等待通知。 +// +// 与 eventfd 的差异:Event 是二元信号而非计数器,多次 SetEvent 只对应 +// 一次唤醒。这不影响正确性——消费者被唤醒后按 readSeq 追 writeSeq +// 批量 drain,一次唤醒能处理累积的全部事件。 +// +// WaitForSingleObject 阻塞的是 OS 线程而非仅 goroutine,故不如 eventfd +// 的 netpoller 路径省线程。每插件一个消费 goroutine,17 插件即 17 线程, +// 在可接受范围(实验 5 实测 17 子进程共 84 线程)。 +type windowsEvtWaiter struct { + h syscall.Handle +} + +func (w *windowsEvtWaiter) Wait(buf []byte) error { + ev, err := syscall.WaitForSingleObject(w.h, syscall.INFINITE) + if err != nil { + return err + } + if ev != syscall.WAIT_OBJECT_0 { + return fmt.Errorf("等待事件对象返回 0x%x", ev) + } + return nil +}