feat(bridge_capi): port fivenode_capi.c / fivenode_buffers.c to Go RTL
Seven HB_FUNCs that fivenode's bridge_*.prg layer relies on: _CTX_SET_JSON / _CTX_GET_JSON — per-request context payload _OUT_APPEND / _OUT_GET / _OUT_CLEAR — response body buffer _BRIDGE_SET_RESULT / _BRIDGE_GET_RESULT — fast-path response Crucially per-thread, not process-global like the original C implementation. fivenode runs single-threaded under N-API so a static buffer per process was fine; fivenode_go runs one *hbrt.Thread per HTTP request goroutine, so the state is keyed by *hbrt.Thread in a sync.Map. The HTTP dispatcher will call CleanupThread once per request to keep the map bounded (sub-phase 1a.3-3). Also exposes Go-side helpers (OutputBytes, Result, SetContextJSON, CleanupThread) so the dispatcher can seed the context and harvest the response without bouncing back through PRG. Verified with app/capi_test.prg: all seven functions behave as expected; combined with the Five hb_jsonDecode byref fix, ctx_get() now correctly returns hash values rather than the fallback default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
44
app/capi_test.prg
Normal file
44
app/capi_test.prg
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// app/capi_test.prg — verifies bridge_capi RTL in isolation
|
||||||
|
// (single thread, no HTTP). Multi-thread isolation will be exercised
|
||||||
|
// once the dispatcher is wired in 1a.3-3.
|
||||||
|
FUNCTION Main()
|
||||||
|
LOCAL cJson, cOut, cResult
|
||||||
|
|
||||||
|
? "=== _CTX_SET_JSON / _CTX_GET_JSON ==="
|
||||||
|
_CTX_SET_JSON( '{"user":"alice","role":"admin"}' )
|
||||||
|
cJson := _CTX_GET_JSON()
|
||||||
|
? "ctx json:", cJson
|
||||||
|
? "ctx_get user:", ctx_get( "user", "?" )
|
||||||
|
? "ctx_get role:", ctx_get( "role", "?" )
|
||||||
|
? "ctx_get missing:", ctx_get( "nope", "DEFAULT" )
|
||||||
|
|
||||||
|
? ""
|
||||||
|
? "=== _OUT_APPEND / _OUT_GET / _OUT_CLEAR ==="
|
||||||
|
_OUT_APPEND( "Hello, " )
|
||||||
|
_OUT_APPEND( "world! " )
|
||||||
|
_OUT_APPEND( "Three." )
|
||||||
|
cOut := _OUT_GET()
|
||||||
|
? "out before clear: [" + cOut + "]"
|
||||||
|
_OUT_CLEAR()
|
||||||
|
? "out after clear: [" + _OUT_GET() + "]"
|
||||||
|
|
||||||
|
? ""
|
||||||
|
? "=== _BRIDGE_SET_RESULT / _BRIDGE_GET_RESULT ==="
|
||||||
|
_BRIDGE_SET_RESULT( "fast-path payload" )
|
||||||
|
cResult := _BRIDGE_GET_RESULT()
|
||||||
|
? "result:", cResult
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
// Need ctx_get here because bridge_context.prg isn't linked in yet.
|
||||||
|
FUNCTION ctx_get( cKey, xDefault )
|
||||||
|
LOCAL cJson := _CTX_GET_JSON()
|
||||||
|
LOCAL hCtx
|
||||||
|
IF Empty( cJson )
|
||||||
|
RETURN xDefault
|
||||||
|
ENDIF
|
||||||
|
hb_jsonDecode( cJson, @hCtx )
|
||||||
|
IF ValType( hCtx ) == "H" .AND. hb_HHasKey( hCtx, cKey )
|
||||||
|
RETURN hCtx[ cKey ]
|
||||||
|
ENDIF
|
||||||
|
RETURN xDefault
|
||||||
@@ -36,6 +36,7 @@ const version = "fnode 0.1.0 — fivenode_go builder for Five"
|
|||||||
var defaultRTL = []string{
|
var defaultRTL = []string{
|
||||||
"fivenode_go/hbrtl_ext/hello",
|
"fivenode_go/hbrtl_ext/hello",
|
||||||
"fivenode_go/hbrtl_ext/httpserver",
|
"fivenode_go/hbrtl_ext/httpserver",
|
||||||
|
"fivenode_go/hbrtl_ext/bridge_capi",
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
175
hbrtl_ext/bridge_capi/capi.go
Normal file
175
hbrtl_ext/bridge_capi/capi.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// Package bridge_capi re-implements the seven C helpers that
|
||||||
|
// fivenode's PRG layer expects (originally provided by
|
||||||
|
// fivenode_capi.c + fivenode_buffers.c). The originals back their
|
||||||
|
// state with process-global static buffers — safe under Node's
|
||||||
|
// single-threaded N-API caller, unsafe for fivenode_go where every
|
||||||
|
// HTTP request runs on its own goroutine and *hbrt.Thread.
|
||||||
|
//
|
||||||
|
// Here, state is keyed by *hbrt.Thread so concurrent requests don't
|
||||||
|
// stomp on each other's output buffer or context JSON.
|
||||||
|
//
|
||||||
|
// PRG-visible names (identical to fivenode):
|
||||||
|
//
|
||||||
|
// _CTX_SET_JSON(cJson) — store the request context as JSON
|
||||||
|
// _CTX_GET_JSON() -> cJson — read it back
|
||||||
|
// _OUT_APPEND(cText) — append to the response body buffer
|
||||||
|
// _OUT_GET() -> cText — read the buffer (host calls this
|
||||||
|
// after the PRG handler returns)
|
||||||
|
// _OUT_CLEAR() — reset the buffer
|
||||||
|
// _BRIDGE_SET_RESULT(cText) — record a fast-path response string
|
||||||
|
// _BRIDGE_GET_RESULT() -> cText
|
||||||
|
//
|
||||||
|
// Memory lifecycle: each request goroutine ends up creating a fresh
|
||||||
|
// Thread, so the per-thread state in stateMap accumulates as the
|
||||||
|
// process runs. The HTTP dispatcher (or any other host) calls
|
||||||
|
// CleanupThread once the request is done — see the public helper
|
||||||
|
// near the bottom of this file. Without that hook, state for dead
|
||||||
|
// threads would leak; with it, the map stays bounded by the number
|
||||||
|
// of in-flight requests.
|
||||||
|
package bridge_capi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"five/hbrt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
ctxJSON string
|
||||||
|
output bytes.Buffer
|
||||||
|
resultS string // _BRIDGE_SET_RESULT payload (separate from output buffer)
|
||||||
|
resultOK bool // distinguishes "" from "never set"
|
||||||
|
}
|
||||||
|
|
||||||
|
var stateMap sync.Map // map[*hbrt.Thread]*state
|
||||||
|
|
||||||
|
func stateFor(t *hbrt.Thread) *state {
|
||||||
|
if v, ok := stateMap.Load(t); ok {
|
||||||
|
return v.(*state)
|
||||||
|
}
|
||||||
|
s := &state{}
|
||||||
|
actual, _ := stateMap.LoadOrStore(t, s)
|
||||||
|
return actual.(*state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupThread releases the per-thread state. The HTTP dispatcher
|
||||||
|
// invokes this in a defer once the handler returns so the map stays
|
||||||
|
// bounded by in-flight requests rather than total requests served.
|
||||||
|
func CleanupThread(t *hbrt.Thread) {
|
||||||
|
stateMap.Delete(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutputBytes returns a copy of the per-thread output buffer for the
|
||||||
|
// host to write to the wire after the handler finishes. Returning a
|
||||||
|
// copy (rather than the underlying slice) avoids any chance of a
|
||||||
|
// later _OUT_APPEND racing the writer.
|
||||||
|
func OutputBytes(t *hbrt.Thread) []byte {
|
||||||
|
s := stateFor(t)
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.output.Len() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]byte, s.output.Len())
|
||||||
|
copy(out, s.output.Bytes())
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result returns the fast-path result payload if PRG set one via
|
||||||
|
// _BRIDGE_SET_RESULT, plus an ok flag so callers can distinguish
|
||||||
|
// "explicitly empty" from "never set".
|
||||||
|
func Result(t *hbrt.Thread) (string, bool) {
|
||||||
|
s := stateFor(t)
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.resultS, s.resultOK
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContextJSON lets the host seed the per-thread context before
|
||||||
|
// invoking the PRG handler. It's a Go-side mirror of _CTX_SET_JSON
|
||||||
|
// so the HTTP dispatcher can hand the request hash to PRG without
|
||||||
|
// going back through PRG itself.
|
||||||
|
func SetContextJSON(t *hbrt.Thread, jsonStr string) {
|
||||||
|
s := stateFor(t)
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.ctxJSON = jsonStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
hbrt.HB_FUNC("_CTX_SET_JSON", capiCtxSetJSON)
|
||||||
|
hbrt.HB_FUNC("_CTX_GET_JSON", capiCtxGetJSON)
|
||||||
|
hbrt.HB_FUNC("_OUT_APPEND", capiOutAppend)
|
||||||
|
hbrt.HB_FUNC("_OUT_GET", capiOutGet)
|
||||||
|
hbrt.HB_FUNC("_OUT_CLEAR", capiOutClear)
|
||||||
|
hbrt.HB_FUNC("_BRIDGE_SET_RESULT", capiBridgeSetResult)
|
||||||
|
hbrt.HB_FUNC("_BRIDGE_GET_RESULT", capiBridgeGetResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func capiCtxSetJSON(ctx *hbrt.HBContext) {
|
||||||
|
if ctx.PCount() < 1 || !ctx.IsChar(1) {
|
||||||
|
ctx.RetNil()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SetContextJSON(ctx.T, ctx.ParC(1))
|
||||||
|
ctx.RetNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func capiCtxGetJSON(ctx *hbrt.HBContext) {
|
||||||
|
s := stateFor(ctx.T)
|
||||||
|
s.mu.Lock()
|
||||||
|
v := s.ctxJSON
|
||||||
|
s.mu.Unlock()
|
||||||
|
ctx.RetC(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func capiOutAppend(ctx *hbrt.HBContext) {
|
||||||
|
if ctx.PCount() < 1 || !ctx.IsChar(1) {
|
||||||
|
ctx.RetNil()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s := stateFor(ctx.T)
|
||||||
|
s.mu.Lock()
|
||||||
|
s.output.WriteString(ctx.ParC(1))
|
||||||
|
s.mu.Unlock()
|
||||||
|
ctx.RetNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func capiOutGet(ctx *hbrt.HBContext) {
|
||||||
|
s := stateFor(ctx.T)
|
||||||
|
s.mu.Lock()
|
||||||
|
v := s.output.String()
|
||||||
|
s.mu.Unlock()
|
||||||
|
ctx.RetC(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func capiOutClear(ctx *hbrt.HBContext) {
|
||||||
|
s := stateFor(ctx.T)
|
||||||
|
s.mu.Lock()
|
||||||
|
s.output.Reset()
|
||||||
|
s.mu.Unlock()
|
||||||
|
ctx.RetNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func capiBridgeSetResult(ctx *hbrt.HBContext) {
|
||||||
|
s := stateFor(ctx.T)
|
||||||
|
s.mu.Lock()
|
||||||
|
if ctx.PCount() >= 1 && ctx.IsChar(1) {
|
||||||
|
s.resultS = ctx.ParC(1)
|
||||||
|
} else {
|
||||||
|
s.resultS = ""
|
||||||
|
}
|
||||||
|
s.resultOK = true
|
||||||
|
s.mu.Unlock()
|
||||||
|
ctx.RetNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func capiBridgeGetResult(ctx *hbrt.HBContext) {
|
||||||
|
s := stateFor(ctx.T)
|
||||||
|
s.mu.Lock()
|
||||||
|
v := s.resultS
|
||||||
|
s.mu.Unlock()
|
||||||
|
ctx.RetC(v)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user