Files
five/hbrt/frb.go
Charles KWON OhJun 8da77b623a fix: Phase 6 — LOW #39,42,44,49,52 final cleanup
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>
2026-04-01 21:11:08 +09:00

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
}