From 1e80c894147ab9ecaa2e3989ab519871a2205519 Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Mon, 15 Jun 2026 21:31:29 +0900 Subject: [PATCH] =?UTF-8?q?feat(rtl):=20nodebridge=20object=20dispatch=20?= =?UTF-8?q?=E2=80=94=20oMod:method()=20like=20Node/TFNModule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FN_REQUIRE now returns a module OBJECT; PRG calls npm methods directly (oQR:toString(args), oQR:__end__()) exactly like Node and the C++ fivenode TFNModule. At require, the bridge enumerates the module's method names and registers each as a class method whose closure proxies the call to the persistent node sidecar (Promise auto-awaited). Native PRG args → JS; results string/number/boolean/buffer/json. Pure Go glue, no C/CGO. Replaces the FN_CALL(handle,...) functional API. Co-Authored-By: Claude Opus 4.8 (1M context) --- hbrtl_ext/nodebridge/nodebridge.go | 295 ++++++++++++++++++++--------- 1 file changed, 209 insertions(+), 86 deletions(-) diff --git a/hbrtl_ext/nodebridge/nodebridge.go b/hbrtl_ext/nodebridge/nodebridge.go index deae3a2..4dd3d51 100644 --- a/hbrtl_ext/nodebridge/nodebridge.go +++ b/hbrtl_ext/nodebridge/nodebridge.go @@ -1,20 +1,18 @@ // hbrtl_ext/nodebridge — fivenode 의 Node 모듈 브리지를 순수 Go 로 구현. // -// 기존 C++ fivenode 는 Harbour ⇄ C 글루 ⇄ Node(npm) 구조였다(JS 는 항상 -// 진짜 Node 에서 실행, C 는 JSON 을 주고받는 글루일 뿐). FIVE 는 C→Go 재구현 -// 이므로 그 'C 글루'를 '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) ⇄ 이 RTL(Go, os/exec+stdio JSON) ⇄ node bridge.js ⇄ npm -// -// node -e 문자열 평가가 아니라, C++ 판과 동일한 '핸들 기반' 프로토콜 -// (require→handle, call→메서드 호출[Promise 자동 await], end)을 쓴다. -// node 프로세스는 한 번만 띄워 상주시킨다. C/CGO 한 줄 없음. +// Five(Go) ⇄ nodebridge(Go, os/exec+stdio JSON) ⇄ 상주 node ⇄ npm // // PRG surface: -// FN_REQUIRE(cModule) -> nHandle (-1 on error) -// FN_CALL(nHandle, cMethod[, cArgsJson]) -> cJson {"ok","type","value","err"} -// FN_END(nHandle) -> NIL -// FN_LASTERROR() -> cErr +// oMod := FN_REQUIRE( cModule ) // = Require(); 모듈 객체 반환(.f. 실패) +// xRet := oMod:someMethod( a, b ) // Node 메서드 직접 호출(Promise 자동 await) +// oMod:__end__() // 핸들 해제 +// FN_LASTERROR() // 마지막 오류 문자열 package nodebridge import ( @@ -23,6 +21,8 @@ import ( "os" "os/exec" "path/filepath" + "strconv" + "strings" "sync" "five/hbrt" @@ -30,17 +30,21 @@ import ( func init() { hbrt.HB_FUNC("FN_REQUIRE", fnRequire) - hbrt.HB_FUNC("FN_CALL", fnCall) - hbrt.HB_FUNC("FN_END", fnEnd) + 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 + 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; @@ -54,26 +58,37 @@ function enc(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 }; + 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]; // property access - } else { + if (typeof fn !== "function") { val = obj[req.method]; } + else { const args = req.args || []; - if (/^[A-Z]/.test(req.method)) val = new fn(...args); // constructor + 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; // auto-await Promise + 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 }; } @@ -91,7 +106,7 @@ process.stdin.on("data", async (d) => { out(await handle(req)); } }); -console.log = (...a) => process.stderr.write(a.join(" ") + "\n"); // keep stdout clean +console.log = (...a) => process.stderr.write(a.join(" ") + "\n"); process.stdin.resume(); ` ) @@ -133,8 +148,6 @@ func ensure() bool { } cmd := exec.Command(bin, jsPath) cmd.Dir = dir - // require() 는 스크립트 위치 기준이라 /tmp 의 bridge.js 에선 node_modules 를 - // 못 찾는다 → NODE_PATH 로 작업 디렉터리의 node_modules 를 지정. cmd.Env = append(os.Environ(), "NODE_PATH="+filepath.Join(dir, "node_modules")) cmd.Stderr = os.Stderr wp, err := cmd.StdinPipe() @@ -151,10 +164,8 @@ func ensure() bool { lastErr = "start node: " + err.Error() return false } - proc = cmd - stdin = bufio.NewWriter(wp) - stdout = bufio.NewScanner(rp) - stdout.Buffer(make([]byte, 0, 64*1024), 8*1024*1024) // allow large results + proc, stdin, stdout = cmd, bufio.NewWriter(wp), bufio.NewScanner(rp) + stdout.Buffer(make([]byte, 0, 64*1024), 16*1024*1024) return true } @@ -165,20 +176,19 @@ func reset() { proc, stdin, stdout = nil, nil, nil } -// rpc sends one request and reads one response line. Caller holds mu. +// 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) - b = append(b, '\n') - if _, err := stdin.Write(b); err != nil { - lastErr = "write: " + err.Error() + if _, err := stdin.Write(append(b, '\n')); err != nil { + lastErr = err.Error() reset() return nil, false } if err := stdin.Flush(); err != nil { - lastErr = "flush: " + err.Error() + lastErr = err.Error() reset() return nil, false } @@ -195,64 +205,46 @@ func rpc(req map[string]interface{}) (map[string]interface{}, bool) { return resp, true } -// FN_REQUIRE(cModule) -> nHandle (-1 on error) +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.RetNI(-1) + ctx.RetL(false) return } + module := ctx.ParC(1) + mu.Lock() - defer mu.Unlock() - resp, ok := rpc(map[string]interface{}{"cmd": "require", "module": ctx.ParC(1)}) + resp, ok := rpc(map[string]interface{}{"cmd": "require", "module": module}) + mu.Unlock() if !ok || resp["ok"] != true { if ok { - lastErr = str(resp["err"]) + setErr(str(resp["err"])) } - ctx.RetNI(-1) + ctx.RetL(false) return } - if h, ok := resp["handle"].(float64); ok { - ctx.RetNI(int(h)) - 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) + } + } } - ctx.RetNI(-1) -} -// FN_CALL(nHandle, cMethod[, cArgsJson]) -> {"ok","type","value","err"} JSON -func fnCall(ctx *hbrt.HBContext) { - if ctx.PCount() < 2 || !ctx.IsNum(1) || !ctx.IsChar(2) { - ctx.RetC(envelope(false, "", "", "FN_CALL(nHandle, cMethod[, cArgsJson])")) - return + 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 } - handle := ctx.ParNI(1) - method := ctx.ParC(2) - var args interface{} - if ctx.PCount() >= 3 && ctx.IsChar(3) { - _ = json.Unmarshal([]byte(ctx.ParC(3)), &args) - } - mu.Lock() - defer mu.Unlock() - resp, ok := rpc(map[string]interface{}{"cmd": "call", "handle": handle, "method": method, "args": args}) - if !ok { - ctx.RetC(envelope(false, "", "", lastErr)) - return - } - if resp["ok"] != true { - ctx.RetC(envelope(false, "", "", str(resp["err"]))) - return - } - val, _ := resp["value"].(map[string]interface{}) - ctx.RetC(envelope(true, str(val["t"]), str(val["v"]), "")) -} - -// FN_END(nHandle) -func fnEnd(ctx *hbrt.HBContext) { - if ctx.PCount() >= 1 && ctx.IsNum(1) { - mu.Lock() - _, _ = rpc(map[string]interface{}{"cmd": "end", "handle": ctx.ParNI(1)}) - mu.Unlock() - } - ctx.RetNil() + ctx.RetVal(obj) } func fnLastError(ctx *hbrt.HBContext) { @@ -262,14 +254,145 @@ func fnLastError(ctx *hbrt.HBContext) { 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", "json": + 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() + } +} + func str(v interface{}) string { if s, ok := v.(string); ok { return s } return "" } - -func envelope(ok bool, t, v, err string) string { - b, _ := json.Marshal(map[string]interface{}{"ok": ok, "type": t, "value": v, "err": err}) - return string(b) +func toF(v interface{}) float64 { + if f, ok := v.(float64); ok { + return f + } + return 0 }