HQQ: Isolering av C-bibliotek från Go via Cache-Line Shared Memory
gosuda.org/hqq är en zero-cgo inter-process communication-körtid och kodgenereringsverktygskedja för Go och C11. Den isolerar ursprungliga C-bibliotek i separata OS-arbetsprocesser och ansluter dem till Go-anropare över minnesmappade, cache-linjeinriktade delade minnesringar. Projektet kombinerar två transportnivåer (StandardLink för dedikerade sessioner med en enskild producent och en enskild konsument samt Advanced för MPMC-dispatch med flera arbetare), hqq-bindgen IDL-kompilatorn och en manipulering verifierad processövervakare (secureexec och launch).
1go get gosuda.org/hqq
Kostnaden för att dela en adressrymd med C
Varje Go-systemingenjör når förr eller senare samma vägskäl. Du bygger en Go-tjänst som komilerar på sekunder med CGO_ENABLED=0, levereras som en enda statisk binärfil, korskompilerar från en bärbar dator till Linux- eller Windows-mål utan en sysroot, och körs inom en minnessäker skräpinsamlad heap. Sedan behöver du ett moget C-bibliotek: SQLite för inbäddad relationell lagring, Zstandard (libzstd) för ordlistekomprimering, eller ett hårdvaruaccelererat media- eller kryptografibibliotek.
Att aktivera cgo för att länka det C-biblioteket till Go-processen löser det omedelbara bindningsproblemet samtidigt som det introducerar tre strukturella skulder:
- Koppling av verktygskedja och korskompilering:
CGO_ENABLED=1kräver en C-kompilator, målets libc-huvuden och en plattformslänkare för varje målområde och arkitektur. Att byggalinux/arm64- ellerwindows/arm64-artefakter från endarwin/arm64-arbetsstation förvandlar ett endago build-kommando till en matris av korskompilatorer och sysroots. - Körningsschemaläggningsomkostnad: Varje
cgo-anrop korsar Go-körningens gräns, växlar från en växas-bar goroutinstack till en systemstack och fäster en OS-tråd (M). För finkorniga anrop ackumuleras övergångsskatten (~50 till 150 ns per anrop) och schemaläggarkonkurrensen snabbt. - Kollaps av feltillstånd: C-kod körs i samma virtuella adressrymd som Go-körningen. En enda skrivning utanför gränserna, användning efter frigöring, stackspill eller
abort()inuti en C-funktion korrumperar Go-heapens metadata eller avslutar hela serverprocessen omedelbart, vilket kringgårrecover()och tar varje pågående HTTP- eller RPC-begäran med sig.
När team försöker undvika cgo tvingar de vanliga alternativen fram en annan kompromiss. Rena Go-mekaniska översättningar av C-kodbaser bevarar CGO_ENABLED=0, men släpar ofta efter uppströms C-releaser, bär på ett emulerat virtuellt minneslager och allokerar kraftigt under belastning. Att flytta C-biblioteket till en hjälpprocess över loopback-TCP eller Unix-domänsocklar återställer feltolerans, men varje anrop betalar för två kärnkontextväxlingar, sockelbuffertkopior och trådserialisering.
Vi byggde HQQ efter att ha ställt en direkt fråga: tänk om C-biblioteket levde i en separat OS-process med sin egen virtuella adressrymd, medan Go och C utbytte ramar direkt genom delade CPU-cache-linjer utan att gå in i kärnan på snabbvägen?
| Integrationsmodell | CGO_ENABLED=0 | Feltolerans | Systemanrop på snabbvägen | Heap-allokeringar per anrop | Uppströms C11-trohet |
|---|---|---|---|---|---|
cgo In-Process-länkning | Nej | Ingen (delad heap) | 0 | 0 till 1+ | Exakt uppströms C |
| Sockel- / rör-underprocess-RPC | Ja | Processgräns | 2 till 4 | Flera (inramning + I/O) | Exakt uppströms C |
| Mekanisk C-till-Go-översättning | Ja | In-process Go | 0 | Hög vid komplexa anrop | Emulerad C-körning |
| HQQ Delat-minne-IPC | Ja | Processgräns | 0 (adaptiv snurrning) | 0 (StandardLink) | Exakt uppströms C |
Arkitektonisk översikt: Fyra lager över processgränser
HQQ separerar processisolering i fyra komponerbara lager så att mekaniken för delat minne, typad RPC-kodgenerering, verifiering av binär integritet och övervakning av barnprocesser förblir oberoende.
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. Transport för delat minne (gosuda.org/hqq och c/)
Baspaketet gosuda.org/hqq och dess C11-motsvarighet i c/ implementerar två transportlägen för delat minne över POSIX mmap (Linux och macOS) och Win32 CreateFileMappingW (Windows). Varje delat segment börjar med en verifierad HQQ_STL3-superblock som fäster protokollversion, buffertgeometri och slutpunkts-PID:er innan något ringindex läsas.
Alla heta kontrollord och fackhuvuden är riktade mot 64-bytes cache-linjegränser så att producent- och konsumentkärnor aldrig konkurrerar om samma cache-linje genom falsk delning.
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 +---------------------------------------------------------------------+
| Fält | Offset | Storlek | Semantik |
|---|---|---|---|
seq | 0x00 | 8 B | Monoton sekvensräknare. Udda värden markerar en pågående skrivning; jämna värden publicerar en bekräftad ram via store-release. |
len | 0x08 | 4 B | Nyttolastens bytlängd som skrivs in i facket (0 <= len <= BufferSize). |
flags | 0x0C | 4 B | Ramkontrollbitar (fortsättning, felstatus eller avstängningsmeddelande). |
payload | 0x40 | BufferSize | Sammanhängande nyttolastområde som läses och skrivs på plats utan mellanliggande kopior. |
Anropare väljer mellan två transportnivåer beroende på samtidighetstruktur:
StandardLink(1:1 SPSC-session): Kopplar ihop en klient- och en serversession över dedikerade begäran- och svarssfack skyddade av ett sekvenslås (SeqLock). På snabbvägen serialiserar skribenten direkt till det mappade facket och bekräftar med en atomär store-release; läsaren snurrar kort i användarrymden innan den faller tillbaka till OS-väckningsprimitiver (futexpå Linux,__ulock_waitpå macOS,WaitOnAddresspå Windows). En enkelramsrundresa slutförs på ~200 ns med0 allocs/op.Advanced(MPMC Dispatch Ring): MultiplexarMsamtidiga Go-anropare överNC-arbetartrådar med hjälp av en lås-fri multi-producer multi-consumer inlämningsring, en delad fackpool och en indexerad komplettingstabell. Anropare reserverar ett fack, publicerar fackindexet till ringen och väntar på sin färdigställandepost medan arbetartrådar tar bort kö och slutför begäranden i oordning.
Båda nivåerna spårar peer-livlighet kontinuerligt. Varje slutpunkt uppdaterar en atomär monoton tidsstämpel via Heartbeat() och inspekterar PeerAlive(timeout) under väntan. Om en arbetsprocess kraschar mitt i ett anrop upptäcker motparten den föråldrade hjärtrytmen eller den döda PID:en omedelbart, avbryter blockerade väntningar med ErrPeerGone och stöder atomär sessionsövertagande (Takeover) utan att lämna fastnade delade minnesfiler efter sig.
2. Noll-allokering RPC och genereringskontrollerade handtag (gosuda.org/hqq/bindgen)
Att skriva delat minnesinramning för hand för ett C-bibliotek med dussintals funktioner är tråkigt och felbenäget. hqq-bindgen är en schemakompilator som läser en Interface Definition Language (.idl)-fil (eller startar upp en direkt från ett C .h-huvud via hqq-bindgen init) och avger ett zero-cgo Go-klientpaket tillsammans med ett C11-serverskelett.
Det farligaste misstaget i alla C-bindningar är att exponera råa C-pekare (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) över språkgränsen. Om en anropare skickar en pekare efter att ha frigjort den, skickar en pekare som tillhör en annan session eller korrumperar en heltalsadress, dereferenserar C-biblioteket ogiltigt minne.
hqq-bindgen förbjuder råa pekare på tråden. Istället modellerar IDL tillståndslösa C-resurser som typade handle-deklarationer:
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}
På tråden är varje handtag ett 64-bitars heltal som packar ett 32-bitars tabellindex och en 32-bitars genereringsräknare som inte är noll: (uint64_t)generation << 32 | index. C-körningen (c/src/hqq_rpc.c) upprätthåller en handtagstabell per session med tre invarianser:
- Genereringsvalidering: Varje uppslag verifierar att facket är upptaget, dess lagrade generering matchar de övre 32 bitarna av handtaget och dess handtagstypstagg matchar parametertypen (
Dbvs.Stmt). Att återanvända ett stängt handtag misslyckas omedelbart medHQQRPC_E_BAD_HANDLEinnan det anropas i C-biblioteket. - Hierarkiskt ägande (
@owned_by): EftersomStmtär annoterad med@owned_by(Db), går stängningen av ettDb-handtag automatiskt igenom alla levande underordnadeStmt-handtag som ägs av detDboch kör deras destruktorer i omvänd allokeringsordning (sqlite3_finalizeföresqlite3_close_v2). - Sessionsavslutningsrensning: Om en Go-klient kopplar från eller kraschar utan att stänga sina handtag, rensar C-serverns sessionsdestruktor hela sessionshandtagstabellen i omvänd ordning, vilket garanterar noll läckta C-allokeringar över klientomstarter.
Innan en session bearbetar sitt första RPC-anrop utbyter klienten och servern ett 32-bytes SHA-256-gränssnittsdigest beräknat över den kanoniserade IDL:en (HQQ-BINDGEN-IDL-V1\n-prefix). Om Go-klienten och C-arbetaren kompilerades från inkompatibla IDL-scheman avvisar handskakningen anslutningen vid uppstart.
3. Manipuleringsfri binär körning (gosuda.org/hqq/secureexec)
Att flytta C-kod till en arbetarbinär väcker en leveranskedjefråga: hur vet Go-processen att arbetarbinären på disken inte har modifierats eller ersatts mellan verifiering och körning?
Att anropa crypto/sha256 på en filväg följt av os/exec.Command(path).Start() lämnar ett Time-of-Check to Time-of-Use (TOCTOU)-fönster där en annan process kan skriva över eller byta ut filen innan kärnan kör den. gosuda.org/hqq/secureexec stänger detta fönster:
- På Linux öppnar
secureexecbinärfilen, strömmar dess innehåll till en anonym minnesfildeskriptor skapad viamemfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING)medan den beräknar sin SHA-256-hash, verifierar hashen mot ett fastsatt 32-bytes digest eller ett Ed25519-signerat releasemanifest (cmd/hqq-manifest), tillämparfcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)så att ingen process på systemet kan mutera sidorna, och skapar barnet från den förseglade deskriptorn. - På macOS och Windows materialiserar
secureexecverifierade binärfiler i behörighetslåsta cache-kataloger som är nycklade med SHA-256-digest och verifierar filstorlek och digestintegritet före start.
4. Processövervakning med föräldralivlina (gosuda.org/hqq/launch)
När en Go-applikation skapar en bakgrunds-C-arbetsprocess får arbetaren aldrig överleva sin förälder som en föräldralös process, även om Go-föräldern avslutas med SIGKILL (kill -9) och inte kan köra defer-rensningshanterare.
gosuda.org/hqq/launch framtvingar livslängdskoppling genom ett anonymt OS-rör som skickas till arbetarens standardinmatning med flaggan --parent-stdin. Go-föräldern håller skrivänden av röret och skriver aldrig till den; C-arbetaren övervakar läsänden på en dedikerad livlinetråd. Om Go-processen avslutas normalt, drabbas av panik eller dödas av OS-kärnan (SIGKILL eller OOM-mördaren), stänger kärnan omedelbart skrivdeskriptorn. C-arbetaren tar emot EOF på röret inom mikrosekunder, avbryter alla aktiva anrop, kör sina sessionshandtagsdestruktor, avlänkar de delade minnesfilerna och avslutas rent.
Snabbstart: Att anropa en genererad tjänst
Med hqq-bindgen och launch ser det ut som en vanlig typad Go-klient att anropa en isolerad C-arbetare från Go samtidigt som CGO_ENABLED=0 behålls:
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}
Repositoriets struktur
Varje undersystem i gosuda.org/hqq är organiserat i fokuserade paket med explicita gränser:
1gosuda.org/hqq
2├── standard.go / ring.go # StandardLink (SPSC) och lågnivå-delat-minne-ringprimitiver
3├── advanced.go / slotpool.go # Advanced (MPMC) dispatcher, fackpool och färdigställandetabell
4├── shm_*.go / wait_*.go # OS-specifika mmap/CreateFileMapping och futex/ulock/WaitOnAddress
5├── c/ # C11-transportbibliotek (hqq.c) och RPC-serverkörning (hqq_rpc.c)
6├── bindgen/ # hqq-bindgen IDL-tolk, verifierare (V1-V41) och Go/C11-kodgenerator
7│ └── rt/ # Noll-allokering Go-klientkörning för genererade tjänster
8├── secureexec/ # SHA-256 / Ed25519 manifestverifiering och memfd-förseglad körning
9├── launch/ # Arbetarprocessövervakare med --parent-stdin-livlinjehantering
10├── cmd/hqq-manifest/ # CLI-verktyg för generering och signering av releasemanifest
11└── spec/ # Exekverbara trådspecifikationer och gyllene testvektorer (BINDGEN.md)
För att se hur HQQ driver drop-in-produktionsersättningar för två av de mest använda cgo-biblioteken i Go-ekosystemet, kolla in följdpaketen som byggts på denna stack:
gosuda.org/sqlite3: Drop-indatabase/sql-drivrutin kompatibel medgithub.com/mattn/go-sqlite3, som kör ursprunglig C SQLite3.54.1över delat minne med pipelined batch-pre-fetch.gosuda.org/zstd: Drop-in-ersättning förgithub.com/DataDog/zstd, som kör ursprunglig Clibzstd 1.5.7i en isolerad delat-minne-arbetare med noll-allokeringskontexter och bulk-batchning.- GitHub-repositorium: https://github.com/gosuda/HQQ