---
id: 43974d116e135fd9d3478f658dcf71c6
author: Lemon Mint
title: 'HQQ: Isolamento delle librerie C dal Go tramite memoria condivisa su cache-line'
description: Runtime IPC senza cgo, compilatore IDL (hqq-bindgen) e supervisore di processi verificato che eseguono librerie native C11 in processi worker isolati tramite ring di memoria condivisa.
language: it
date: 2026-10-18T13:56:46.094643498Z
path: /hqq
go_package: gosuda.org/hqq
go_repourl: https://github.com/gosuda/HQQ.git
category: lowlevel
---

`gosuda.org/hqq` è un runtime di comunicazione tra processi zero-cgo e una toolchain di generazione di codice per Go e C11. Isola le librerie native C all'interno di processi worker del sistema operativo separati e le collega ai chiamanti Go tramite ring di memoria condivisa mappata in memoria, allineati alla cache line. Il progetto combina due livelli di trasporto (`StandardLink` per sessioni dedicate single-producer single-consumer e `Advanced` per il dispatch MPMC multi-worker), il compilatore IDL `hqq-bindgen` e un supervisore di processi con verifica di integrità (`secureexec` e `launch`).

```bash
go get gosuda.org/hqq
```

## Il costo di condividere uno spazio di indirizzi con C

Ogni ingegnere di sistemi Go si trova prima o poi di fronte allo stesso bivio. Si sta costruendo un servizio Go che compila in pochi secondi con `CGO_ENABLED=0`, viene distribuito come un unico binario statico, esegue la cross-compilazione da un laptop verso target Linux o Windows senza un sysroot e opera all'interno di un heap con garbage collection e sicurezza della memoria. Successivamente, è necessaria una libreria C matura: SQLite per l'archiviazione relazionale embedded, Zstandard (`libzstd`) per la compressione basata su dizionario, oppure una libreria multimediale o crittografica con accelerazione hardware.

Abilitare `cgo` per collegare tale libreria C nel processo Go risolve il problema immediato del binding, ma introduce tre vincoli strutturali:

1. **Accoppiamento tra toolchain e cross-compilazione**: `CGO_ENABLED=1` richiede un compilatore C, gli header della libc di destinazione e un linker per ogni sistema operativo e architettura di destinazione. Costruire artefatti `linux/arm64` o `windows/arm64` da una workstation `darwin/arm64` trasforma un singolo comando `go build` in una matrice di cross-compilatori e sysroot.
2. **Overhead di scheduling del runtime**: Ogni chiamata `cgo` attraversa il confine del runtime Go, passa da uno stack di goroutine espandibile a uno stack di sistema e vincola un thread del sistema operativo (`M`). Per chiamate a grana fine, il costo di transizione (~50 a 150 ns per chiamata) e la contesa dello scheduler si accumulano rapidamente.
3. **Collasso del dominio di guasto**: Il codice C viene eseguito nello stesso spazio di indirizzi virtuale del runtime Go. Una singola scrittura fuori dai limiti, un use-after-free, un overflow dello stack o un `abort()` all'interno di una funzione C corrompe i metadati dell'heap Go o termina immediatamente l'intero processo server, aggirando `recover()` e interrompendo ogni richiesta HTTP o RPC in corso.

Quando i team cercano di evitare `cgo`, le alternative comuni impongono un compromesso diverso. Le traduzioni meccaniche pure in Go di basi di codice C preservano `CGO_ENABLED=0`, ma spesso restano indietro rispetto alle release C upstream, introducono uno strato di memoria virtuale emulato e allocano pesantemente sotto carico. Spostare la libreria C in un processo ausiliario tramite TCP loopback o socket di dominio Unix ripristina l'isolamento dei guasti, ma ogni chiamata paga due cambi di contesto del kernel, copie nei buffer dei socket e serializzazione sul filo.

Abbiamo costruito HQQ ponendoci una domanda diretta: che cosa accadrebbe se la libreria C risiedesse in un processo del sistema operativo separato, con il proprio spazio di indirizzi virtuale, mentre Go e C scambiassero frame direttamente attraverso le cache line condivise della CPU, senza entrare nel kernel nel percorso veloce?

| Modello di integrazione            | `CGO_ENABLED=0` | Isolamento dei guasti    | Syscall nel percorso veloce | Allocazioni heap per chiamata | Fedeltà al C11 upstream |
| :--------------------------------- | :-------------- | :----------------------- | --------------------------: | ----------------------------: | :---------------------- |
| **`cgo` con collegamento in-process** | No           | Nessuno (heap condiviso) |                           0 |                       0 a 1+ | C upstream esatto       |
| **RPC su subprocesso via socket / pipe** | Sì         | Confine di processo      |                       2 a 4 |   Multiple (framing + I/O)    | C upstream esatto       |
| **Traduzione meccanica da C a Go**  | Sì              | Go in-process            |                           0 |          Alta nelle chiamate complesse | Runtime C emulato |
| **HQQ Shared-Memory IPC**          | Sì              | Confine di processo      |     0 (spin adattivo)       |        0 (`StandardLink`)     | C upstream esatto       |

## Panoramica architetturale: quattro livelli attraverso i confini di processo

HQQ separa l'isolamento dei processi in quattro livelli componibili, così che la meccanica dei ring in memoria condivisa, la generazione di codice RPC tipizzato, la verifica di integrità binaria e la supervisione dei processi figli restino indipendenti.

```text
+-------------------------------------------------------------------+
| Go Application Process (CGO_ENABLED=0)                            |
|                                                                   |
|  +-----------------------+     +-------------------------------+  |
|  | Generated Go Client   |     | Process Supervisor            |  |
|  | (gosuda.org/hqq/      |     | (gosuda.org/hqq/launch +      |  |
|  |  bindgen/rt)          |     |  gosuda.org/hqq/secureexec)   |  |
|  +-----------+-----------+     +---------------+---------------+  |
|              |                                 |                  |
|  +-----------v-----------+                     | Anonymous Pipe   |
|  | Transport Tier        |                     | Lifeline         |
|  | (StandardLink /       |                     | (--parent-stdin) |
|  |  Advanced MPMC)       |                     |                  |
+--------------+---------------------------------+------------------+
               |                                 |
===============|========= Shared Memory =========|===================
               |     (mmap / CreateFileMapping)  |
+--------------v---------------------------------v------------------+
| C11 Worker Process (Isolated Address Space)                       |
|                                                                   |
|  +-----------------------+     +-------------------------------+  |
|  | Generated C11 Server  |---->| Generation-Checked Handle     |  |
|  | & Batch Shims         |     | Table (hqq_rpc.c)             |  |
|  +-----------+-----------+     +---------------+---------------+  |
|              |                                 |                  |
|              +----------------+----------------+                  |
|                               |                                   |
|                     +---------v---------+                         |
|                     | Native C Library  |                         |
|                     | (SQLite, Zstd...) |                         |
|                     +-------------------+                         |
+-------------------------------------------------------------------+
```

### 1. Trasporto a memoria condivisa (`gosuda.org/hqq` e `c/`)

Il pacchetto base `gosuda.org/hqq` e la sua controparte C11 in `c/` implementano due modalità di trasporto a memoria condivisa tramite `mmap` POSIX (Linux e macOS) e `CreateFileMappingW` di Win32 (Windows). Ogni segmento condiviso inizia con un superblocco `HQQ_STL3` verificato, che fissa la versione del protocollo, la geometria dei buffer e i PID degli endpoint prima che venga letto qualsiasi indice del ring.

Tutte le word di controllo attive e gli header degli slot sono allineati ai confini di cache line da 64 byte, in modo che i core produttore e consumatore non entrino mai in contesa sulla stessa cache line a causa del false sharing.

```text
Layout dello slot di StandardLink (allineato alla cache line da 64 byte)
Offset in byte:  0x00                0x08                0x10                          0x40
                 +-------------------+-------------------+-----------------------------+
Header Line:     | seq (u64 atomic)  | len (u32) | flags | reserved (zero-padded)      |
                 +-------------------+-------------------+-----------------------------+
Payload Area:    | payload bytes [0 .. BufferSize - 1] (directly encoded / decoded)    |
                 +---------------------------------------------------------------------+
```

| Campo     | Offset |        Dimensione | Semantica                                                                                                                  |
| :-------- | -----: | ----------------: | :------------------------------------------------------------------------------------------------------------------------- |
| `seq`     | `0x00` |              8 B  | Contatore di sequenza monotono. I valori dispari marcano una scrittura in corso; i valori pari pubblicano un frame confermato tramite store-release. |
| `len`     | `0x08` |              4 B  | Lunghezza in byte del payload scritto nello slot (`0 <= len <= BufferSize`).                                               |
| `flags`   | `0x0C` |              4 B  | Bit di controllo del frame (continuazione, stato di errore o notifica di chiusura).                                        |
| `payload` | `0x40` | `BufferSize`      | Regione contigua del payload letta e scritta sul posto senza copie intermedie.                                             |

I chiamanti scelgono tra due livelli di trasporto in base alla struttura di concorrenza:

- **`StandardLink` (sessione SPSC 1:1)**: Associa una sessione client e una sessione server tramite slot dedicati di richiesta e risposta, protetti da un seqlock (`SeqLock`). Nel percorso veloce, lo scrittore serializza direttamente nello slot mappato e conferma tramite store-release atomico; il lettore effettua brevemente spin in user space prima di ricorrere alle primitive di risveglio del sistema operativo (`futex` su Linux, `__ulock_wait` su macOS, `WaitOnAddress` su Windows). Un round trip di un singolo frame si completa in ~200 ns con `0 allocs/op`.
- **`Advanced` (ring di dispatch MPMC)**: Multiplexa `M` chiamanti Go concorrenti su `N` thread worker C, utilizzando un ring di sottomissione lock-free multi-producer multi-consumer, un pool di slot condiviso e una tabella di completamento indicizzata. I chiamanti riservano uno slot, pubblicano l'indice dello slot nel ring e attendono la propria voce di completamento, mentre i thread worker estraggono le richieste e le completano fuori ordine.

Entrambi i livelli monitorano continuamente la vitalità dei peer. Ogni endpoint aggiorna un timestamp monotono atomico tramite `Heartbeat()` e verifica `PeerAlive(timeout)` durante l'attesa. Se un processo worker si arresta in modo anomalo durante una chiamata, il peer rileva immediatamente l'heartbeat obsoleto o il PID non più attivo, interrompe le attese bloccate con `ErrPeerGone` e supporta il `Takeover` atomico della sessione senza lasciare file di memoria condivisa bloccati.

### 2. RPC a allocazione zero e handle con verifica della generazione (`gosuda.org/hqq/bindgen`)

Scrivere a mano il framing a memoria condivisa per una libreria C con decine di funzioni è noioso e soggetto a errori. `hqq-bindgen` è un compilatore di schemi che legge un file di Interface Definition Language (`.idl`) (oppure ne crea uno direttamente da un header C `.h` tramite `hqq-bindgen init`) e produce un pacchetto client Go zero-cgo insieme a uno scheletro server C11.

L'errore più pericoloso in qualsiasi binding C è esporre puntatori C grezzi (`sqlite3*`, `sqlite3_stmt*`, `ZSTD_CCtx*`) attraverso il confine del linguaggio. Se un chiamante passa un puntatore dopo averlo liberato, passa un puntatore appartenente a un'altra sessione o corrompe un indirizzo intero, la libreria C dereferenzia memoria non valida.

`hqq-bindgen` vieta i puntatori grezzi sul filo. Al loro posto, l'IDL modella le risorse C con stato come dichiarazioni `handle` tipizzate:

```text
service sqlite3 {
    handle Db;
    handle Stmt @owned_by(Db);

    fn open_v2(path: string, flags: i32) -> result<Db, i32>;
    fn prepare_one(db: Db, sql: string) -> result<Stmt, i32>;
    fn finalize(stmt: @consume Stmt) -> result<void, i32>;
    fn close(db: @consume Db) -> result<void, i32>;
}
```

Sul filo, ogni handle è un intero a 64 bit che combina un indice di tabella a 32 bit e un contatore di generazione non nullo a 32 bit: `(uint64_t)generation << 32 | index`. Il runtime C (`c/src/hqq_rpc.c`) mantiene una tabella di handle per sessione con tre invarianti:

1. **Validazione della generazione**: Ogni ricerca verifica che lo slot sia occupato, che la generazione memorizzata corrisponda ai 32 bit superiori dell'handle e che il tag di tipo dell'handle corrisponda al tipo del parametro (`Db` contro `Stmt`). Il riutilizzo di un handle chiuso fallisce immediatamente con `HQQRPC_E_BAD_HANDLE` prima di invocare la libreria C.
2. **Proprietà gerarchica (`@owned_by`)**: Poiché `Stmt` è annotato con `@owned_by(Db)`, la chiusura di un handle `Db` percorre automaticamente tutti gli handle `Stmt` figli attivi di proprietà di quel `Db` ed esegue i loro distruttori in ordine inverso di allocazione (`sqlite3_finalize` prima di `sqlite3_close_v2`).
3. **Scansione di chiusura della sessione**: Se un client Go si disconnette o si arresta in modo anomalo senza chiudere i propri handle, il distruttore di sessione del server C scansiona l'intera tabella degli handle della sessione in ordine inverso, garantendo l'assenza di allocazioni C trapelate tra un riavvio del client e l'altro.

Prima che una sessione elabori la prima chiamata RPC, client e server si scambiano un digest di interfaccia SHA-256 di 32 byte calcolato sull'IDL canonicalizzato (con prefisso `HQQ-BINDGEN-IDL-V1\n`). Se il client Go e il worker C sono stati compilati a partire da schemi IDL incompatibili, l'handshake rifiuta la connessione in fase di bootstrap.

### 3. Esecuzione binaria a prova di manomissione (`gosuda.org/hqq/secureexec`)

Spostare il codice C in un binario worker solleva una questione di supply chain: come fa il processo Go a sapere che il binario worker su disco non è stato modificato o sostituito tra la verifica e l'esecuzione?

Chiamare `crypto/sha256` su un percorso di file seguito da `os/exec.Command(path).Start()` lascia una finestra di Time-of-Check to Time-of-Use (TOCTOU) in cui un altro processo può sovrascrivere o sostituire il file prima che il kernel lo esegua. `gosuda.org/hqq/secureexec` chiude questa finestra:

- Su Linux, `secureexec` apre il binario, ne trasmette il contenuto in un descrittore di file di memoria anonimo creato tramite `memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING)` calcolandone contemporaneamente l'hash SHA-256, verifica l'hash rispetto a un digest fissato di 32 byte o a un manifest di rilascio firmato con Ed25519 (`cmd/hqq-manifest`), applica `fcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)` in modo che nessun processo nel sistema possa modificare le pagine, e avvia il processo figlio dal descrittore sigillato.
- Su macOS e Windows, `secureexec` materializza i binari verificati in directory di cache con permessi bloccati, indicizzate tramite digest SHA-256, e verifica l'integrità della dimensione del file e del digest prima dell'avvio.

### 4. Supervisione dei processi con legame genitore (`gosuda.org/hqq/launch`)

Quando un'applicazione Go avvia un processo worker C in background, il worker non deve mai sopravvivere al proprio genitore come processo orfano, anche se il genitore Go viene terminato con `SIGKILL` (`kill -9`) e non può eseguire gli handler di pulizia `defer`.

`gosuda.org/hqq/launch` impone l'accoppiamento del ciclo di vita tramite una pipe anonima del sistema operativo, passata allo standard input del worker con il flag `--parent-stdin`. Il genitore Go detiene l'estremità di scrittura della pipe e non vi scrive mai; il worker C monitora l'estremità di lettura su un thread dedicato di legame vitale. Se il processo Go termina normalmente, va in panico o viene ucciso dal kernel del sistema operativo (`SIGKILL` o OOM killer), il kernel chiude immediatamente il descrittore di scrittura. Il worker C riceve `EOF` sulla pipe entro pochi microsecondi, interrompe le chiamate attive, esegue i distruttori degli handle di sessione, rimuove i file di memoria condivisa ed esce in modo pulito.

## Guida rapida: chiamare un servizio generato

Con `hqq-bindgen` e `launch`, chiamare un worker C isolato da Go appare come un normale client Go tipizzato, pur mantenendo `CGO_ENABLED=0`:

```go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"gosuda.org/hqq/bindgen/rt"
	"gosuda.org/hqq/launch"
	"gosuda.org/hqq/secureexec"
)

func main() {
	// Crea un contesto con timeout di 5 secondi
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// Analizza l'hash SHA-256 atteso del worker
	hash, err := secureexec.ParseHash("7649b1b45dacd934cde8149f35f4fcc3937f6486fbac23ce976466719b3752bb")
	if err != nil {
		log.Fatal(err)
	}

	// Crea il listener di memoria condivisa
	listener, err := launch.NewListenerName("demo")
	if err != nil {
		log.Fatal(err)
	}

	// Avvia il worker verificato
	srv, err := launch.Start(ctx, launch.Config{
		Binary:   "/path/to/verified-worker",
		SHA256:   hash,
		Listener: listener,
		Args:     []string{listener},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer srv.Close()

	fmt.Printf("worker running on shared-memory listener %s (opts=%+v)\n", listener, rt.SessionOptions{})
}
```

## Struttura del repository

Ogni sottosistema di `gosuda.org/hqq` è organizzato in pacchetti mirati con confini espliciti:

```text
gosuda.org/hqq
├── standard.go / ring.go      # StandardLink (SPSC) e primitive di ring a memoria condivisa di basso livello
├── advanced.go / slotpool.go  # Dispatcher Advanced (MPMC), pool di slot e tabella di completamento
├── shm_*.go / wait_*.go       # mmap/CreateFileMapping specifici del sistema operativo e futex/ulock/WaitOnAddress
├── c/                         # Libreria di trasporto C11 (hqq.c) e runtime del server RPC (hqq_rpc.c)
├── bindgen/                   # Parser IDL di hqq-bindgen, verificatore (V1-V41) e generatore di codice Go/C11
│   └── rt/                    # Runtime client Go a allocazione zero per i servizi generati
├── secureexec/                # Verifica di manifest SHA-256 / Ed25519 ed esecuzione sigillata con memfd
├── launch/                    # Supervisore dei processi worker con gestione del legame --parent-stdin
├── cmd/hqq-manifest/          # Strumento CLI per generare e firmare manifest dei binari di rilascio
└── spec/                      # Specifiche di protocollo eseguibili e vettori di test di riferimento (BINDGEN.md)
```

Per vedere come HQQ alimenti sostituti drop-in di produzione per due tra le librerie `cgo` più utilizzate nell'ecosistema Go, consultate i pacchetti complementari costruiti su questo stack:

- **[`gosuda.org/sqlite3`](https://gosuda.org/sqlite3)**: Driver `database/sql` drop-in compatibile con `github.com/mattn/go-sqlite3`, che esegue la SQLite C nativa `3.54.1` su memoria condivisa con prefetch batch in pipeline.
- **[`gosuda.org/zstd`](https://gosuda.org/zstd)**: Sostituto drop-in di `github.com/DataDog/zstd`, che esegue `libzstd 1.5.7` C nativa in un worker isolato a memoria condivisa con contesti a allocazione zero e batch bulk.
- **Repository GitHub**: [https://github.com/gosuda/HQQ](https://github.com/gosuda/HQQ)
