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:
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