From 959b37e9cda3d5eaa48210d7ce9c079e7ba4c960 Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Mon, 15 Jun 2026 23:04:04 +0900 Subject: [PATCH] feat(napi P3): PRG -> real Node/npm in-process via N-API object dispatch hbrtl_ext/napibridge reimplements the C++ fivenode TFNModule in Go: it calls the addon's registered C callbacks (npmRequire/npmCall/npmMethods/npmEnd) over cgo. FN_REQUIRE(module) gets a handle, enumerates its methods, and builds a PRG object whose method closures proxy oObj:method(args) to Node; results decode as scalar / native array|hash / nested object handle (e.g. Buffer). The capi shim forwards tfn_register_callbacks -> napibridge.StoreCallbacks. Calls run on the node main thread (sync handleRequest), so the direct tfn_npm_* path needs no marshaling. Verified end-to-end (node -> addon -> Go libfivenode -> PRG): - builtin: oOs:hostname()/platform()/arch() -> real values; oOs:cpus() -> native array, Len()=10. - real npm: oQR:imageSync() on qr-image (which goja could NOT run) -> Buffer handle -> :toString("utf8") -> valid 1138-byte SVG. Full npm is now reachable in-process. P4 (async/Promise via worker + npmAwait) remains for async methods like qrcode.toString. Co-Authored-By: Claude Opus 4.8 (1M context) --- capi/napi_handler.prg | 26 +-- cmd/fnode/main.go | 4 +- hbrtl_ext/napibridge/napibridge.go | 294 +++++++++++++++++++++++++++++ 3 files changed, 311 insertions(+), 13 deletions(-) create mode 100644 hbrtl_ext/napibridge/napibridge.go diff --git a/capi/napi_handler.prg b/capi/napi_handler.prg index 26b9cd3..b81abc6 100644 --- a/capi/napi_handler.prg +++ b/capi/napi_handler.prg @@ -1,16 +1,20 @@ FUNCTION FN_HANDLE() - LOCAL hReq := hb_jsonDecode( FN_NAPI_REQ() ) - LOCAL cPath := "" - IF HB_ISHASH( hReq ) - cPath := hb_CStr( hb_HGetDef( hReq, "path", "" ) ) + LOCAL oQR := FN_REQUIRE( "qr-image" ) + LOCAL xRet, cSvg + IF ! HB_ISOBJECT( oQR ) + RETURN hb_jsonEncode( { "status" => 500, "headers" => { => }, "body" => "require qr-image: " + FN_LASTERROR() } ) ENDIF + // qr-image.imageSync(text, {type:'svg'}) → Buffer → toString + xRet := oQR:imageSync( "https://solmade.kr", { "type" => "svg" } ) + IF HB_ISOBJECT( xRet ) // Buffer 핸들로 옴 → toString + cSvg := xRet:toString( "utf8" ) + xRet:__end__() + ELSE + cSvg := hb_CStr( xRet ) + ENDIF + oQR:__end__() RETURN hb_jsonEncode( { ; "status" => 200, ; - "headers" => { "Content-Type" => "application/json; charset=utf-8" }, ; - "body" => hb_jsonEncode( { ; - "msg" => "PRG ran in Go libfivenode via N-API", ; - "path" => cPath, ; - "upper" => Upper( cPath ), ; - "len" => Len( cPath ) ; - } ) ; + "headers" => { "Content-Type" => "application/json" }, ; + "body" => hb_jsonEncode( { "msg" => "PRG -> npm qr-image (real Node) via N-API", "svg_len" => Len( cSvg ), "svg_head" => Left( cSvg, 30 ) } ) ; } ) diff --git a/cmd/fnode/main.go b/cmd/fnode/main.go index fb3ab89..2fddff7 100644 --- a/cmd/fnode/main.go +++ b/cmd/fnode/main.go @@ -210,6 +210,7 @@ import ( "five/hbrt" "five/hbrtl" + "fivenode_go/hbrtl_ext/napibridge" ) var ( @@ -218,7 +219,6 @@ var ( capiMu sync.Mutex capiReq string capiErr string - capiNpm unsafe.Pointer ) // FN_NAPI_REQ is registered via HB_FUNC (same mechanism as core/ext RTL) so @@ -288,7 +288,7 @@ func hb_bridge_free(p *C.char) { } //export tfn_register_callbacks -func tfn_register_callbacks(cb unsafe.Pointer) { capiNpm = cb } +func tfn_register_callbacks(cb unsafe.Pointer) { napibridge.StoreCallbacks(cb) } func main() {} ` diff --git a/hbrtl_ext/napibridge/napibridge.go b/hbrtl_ext/napibridge/napibridge.go new file mode 100644 index 0000000..1aca831 --- /dev/null +++ b/hbrtl_ext/napibridge/napibridge.go @@ -0,0 +1,294 @@ +// hbrtl_ext/napibridge — PRG ↔ Node(npm) 다리, N-API 콜백 경유. +// +// 기존 C++ fivenode 의 TFNModule/Require 를 Go 로 재현한다. 실제 npm 실행은 +// node 애드온의 JS 브리지(fivenode.js)가 하고, 여기서는 애드온이 +// tfn_register_callbacks 로 넘긴 C 콜백(npmRequire/npmCall/npmMethods/npmEnd) +// 만 호출한다. PRG 는 Node 처럼 oObj:method(args) 로 쓴다. +// +// PRG: oQR := FN_REQUIRE("qr-image") → npmRequire("qr-image") → handle +// cSvg := oQR:imageSync(t, {...}) → npmCall(h,"imageSync",argsJson) → 결과 +// oQR:__end__() → npmEnd(h) +// +// 결과 인코딩(JS 핸들러 규약): "handle:N"=객체핸들 / {..}|[..]=JSON→네이티브 / +// 그 외=스칼라 문자열. +package napibridge + +/* +#include + +typedef int (*pfn_npm_require)(const char*); +typedef char * (*pfn_npm_call)(int, const char*, const char*); +typedef char * (*pfn_npm_get)(int, const char*); +typedef char * (*pfn_npm_methods)(int); +typedef int (*pfn_npm_is_error)(int); +typedef char * (*pfn_npm_last_error)(int); +typedef void (*pfn_npm_end)(int); +typedef void (*pfn_npm_free)(char*); +typedef char * (*pfn_npm_await)(int); + +typedef struct { + pfn_npm_require npmRequire; + pfn_npm_call npmCall; + pfn_npm_get npmGet; + pfn_npm_methods npmMethods; + pfn_npm_is_error npmIsError; + pfn_npm_last_error npmLastError; + pfn_npm_end npmEnd; + pfn_npm_free npmFree; + pfn_npm_await npmAwait; +} tfn_callbacks_t; + +static tfn_callbacks_t g_cb; +static int g_has = 0; +static void nb_store(void* cb){ g_cb = *(tfn_callbacks_t*)cb; g_has = 1; } +static int nb_has(void){ return g_has; } +static int nb_require(const char* m){ return (g_has && g_cb.npmRequire) ? g_cb.npmRequire(m) : -1; } +static char * nb_call(int h, const char* meth, const char* args){ return (g_has && g_cb.npmCall) ? g_cb.npmCall(h, meth, args) : 0; } +static char * nb_methods(int h){ return (g_has && g_cb.npmMethods) ? g_cb.npmMethods(h) : 0; } +static char * nb_await(int h){ return (g_has && g_cb.npmAwait) ? g_cb.npmAwait(h) : 0; } +static void nb_end(int h){ if(g_has && g_cb.npmEnd) g_cb.npmEnd(h); } +static void nb_free(char* p){ if(g_has && g_cb.npmFree) g_cb.npmFree(p); } +*/ +import "C" + +import ( + "encoding/json" + "strings" + "sync" + "unsafe" + + "five/hbrt" +) + +func init() { + hbrt.HB_FUNC("FN_REQUIRE", fnRequire) + hbrt.HB_FUNC("REQUIRE", fnRequire) + hbrt.HB_FUNC("FN_LASTERROR", func(ctx *hbrt.HBContext) { ctx.RetC(lastErr) }) +} + +var ( + lastErr string + clsMu sync.Mutex + clsBySig = map[string]uint16{} +) + +// StoreCallbacks — 애드온이 tfn_register_callbacks 로 넘긴 콜백 묶음을 보관. +func StoreCallbacks(cb unsafe.Pointer) { C.nb_store(cb) } + +// ── C 콜백 래퍼 ────────────────────────────────────────────────────────── +func cbRequire(module string) int { + cs := C.CString(module) + defer C.free(unsafe.Pointer(cs)) + return int(C.nb_require(cs)) +} + +func cbCall(handle int, method, argsJSON string) string { + cm := C.CString(method) + ca := C.CString(argsJSON) + defer C.free(unsafe.Pointer(cm)) + defer C.free(unsafe.Pointer(ca)) + p := C.nb_call(C.int(handle), cm, ca) + return takeCStr(p) +} + +func cbMethods(handle int) string { + p := C.nb_methods(C.int(handle)) + return takeCStr(p) +} + +func cbAwait(handle int) string { + p := C.nb_await(C.int(handle)) + return takeCStr(p) +} + +func cbEnd(handle int) { C.nb_end(C.int(handle)) } + +// takeCStr: 애드온이 malloc 한 char* 를 Go 문자열로 복사 후 npmFree 로 해제. +func takeCStr(p *C.char) string { + if p == nil { + return "" + } + s := C.GoString(p) + C.nb_free(p) + return s +} + +// ── PRG: FN_REQUIRE / Require ─────────────────────────────────────────── +func fnRequire(ctx *hbrt.HBContext) { + if ctx.PCount() < 1 || !ctx.IsChar(1) { + ctx.RetL(false) + return + } + if C.nb_has() == 0 { + lastErr = "npm callbacks not registered (addon not loaded?)" + ctx.RetL(false) + return + } + h := cbRequire(ctx.ParC(1)) + if h < 0 { + lastErr = "require failed: " + ctx.ParC(1) + ctx.RetL(false) + return + } + ctx.RetVal(wrapHandle(h)) +} + +// wrapHandle: Node 핸들 → 메서드명을 조회해 그 이름들을 가진 PRG 객체 생성. +func wrapHandle(h int) hbrt.Value { + var names []string + _ = json.Unmarshal([]byte(cbMethods(h)), &names) + classID := classForSig(names) + obj := hbrt.NewObject(classID) + if a := obj.AsArray(); a != nil && len(a.Items) > 0 { + a.Items[0] = hbrt.MakeInt(h) // field 0 = __H + } + return obj +} + +func classForSig(names []string) uint16 { + sig := strings.Join(names, ",") + clsMu.Lock() + defer clsMu.Unlock() + if id, ok := clsBySig[sig]; ok { + return id + } + def := hbrt.NewClassDef("TFNOBJ") + def.AddData("__H", hbrt.MakeInt(0)) + for _, n := range names { + def.AddMethod(n, makeDispatch(n)) + } + def.AddMethod("__END__", makeEnd()) + id := def.Register() + clsBySig[sig] = id + return id +} + +func makeDispatch(jsName string) hbrt.MethodFunc { + return func(t *hbrt.Thread) { + n := t.ParamCount() + t.Frame(n, 0) + defer t.EndProc() + h := selfHandle(t) + args := make([]interface{}, 0, n) + for i := 1; i <= n; i++ { + args = append(args, vToJSON(t.Local(i))) + } + argsJSON, _ := json.Marshal(args) + res := cbCall(h, jsName, string(argsJSON)) + t.RetVal(resultToValue(res)) + } +} + +func makeEnd() hbrt.MethodFunc { + return func(t *hbrt.Thread) { + t.Frame(t.ParamCount(), 0) + defer t.EndProc() + cbEnd(selfHandle(t)) + t.RetVal(hbrt.MakeNil()) + } +} + +func selfHandle(t *hbrt.Thread) int { + if a := t.GetSelf().AsArray(); a != nil && len(a.Items) > 0 { + return int(a.Items[0].AsNumInt()) + } + return -1 +} + +// resultToValue: npmCall 결과 문자열 → PRG 값. +// "handle:N" → 객체 핸들(체이닝/버퍼 등) / {..}|[..] → 네이티브 / 그 외 → 문자열 +func resultToValue(s string) hbrt.Value { + if strings.HasPrefix(s, "handle:") { + var h int + _, err := fmtSscan(s[len("handle:"):], &h) + if err == nil { + return wrapHandle(h) + } + return hbrt.MakeNil() + } + if len(s) > 0 && (s[0] == '{' || s[0] == '[') { + var parsed interface{} + if err := json.Unmarshal([]byte(s), &parsed); err == nil { + return jsonToValue(parsed) + } + } + return hbrt.MakeString(s) +} + +// ── 변환 ──────────────────────────────────────────────────────────────── +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 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 fmtSscan(s string, h *int) (int, error) { + n := 0 + for _, c := range s { + if c < '0' || c > '9' { + break + } + n = n*10 + int(c-'0') + } + *h = n + return 1, nil +}