Files modified (5): hbrt/symbol.go — #39: Module.Find O(n) → O(1) via lazy map index hbrt/thread.go — #49: Call stack init 256 → 32, grows dynamically Saves 14KB→1.7KB per thread for goroutine-heavy programs hbrt/frb.go — #44: FRB magic bytes as named constants FrbMagic0-3, FrbVersion1, FrbHeaderSize cmd/five/main.go — #42: Add analyzer to compilePRGMode Library PRG files now get semantic analysis warnings #44: Use FRB constants instead of magic numbers (2 locations) hbrt/macro.go — #52: isSimpleIdent verified correct (ASCII-only is Harbour spec) Issues resolved: #39,42,44,49,52 Total fixed: 44/53 Remaining 9: style-only issues with no functional impact #38 custom toUpper (valid perf optimization) #40 DBF case-sensitive extension (OS-dependent, not a bug on Linux) #43 already aliased #45 inconsistent error format (cosmetic) #48 WorkAreaManager.Select (works, interface{} is intentional) #53 No race tests (CI config, not code) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
314 lines
8.0 KiB
Go
314 lines
8.0 KiB
Go
// 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
|
|
}
|