// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // FRB (Five Runtime Binary) — dynamic module loading. // // Unlike Harbour's HRB (pcode interpreter), Five FRB compiles PRG to native // Go shared library (.so/.dll) for full native speed execution. // // FRB file format: // Magic: 0xC0 'F' 'R' 'B' (4 bytes) // Version: uint16 LE (2 bytes) — currently 1 // Flags: uint16 LE (2 bytes) // SymCount: uint32 LE (4 bytes) // Symbols: []{Name: null-terminated, Scope: byte} // SharedLib: remaining bytes = embedded .so/.dll binary // // Usage from PRG: // pMod := FrbLoad("mymodule.frb") // FrbDo(pMod, "MYFUNC", arg1, arg2) // xResult := FrbDo(pMod, "CALCULATE", 42) // FrbUnload(pMod) package hbrt import ( "encoding/binary" "fmt" "os" "os/exec" "path/filepath" "plugin" "runtime" "strings" ) // FRB magic bytes var frbMagic = []byte{0xC0, 'F', 'R', 'B'} const frbVersion = 2 // FRB file format constants const ( FrbMagic0 byte = 0xC0 // magic byte 0 FrbMagic1 byte = 'F' // magic byte 1 FrbMagic2 byte = 'R' // magic byte 2 FrbMagic3 byte = 'B' // magic byte 3 FrbVersion1 byte = 1 // format version FrbModeNative byte = 0x01 // Go plugin (.so) FrbModePcode byte = 0x02 // Five pcode (interpreter) FrbHeaderSize = 12 // header bytes before payload ) // FrbModule represents a loaded FRB module. // FRB binding modes (how module symbols interact with VM globals) const ( FrbBindDefault = 0 // Module-local; only accessible via FrbDo() FrbBindOverload = 1 // Overwrite existing VM symbols FrbBindExport = 2 // Register in VM but don't overwrite existing ) type FrbModule struct { Name string LocalSyms map[string]*Symbol // module-scoped symbols (isolated) Plugin *plugin.Plugin // Go plugin handle (native mode) TempDir string // temp dir for extracted .so BindMode int // how symbols are registered VM *VM // owning VM Registered []string // names registered in VM (for unload cleanup) OldSyms map[string]*Symbol // previous symbols overwritten (for restore) } // FindFunc looks up a function in this module's local scope. // Main() is always module-local and never leaks to the host VM. func (m *FrbModule) FindFunc(name string) func(*Thread) { if m.LocalSyms != nil { if sym, ok := m.LocalSyms[name]; ok && sym.Func != nil { return sym.Func } } return nil } // FrbBuild compiles a PRG file to FRB format. // Steps: PRG → gengo → Go source → go build -buildmode=plugin → FRB func FrbBuild(prgFile, outputFile string, fiveExe string) error { if runtime.GOOS == "windows" { return fmt.Errorf("FRB plugins not supported on Windows (Go plugin limitation)") } // 1. Generate Go source tmpDir, err := os.MkdirTemp("", "frb-build-*") if err != nil { return err } defer os.RemoveAll(tmpDir) // Run five gen to produce Go source genCmd := exec.Command(fiveExe, "gen", prgFile) goSrc, err := genCmd.Output() if err != nil { return fmt.Errorf("gen failed: %w", err) } // Modify source: change package main → package main (plugin compatible) goSrcStr := string(goSrc) goSrcStr = strings.Replace(goSrcStr, "func main() {", "// FRB module — no main()", 1) // Add plugin exports goSrcStr += ` // FRB plugin exports var FRB_Symbols = symbols ` goFile := filepath.Join(tmpDir, "frb_module.go") if err := os.WriteFile(goFile, []byte(goSrcStr), 0644); err != nil { return err } // 2. Build as Go plugin soFile := filepath.Join(tmpDir, "module.so") buildCmd := exec.Command("go", "build", "-buildmode=plugin", "-o", soFile, goFile) buildCmd.Dir = tmpDir if output, err := buildCmd.CombinedOutput(); err != nil { return fmt.Errorf("build failed: %s\n%w", string(output), err) } // 3. Package as FRB soData, err := os.ReadFile(soFile) if err != nil { return err } f, err := os.Create(outputFile) if err != nil { return err } defer f.Close() // Write header f.Write(frbMagic) binary.Write(f, binary.LittleEndian, uint16(frbVersion)) binary.Write(f, binary.LittleEndian, uint16(0)) // flags // Write symbol count (placeholder — extracted from Go source) binary.Write(f, binary.LittleEndian, uint32(0)) // Write embedded .so f.Write(soData) return nil } // FrbLoad loads an FRB module from file. func FrbLoad(vm *VM, filename string) (*FrbModule, error) { data, err := os.ReadFile(filename) if err != nil { return nil, err } // Validate magic if len(data) < 12 || data[0] != 0xC0 || data[1] != 'F' || data[2] != 'R' || data[3] != 'B' { return nil, fmt.Errorf("invalid FRB file: bad magic") } version := binary.LittleEndian.Uint16(data[4:6]) _ = version mode := data[6] // flags byte 1 = mode // Pcode mode — use interpreter, no Go needed if mode == FrbModePcode { return frbLoadPcode(vm, data[12:], filename) } // Native mode — load Go plugin soData := data[12:] // Extract .so to temp file tmpDir, err := os.MkdirTemp("", "frb-load-*") if err != nil { return nil, err } soFile := filepath.Join(tmpDir, "module.so") if err := os.WriteFile(soFile, soData, 0755); err != nil { os.RemoveAll(tmpDir) return nil, err } // Snapshot current symbols before loading oldSymNames := vm.SymbolNames() // Load as Go plugin — init() auto-registers symbols via RegisterLibModule p, err := plugin.Open(soFile) if err != nil { os.RemoveAll(tmpDir) return nil, fmt.Errorf("plugin load failed: %w", err) } // Register any lib modules that were added by the plugin's init() vm.RegisterLibModules() // Determine which symbols were added by the plugin frbMod := &FrbModule{ Name: filename, LocalSyms: make(map[string]*Symbol), OldSyms: make(map[string]*Symbol), Plugin: p, TempDir: tmpDir, VM: vm, } newSymNames := vm.SymbolNames() for _, name := range newSymNames { if !containsStr(oldSymNames, name) { sym := vm.FindSymbol(name) if sym != nil { frbMod.LocalSyms[name] = sym frbMod.Registered = append(frbMod.Registered, name) } } } return frbMod, nil } // frbLoadPcode loads a pcode-mode FRB. func frbLoadPcode(vm *VM, data []byte, filename string) (*FrbModule, error) { pcMod, err := DeserializePcodeModule(data) if err != nil { return nil, fmt.Errorf("pcode parse failed: %w", err) } frbMod := &FrbModule{ Name: filename, LocalSyms: make(map[string]*Symbol), OldSyms: make(map[string]*Symbol), BindMode: FrbBindDefault, VM: vm, } // Build module-local symbols for name, fn := range pcMod.Funcs { pcFn := fn pcModRef := pcMod goFunc := func(t *Thread) { ExecPcode(t, pcFn, pcModRef) } frbMod.LocalSyms[name] = &Symbol{ Name: name, Scope: FsPublic | FsLocal, Func: goFunc, } } // Register non-Main symbols in VM (save old for restore on unload) for name, sym := range frbMod.LocalSyms { if name == "MAIN" { continue // Main is always module-local } old := vm.FindSymbol(name) if old != nil { // Default mode: don't overwrite existing host functions // Module function accessible via FrbDo() only frbMod.OldSyms[name] = old continue } // New symbol — register globally vm.RegisterSymbol(sym) frbMod.Registered = append(frbMod.Registered, name) } return frbMod, nil } // FrbUnload unloads an FRB module. // Removes registered symbols from VM and restores any overwritten ones. func FrbUnload(mod *FrbModule) { if mod == nil { return } // Restore VM symbols if mod.VM != nil { for _, name := range mod.Registered { if old, exists := mod.OldSyms[name]; exists { // Restore previous symbol mod.VM.RegisterSymbol(old) } else { // Remove symbol that didn't exist before mod.VM.UnregisterSymbol(name) } } } // Clean up temp files if mod.TempDir != "" { os.RemoveAll(mod.TempDir) } // Clear references mod.LocalSyms = nil mod.OldSyms = nil mod.Registered = nil } func containsStr(slice []string, s string) bool { for _, v := range slice { if v == s { return true } } return false }