mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-03 23:54:12 +00:00
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:
@ -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
|
||||
}
|
||||
|
||||
62
tools/plugindev/templates/proc_shm_unix.go.tmpl
Normal file
62
tools/plugindev/templates/proc_shm_unix.go.tmpl
Normal 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 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
|
||||
}
|
||||
153
tools/plugindev/templates/proc_shm_windows.go.tmpl
Normal file
153
tools/plugindev/templates/proc_shm_windows.go.tmpl
Normal file
@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user