plugindev: entry 语义收敛 + 删 C ABI 工具链 + Windows 共享内存适配(Part 6.1)

## 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
This commit is contained in:
JianFeeeee
2026-09-02 18:39:02 +08:00
parent ef0e58ee23
commit 9f844123fe
8 changed files with 439 additions and 1071 deletions

View File

@ -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.2Windows 不再是能力退化的第三套实现)。
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 configproc 模式由 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() {

View File

@ -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.

View File

@ -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
//
// 为何忽略 entry17 个存量插件的 plg.json 都写着 "plugin.so"。若把 entry 当作
// 通道开关,迁移就得改 17 个文件——而「外部插件零改动」是本次迁移的硬约束。
// entry 现在只用于区分 Luamain.lua与 Go 插件。

View File

@ -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 cfgLua 不经 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 常量")
}
}

View File

@ -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 <stdlib.h>
*/
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+minorv0.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 <stdlib.h>
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 <stdlib.h>
#include <string.h>
// HOMEAGENT_ABI_VERSION 与 sdk/meta/meta.go CABINum 同步major*100+minorv0.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
// ============================================================

View File

@ -19,7 +19,6 @@ import (
"log"
"os"
"sync"
"syscall"
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
@ -96,7 +95,7 @@ var (
// 事件环§3.6fd 4 = 事件环段 mmapfd 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 用继承的 fdWindows 用命名段),
// 由 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 抽象事件通知等待。
//
// UnixeventfdLinux/ pipemacOS的读端阻塞 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
}

View File

@ -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 netpollerRead 阻塞时只 park goroutine
// 不占 OS 线程(实验 1200 个等待者仅增 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
}

View File

@ -0,0 +1,153 @@
//go:build windows
package main
import (
"fmt"
"os"
"syscall"
"unsafe"
)
// Windows 侧共享段挂载:走命名对象而非继承 fd。
//
// 为何不能照抄 UnixWindows 没有 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 路径省线程。每插件一个消费 goroutine17 插件即 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
}