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() {