1111// - BridgeCall -> JSON envelope string {"ok":true,"data":...} | {"ok":false,"error":"..."}
1212// - BridgeCallBinary -> raw bytes (for getSourceFile etc.), single-copy into a Node Buffer
1313//
14- // Memory model: both paths write into reusable C buffers. Calls are fully
15- // synchronous, so the NAPI shim copies the data out to V8 before the next call
16- // reuses the buffer. Zero per-call malloc, zero leaks.
14+ // Memory model: string envelopes reuse a process-lifetime C buffer (the NAPI
15+ // shim copies to V8 synchronously before the next call). Binary responses are
16+ // pinned Go slices (cgo.Handle) handed to V8 with zero copies — the pin is
17+ // released by BridgeReleaseBinary, immediately after a copy or from the
18+ // external buffer's finalizer on GC.
1719package main
1820
1921/*
@@ -22,18 +24,22 @@ package main
2224#include <string.h>
2325
2426// BridgeBinary is returned by value from BridgeCallBinary. Layout matches
25- // napi_shim.c's declaration: { void* data; int64_t len; }.
26- struct BridgeBinary { void* data; long long len; };
27+ // napi_shim.c's declaration: { void* data; int64_t len; uint64_t handle; }.
28+ // data is a view into the pinned Go slice; handle releases the pin via
29+ // BridgeReleaseBinary.
30+ struct BridgeBinary { void* data; long long len; unsigned long long handle; };
2731*/
2832import "C"
2933
3034import (
3135 "context"
3236 "encoding/base64"
3337 "os"
38+ "runtime"
3439 "strconv"
3540 "strings"
3641 "sync"
42+ "sync/atomic"
3743 "time"
3844 "unsafe"
3945
5460 // resultBuf is a single reusable C buffer for JSON envelope responses.
5561 resultBuf * C.char
5662 resultBufLen C.size_t
57-
58- // binBuf is a single reusable C buffer for binary responses (getSourceFile).
59- // Grows only; never freed (one buffer for the process lifetime).
60- binBuf unsafe.Pointer
61- binBufLen C.size_t
6263)
6364
6465type sessionEntry struct {
@@ -100,17 +101,6 @@ func returnEnvelope(env envelope) *C.char {
100101 return resultBuf
101102}
102103
103- // ensureBinBuf grows the reusable binary buffer to at least need bytes.
104- func ensureBinBuf (need C.size_t ) {
105- if binBuf == nil || binBufLen < need {
106- if binBuf != nil {
107- C .free (binBuf )
108- }
109- binBuf = C .malloc (need )
110- binBufLen = need
111- }
112- }
113-
114104// bridgeCheckerPoolOptions returns checker pool sizing for the in-process bridge.
115105// Default MaxCheckers=5 (1 diagnostics + 4 query) aligns with tsgo CLI parallelism.
116106// Override with TSGO_CHECKERS (minimum 2).
@@ -281,13 +271,17 @@ func BridgeCall(session C.int64_t, method *C.char, paramsJson *C.char) *C.char {
281271// api.RawBinary which is handed straight through with no base64. For non-binary
282272// methods, the JSON-marshaled bytes are returned (caller can JSON.parse).
283273//
284- // Returns a struct by value: { void* data; int64_t len }. data points into the
285- // reusable binary buffer and stays valid until the next bridge call. len is 0
286- // for nil/empty results.
274+ // Returns a struct by value: { void* data; int64_t len; uint64_t handle }.
275+ // data points INTO the Go slice (no copy on this side); the slice is pinned via
276+ // cgo.NewHandle so Go's (non-moving) GC keeps it valid. Ownership of the pin
277+ // transfers to the caller (napi_shim.c), which must call BridgeReleaseBinary
278+ // exactly once — immediately after copying, or from the external buffer's
279+ // finalizer on GC in the zero-copy path. len is 0 for nil/empty results
280+ // (handle is 0 then; nothing to release).
287281//
288282//export BridgeCallBinary
289283func BridgeCallBinary (session C.int64_t , method * C.char , paramsJson * C.char ) C.struct_BridgeBinary {
290- empty := C.struct_BridgeBinary {data : nil , len : 0 }
284+ empty := C.struct_BridgeBinary {data : nil , len : 0 , handle : 0 }
291285
292286 mu .Lock ()
293287 entry , ok := sessions [int64 (session )]
@@ -308,14 +302,12 @@ func BridgeCallBinary(session C.int64_t, method *C.char, paramsJson *C.char) C.s
308302 res , err := entry .api .HandleRequest (context .Background (), methodStr , params )
309303 if err != nil {
310304 // No way to surface an error through the binary path without a side
311- // channel; fall back to a JSON error envelope written into the binary
312- // buffer so the caller can detect it by parsing. The contract is:
313- // callers use BridgeCallBinary only for methods known to return binary.
305+ // channel; fall back to a JSON error envelope so the caller can detect
306+ // it by parsing. The contract is: callers use BridgeCallBinary only for
307+ // methods known to return binary.
314308 errEnv := envelope {OK : false , Error : err .Error ()}
315309 b , _ := json .Marshal (errEnv )
316- ensureBinBuf (C .size_t (len (b )))
317- C .memcpy (binBuf , unsafe .Pointer (& b [0 ]), C .size_t (len (b )))
318- return C.struct_BridgeBinary {data : binBuf , len : C .longlong (len (b ))}
310+ return pinBinary (b )
319311 }
320312
321313 var bytes []byte
@@ -334,9 +326,39 @@ func BridgeCallBinary(session C.int64_t, method *C.char, paramsJson *C.char) C.s
334326 if len (bytes ) == 0 {
335327 return empty
336328 }
337- ensureBinBuf (C .size_t (len (bytes )))
338- C .memcpy (binBuf , unsafe .Pointer (& bytes [0 ]), C .size_t (len (bytes )))
339- return C.struct_BridgeBinary {data : binBuf , len : C .longlong (len (bytes ))}
329+ return pinBinary (bytes )
330+ }
331+
332+ // pinBinary pins b via runtime.Pinner (the cgo-legal way to return a Go heap
333+ // pointer: cgo.Handle would keep the object alive but does NOT mark the
334+ // pointer pinned, and the runtime's cgoCheckResult panics on it) and returns
335+ // the view + a registry id for BridgeReleaseBinary.
336+ var (
337+ binPinnerNext uint64
338+ binPinners sync.Map // uint64 -> *runtime.Pinner
339+ )
340+
341+ func pinBinary (b []byte ) C.struct_BridgeBinary {
342+ p := & runtime.Pinner {}
343+ p .Pin (& b [0 ])
344+ id := atomic .AddUint64 (& binPinnerNext , 1 )
345+ binPinners .Store (id , p )
346+ return C.struct_BridgeBinary {
347+ data : unsafe .Pointer (& b [0 ]),
348+ len : C .longlong (len (b )),
349+ handle : C .uint64_t (id ),
350+ }
351+ }
352+
353+ // BridgeReleaseBinary releases a pin from pinBinary. Called exactly once per
354+ // BridgeCallBinary result: immediately after the caller copies the bytes, or
355+ // from the external buffer's finalizer when V8 is done with the zero-copy view.
356+ //
357+ //export BridgeReleaseBinary
358+ func BridgeReleaseBinary (handle C.uint64_t ) {
359+ if p , ok := binPinners .LoadAndDelete (uint64 (handle )); ok {
360+ p .(* runtime.Pinner ).Unpin ()
361+ }
340362}
341363
342364// BridgeDisposeSession closes the session and releases all refs.
0 commit comments