GoSuda

HQQ: Isolating C Libraries from Go Over Cache-Line Shared Memory

By Lemon Mint
views ...

gosuda.org/hqq is een zero-cgo inter-process communication runtime en code generation toolchain voor Go en C11. Het isoleert native C libraries binnen afzonderlijke OS worker processes en verbindt ze met Go callers via memory-mapped, cache-line-aligned shared-memory rings. Het project combineert twee transport tiers (StandardLink voor dedicated single-producer single-consumer sessions en Advanced voor multi-worker MPMC dispatch), de hqq-bindgen IDL compiler, en een tamper-verified process supervisor (secureexec en launch).

1go get gosuda.org/hqq

The Cost of Sharing an Address Space with C

Elke Go systems engineer komt vroeg of laat op dezelfde tweesprong terecht. U bouwt een Go service die binnen enkele seconden compileert met CGO_ENABLED=0, wordt geleverd als een enkele statische binary, cross-compileert van een laptop naar Linux of Windows targets zonder sysroot, en draait binnen een memory-safe garbage-collected heap. Dan heeft u een mature C library nodig: SQLite voor embedded relational storage, Zstandard (libzstd) voor dictionary compression, of een hardware-accelerated media- of cryptographylibrary.

Het inschakelen van cgo om die C library te linken in het Go process lost het onmiddellijke bindingsprobleem op terwijl het drie structurele aansprakelijkheden introduceert:

  1. Toolchain and Cross-Compilation Coupling: CGO_ENABLED=1 vereist een C compiler, target libc headers, en een platform linker voor elk target OS en elke architecture. Het bouwen van linux/arm64 of windows/arm64 artifacts vanaf een darwin/arm64 workstation verandert een enkel go build commando in een matrix van cross-compilers en sysroots.
  2. Runtime Scheduling Overhead: Elke cgo call kruist de Go runtime boundary, schakelt over van een growable goroutine stack naar een system stack, en pint een OS thread (M). Voor fine-grained calls accumuleren de transition tax (~50 tot 150 ns per call) en scheduler contention snel.
  3. Fault Domain Collapse: C code draait in dezelfde virtual address space als de Go runtime. Een enkel out-of-bounds write, use-after-free, stack overflow, of abort() binnen een C function corrumpeert Go heap metadata of beëindigt het gehele serverprocess onmiddellijk, waarbij recover() wordt omzeild en elke in-flight HTTP of RPC request met zich meekomt.

Wanneer teams proberen om cgo te vermijden, dwingen de gangbare alternatieven een ander compromis af. Pure-Go mechanical translations van C codebases behouden CGO_ENABLED=0, maar lopen vaak achter op upstream C releases, dragen een emulated virtual memory layer met zich mee, en alloceren zwaar onder load. Het verplaatsen van de C library naar een helper process via loopback TCP of Unix domain sockets herstelt fault isolation, maar elke call betaalt voor twee kernel context switches, socket buffer copies, en wire serialization.

We hebben HQQ gebouwd na het stellen van een directe vraag: wat als de C library in een afzonderlijk OS process zou leven met zijn eigen virtual address space, terwijl Go en C frames direct uitwisselden via shared CPU cache lines zonder de kernel binnen te gaan op het fast path?

Integration ModelCGO_ENABLED=0Fault IsolationFast-Path SyscallsPer-Call Heap AllocationsUpstream C11 Fidelity
cgo In-Process LinkingNeeGeen (gedeelde heap)00 tot 1+Exacte upstream C
Socket / Pipe Subprocess RPCJaProcess boundary2 tot 4Meervoudig (framing + I/O)Exacte upstream C
Mechanical C-to-Go TranslationJaIn-process Go0Hoog bij complexe callsEmulated C runtime
HQQ Shared-Memory IPCJaProcess boundary0 (adaptive spin)0 (StandardLink)Exacte upstream C

Architectural Overview: Four Layers Across Process Boundaries

HQQ scheidt process isolation in vier composable layers zodat shared-memory ring mechanics, typed RPC code generation, binary integrity verification, and child process supervision onafhankelijk blijven.

 1+-------------------------------------------------------------------+
 2| Go Application Process (CGO_ENABLED=0)                            |
 3|                                                                   |
 4|  +-----------------------+     +-------------------------------+  |
 5|  | Generated Go Client   |     | Process Supervisor            |  |
 6|  | (gosuda.org/hqq/      |     | (gosuda.org/hqq/launch +      |  |
 7|  |  bindgen/rt)          |     |  gosuda.org/hqq/secureexec)   |  |
 8|  +-----------+-----------+     +---------------+---------------+  |
 9|              |                                 |                  |
10|  +-----------v-----------+                     | Anonymous Pipe   |
11|  | Transport Tier        |                     | Lifeline         |
12|  | (StandardLink /       |                     | (--parent-stdin) |
13|  |  Advanced MPMC)       |                     |                  |
14+--------------+---------------------------------+------------------+
15               |                                 |
16===============|========= Shared Memory =========|===================
17               |     (mmap / CreateFileMapping)  |
18+--------------v---------------------------------v------------------+
19| C11 Worker Process (Isolated Address Space)                       |
20|                                                                   |
21|  +-----------------------+     +-------------------------------+  |
22|  | Generated C11 Server  |---->| Generation-Checked Handle     |  |
23|  | & Batch Shims         |     | Table (hqq_rpc.c)             |  |
24|  +-----------+-----------+     +---------------+---------------+  |
25|              |                                 |                  |
26|              +----------------+----------------+                  |
27|                               |                                   |
28|                     +---------v---------+                         |
29|                     | Native C Library  |                         |
30|                     | (SQLite, Zstd...) |                         |
31|                     +-------------------+                         |
32+-------------------------------------------------------------------+

1. Shared-Memory Transport (gosuda.org/hqq and c/)

Het base package gosuda.org/hqq en de C11 tegenhanger daarvan in c/ implementeren twee shared-memory transport modi over POSIX mmap (Linux en macOS) en Win32 CreateFileMappingW (Windows). Elk shared segment begint met een geverifieerd HQQ_STL3 superblock dat protocol version, buffer geometry, en endpoint PIDs vastzet voordat een ring index wordt gelezen.

Alle hot control words en slot headers zijn uitgelijnd op 64-byte cache-line boundaries zodat producer en consumer cores nooit contenden om dezelfde cache line via false sharing.

1StandardLink Slot Layout (64-Byte Cache-Line Aligned)
2Byte Offset:  0x00                0x08                0x10                          0x40
3              +-------------------+-------------------+-----------------------------+
4Header Line:  | seq (u64 atomic)  | len (u32) | flags | reserved (zero-padded)      |
5              +-------------------+-------------------+-----------------------------+
6Payload Area: | payload bytes [0 .. BufferSize - 1] (directly encoded / decoded)    |
7              +---------------------------------------------------------------------+
FieldOffsetSizeSemantics
seq0x008 BMonotonic sequence counter. Oneven waarden markeren een in-progress write; even waarden publiceren een committed frame via store-release.
len0x084 BPayload byte length geschreven in het slot (0 <= len <= BufferSize).
flags0x0C4 BFrame control bits (continuation, error status, of shutdown notice).
payload0x40BufferSizeContiguous payload region gelezen en geschreven in place zonder intermediate copies.

Callers kiezen tussen twee transport tiers afhankelijk van de concurrency structure:

  • StandardLink (1:1 SPSC Session): Koppelt een client en een server session over dedicated request en response slots beschermd door een sequence lock (SeqLock). Op het fast path serialiseert de writer direct in het gemapte slot en commit met een atomic store-release; de reader spint kort in user space alvorens terug te vallen op OS wake primitives (futex op Linux, __ulock_wait op macOS, WaitOnAddress op Windows). Een single-frame round trip voltooiing duurt ~200 ns met 0 allocs/op.
  • Advanced (MPMC Dispatch Ring): Multiplecteert M concurrent Go callers over N C worker threads met behulp van een lock-free multi-producer multi-consumer submission ring, een shared slot pool, en een indexed completion table. Callers reserveren een slot, publiceren de slot index naar de ring, en wachten op hun completion entry terwijl worker threads requests uit de queue halen en afhandelen out of order.

Beide tiers volgen peer liveness continu. Elk endpoint werkt een atomic monotonic timestamp bij via Heartbeat() en inspecteert PeerAlive(timeout) tijdens het wachten. Als een worker process crasht mid-call, detecteert de peer de stale heartbeat of dode PID onmiddellijk, breekt geblokkeerde wachten af met ErrPeerGone, en ondersteunt atomische session Takeover zonder wedged shared-memory files achter te laten.

2. Zero-Allocation RPC and Generation-Checked Handles (gosuda.org/hqq/bindgen)

Het met de hand schrijven van shared-memory framing voor een C library met tientallen functies is saai en foutgevoelig. hqq-bindgen is een schema compiler die een Interface Definition Language (.idl) file leest (of er een bootstrapt direct vanuit een C .h header via hqq-bindgen init) en een zero-cgo Go client package uititst naast een C11 server skeleton.

De gevaarlijkste fout in elke C binding is het exposeren van raw C pointers (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) over de language boundary heen. Als een caller een pointer doorgeeft nadat deze is vrijgegeven, een pointer doorgeeft die toebehoort aan een andere session, of een integer address corrumpeert, dereferenceert de C library ongeldig geheugen.

hqq-bindgen verbiedt raw pointers op de wire. In plaats daarvan modeleert de IDL stateful C resources als getypte handle declaraties:

1service sqlite3 {
2    handle Db;
3    handle Stmt @owned_by(Db);
4
5    fn open_v2(path: string, flags: i32) -> result<Db, i32>;
6    fn prepare_one(db: Db, sql: string) -> result<Stmt, i32>;
7    fn finalize(stmt: @consume Stmt) -> result<void, i32>;
8    fn close(db: @consume Db) -> result<void, i32>;
9}

Op de wire is elke handle een 64-bit integer die een 32-bit table index inpakt en een 32-bit non-zero generation counter: (uint64_t)generation << 32 | index. De C runtime (c/src/hqq_rpc.c) onderhoudt een per-session handle table met drie invarianten:

  1. Generation Validation: Elke lookup verifieert dat het slot bezet is, dat de opgeslagen generation overeenkomt met de bovenste 32 bits van de handle, en dat de handle type tag overeenkomt met het parameter type (Db vs. Stmt). Het hergebruiken van een gesloten handle faalt onmiddellijk met HQQRPC_E_BAD_HANDLE alvorens aan te roepen in de C library.
  2. Hierarchical Ownership (@owned_by): Omdat Stmt is geannoteerd met @owned_by(Db), loopt het sluiten van een Db handle automatisch door alle live child Stmt handles die eigendom zijn van die Db en draait het hun destructors in omgekeerde allocatievolgorde (sqlite3_finalize vóór sqlite3_close_v2).
  3. Session Teardown Sweep: Als een Go client verbinding verbreidt of crasht zonder zijn handles te sluiten, veegt de session destructor van de C server de hele session handle table in omgekeerde volgorde weg, wat nul gelekte C allocaties garandeert over client restarts heen.

Voordat een session zijn eerste RPC call verwerkt, wisselen de client en server een 32-byte SHA-256 interface digest uit die is berekend over de genormaliseerde IDL (HQQ-BINDGEN-IDL-V1\n prefix). Als de Go client en C worker zijn gecompileerd uit incompatibele IDL schemas, weigert de handshake de verbinding bij bootstrap.

3. Tamper-Free Binary Execution (gosuda.org/hqq/secureexec)

Het verplaatsen van C code naar een worker binary roept een supply-chain vraag op: hoe weet het Go process dat de worker binary op schijf niet is gewijzigd of vervangen tussen verificatie en uitvoering?

Het aanroepen van crypto/sha256 op een file path gevolgd door os/exec.Command(path).Start() laat een Time-of-Check to Time-of-Use (TOCTOU) window achter waar een ander process het bestand kan overschrijven of verwisselen voordat de kernel het uitvoert. gosuda.org/hqq/secureexec sluit dit window:

  • Op Linux opent secureexec de binary, streamt de inhoud ervan naar een anonymous memory file descriptor gemaakt via memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING) terwijl het de SHA-256 hash berekent, verifieert de hash tegen een gepinde 32-byte digest of een Ed25519-signed release manifest (cmd/hqq-manifest), past fcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE) toe zodat geen enkele process op het systeem de pagina's kan muteren, en spawnt het child vanuit de sealed descriptor.
  • Op macOS en Windows materialiseert secureexec geverifieerde binaries in permission-locked cache directories gesleuteld op SHA-256 digest en verifieert file size en digest integrity voorafgaand aan launch.

4. Parent-Lifeline Process Supervision (gosuda.org/hqq/launch)

Wanneer een Go application een background C worker process spawnt, mag de worker zijn ouder nooit overleven als een orphan process, zelfs niet als de Go ouder wordt beëindigd met SIGKILL (kill -9) en geen defer cleanup handlers kan uitvoeren.

gosuda.org/hqq/launch afdwingt lifetime coupling via een anonymous OS pipe doorgegeven aan de standard input van de worker met de --parent-stdin vlag. De Go ouder houdt het write-uiteinde van de pipe vast en schreef er nooit naar; de C worker bewaakt het read-uiteinde op een dedicated lifeline thread. Als het Go process normaal afsluit, panikt, of wordt gedood door de OS kernel (SIGKILL of OOM killer), sluit de kernel het write-descriptor onmiddellijk. De C worker ontvangt EOF op de pipe binnen microseconden, onderbreekt actieve calls, voert zijn session handle destructors uit, unlinked de shared-memory files, en sluit netjes af.

Quickstart: Calling a Generated Service

Met hqq-bindgen en launch ziet het aanroepen van een geïsoleerde C worker vanuit Go eruit als een reguliere getypte Go client met behoud van CGO_ENABLED=0:

 1package main
 2
 3import (
 4	"context"
 5	"fmt"
 6	"log"
 7	"time"
 8
 9	"gosuda.org/hqq/bindgen/rt"
10	"gosuda.org/hqq/launch"
11	"gosuda.org/hqq/secureexec"
12)
13
14func main() {
15	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
16	defer cancel()
17
18	hash, err := secureexec.ParseHash("7649b1b45dacd934cde8149f35f4fcc3937f6486fbac23ce976466719b3752bb")
19	if err != nil {
20		log.Fatal(err)
21	}
22
23	listener, err := launch.NewListenerName("demo")
24	if err != nil {
25		log.Fatal(err)
26	}
27
28	srv, err := launch.Start(ctx, launch.Config{
29		Binary:   "/path/to/verified-worker",
30		SHA256:   hash,
31		Listener: listener,
32		Args:     []string{listener},
33	})
34	if err != nil {
35		log.Fatal(err)
36	}
37	defer srv.Close()
38
39	fmt.Printf("worker running on shared-memory listener %s (opts=%+v)\n", listener, rt.SessionOptions{})
40}

Repository Structure

Elk subsysteem in gosuda.org/hqq is georganiseerd in gerichte pakketten met expliciete grenzen:

 1gosuda.org/hqq
 2├── standard.go / ring.go      # StandardLink (SPSC) en low-level shared-memory ring primitives
 3├── advanced.go / slotpool.go  # Advanced (MPMC) dispatcher, slot pool, en completion table
 4├── shm_*.go / wait_*.go       # OS-specifieke mmap/CreateFileMapping en futex/ulock/WaitOnAddress
 5├── c/                         # C11 transport library (hqq.c) en RPC server runtime (hqq_rpc.c)
 6├── bindgen/                   # hqq-bindgen IDL parser, verifier (V1-V41), en Go/C11 code generator
 7│   └── rt/                    # Zero-allocation Go client runtime voor gegenereerde services
 8├── secureexec/                # SHA-256 / Ed25519 manifest verification en memfd-sealed execution
 9├── launch/                    # Worker process supervisor met --parent-stdin lifeline management
10├── cmd/hqq-manifest/          # CLI tool voor het genereren en ondertekenen van release binary manifests
11└── spec/                      # Executable wire specifications en golden test vectors (BINDGEN.md)

Om te zien hoe HQQ drop-in production replacements aandrijft voor twee van de meest gebruikte cgo libraries in het Go ecosysteem, bekijk de bijbehorende pakketten gebouwd op deze stack:

  • gosuda.org/sqlite3: Drop-in database/sql driver compatibel met github.com/mattn/go-sqlite3, die native C SQLite 3.54.1 over shared memory draait met gepipelineerde batch prefetching.
  • gosuda.org/zstd: Drop-in vervanging voor github.com/DataDog/zstd, die native C libzstd 1.5.7 draait in een geïsoleerde shared-memory worker met zero-allocation contexts en bulk batching.
  • GitHub Repository: https://github.com/gosuda/HQQ