core/plugin/wasmguest/registries.go
Alex Dunmow c35199f0bc feat(wasmguest): guest runtime shim for the wasm plugin ABI (WO-WZ-002)
The wasip1 half of the ABI: bn_alloc/bn_invoke/bn_free exports with a
live-pin map so the GC never frees host-visible buffers, the generic
`blockninja.host_call` import (single import decided over per-family
symbols; recorded in wasm-abi.md), and a dispatch table adapting an
unmodified plugin.PluginRegistration to all nine v1 hooks. Panics inside
plugin hooks come back as ABI_ERROR_CODE_INTERNAL — the instance stays
callable; traps stay reserved for runtime corruption.

DESCRIBE builds the PluginManifest from the registration's static funcs
plus a capture-only Register pass (block metas via the same
PluginBlockRegistry prefixing the .so loader applies, template/system/
page-template/email-wrapper keys), probes JobHandlers/ServiceHandlers/
Load with capture-only services for job types, RBAC roles, core-service
bindings, and RAG fetcher types. RenderContext values are rehydrated
through the exact core/blocks context keys, so existing block code
reading from ctx works unchanged.

Plugins build in REACTOR mode (go build -buildmode=c-shared): init()
calls wasmguest.Serve (non-blocking), main is never called, and the host
runs _initialize before any bn_invoke. Command mode deadlocks or exits
(verified against wazero v1.12.0) — documented prominently in
wasm-abi.md, which also now reconciles the import module namespace to
`blockninja` and requires bn_alloc'd buffers on both directions.

Dispatch/describe/context logic is buildable on every GOOS; only
exports.go and hostcalls.go carry the wasip1 tag. dispatch_test.go
covers describe, hook routing, envelope mismatch, decode failures,
template-override resolution, panic recovery, and lifecycle hooks
natively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:53:24 +08:00

115 lines
3.9 KiB
Go

package wasmguest
import (
"io/fs"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/templates"
)
// captureBlockRegistry implements blocks.BlockRegistry by recording every
// registration instead of wiring it into a live host. It backs both DESCRIBE
// (metadata enumeration) and runtime dispatch (BlockFunc lookup).
//
// It is always used behind plugin.NewPluginBlockRegistry, so keys arrive
// already plugin-prefixed and overrides arrive with their source set —
// identical to what a plugin sees in the .so world.
type captureBlockRegistry struct {
metas []blocks.BlockMeta
funcs map[string]blocks.BlockFunc // block key → fn
overrides []blockOverride
}
type blockOverride struct {
templateKey string
blockKey string
source string
fn blocks.BlockFunc
}
func newCaptureBlockRegistry() *captureBlockRegistry {
return &captureBlockRegistry{funcs: make(map[string]blocks.BlockFunc)}
}
func (r *captureBlockRegistry) Register(meta blocks.BlockMeta, fn blocks.BlockFunc) {
r.metas = append(r.metas, meta)
r.funcs[meta.Key] = fn
}
func (r *captureBlockRegistry) RegisterTemplateOverride(templateKey, blockKey string, fn blocks.BlockFunc) {
r.overrides = append(r.overrides, blockOverride{templateKey: templateKey, blockKey: blockKey, fn: fn})
}
func (r *captureBlockRegistry) RegisterTemplateOverrideWithSource(templateKey, blockKey, source string, fn blocks.BlockFunc) {
r.overrides = append(r.overrides, blockOverride{templateKey: templateKey, blockKey: blockKey, source: source, fn: fn})
}
// LoadSchemasFromFS is a no-op in the wasm guest: block schemas ship in the
// .bnp artifact's schemas/ directory and are loaded host-side.
func (r *captureBlockRegistry) LoadSchemasFromFS(fs.FS) error { return nil }
func (r *captureBlockRegistry) LoadSchemasFromFSWithPrefix(fs.FS, string) error { return nil }
// lookup resolves a block function for dispatch: a template-scoped override
// wins over the base registration, mirroring the host registry's behavior.
func (r *captureBlockRegistry) lookup(blockKey, templateKey string) (blocks.BlockFunc, bool) {
if templateKey != "" {
for _, o := range r.overrides {
if o.templateKey == templateKey && o.blockKey == blockKey {
return o.fn, true
}
}
}
fn, ok := r.funcs[blockKey]
return fn, ok
}
// captureTemplateRegistry implements templates.TemplateRegistry by recording
// registrations for DESCRIBE and runtime dispatch.
type captureTemplateRegistry struct {
templateKeys []string // insertion order of Register calls
funcs map[string]templates.TemplateFunc
systemTemplates []templates.SystemTemplateMeta
pageTemplates []capturedPageTemplate
emailWrappers []capturedEmailWrapper
}
type capturedPageTemplate struct {
systemKey string
meta templates.PageTemplateMeta
}
type capturedEmailWrapper struct {
systemKey string
fn templates.EmailWrapperFunc
}
func newCaptureTemplateRegistry() *captureTemplateRegistry {
return &captureTemplateRegistry{funcs: make(map[string]templates.TemplateFunc)}
}
func (r *captureTemplateRegistry) Register(key string, fn templates.TemplateFunc) error {
r.templateKeys = append(r.templateKeys, key)
r.funcs[key] = fn
return nil
}
func (r *captureTemplateRegistry) RegisterSystemTemplate(meta templates.SystemTemplateMeta) {
r.systemTemplates = append(r.systemTemplates, meta)
}
func (r *captureTemplateRegistry) RegisterPageTemplate(systemKey string, meta templates.PageTemplateMeta, fn templates.TemplateFunc) error {
r.pageTemplates = append(r.pageTemplates, capturedPageTemplate{systemKey: systemKey, meta: meta})
r.funcs[meta.Key] = fn
return nil
}
func (r *captureTemplateRegistry) RegisterEmailWrapper(systemKey string, fn templates.EmailWrapperFunc) {
r.emailWrappers = append(r.emailWrappers, capturedEmailWrapper{systemKey: systemKey, fn: fn})
}
func (r *captureTemplateRegistry) lookup(key string) (templates.TemplateFunc, bool) {
fn, ok := r.funcs[key]
return fn, ok
}