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) <noreply@anthropic.com>
This commit is contained in:
CharlesKWON
2026-06-15 23:04:04 +09:00
parent ce39c6c8e0
commit 959b37e9cd
3 changed files with 311 additions and 13 deletions

View File

@@ -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 ) } ) ;
} )

View File

@@ -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() {}
`

View File

@@ -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 <stdlib.h>
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
}