feat(rtl): nodebridge object dispatch — oMod:method() like Node/TFNModule
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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:<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", "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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user