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:
@@ -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 ) } ) ;
|
||||
} )
|
||||
|
||||
@@ -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() {}
|
||||
`
|
||||
|
||||
294
hbrtl_ext/napibridge/napibridge.go
Normal file
294
hbrtl_ext/napibridge/napibridge.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user