From ce39c6c8e04c1b1505d574c5ca53693cad8870f3 Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Mon, 15 Jun 2026 22:54:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(napi=20P2):=20fnode=20capi=20mode=20?= =?UTF-8?q?=E2=80=94=20c-shared=20libfivenode=20runs=20real=20PRG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `fnode capi -o libfivenode.dylib` generates all PRG as library funcs + a cgo shim exporting the N-API C ABI, and builds buildmode=c-shared. hb_bridge_handle_request stashes the request (read by PRG via FN_NAPI_REQ, registered with HB_FUNC) and runs the PRG FN_HANDLE via capiVM.Run, returning its JSON. capiEnsure calls RegisterLibModules so HB_FUNC/--rtl symbols install into the VM (vm.Run drains libModules but not dynamicFuncs). Verified: node → addon → Go libfivenode → PRG parses the request and runs string/hash/JSON ops (Upper/Len/HB_ISHASH/hb_jsonDecode/Encode), round-trips a 200 response. Supersedes the P1 hand-written cmd/libfivenode (removed). Co-Authored-By: Claude Opus 4.8 (1M context) --- capi/napi_handler.prg | 16 +++++ cmd/fnode/main.go | 145 ++++++++++++++++++++++++++++++++++++++-- cmd/libfivenode/main.go | 62 ----------------- 3 files changed, 157 insertions(+), 66 deletions(-) create mode 100644 capi/napi_handler.prg delete mode 100644 cmd/libfivenode/main.go diff --git a/capi/napi_handler.prg b/capi/napi_handler.prg new file mode 100644 index 0000000..26b9cd3 --- /dev/null +++ b/capi/napi_handler.prg @@ -0,0 +1,16 @@ +FUNCTION FN_HANDLE() + LOCAL hReq := hb_jsonDecode( FN_NAPI_REQ() ) + LOCAL cPath := "" + IF HB_ISHASH( hReq ) + cPath := hb_CStr( hb_HGetDef( hReq, "path", "" ) ) + ENDIF + 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 ) ; + } ) ; + } ) diff --git a/cmd/fnode/main.go b/cmd/fnode/main.go index db312ae..fb3ab89 100644 --- a/cmd/fnode/main.go +++ b/cmd/fnode/main.go @@ -52,6 +52,8 @@ func main() { switch os.Args[1] { case "build": cmdBuild(os.Args[2:]) + case "capi": + cmdCapi(os.Args[2:]) case "run": cmdRun(os.Args[2:]) case "version", "-v", "--version": @@ -149,7 +151,7 @@ func cmdBuild(args []string) { tmpDir := mkBuildDir() defer cleanupBuildDir(tmpDir) - emitGeneratedSources(tmpDir, o) + emitGeneratedSources(tmpDir, o, false) writeRootGo(tmpDir, o.rtlPkgs) writeGoMod(tmpDir, o.goReplace, o.goModule) @@ -158,6 +160,141 @@ func cmdBuild(args []string) { fmt.Fprintf(os.Stderr, "Built: %s\n", absOut) } +// cmdCapi builds a c-shared library (libfivenode) exposing the N-API C ABI +// the fivenode addon expects. All PRG is generated as library functions; the +// entry app must define FUNCTION FN_HANDLE() -> cRespJson, which reads the +// request via FN_NAPI_REQ(). The Go runtime hosts the PRG VM in-process; node +// hosts the addon. (faithful port of the C++ fivenode topology, Harbour→Go) +func cmdCapi(args []string) { + o := parseCommon(args) + if len(o.prgFiles) == 0 { + fatal("capi: no PRG files given") + } + if o.output == "" { + o.output = "libfivenode.dylib" + } + absOut, err := filepath.Abs(o.output) + if err != nil { + fatal("cannot resolve output path: " + err.Error()) + } + tmpDir := mkBuildDir() + defer cleanupBuildDir(tmpDir) + + emitGeneratedSources(tmpDir, o, true) // all library — no main() + writeCapiShim(tmpDir) + writeRootGo(tmpDir, o.rtlPkgs) + writeGoMod(tmpDir, o.goReplace, o.goModule) + + installName := "@rpath/" + filepath.Base(absOut) + runGo(tmpDir, "mod", "tidy") + runGo(tmpDir, "build", "-buildmode=c-shared", + "-ldflags", "-extldflags=-Wl,-install_name,"+installName, + "-o", absOut, ".") + fmt.Fprintf(os.Stderr, "Built (c-shared): %s\n", absOut) +} + +// writeCapiShim emits the cgo file that exports the libfivenode C ABI and +// drives the PRG VM in-process. The addon calls hb_bridge_handle_request with +// the request JSON; we stash it (read by PRG via FN_NAPI_REQ) and run the +// PRG FN_HANDLE function, returning its string result. +func writeCapiShim(tmpDir string) { + const shim = `package main + +// #include +import "C" + +import ( + "fmt" + "sync" + "unsafe" + + "five/hbrt" + "five/hbrtl" +) + +var ( + capiInit sync.Once + capiVM *hbrt.VM + 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 +// PRG direct calls resolve it. RegisterDynamicFunc lands in a separate table +// that direct symbol calls don't consult. +func init() { + hbrt.HB_FUNC("FN_NAPI_REQ", func(ctx *hbrt.HBContext) { + ctx.RetC(capiReq) + }) +} + +func capiEnsure() { + capiInit.Do(func() { + capiVM = hbrt.NewVM() + hbrtl.RegisterRTL(capiVM) + // Install dynamic funcs (HB_FUNC from this shim + --rtl ext packages). + // vm.Run drains libModules but NOT dynamicFuncs, so do it explicitly. + capiVM.RegisterLibModules() + }) +} + +//export hb_bridge_init +func hb_bridge_init() C.int { + capiEnsure() + return 1 +} + +//export hb_bridge_shutdown +func hb_bridge_shutdown() {} + +//export hb_bridge_handle_request +func hb_bridge_handle_request(req *C.char) *C.char { + capiMu.Lock() + defer capiMu.Unlock() + capiEnsure() + capiReq = C.GoString(req) + capiErr = "" + out := "" + func() { + defer func() { + if r := recover(); r != nil { + capiErr = fmt.Sprintf("%v", r) + } + }() + out = capiVM.Run("FN_HANDLE").AsString() + }() + if capiErr != "" { + out = "{\"status\":500,\"headers\":{\"Content-Type\":\"text/plain\"},\"body\":\"PRG error\"}" + } + return C.CString(out) +} + +//export hb_bridge_last_error +func hb_bridge_last_error() *C.char { return C.CString(capiErr) } + +//export hb_bridge_set_auth +func hb_bridge_set_auth(a *C.char) {} + +//export hb_bridge_clear_auth +func hb_bridge_clear_auth() {} + +//export hb_bridge_free +func hb_bridge_free(p *C.char) { + if p != nil { + C.free(unsafe.Pointer(p)) + } +} + +//export tfn_register_callbacks +func tfn_register_callbacks(cb unsafe.Pointer) { capiNpm = cb } + +func main() {} +` + writeFile(filepath.Join(tmpDir, "capi_shim.go"), shim) +} + func cmdRun(args []string) { o := parseCommon(args) if len(o.prgFiles) == 0 { @@ -166,7 +303,7 @@ func cmdRun(args []string) { tmpDir := mkBuildDir() defer cleanupBuildDir(tmpDir) - emitGeneratedSources(tmpDir, o) + emitGeneratedSources(tmpDir, o, false) writeRootGo(tmpDir, o.rtlPkgs) writeGoMod(tmpDir, o.goReplace, o.goModule) @@ -177,7 +314,7 @@ func cmdRun(args []string) { // emitGeneratedSources preprocesses, parses, analyzes, and emits Go for // every PRG file. The first file is treated as the entry module // (gengo.Generate emits a main()), the rest as library modules. -func emitGeneratedSources(tmpDir string, o buildOpts) { +func emitGeneratedSources(tmpDir string, o buildOpts, allLibrary bool) { // Parse all files first so cross-file function references resolve. type parsed struct { file *ast.File @@ -227,7 +364,7 @@ func emitGeneratedSources(tmpDir string, o buildOpts) { } } var code string - if i == 0 { + if i == 0 && !allLibrary { code = gengo.Generate(p.file) } else { code = gengo.GenerateLibrary(p.file) diff --git a/cmd/libfivenode/main.go b/cmd/libfivenode/main.go deleted file mode 100644 index 3670a1f..0000000 --- a/cmd/libfivenode/main.go +++ /dev/null @@ -1,62 +0,0 @@ -// cmd/libfivenode — Go c-shared 구현의 libfivenode (P1 스켈레톤). -// -// 기존 C++ fivenode N-API 애드온(fivenode.node)이 기대하는 C ABI 를 Go 가 -// CGO //export 로 제공한다. Harbour-C 코어를 Go 런타임으로 교체하는 첫 단계. -// -// go build -buildmode=c-shared -o ../../native/output/darwin/libfivenode.dylib ./cmd/libfivenode -// -// P1 목표: 심볼 export + 애드온 로드·왕복(canned 응답). 실제 PRG 디스패치/ -// npm 콜백은 P2~P4 에서. -package main - -/* -#include -*/ -import "C" -import "unsafe" - -//export hb_bridge_init -func hb_bridge_init() C.int { - return 1 -} - -//export hb_bridge_shutdown -func hb_bridge_shutdown() {} - -//export hb_bridge_handle_request -func hb_bridge_handle_request(reqJSON *C.char) *C.char { - // P1: 고정 응답으로 node↔Go 왕복만 증명. - _ = C.GoString(reqJSON) - resp := `{"status":200,"headers":{"Content-Type":"text/plain; charset=utf-8"},"body":"hello from Go-implemented libfivenode (N-API P1)"}` - return C.CString(resp) -} - -//export hb_bridge_last_error -func hb_bridge_last_error() *C.char { - return C.CString("") -} - -//export hb_bridge_set_auth -func hb_bridge_set_auth(authJSON *C.char) {} - -//export hb_bridge_clear_auth -func hb_bridge_clear_auth() {} - -//export hb_bridge_free -func hb_bridge_free(ptr *C.char) { - if ptr != nil { - C.free(unsafe.Pointer(ptr)) - } -} - -// tfn_register_callbacks — 애드온이 npm 콜백 묶음을 등록(P3 에서 저장·사용). -// P1 에선 void* 로 받아 보관만(구조체 레이아웃은 P3 에서 정의). -// -//export tfn_register_callbacks -func tfn_register_callbacks(cb unsafe.Pointer) { - npmCallbacks = cb -} - -var npmCallbacks unsafe.Pointer - -func main() {}