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

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