// 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:(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 }