Files
fivenode_go/hbrtl_ext/nodebridge/nodebridge.go
CharlesKWON 76853d4beb feat(rtl): nodebridge decodes JS arrays/objects to native PRG values
jsValToValue now recursively converts JS arrays/objects (the "json" wire
type) into native hbrt arrays/hashes, so PRG can Len()/index/hb_HGetDef the
results of npm methods (e.g. os.cpus()[1]:model). Strings/numbers/booleans/
buffers unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:42:00 +09:00

433 lines
11 KiB
Go

// hbrtl_ext/nodebridge — fivenode 의 Node 모듈 브리지를 순수 Go 로 구현.
//
// 기존 C++ fivenode 의 TFNModule/Require() 를 그대로 본뜬다(단 글루만 Go):
// - require 시 모듈의 메서드명을 열거해, 각 이름을 클래스 메서드로 등록한다
// (C++: hb_clsAdd(clsH, NAME, DISPATCH); 여기선 메서드명을 클로저로 캡처).
// - 객체를 돌려주고, PRG 는 Node 처럼 oObj:method(args) 로 호출한다.
// - JS 는 상주 node 프로세스에서 실제 실행(전체 npm). C/CGO 한 줄 없음.
//
// Five(Go) ⇄ nodebridge(Go, os/exec+stdio JSON) ⇄ 상주 node ⇄ npm
//
// PRG surface:
// oMod := FN_REQUIRE( cModule ) // = Require(); 모듈 객체 반환(.f. 실패)
// xRet := oMod:someMethod( a, b ) // Node 메서드 직접 호출(Promise 자동 await)
// oMod:__end__() // 핸들 해제
// FN_LASTERROR() // 마지막 오류 문자열
package nodebridge
import (
"bufio"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"five/hbrt"
)
func init() {
hbrt.HB_FUNC("FN_REQUIRE", fnRequire)
hbrt.HB_FUNC("REQUIRE", fnRequire) // Node/원조 fivenode 동일 명칭 별칭
hbrt.HB_FUNC("FN_LASTERROR", fnLastError)
}
// ── 상주 node 브리지 (요청/응답 JSON, 한 번에 하나) ──────────────────────
var (
mu sync.Mutex
proc *exec.Cmd
stdin *bufio.Writer
stdout *bufio.Scanner
lastErr string
clsMu sync.Mutex
clsCache = map[string]uint16{} // module name → classID
bridgeJS = `
'use strict';
const handles = new Map(); let next = 1;
function out(o){ process.stdout.write(JSON.stringify(o) + "\n"); }
function enc(v){
if (v === null || v === undefined) return { t: "null" };
if (Buffer.isBuffer(v)) return { t: "buffer", v: v.toString("base64") };
const tp = typeof v;
if (tp === "string") return { t: "string", v: v };
if (tp === "number") return { t: "number", v: String(v) };
if (tp === "boolean") return { t: "boolean", v: v ? "true" : "false" };
try { return { t: "json", v: JSON.stringify(v) }; } catch (e) { return { t: "string", v: String(v) }; }
}
function methodNames(obj){
const names = new Set();
let o = obj;
while (o && o !== Object.prototype && o !== Function.prototype) {
for (const k of Object.getOwnPropertyNames(o)) {
if (k === "constructor") continue;
try { if (typeof obj[k] === "function") names.add(k); } catch (e) {}
}
o = Object.getPrototypeOf(o);
}
return [...names].slice(0, 500);
}
async function handle(req){
try {
if (req.cmd === "require") {
const m = require(req.module);
const h = next++; handles.set(h, m);
return { ok: true, handle: h, names: methodNames(m) };
}
if (req.cmd === "call") {
const obj = handles.get(req.handle);
if (obj === undefined) return { ok: false, err: "invalid handle" };
const fn = obj[req.method];
let val;
if (typeof fn !== "function") { val = obj[req.method]; }
else {
const args = req.args || [];
if (/^[A-Z]/.test(req.method)) val = new fn(...args);
else val = fn.apply(obj, args);
}
if (val && typeof val.then === "function") val = await val;
return { ok: true, value: enc(val) };
}
if (req.cmd === "end") { handles.delete(req.handle); return { ok: true }; }
return { ok: false, err: "unknown cmd" };
} catch (e) { return { ok: false, err: String((e && e.message) || e) }; }
}
let buf = "";
process.stdin.on("data", async (d) => {
buf += d;
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i); buf = buf.slice(i + 1);
if (!line.trim()) continue;
let req; try { req = JSON.parse(line); } catch (e) { out({ ok: false, err: "bad json" }); continue; }
out(await handle(req));
}
});
console.log = (...a) => process.stderr.write(a.join(" ") + "\n");
process.stdin.resume();
`
)
func nodeBin() string {
if p, err := exec.LookPath("node"); err == nil {
return p
}
for _, p := range []string{"/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"} {
if _, err := os.Stat(p); err == nil {
return p
}
}
return ""
}
func workDir() string {
if d := os.Getenv("SOLMADE_NODE_DIR"); d != "" {
return d
}
return "/Users/charleskwon/solmade/node"
}
// ensure starts the persistent node bridge once. Caller holds mu.
func ensure() bool {
if proc != nil {
return true
}
bin := nodeBin()
if bin == "" {
lastErr = "node not found"
return false
}
dir := workDir()
jsPath := filepath.Join(os.TempDir(), "fivenode_bridge.js")
if err := os.WriteFile(jsPath, []byte(bridgeJS), 0o644); err != nil {
lastErr = "write bridge: " + err.Error()
return false
}
cmd := exec.Command(bin, jsPath)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "NODE_PATH="+filepath.Join(dir, "node_modules"))
cmd.Stderr = os.Stderr
wp, err := cmd.StdinPipe()
if err != nil {
lastErr = err.Error()
return false
}
rp, err := cmd.StdoutPipe()
if err != nil {
lastErr = err.Error()
return false
}
if err := cmd.Start(); err != nil {
lastErr = "start node: " + err.Error()
return false
}
proc, stdin, stdout = cmd, bufio.NewWriter(wp), bufio.NewScanner(rp)
stdout.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
return true
}
func reset() {
if proc != nil {
_ = proc.Process.Kill()
}
proc, stdin, stdout = nil, nil, nil
}
// rpc sends one request and reads one response. Caller holds mu.
func rpc(req map[string]interface{}) (map[string]interface{}, bool) {
if !ensure() {
return nil, false
}
b, _ := json.Marshal(req)
if _, err := stdin.Write(append(b, '\n')); err != nil {
lastErr = err.Error()
reset()
return nil, false
}
if err := stdin.Flush(); err != nil {
lastErr = err.Error()
reset()
return nil, false
}
if !stdout.Scan() {
lastErr = "node bridge closed"
reset()
return nil, false
}
var resp map[string]interface{}
if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil {
lastErr = "bad response: " + err.Error()
return nil, false
}
return resp, true
}
func setErr(s string) {
mu.Lock()
lastErr = s
mu.Unlock()
}
// ── PRG: FN_REQUIRE / Require ───────────────────────────────────────────
func fnRequire(ctx *hbrt.HBContext) {
if ctx.PCount() < 1 || !ctx.IsChar(1) {
ctx.RetL(false)
return
}
module := ctx.ParC(1)
mu.Lock()
resp, ok := rpc(map[string]interface{}{"cmd": "require", "module": module})
mu.Unlock()
if !ok || resp["ok"] != true {
if ok {
setErr(str(resp["err"]))
}
ctx.RetL(false)
return
}
handle := int(toF(resp["handle"]))
var names []string
if arr, ok := resp["names"].([]interface{}); ok {
for _, n := range arr {
if s, ok := n.(string); ok {
names = append(names, s)
}
}
}
classID := classFor(module, names)
obj := hbrt.NewObject(classID)
if a := obj.AsArray(); a != nil && len(a.Items) > 0 {
a.Items[0] = hbrt.MakeInt(handle) // field 0 = __H
}
ctx.RetVal(obj)
}
func fnLastError(ctx *hbrt.HBContext) {
mu.Lock()
e := lastErr
mu.Unlock()
ctx.RetC(e)
}
// classFor builds (or reuses) a class whose methods proxy to the JS object.
func classFor(module string, names []string) uint16 {
clsMu.Lock()
defer clsMu.Unlock()
if id, ok := clsCache[module]; ok {
return id
}
def := hbrt.NewClassDef("TFNMOD_" + strings.ToUpper(module))
def.AddData("__H", hbrt.MakeInt(0)) // field 0: node-side handle
for _, n := range names {
def.AddMethod(n, makeDispatch(n)) // key uppercased internally; closure keeps camelCase
}
def.AddMethod("__END__", makeEnd())
id := def.Register()
clsCache[module] = id
return id
}
// makeDispatch returns a method that proxies oObj:<jsName>(args...) to Node.
func makeDispatch(jsName string) hbrt.MethodFunc {
return func(t *hbrt.Thread) {
n := t.ParamCount()
t.Frame(n, 0)
defer t.EndProc()
handle := selfHandle(t)
args := make([]interface{}, 0, n)
for i := 1; i <= n; i++ {
args = append(args, vToJSON(t.Local(i)))
}
argsJSON, _ := json.Marshal(args)
mu.Lock()
resp, ok := rpc(map[string]interface{}{
"cmd": "call", "handle": handle, "method": jsName, "args": json.RawMessage(argsJSON),
})
mu.Unlock()
if !ok || resp["ok"] != true {
if ok {
setErr(str(resp["err"]))
}
t.RetVal(hbrt.MakeNil())
return
}
val, _ := resp["value"].(map[string]interface{})
t.RetVal(jsValToValue(val))
}
}
func makeEnd() hbrt.MethodFunc {
return func(t *hbrt.Thread) {
t.Frame(t.ParamCount(), 0)
defer t.EndProc()
handle := selfHandle(t)
mu.Lock()
_, _ = rpc(map[string]interface{}{"cmd": "end", "handle": handle})
mu.Unlock()
t.RetVal(hbrt.MakeNil())
}
}
func selfHandle(t *hbrt.Thread) int {
self := t.GetSelf()
if a := self.AsArray(); a != nil && len(a.Items) > 0 {
return int(a.Items[0].AsNumInt())
}
return -1
}
// ── 변환 ────────────────────────────────────────────────────────────────
func vToJSON(v hbrt.Value) interface{} {
switch {
case v.IsNil():
return nil
case v.IsLogical():
return v.AsBool()
case v.IsNumeric():
if v.IsNumInt() {
return v.AsNumInt()
}
return v.AsNumDouble()
case v.IsString():
return v.AsString()
case v.IsHash():
h := v.AsHash()
m := map[string]interface{}{}
if h != nil {
for i, k := range h.Keys {
if i < len(h.Values) {
m[k.AsString()] = vToJSON(h.Values[i])
}
}
}
return m
case v.IsArray():
a := v.AsArray()
out := make([]interface{}, 0)
if a != nil {
for _, it := range a.Items {
out = append(out, vToJSON(it))
}
}
return out
default:
return v.AsString()
}
}
func jsValToValue(val map[string]interface{}) hbrt.Value {
if val == nil {
return hbrt.MakeNil()
}
t := str(val["t"])
v := str(val["v"])
switch t {
case "string", "buffer":
return hbrt.MakeString(v)
case "json": // JS array/object → 네이티브 PRG 배열/해시로 디코드
var parsed interface{}
if err := json.Unmarshal([]byte(v), &parsed); err == nil {
return jsonToValue(parsed)
}
return hbrt.MakeString(v)
case "number":
if f, err := strconv.ParseFloat(v, 64); err == nil {
return hbrt.MakeDoubleAuto(f)
}
return hbrt.MakeString(v)
case "boolean":
return hbrt.MakeBool(v == "true")
default:
return hbrt.MakeNil()
}
}
// jsonToValue: 디코드된 JSON 값을 네이티브 hbrt.Value(배열/해시/스칼라)로.
func jsonToValue(x interface{}) hbrt.Value {
switch v := x.(type) {
case nil:
return hbrt.MakeNil()
case bool:
return hbrt.MakeBool(v)
case float64:
return hbrt.MakeDoubleAuto(v)
case string:
return hbrt.MakeString(v)
case []interface{}:
items := make([]hbrt.Value, len(v))
for i, e := range v {
items[i] = jsonToValue(e)
}
return hbrt.MakeArrayFrom(items)
case map[string]interface{}:
pairs := make([]hbrt.Value, 0, len(v)*2)
for k, e := range v {
pairs = append(pairs, hbrt.MakeString(k), jsonToValue(e))
}
return hbrt.MakeHashFrom(hbrt.HashFromPairs(pairs))
default:
return hbrt.MakeNil()
}
}
func str(v interface{}) string {
if s, ok := v.(string); ok {
return s
}
return ""
}
func toF(v interface{}) float64 {
if f, ok := v.(float64); ok {
return f
}
return 0
}