WASM Plugin——沙箱化第三方插件运行时¶
插件系统的终极愿景是让第三方开发者能安全地贡献功能。Go 插件(
plugin.Open)绑定宿主 Go 版本且无安全隔离,gRPC 插件有网络延迟和部署复杂度。WASM 插件通过 wazero v1.11.0 提供纯 Go 实现的沙箱运行时——无 CGo、无系统调用、资源可控。
问题背景¶
现有插件系统的局限性:
| 方案 | 隔离性 | 部署复杂度 | 语言限制 | 性能 |
|---|---|---|---|---|
Go plugin (plugin.Open) |
无(同一进程) | 低(.so 文件) | 仅 Go | 高(直接调用) |
| gRPC 微服务 | 强(独立进程) | 高(部署 + 注册) | 不限 | 中(网络开销) |
| 内置 Descriptor | 框架级别 | 低(代码内) | 仅 Go | 最高 |
WASM 插件的定位:在隔离性和便捷性之间取得平衡——无需独立部署,但提供内存安全、资源限制的沙箱执行环境。
核心设计¶
架构概览¶
┌──────────────────────────────────────────────┐
│ Host Process │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Runtime │ │ Manager │ │ Bridge │ │
│ │ (wazero) │ │ │ │ │ │
│ │ ├─ModuleA│ │ Load │ │ WASM │ │
│ │ ├─ModuleB│ │ Call │ │ reg. │ │
│ │ └─ModuleC│ │ Close │ │ →Engine │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ Sandbox │ │
│ │ TokenBucket│ │
│ │ MemLimit │ │
│ └─────────────┘ │
└──────────────────────────────────────────────┘
WASM ABI¶
数据通道:WASM 线性内存
序列化:JSON
方向:Host → Guest(调用)/ Guest → Host(宿主函数)
导出函数¶
// 每个 WASM 模块必须导出
// plugin_init: 初始化函数,返回 JSON 格式的初始化结果
// plugin_handle: 事件处理函数,接收 JSON 格式的事件,返回 JSON 格式的响应
// malloc: 内存分配函数,供 Host 在 Guest 线性内存中分配空间
宿主函数(remilia_host 模块)¶
// 通过 remilia_host 模块暴露给 WASM
"remilia_host": map[string]interface{}{
"log": func(level, msg uint32) { /* level=0debug,1info,2warn,3error */ },
"get_config": func(key, defaultVal uint32) uint32,
"register_command": func(pattern uint32) uint32,
"get_sender": func() uint32,
"reply_message": func(content uint32) uint32,
"get_event": func() uint32,
"now": func() uint64,
}
所有字符串参数通过线性内存传递:Guest 分配内存 → Host 写入 → 函数签名使用内存偏移量。
Runtime 核心流程¶
type Runtime struct {
wasmCtx context.Context
moduleCache map[string]wazero.CompiledModule
l wazero.Runtime
config RuntimeConfig
}
func NewRuntime(ctx context.Context, config RuntimeConfig) (*Runtime, error) {
runtime := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().
WithCloseOnContextDone(true))
return &Runtime{
wasmCtx: ctx,
moduleCache: make(map[string]wazero.CompiledModule),
l: runtime,
config: config,
}, nil
}
func (r *Runtime) LoadModule(name string, wasmBytes []byte) (*Module, error) {
compiled, err := r.l.CompileModule(r.wasmCtx, wasmBytes)
if err != nil {
return nil, err
}
r.moduleCache[name] = compiled
return newModule(r.wasmCtx, r.l, compiled, r.config), nil
}
func (m *Module) CallInit(configJSON string) ([]byte, error) {
// 将 configJSON 写入线性内存
inputPtr := m.malloc(len(configJSON))
m.writeMemory(inputPtr, []byte(configJSON))
// 调用 plugin_init
results, err := m.instance.ExportedFunction("plugin_init").Call(m.ctx, inputPtr, uint64(len(configJSON)))
if err != nil {
return nil, err
}
return m.readMemory(uint32(results[0])), nil
}
func (m *Module) CallHandle(eventJSON []byte) ([]byte, error) {
inputPtr := m.malloc(len(eventJSON))
m.writeMemory(inputPtr, eventJSON)
results, err := m.instance.ExportedFunction("plugin_handle").Call(m.ctx, inputPtr, uint64(len(eventJSON)))
if err != nil {
return nil, err
}
return m.readMemory(uint32(results[0])), nil
}
func (m *Module) Close() error {
return m.instance.Close(m.ctx)
}
Bridge:WASM → Engine 适配¶
type Bridge struct {
engine *engine.Engine
modules map[string]*Module
}
func (b *Bridge) handleRegistration(moduleName string, reg WASMRegistration) {
matcher := &Matcher{
Type: reg.EventType,
Handler: func(ctx *Context) error {
module := b.modules[moduleName]
eventJSON := marshalEvent(ctx)
result, err := module.CallHandle(eventJSON)
if err != nil {
return err
}
return unmarshalResult(ctx, result)
},
}
b.engine.RegisterMatcher(matcher)
}
WASM 插件在 CallInit 返回注册请求(命令、事件类型等),Bridge 将这些转换为 Engine Matcher,之后事件到来时由 Bridge 转发到对应 WASM Module。
沙箱(Sandbox)¶
type ResourceLimit struct {
MaxMemory uint64 // WASM 线性内存上限(默认 10MB)
MaxCPU time.Duration // 单次调用 CPU 时间上限
MaxRate float64 // 每秒最大调用次数
MaxBurst int // 令牌桶容量
}
type Sandbox struct {
module *Module
limiter *rate.Limiter // 令牌桶限流
memLimit uint64
}
TokenBucket 限流:
func (s *Sandbox) Allow() bool {
return s.limiter.Allow()
}
不通过则拒绝调用,返回限流错误,避免 WASM 插件过度消耗宿主资源。
WASMDescriptor¶
type WASMDescriptor struct {
Name string
Version string
Path string // .wasm 文件路径或 embed
Config map[string]any
ResourceLimit ResourceLimit
}
Manager 独立管理,不修改 plugin.Manager:
type Manager struct {
runtime *Runtime
bridge *Bridge
modules map[string]*Module
sandboxes map[string]*Sandbox
mu sync.RWMutex
}
用法示例¶
wasmManager := wasm.NewManager(wasm.RuntimeConfig{
CacheDir: "./wasm-cache",
})
desc := &wasm.WASMDescriptor{
Name: "hello-plugin",
Version: "1.0.0",
Path: "./plugins/hello/hello.wasm",
Config: map[string]any{
"greeting": "你好",
},
ResourceLimit: wasm.ResourceLimit{
MaxMemory: 10 * 1024 * 1024, // 10MB
MaxRate: 50, // 每秒 50 次调用
MaxBurst: 10,
},
}
if err := wasmManager.Load(desc); err != nil {
log.Fatal(err)
}
defer wasmManager.Close()
文件清单¶
plugin/wasm/
├── runtime.go # Runtime: wazero 封装,LoadModule/CallInit/CallHandle/Close
├── module.go # Module: 单个 WASM 实例,线性内存读写
├── bridge.go # Bridge: WASM 注册 → Engine Matcher 转换
├── sandbox.go # Sandbox: 令牌桶限流 + 内存限制
├── manager.go # Manager: 独立管理 Load/Unload/List
├── descriptor.go # WASMDescriptor 定义
├── host.go # remilia_host 模块注册
├── abi.go # ABI 常量和序列化工具
└── errors.go # 错误类型
依赖¶
core/engine:Bridge 将 WASM 注册请求转为 Engine Matcherrouter(可选):Router 可将 WASM 路由加入规则链- 外部依赖:
github.com/tetratelabs/wazero v1.11.0
设计权衡¶
| 方面 | 选择 | 理由 |
|---|---|---|
| 运行时 | wazero(纯 Go,无 CGo) | 无平台依赖,无系统调用 => 天然沙箱 |
| ABI | 线性内存 + JSON | 简单成熟,所有语言都能编译到 WASM 的目标都支持 |
| 模块注册入口 | plugin_init 返回注册描述 |
无需在宿主硬编码注册逻辑,WASM 自描述 |
| 限流 | 令牌桶 | 平滑突发,实现简单 |
| 与 plugin.Manager 关系 | 独立 Manager,不修改 | 保持已有系统稳定,避免耦合爆炸 |