feat(napi P2): fnode capi mode — c-shared libfivenode runs real PRG
`fnode capi <entry.prg> -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) <noreply@anthropic.com>
This commit is contained in:
16
capi/napi_handler.prg
Normal file
16
capi/napi_handler.prg
Normal file
@@ -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 ) ;
|
||||
} ) ;
|
||||
} )
|
||||
@@ -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 <stdlib.h>
|
||||
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)
|
||||
|
||||
@@ -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 <stdlib.h>
|
||||
*/
|
||||
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() {}
|
||||
Reference in New Issue
Block a user