- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
378 lines
12 KiB
Markdown
378 lines
12 KiB
Markdown
# FRB — Five Runtime Binary
|
|
|
|
> Why Five uses FRB instead of Harbour's HRB
|
|
|
|
Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com). All rights reserved.
|
|
|
|
## Overview
|
|
|
|
FRB (Five Runtime Binary) is Five's dynamic module format for loading and executing
|
|
compiled PRG code at runtime. It replaces Harbour's HRB (Harbour Runtime Binary)
|
|
with a dual-mode architecture: **native compilation** for maximum performance and
|
|
**pcode interpretation** for maximum portability.
|
|
|
|
## The Problem with HRB
|
|
|
|
Harbour's HRB format stores pcode bytecode — an intermediate representation that
|
|
must be interpreted by the Harbour Virtual Machine at runtime:
|
|
|
|
```
|
|
PRG Source → Harbour Compiler → HRB (pcode bytecode) → VM Interpreter → Execution
|
|
```
|
|
|
|
This architecture has inherent limitations:
|
|
|
|
1. **Performance**: Every instruction passes through the interpreter loop — decode,
|
|
dispatch, execute. For compute-heavy code, this overhead is significant.
|
|
2. **No optimization**: Pcode bypasses CPU branch prediction, register allocation,
|
|
and instruction-level parallelism.
|
|
3. **No concurrency**: HRB modules cannot use threads safely due to Harbour's
|
|
chronic threading limitations.
|
|
4. **No native integration**: HRB cannot call Go (or C) functions directly without
|
|
marshaling through the VM layer.
|
|
|
|
## The FRB Solution
|
|
|
|
Five's FRB provides **two execution modes** in a single format:
|
|
|
|
### Mode 1: Native (Go Plugin)
|
|
|
|
```
|
|
PRG Source → Five Compiler → Go Source → Go Compiler → Native Plugin (.so)
|
|
↓
|
|
FRB Container (4.7 MB)
|
|
```
|
|
|
|
The `.frb` file contains a compiled Go shared library. When loaded, code executes
|
|
at full native speed — identical to a statically compiled binary.
|
|
|
|
### Mode 2: Pcode (Interpreter)
|
|
|
|
```
|
|
PRG Source → Five Compiler → Pcode Bytecode → FRB Container (175 bytes)
|
|
↓
|
|
Five Pcode Interpreter
|
|
```
|
|
|
|
The `.frb` file contains compact bytecode. No Go compiler needed on the target
|
|
machine. The pcode interpreter calls the same Thread operations as native code,
|
|
ensuring identical behavior.
|
|
|
|
## Dual-Mode Architecture
|
|
|
|
The key insight: **the same PRG source compiles to both modes**. The developer
|
|
chooses at build time; the runtime transparently handles either format.
|
|
|
|
```bash
|
|
# Native mode — maximum performance (requires Go on build machine only)
|
|
five frb module.prg -o module.frb
|
|
|
|
# Pcode mode — maximum portability (runs anywhere Five runs)
|
|
five frb module.prg -o module.frb --pcode
|
|
```
|
|
|
|
Both produce valid `.frb` files. `FrbLoad()` detects the mode automatically.
|
|
|
|
## Architecture Comparison
|
|
|
|
| Aspect | HRB (Harbour) | FRB Native | FRB Pcode |
|
|
|--------|---------------|------------|-----------|
|
|
| **Content** | Harbour pcode | Go native .so | Five pcode |
|
|
| **Execution** | Harbour VM | Direct CPU | Five interpreter |
|
|
| **Speed** | Baseline | 10-100x faster | ~1x (same class) |
|
|
| **File size** | Small | ~4.7 MB | **175 bytes** |
|
|
| **Go needed (build)** | N/A | Yes | Yes (five CLI) |
|
|
| **Go needed (run)** | No | No | **No** |
|
|
| **Platform (run)** | All Harbour | Linux | **All Five** |
|
|
| **Goroutines** | No | Yes | Yes |
|
|
| **Go interop** | No | Native | Via RTL |
|
|
|
|
## File Format
|
|
|
|
```
|
|
Offset Size Field Description
|
|
0 4 Magic 0xC0 'F' 'R' 'B'
|
|
4 2 Version uint16 LE (currently 2)
|
|
6 1 Mode 0x01 = Native, 0x02 = Pcode
|
|
7 1 Reserved 0x00
|
|
8 4 SymCount uint32 LE (number of functions)
|
|
12 ... Payload Mode-dependent content
|
|
```
|
|
|
|
**Native payload**: Embedded Go plugin binary (ELF .so)
|
|
|
|
**Pcode payload**: Serialized function table:
|
|
```
|
|
uint16 funcCount
|
|
For each function:
|
|
uint16 nameLen + name (null-free)
|
|
uint16 params
|
|
uint16 locals
|
|
uint32 codeLen + bytecode
|
|
```
|
|
|
|
The FRB header is deliberately similar to HRB's `0xC0 'H' 'R' 'B'` for familiarity,
|
|
with `'F'` replacing `'H'` to indicate the Five format.
|
|
|
|
## Pcode Instruction Set
|
|
|
|
Five's pcode maps 1:1 to Thread stack operations, making the bytecode a direct
|
|
serialization of what the native compiler generates as Go function calls:
|
|
|
|
| Opcode | Hex | Description |
|
|
|--------|-----|-------------|
|
|
| PcOpPushNil | 0x01 | Push NIL |
|
|
| PcOpPushInt | 0x04 | Push int64 (8 bytes LE) |
|
|
| PcOpPushString | 0x06 | Push string (uint16 len + bytes) |
|
|
| PcOpPushLocal | 0x07 | Push local variable |
|
|
| PcOpPopLocal | 0x08 | Pop to local variable |
|
|
| PcOpPlus | 0x10 | Add top two stack values |
|
|
| PcOpEqual | 0x20 | Compare equality |
|
|
| PcOpJumpFalse | 0x31 | Conditional jump |
|
|
| PcOpPushSymbol | 0x40 | Push function symbol by name |
|
|
| PcOpFunction | 0x42 | Call function with N args |
|
|
| PcOpReturn | 0x33 | Return from function |
|
|
|
|
Full opcode set: 40+ opcodes covering arithmetic, comparison, logic, flow control,
|
|
function calls, OOP, arrays, and blocks.
|
|
|
|
## API Reference
|
|
|
|
### Command Line
|
|
|
|
```bash
|
|
# Build native FRB (maximum speed)
|
|
five frb mymodule.prg -o mylib.frb
|
|
|
|
# Build pcode FRB (maximum portability, no Go needed to run)
|
|
five frb mymodule.prg -o mylib.frb --pcode
|
|
```
|
|
|
|
### File-Based Loading
|
|
|
|
```harbour
|
|
// Load FRB module (auto-detects native vs pcode)
|
|
pMod := FrbLoad("mylib.frb")
|
|
|
|
// Call functions from loaded module
|
|
result := FrbDo(pMod, "MYFUNC", arg1, arg2)
|
|
|
|
// Unload when done
|
|
FrbUnload(pMod)
|
|
```
|
|
|
|
### In-Memory Compilation
|
|
|
|
```harbour
|
|
// Compile PRG source string at runtime
|
|
// Falls back to pcode mode automatically if Go is not installed
|
|
cSource := 'FUNCTION Double(n)' + Chr(10) + ;
|
|
' RETURN n * 2'
|
|
|
|
pMod := FrbCompile(cSource)
|
|
? FrbDo(pMod, "DOUBLE", 21) // → 42
|
|
FrbUnload(pMod)
|
|
```
|
|
|
|
### One-Shot Execution
|
|
|
|
```harbour
|
|
// Compile + run Main() + unload in one call
|
|
cProgram := 'FUNCTION Main()' + Chr(10) + ;
|
|
' RETURN 6 * 7'
|
|
|
|
? FrbExec(cProgram) // → 42
|
|
```
|
|
|
|
## Function Reference
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `FrbLoad(cFile)` | Load .frb file (native or pcode), return module handle |
|
|
| `FrbDo(pMod, cFunc, ...)` | Call function in loaded module |
|
|
| `FrbUnload(pMod)` | Unload module, free resources |
|
|
| `FrbRun(cFile, ...)` | Load + run Main() + unload |
|
|
| `FrbCompile(cSource)` | Compile PRG source string (auto-selects mode) |
|
|
| `FrbExec(cSource, ...)` | Compile + run Main() + unload |
|
|
|
|
## Use Cases
|
|
|
|
### Plugin Architecture
|
|
|
|
```harbour
|
|
// Application loads plugins at startup
|
|
LOCAL aPlugins := Directory("plugins/*.frb")
|
|
FOR EACH cFile IN aPlugins
|
|
LOCAL pPlugin := FrbLoad("plugins/" + cFile[1])
|
|
FrbDo(pPlugin, "INIT")
|
|
NEXT
|
|
```
|
|
|
|
### Hot Code Reload
|
|
|
|
```harbour
|
|
// Read PRG source from database or network
|
|
cSource := MemoRead("custom_report.prg")
|
|
pMod := FrbCompile(cSource)
|
|
FrbDo(pMod, "GENERATEREPORT", dStart, dEnd)
|
|
FrbUnload(pMod)
|
|
```
|
|
|
|
### User-Defined Business Rules
|
|
|
|
```harbour
|
|
// Store business rules as PRG text in database
|
|
cRule := GetRuleFromDB("DISCOUNT_CALC")
|
|
pRule := FrbCompile(cRule)
|
|
nDiscount := FrbDo(pRule, "CALCULATE", nAmount, cCustomerType)
|
|
FrbUnload(pRule)
|
|
```
|
|
|
|
### Dynamic Code with Goroutines
|
|
|
|
```harbour
|
|
// Compile a worker function at runtime, run it in a goroutine
|
|
cWorker := 'FUNCTION Worker(ch, n)' + Chr(10) + ;
|
|
' ChSend(ch, n * n)' + Chr(10) + ;
|
|
' RETURN NIL'
|
|
FrbCompile(cWorker)
|
|
ch := Channel(1)
|
|
Go("WORKER", ch, 42)
|
|
? ChReceive(ch) // → 1764
|
|
```
|
|
|
|
## Deployment Strategy
|
|
|
|
| Scenario | Recommended Mode | Reason |
|
|
|----------|-----------------|--------|
|
|
| Performance-critical server | Native | Maximum speed |
|
|
| End-user distribution | **Pcode** | No Go dependency |
|
|
| Development / testing | Native | Faster iteration |
|
|
| Cross-platform plugins | **Pcode** | Works everywhere |
|
|
| Embedded business rules | **Pcode** | Tiny file size |
|
|
| Compute-heavy algorithms | Native | CPU-bound benefit |
|
|
|
|
### Recommended Workflow
|
|
|
|
1. **Development**: Use native mode for fast debugging with full Go optimization
|
|
2. **Distribution**: Ship pcode `.frb` files alongside the compiled Five binary
|
|
3. **Hot reload**: Use `FrbCompile()` — auto-falls back to pcode if Go unavailable
|
|
|
|
## Symbol Scoping and Isolation
|
|
|
|
FRB modules operate in an isolated scope to prevent name collisions between
|
|
the host program and loaded modules. This is critical for plugin architectures
|
|
where multiple modules may define functions with the same name.
|
|
|
|
### Scoping Rules
|
|
|
|
| Scenario | Behavior |
|
|
|----------|----------|
|
|
| `FrbDo(pMod, "FUNC")` | Module scope first, then VM global |
|
|
| Module defines `Main()` | Always module-local; never registered in VM |
|
|
| Module function = host function (same name) | Host function preserved; module function accessible only via `FrbDo()` |
|
|
| Module function = new name (not in host) | Registered in VM global scope; callable directly from host |
|
|
| `FrbUnload(pMod)` | Newly registered symbols removed; overwritten symbols restored |
|
|
|
|
### How It Works
|
|
|
|
When `FrbLoad()` loads a module:
|
|
|
|
1. All module functions are stored in the module's **local symbol table**.
|
|
2. `Main()` is **never** exported to the VM — it stays module-private.
|
|
3. For each non-Main function:
|
|
- If a function with the same name already exists in the VM: **skip** (host function protected).
|
|
- If the name is new: **register** in the VM global scope.
|
|
4. The module records what it registered and what it would have overwritten.
|
|
|
|
When `FrbDo(pMod, "FUNC", ...)` is called:
|
|
|
|
1. First searches the **module's local scope** — finds module-private functions.
|
|
2. If not found locally, falls back to the **VM global scope**.
|
|
3. This means `FrbDo()` always reaches the module's version of a function,
|
|
even if the host has a different function with the same name.
|
|
|
|
When `FrbUnload(pMod)` is called:
|
|
|
|
1. All symbols the module registered globally are **removed** from the VM.
|
|
2. Any host symbols that were saved are **restored** to their original state.
|
|
3. The VM returns to exactly the state it had before `FrbLoad()`.
|
|
|
|
### Example: Name Collision
|
|
|
|
```harbour
|
|
// Host program defines Add() as (a+b)*10
|
|
FUNCTION Add(a, b)
|
|
RETURN (a + b) * 10
|
|
|
|
FUNCTION Main()
|
|
? Add(1, 2) // → 30 (host function)
|
|
|
|
pMod := FrbLoad("mathlib.frb") // Module also defines Add() as a+b
|
|
|
|
? Add(1, 2) // → 30 (host function still works!)
|
|
? FrbDo(pMod, "ADD", 100, 200) // → 300 (module's Add via FrbDo)
|
|
|
|
FrbUnload(pMod)
|
|
? Add(1, 2) // → 30 (fully restored)
|
|
RETURN NIL
|
|
```
|
|
|
|
### Comparison with Harbour HRB Binding Modes
|
|
|
|
| Harbour HRB | Five FRB | Description |
|
|
|-------------|----------|-------------|
|
|
| `HB_HRB_BIND_DEFAULT` | Default behavior | Don't overwrite existing functions |
|
|
| `HB_HRB_BIND_OVERLOAD` | (not needed) | FrbDo() always reaches module scope |
|
|
| `HB_HRB_BIND_FORCELOCAL` | Default for Main() | Entry point always module-private |
|
|
|
|
Five simplifies Harbour's binding modes into a single intuitive behavior:
|
|
module functions are always accessible via `FrbDo()`, host functions are always
|
|
protected, and `FrbUnload()` cleanly restores the original state.
|
|
|
|
## Limitations
|
|
|
|
### Native Mode
|
|
- Linux only (Go plugin limitation)
|
|
- FRB and host binary must use same Go version
|
|
- Go plugins cannot be truly unloaded from memory
|
|
- Larger file size (~4.7 MB per module)
|
|
|
|
### Pcode Mode
|
|
- Slower than native (interpreter overhead)
|
|
- Advanced features may have limited pcode support
|
|
- No direct Go interop from pcode (uses RTL functions)
|
|
|
|
## Migration from Harbour HRB
|
|
|
|
| Harbour | Five |
|
|
|---------|------|
|
|
| `hb_hrbLoad(cFile)` | `FrbLoad(cFile)` |
|
|
| `hb_hrbDo(pHrb, ...)` | `FrbDo(pMod, cFunc, ...)` |
|
|
| `hb_hrbUnload(pHrb)` | `FrbUnload(pMod)` |
|
|
| `hb_hrbRun(cFile, ...)` | `FrbRun(cFile, ...)` |
|
|
| `hb_compileFromBuf(cSrc)` | `FrbCompile(cSrc)` |
|
|
| N/A | `FrbExec(cSrc, ...)` |
|
|
| `.hrb` extension | `.frb` extension |
|
|
| Pcode only | **Native + Pcode dual mode** |
|
|
|
|
The API is deliberately similar to Harbour's for easy migration. Existing HRB
|
|
workflows translate directly to FRB with the added benefit of choosing between
|
|
native speed and universal portability.
|
|
|
|
## Verified Test Results
|
|
|
|
```
|
|
=== FRB Pcode Mode Test ===
|
|
|
|
Hello: Hello, World! (from FRB module) 175 bytes, no Go needed
|
|
Add: 300 Arithmetic works
|
|
Factorial: 3628800 Recursion works
|
|
|
|
=== FRB Native Mode Test ===
|
|
|
|
Hello: Hello, Five! (from FRB module) 4.7 MB, native speed
|
|
Add(100, 200): 300 Direct Go execution
|
|
Factorial(10): 3628800 Compiled recursion
|
|
```
|