HQQ: C-bibliotheken isoleren van Go via gedeeld geheugen op cachelijnniveau

gosuda.org/hqq is een zero-cgo runtime voor interprocescommunicatie en een toolchain voor codegeneratie voor Go en C11. Het isoleert native C-bibliotheken in afzonderlijke OS-workerprocessen en verbindt deze met Go-aanroepers via geheugengemapte, op cache-lijnen uitgelijnde shared-memory ringbuffers. Het project combineert twee transportlagen (StandardLink voor toegewijde single-producer single-consumer sessies en Advanced voor multi-worker MPMC-dispatch), de hqq-bindgen IDL-compiler, en een door middel van integriteitscontrole geverifieerde processupervisor (secureexec en launch).
1go get gosuda.org/hqq
De kosten van het delen van een adresruimte met C
Elke Go-systeemingenieur komt vroeg of laat voor dezelfde splitsing te staan. U bouwt een Go-service die in enkele seconden compileert met CGO_ENABLED=0, als één statisch binair bestand wordt uitgeleverd, vanaf een laptop zonder sysroot kruiscompileert naar Linux- of Windows-doelen, en binnen een geheugenveilige, door een garbage collector beheerde heap draait. Vervolgens hebt u een volwassen C-bibliotheek nodig: SQLite voor ingebedde relationele opslag, Zstandard (libzstd) voor compressie met woordenboeken, of een hardwareversneld media- of cryptografiebibliotheek.
Het inschakelen van cgo om die C-bibliotheek in het Go-proces te linken lost het directe bindingsprobleem op, maar introduceert drie structurele nadelen:
- Koppeling van toolchain en kruiscompilatie:
CGO_ENABLED=1vereist een C-compiler, headers van de doel-libc en een platformlinker voor elk doelbesturingssysteem en elke doelarchitectuur. Het bouwen vanlinux/arm64- ofwindows/arm64-artefacten vanaf eendarwin/arm64-werkstation maakt van ééngo build-commando een matrix van kruiscompilers en sysroots. - Overhead van runtime-scheduling: Elke
cgo-aanroep passeert de grens van de Go-runtime, schakelt van een groeibare goroutine-stack naar een systeemstack en koppelt een OS-thread (M) vast. Bij fijnmazige aanroepen stapelen de overgangskosten (ongeveer 50 tot 150 ns per aanroep) en de schedulercontentie zich snel op. - Instorting van de fout-domein: C-code draait in dezelfde virtuele adresruimte als de Go-runtime. Een enkele out-of-bounds-schrijfactie, use-after-free, stack-overflow of
abort()in een C-functie beschadigt de metadata van de Go-heap of beëindigt het volledige serverproces onmiddellijk, waarbijrecover()wordt omzeild en alle lopende HTTP- of RPC-verzoeken worden meegesleept.
Wanneer teams cgo proberen te vermijden, dwingen de gangbare alternatieven tot een andere afweging. Pure-Go mechanische vertalingen van C-codebases behouden CGO_ENABLED=0, maar lopen vaak achter op upstream C-releases, bevatten een geëmuleerde virtuele geheugenlaag en alloceren zwaar onder belasting. Het verplaatsen van de C-bibliotheek naar een helperproces via loopback-TCP of Unix-domeinsockets herstelt de foutisolatie, maar elke aanroep kost twee kernel-contextwisselingen, kopieën van socketbuffers en wire-serialisatie.
Wij hebben HQQ gebouwd nadat we een directe vraag hadden gesteld: wat als de C-bibliotheek in een afzonderlijk OS-proces met een eigen virtuele adresruimte zou leven, terwijl Go en C frames rechtstreeks uitwisselen via gedeelde CPU-cache-lijnen zonder op het snelle pad de kernel te betreden?
| Integratiemodel | CGO_ENABLED=0 | Foutisolatie | Syscalls op het snelle pad | Heap-allocaties per aanroep | Upstream C11-getrouwheid |
|---|---|---|---|---|---|
cgo In-Process Linking | Nee | Geen (gedeelde heap) | 0 | 0 tot 1+ | Exact upstream C |
| Socket / Pipe Subprocess RPC | Ja | Procesgrens | 2 tot 4 | Meerdere (framing + I/O) | Exact upstream C |
| Mechanische C-naar-Go-vertaling | Ja | In-process Go | 0 | Hoog bij complexe aanroepen | Geëmuleerde C-runtime |
| HQQ Shared-Memory IPC | Ja | Procesgrens | 0 (adaptief spinnen) | 0 (StandardLink) | Exact upstream C |
Architectuuroverzicht: vier lagen over procesgrenzen heen
HQQ scheidt procesisolatie in vier combineerbare lagen, zodat de mechaniek van shared-memory-ringbuffers, getypeerde RPC-codegeneratie, verificatie van binaire integriteit en supervisie van kindprocessen 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 en c/)
Het basispakket gosuda.org/hqq en het C11-equivalent in c/ implementeren twee shared-memory-transportmodi via POSIX mmap (Linux en macOS) en Win32 CreateFileMappingW (Windows). Elk gedeeld segment begint met een geverifieerde HQQ_STL3-superblock die de protocolversie, de geometrie van de buffers en de endpoint-PID's vastlegt voordat enige ringindex wordt gelezen.
Alle hot control words en slotheaders zijn uitgelijnd op grenzen van 64 bytes (cache-lijnen), zodat producer- en consumercores nooit om dezelfde cache-lijn concurreren 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 +---------------------------------------------------------------------+
| Veld | Offset | Grootte | Betekenis |
|---|---|---|---|
seq | 0x00 | 8 B | Monotone sequentieteller. Oneven waarden markeren een schrijfactie in uitvoering; even waarden publiceren een vastgelegd frame via store-release. |
len | 0x08 | 4 B | Lengte in bytes van de payload die in de slot is geschreven (0 <= len <= BufferSize). |
flags | 0x0C | 4 B | Besturingsbits van het frame (vervolg, foutstatus of melding van afsluiting). |
payload | 0x40 | BufferSize | Aaneengesloten payloadgebied dat ter plaatse wordt gelezen en geschreven zonder tussentijdse kopieën. |
Aanroepers kiezen tussen twee transportlagen, afhankelijk van de concurrentiestructuur:
StandardLink(1:1 SPSC-sessie): Koppelt een client- en een serversessie via toegewijde verzoek- en antwoordslots die worden beschermd door een sequence lock (SeqLock). Op het snelle pad serialiseert de schrijver rechtstreeks in de gemapte slot en legt vast met een atomaire store-release; de lezer spint kort in gebruikersruimte voordat hij terugvalt op OS-wekprimitieven (futexop Linux,__ulock_waitop macOS,WaitOnAddressop Windows). Een enkele frame-round-trip voltooit in ongeveer 200 ns met0 allocs/op.Advanced(MPMC-dispatchring): MultiplextMgelijktijdige Go-aanroepers overNC-workerthreads met behulp van een lock-free multi-producer multi-consumer-indieningsring, een gedeelde slotpool en een geïndexeerde completion-tabel. Aanroepers reserveren een slot, publiceren de slotindex naar de ring en wachten op hun completion-entry, terwijl workerthreads verzoeken uit de ring halen en deze buiten volgorde afhandelen.
Beide lagen volgen de levendigheid van de peer doorlopend. Elk endpoint werkt via Heartbeat() een atomaire, monotone tijdstempel bij en controleert tijdens het wachten PeerAlive(timeout). Als een workerproces midden in een aanroep crasht, detecteert de peer de verouderde heartbeat of de dode PID onmiddellijk, breekt blokkerende wachtacties af met ErrPeerGone en ondersteunt atomaire sessieovername via Takeover, zonder achtergebleven shared-memory-bestanden.
2. Zero-allocation RPC en handles met generatiecontrole (gosuda.org/hqq/bindgen)
Shared-memory-framing met de hand schrijven voor een C-bibliotheek met tientallen functies is vervelend en foutgevoelig. hqq-bindgen is een schemacompiler die een Interface Definition Language-bestand (.idl) leest (of er direct een opzet van maakt vanuit een C-.h-header via hqq-bindgen init) en een zero-cgo Go-clientpakket genereert, samen met een C11-serverskelet.
De gevaarlijkste fout bij elke C-binding is het blootstellen van ruwe C-pointers (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) over de taalgrens heen. Als een aanroeper een pointer doorgeeft nadat deze is vrijgegeven, een pointer doorgeeft die aan een andere sessie toebehoort, of een geheugenadres als geheel getal beschadigt, dereferentieert de C-bibliotheek ongeldig geheugen.
hqq-bindgen verbiedt ruwe pointers op de wire. In plaats daarvan modelleert de IDL stateful C-resources als getypeerde 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-bits geheel getal dat een 32-bits tabelindex en een 32-bits generatieteller die niet nul is, bevat: (uint64_t)generation << 32 | index. De C-runtime (c/src/hqq_rpc.c) onderhoudt per sessie een handletabel met drie invarianten:
- Generatievalidatie: Elke opzoekactie verifieert dat de slot bezet is, dat de opgeslagen generatie overeenkomt met de bovenste 32 bits van de handle, en dat het typelabel van de handle overeenkomt met het parametertype (
DbversusStmt). Het hergebruiken van een gesloten handle mislukt onmiddellijk metHQQRPC_E_BAD_HANDLE, nog voordat de C-bibliotheek wordt aangeroepen. - Hiërarchisch eigendom (
@owned_by): OmdatStmtis geannoteerd met@owned_by(Db), doorloopt het sluiten van eenDb-handle automatisch alle levende kind-Stmt-handles die aan dieDbtoebehoren, en voert hun destructors uit in omgekeerde volgorde van allocatie (sqlite3_finalizevóórsqlite3_close_v2). - Opruimingsveegactie bij sessie-einde: Als een Go-client de verbinding verbreekt of crasht zonder zijn handles te sluiten, veegt de destructor van de C-serversessie de volledige handletabel van de sessie in omgekeerde volgorde leeg, waarmee nul gelekte C-allocaties over clientherstarts heen wordt gegarandeerd.
Voordat een sessie zijn eerste RPC-aanroep verwerkt, wisselen client en server een 32-byte SHA-256-interfacedigest uit, berekend over de gecanonicaliseerde IDL (met voorvoegsel HQQ-BINDGEN-IDL-V1\n). Als de Go-client en de C-worker zijn gecompileerd vanuit onverenigbare IDL-schema's, wijst de handshake de verbinding af bij de opstart.
3. Manipulatievrije binaire uitvoering (gosuda.org/hqq/secureexec)
Het verplaatsen van C-code naar een workerbinair bestand roept een vraag over de toeleveringsketen op: hoe weet het Go-proces dat het workerbestand op schijf niet is gewijzigd of vervangen tussen verificatie en uitvoering?
Het aanroepen van crypto/sha256 op een bestandspad, gevolgd door os/exec.Command(path).Start(), laat een Time-of-Check to Time-of-Use (TOCTOU)-venster open waarin een ander proces het bestand kan overschrijven of verwisselen voordat de kernel het uitvoert. gosuda.org/hqq/secureexec sluit dit venster:
- Op Linux opent
secureexecde binaire, streamt de inhoud naar een anonieme geheugenbestandsdescriptor die is aangemaakt viamemfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING), terwijl de SHA-256-hash wordt berekend, verifieert de hash tegen een vastgelegde 32-byte digest of een door Ed25519 ondertekend releasemanifest (cmd/hqq-manifest), pastfcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)toe zodat geen enkel proces op het systeem de pagina's kan wijzigen, en start het kindproces vanaf de verzegelde descriptor. - Op macOS en Windows materialiseert
secureexecgeverifieerde binaire bestanden in cachemappen met vergrendelde rechten, die worden geïdentificeerd op basis van de SHA-256-digest, en verifieert de bestandsgrootte en digestintegriteit vóór de start.
4. Supervisie van kindprocessen via een ouder-lifeline (gosuda.org/hqq/launch)
Wanneer een Go-applicatie een C-workerproces op de achtergrond start, mag de worker nooit als wees na zijn ouder blijven voortbestaan, zelfs wanneer het Go-ouderproces wordt beëindigd met SIGKILL (kill -9) en geen defer-opruimhandlers kan uitvoeren.
gosuda.org/hqq/launch dwingt levenscycluskoppeling af via een anonieme OS-pipe die met de vlag --parent-stdin aan de standaardinvoer van de worker wordt doorgegeven. Het Go-ouderproces houdt het schrijfeinde van de pipe vast en schrijft er nooit naartoe; de C-worker bewaakt het leeseinde op een speciale lifeline-thread. Als het Go-proces normaal afsluit, in paniek raakt of door de kernel van het besturingssysteem wordt gedood (SIGKILL of de OOM-killer), sluit de kernel het schrijfdescriptor onmiddellijk. De C-worker ontvangt binnen microseconden EOF op de pipe, onderbreekt eventuele actieve aanroepen, voert zijn destructors van sessiehandles uit, verwijdert de shared-memory-bestanden en beëindigt zich netjes.
Snelstart: een gegenereerde service aanroepen
Met hqq-bindgen en launch lijkt het aanroepen van een geïsoleerde C-worker vanuit Go op een gewone getypeerde Go-client, terwijl CGO_ENABLED=0 behouden blijft:
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}
Repositorystructuur
Elk subsysteem in gosuda.org/hqq is georganiseerd in gerichte pakketten met expliciete grenzen:
1gosuda.org/hqq
2├── standard.go / ring.go # StandardLink (SPSC) and low-level shared-memory ring primitives
3├── advanced.go / slotpool.go # Advanced (MPMC) dispatcher, slot pool, and completion table
4├── shm_*.go / wait_*.go # OS-specific mmap/CreateFileMapping and futex/ulock/WaitOnAddress
5├── c/ # C11 transport library (hqq.c) and RPC server runtime (hqq_rpc.c)
6├── bindgen/ # hqq-bindgen IDL parser, verifier (V1-V41), and Go/C11 code generator
7│ └── rt/ # Zero-allocation Go client runtime for generated services
8├── secureexec/ # SHA-256 / Ed25519 manifest verification and memfd-sealed execution
9├── launch/ # Worker process supervisor with --parent-stdin lifeline management
10├── cmd/hqq-manifest/ # CLI tool for generating and signing release binary manifests
11└── spec/ # Executable wire specifications and golden test vectors (BINDGEN.md)
Om te zien hoe HQQ drop-in productievervangers aandrijft voor twee van de meest gebruikte cgo-bibliotheken in het Go-ecosysteem, bekijkt u de bijbehorende pakketten die op deze stack zijn gebouwd:
gosuda.org/sqlite3: Drop-indatabase/sql-driver compatibel metgithub.com/mattn/go-sqlite3, die native C-SQLite3.54.1via shared memory uitvoert met gepijplijnde batch-prefetching.gosuda.org/zstd: Drop-in vervanging vangithub.com/DataDog/zstd, die native Clibzstd 1.5.7uitvoert in een geïsoleerde shared-memory-worker met zero-allocation-contexten en bulkbatching.- GitHub Repository: https://github.com/gosuda/HQQ