---
id: 43974d116e135fd9d3478f658dcf71c6
author: Lemon Mint
title: 'HQQ: Aislamiento de bibliotecas C desde Go mediante memoria compartida en líneas de caché'
description: Entorno de ejecución IPC sin cgo, compilador de IDL (hqq-bindgen) y supervisor de procesos verificado que ejecutan bibliotecas nativas C11 en procesos de trabajo aislados mediante anillos de memoria compartida.
language: es
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` es un runtime de comunicación entre procesos sin cgo y un conjunto de herramientas de generación de código para Go y C11. Aísla las bibliotecas nativas en C dentro de procesos trabajadores del sistema operativo independientes y las conecta con los llamadores de Go mediante anillos de memoria compartida mapeados en memoria y alineados a líneas de caché. El proyecto combina dos niveles de transporte (`StandardLink` para sesiones dedicadas de productor único y consumidor único, y `Advanced` para el despacho MPMC con múltiples trabajadores), el compilador de IDL `hqq-bindgen` y un supervisor de procesos con verificación de integridad (`secureexec` y `launch`).

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

## El costo de compartir un espacio de direcciones con C

Todo ingeniero de sistemas en Go termina enfrentándose tarde o temprano a la misma bifurcación. Usted está desarrollando un servicio en Go que compila en segundos con `CGO_ENABLED=0`, se distribuye como un único binario estático, se compila de forma cruzada desde una computadora portátil hacia destinos Linux o Windows sin un sysroot y se ejecuta dentro de un heap con recolección de basura y seguridad de memoria. Luego necesita una biblioteca de C madura: SQLite para almacenamiento relacional embebido, Zstandard (`libzstd`) para compresión con diccionario, o una biblioteca de medios o criptografía acelerada por hardware.

Habilitar `cgo` para enlazar esa biblioteca de C dentro del proceso de Go resuelve el problema inmediato de vinculación, pero introduce tres pasivos estructurales:

1. **Acoplamiento de la cadena de herramientas y de la compilación cruzada**: `CGO_ENABLED=1` requiere un compilador de C, cabeceras de la libc del destino y un enlazador de plataforma para cada sistema operativo y arquitectura de destino. Construir artefactos `linux/arm64` o `windows/arm64` desde una estación de trabajo `darwin/arm64` convierte un único comando `go build` en una matriz de compiladores cruzados y sysroots.
2. **Sobrecarga de planificación en tiempo de ejecución**: Cada llamada de `cgo` cruza la frontera del runtime de Go, cambia de una pila de goroutine expandible a una pila del sistema y fija un hilo del sistema operativo (`M`). En llamadas de grano fino, el costo de transición (~50 a 150 ns por llamada) y la contención del planificador se acumulan rápidamente.
3. **Colapso del dominio de fallos**: El código en C se ejecuta en el mismo espacio de direcciones virtual que el runtime de Go. Una escritura fuera de límites, un uso tras liberación, un desbordamiento de pila o un `abort()` dentro de una función de C corrompe los metadatos del heap de Go o termina de inmediato todo el proceso del servidor, eludiendo `recover()` y arrastrando consigo cada solicitud HTTP o RPC en curso.

Cuando los equipos intentan evitar `cgo`, las alternativas habituales imponen un compromiso distinto. Las traducciones mecánicas puras en Go de bases de código en C preservan `CGO_ENABLED=0`, pero a menudo se retrasan respecto de las versiones upstream de C, incorporan una capa de memoria virtual emulada y asignan memoria de forma intensiva bajo carga. Trasladar la biblioteca de C a un proceso auxiliar mediante TCP de loopback o sockets de dominio Unix restaura el aislamiento de fallos, pero cada llamada paga dos cambios de contexto del kernel, copias de búferes de socket y serialización de red.

Construimos HQQ a partir de una pregunta directa: ¿qué ocurriría si la biblioteca de C residiera en un proceso del sistema operativo separado, con su propio espacio de direcciones virtual, mientras Go y C intercambian tramas directamente a través de líneas de caché de CPU compartidas, sin entrar en el kernel en la ruta rápida?

| Modelo de integración               | `CGO_ENABLED=0` | Aislamiento de fallos       | Llamadas al sistema en la ruta rápida | Asignaciones de heap por llamada | Fidelidad al C11 upstream |
| :---------------------------------- | :-------------- | :-------------------------- | ------------------------------------: | -------------------------------: | :------------------------ |
| **Enlace en proceso con `cgo`**     | No              | Ninguno (heap compartido)   |                                     0 |                        0 a 1+    | C upstream exacto         |
| **RPC de subproceso por socket / tubería** | Sí       | Frontera de proceso         |                                   2 a 4 |   Múltiples (encuadre + E/S)     | C upstream exacto         |
| **Traducción mecánica de C a Go**   | Sí              | Go en proceso               |                                     0 |          Alta en llamadas complejas | Runtime de C emulado   |
| **IPC de memoria compartida de HQQ** | Sí             | Frontera de proceso         |          0 (espera activa adaptativa) |        0 (`StandardLink`)        | C upstream exacto         |

## Visión general de la arquitectura: cuatro capas a través de las fronteras de proceso

HQQ separa el aislamiento de procesos en cuatro capas componibles, de modo que la mecánica de los anillos de memoria compartida, la generación de RPC tipado, la verificación de integridad binaria y la supervisión de procesos hijos permanezcan independientes.

```text
+-------------------------------------------------------------------+
| Proceso de la aplicación Go (CGO_ENABLED=0)                       |
|                                                                   |
|  +-----------------------+     +-------------------------------+  |
|  | Cliente Go generado   |     | Supervisor de procesos        |  |
|  | (gosuda.org/hqq/      |     | (gosuda.org/hqq/launch +      |  |
|  |  bindgen/rt)          |     |  gosuda.org/hqq/secureexec)   |  |
|  +-----------+-----------+     +---------------+---------------+  |
|              |                                 |                  |
|  +-----------v-----------+                     | Tubería anónima  |
|  | Nivel de transporte   |                     | de vida          |
|  | (StandardLink /       |                     | (--parent-stdin) |
|  |  Advanced MPMC)       |                     |                  |
+--------------+---------------------------------+------------------+
               |                                 |
===============|========= Memoria compartida ====|===================
               |     (mmap / CreateFileMapping)  |
+--------------v---------------------------------v------------------+
| Proceso trabajador C11 (espacio de direcciones aislado)           |
|                                                                   |
|  +-----------------------+     +-------------------------------+  |
|  | Servidor C11 generado |---->| Tabla de manejadores con      |  |
|  | y shims por lotes     |     | verificación de generación    |  |
|  +-----------+-----------+     | (hqq_rpc.c)                   |  |
|              |                 +---------------+---------------+  |
|              |                                 |                  |
|              +----------------+----------------+                  |
|                               |                                   |
|                     +---------v---------+                         |
|                     | Biblioteca C      |                         |
|                     | nativa (SQLite,   |                         |
|                     | Zstd...)          |                         |
|                     +-------------------+                         |
+-------------------------------------------------------------------+
```

### 1. Transporte de memoria compartida (`gosuda.org/hqq` y `c/`)

El paquete base `gosuda.org/hqq` y su contraparte en C11 dentro de `c/` implementan dos modos de transporte por memoria compartida sobre `mmap` de POSIX (Linux y macOS) y `CreateFileMappingW` de Win32 (Windows). Cada segmento compartido comienza con un superbloque `HQQ_STL3` verificado que fija la versión del protocolo, la geometría de los búferes y los PID de los extremos antes de que se lea cualquier índice de anillo.

Todas las palabras de control activas y los encabezados de ranura están alineados a límites de línea de caché de 64 bytes, de modo que los núcleos productor y consumidor nunca compitan por la misma línea de caché mediante falso compartido.

```text
Disposición de ranura de StandardLink (alineada a líneas de caché de 64 bytes)
Desplazamiento en bytes:  0x00              0x08              0x10                          0x40
              +-------------------+-------------------+-----------------------------+
Línea de encabezado:  | seq (u64 atómico) | len (u32) | flags | reservado (relleno con ceros) |
              +-------------------+-------------------+-----------------------------+
Área de carga útil:   | bytes de carga útil [0 .. BufferSize - 1] (codificados / decodificados directamente) |
              +---------------------------------------------------------------------+
```

| Campo     | Desplazamiento |         Tamaño | Semántica                                                                                                                  |
| :-------- | -------------: | -------------: | :------------------------------------------------------------------------------------------------------------------------- |
| `seq`     | `0x00`         |           8 B  | Contador de secuencia monótono. Los valores impares marcan una escritura en curso; los valores pares publican una trama confirmada mediante store-release. |
| `len`     | `0x08`         |           4 B  | Longitud en bytes de la carga útil escrita en la ranura (`0 <= len <= BufferSize`).                                        |
| `flags`   | `0x0C`         |           4 B  | Bits de control de la trama (continuación, estado de error o aviso de cierre).                                             |
| `payload` | `0x40`         | `BufferSize`   | Región contigua de carga útil leída y escrita en su lugar, sin copias intermedias.                                         |

Los llamadores eligen entre dos niveles de transporte según la estructura de concurrencia:

- **`StandardLink` (sesión SPSC 1:1)**: Empareja una sesión de cliente y otra de servidor mediante ranuras dedicadas de solicitud y respuesta protegidas por un cerrojo de secuencia (`SeqLock`). En la ruta rápida, el escritor serializa directamente en la ranura mapeada y confirma mediante un store-release atómico; el lector realiza una espera activa breve en espacio de usuario antes de recurrir a primitivas de despertar del sistema operativo (`futex` en Linux, `__ulock_wait` en macOS, `WaitOnAddress` en Windows). Un ciclo completo de una sola trama se completa en ~200 ns con `0 allocs/op`.
- **`Advanced` (anillo de despacho MPMC)**: Multiplexa `M` llamadores concurrentes de Go sobre `N` hilos trabajadores de C mediante un anillo de envío multiproductor y multiconsumidor sin cerrojos, un conjunto compartido de ranuras y una tabla de finalización indexada. Los llamadores reservan una ranura, publican el índice de la ranura en el anillo y esperan su entrada de finalización mientras los hilos trabajadores extraen las solicitudes y las completan fuera de orden.

Ambos niveles supervisan la vitalidad del par de forma continua. Cada extremo actualiza una marca de tiempo monótona atómica mediante `Heartbeat()` e inspecciona `PeerAlive(timeout)` mientras espera. Si un proceso trabajador falla a mitad de una llamada, el par detecta de inmediato el latido obsoleto o el PID inactivo, interrumpe las esperas bloqueadas con `ErrPeerGone` y admite un `Takeover` atómico de la sesión sin dejar archivos de memoria compartida bloqueados.

### 2. RPC sin asignaciones y manejadores con verificación de generación (`gosuda.org/hqq/bindgen`)

Escribir a mano el encuadre de memoria compartida para una biblioteca de C con decenas de funciones es tedioso y propenso a errores. `hqq-bindgen` es un compilador de esquemas que lee un archivo de Lenguaje de Definición de Interfaces (`.idl`) (o genera uno directamente a partir de una cabecera `.h` de C mediante `hqq-bindgen init`) y emite un paquete cliente de Go sin cgo junto con un esqueleto de servidor C11.

El error más peligroso en cualquier enlace con C es exponer punteros de C sin procesar (`sqlite3*`, `sqlite3_stmt*`, `ZSTD_CCtx*`) a través de la frontera del lenguaje. Si un llamador pasa un puntero después de liberarlo, pasa un puntero perteneciente a otra sesión o corrompe una dirección entera, la biblioteca de C desreferencia memoria inválida.

`hqq-bindgen` prohíbe los punteros sin procesar en el canal. En su lugar, el IDL modela los recursos con estado de C como declaraciones `handle` tipadas:

```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>;
}
```

En el canal, cada manejador es un entero de 64 bits que empaqueta un índice de tabla de 32 bits y un contador de generación de 32 bits distinto de cero: `(uint64_t)generation << 32 | index`. El runtime de C (`c/src/hqq_rpc.c`) mantiene una tabla de manejadores por sesión con tres invariantes:

1. **Validación de generación**: Cada búsqueda verifica que la ranura esté ocupada, que su generación almacenada coincida con los 32 bits superiores del manejador y que su etiqueta de tipo de manejador coincida con el tipo del parámetro (`Db` frente a `Stmt`). Reutilizar un manejador cerrado falla de inmediato con `HQQRPC_E_BAD_HANDLE` antes de invocar la biblioteca de C.
2. **Propiedad jerárquica (`@owned_by`)**: Como `Stmt` está anotado con `@owned_by(Db)`, cerrar un manejador `Db` recorre automáticamente todos los manejadores `Stmt` hijos vivos que pertenecen a ese `Db` y ejecuta sus destructores en orden inverso de asignación (`sqlite3_finalize` antes de `sqlite3_close_v2`).
3. **Barrido de terminación de sesión**: Si un cliente de Go se desconecta o falla sin cerrar sus manejadores, el destructor de sesión del servidor de C recorre toda la tabla de manejadores de la sesión en orden inverso, garantizando cero asignaciones de C filtradas entre reinicios del cliente.

Antes de que una sesión procese su primera llamada RPC, el cliente y el servidor intercambian un resumen de interfaz SHA-256 de 32 bytes calculado sobre el IDL canonicalizado (con el prefijo `HQQ-BINDGEN-IDL-V1\n`). Si el cliente de Go y el trabajador de C se compilaron a partir de esquemas IDL incompatibles, el protocolo de enlace rechaza la conexión durante el arranque.

### 3. Ejecución binaria a prueba de manipulaciones (`gosuda.org/hqq/secureexec`)

Trasladar código de C a un binario trabajador plantea una cuestión de cadena de suministro: ¿cómo sabe el proceso de Go que el binario trabajador en disco no fue modificado ni reemplazado entre la verificación y la ejecución?

Calcular `crypto/sha256` sobre una ruta de archivo y, a continuación, ejecutar `os/exec.Command(path).Start()` deja una ventana de tiempo de verificación frente a tiempo de uso (TOCTOU), durante la cual otro proceso puede sobrescribir o intercambiar el archivo antes de que el kernel lo ejecute. `gosuda.org/hqq/secureexec` cierra esta ventana:

- En Linux, `secureexec` abre el binario, transmite su contenido a un descriptor de archivo de memoria anónimo creado mediante `memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING)` mientras calcula su hash SHA-256, verifica el hash contra un resumen fijado de 32 bytes o contra un manifiesto de versión firmado con Ed25519 (`cmd/hqq-manifest`), aplica `fcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)` para que ningún proceso del sistema pueda modificar las páginas, y lanza el proceso hijo a partir del descriptor sellado.
- En macOS y Windows, `secureexec` materializa los binarios verificados en directorios de caché con bloqueo de permisos, indexados por el resumen SHA-256, y verifica la integridad del tamaño del archivo y del resumen antes del lanzamiento.

### 4. Supervisión de procesos mediante cordón de vida con el padre (`gosuda.org/hqq/launch`)

Cuando una aplicación de Go lanza un proceso trabajador de C en segundo plano, el trabajador nunca debe sobrevivir a su padre como proceso huérfano, incluso si el padre de Go termina con `SIGKILL` (`kill -9`) y no puede ejecutar los manejadores de limpieza de `defer`.

`gosuda.org/hqq/launch` impone el acoplamiento de vida mediante una tubería anónima del sistema operativo que se pasa a la entrada estándar del trabajador con la bandera `--parent-stdin`. El padre de Go conserva el extremo de escritura de la tubería y nunca escribe en ella; el trabajador de C supervisa el extremo de lectura en un hilo de cordón de vida dedicado. Si el proceso de Go termina normalmente, entra en pánico o es eliminado por el kernel del sistema operativo (`SIGKILL` o el asesino de OOM), el kernel cierra de inmediato el descriptor de escritura. El trabajador de C recibe `EOF` en la tubería en cuestión de microsegundos, interrumpe las llamadas activas, ejecuta sus destructores de manejadores de sesión, desvincula los archivos de memoria compartida y finaliza de forma limpia.

## Inicio rápido: invocar un servicio generado

Con `hqq-bindgen` y `launch`, invocar desde Go un trabajador de C aislado se parece a un cliente tipado y ordinario de Go, manteniendo `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() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	hash, err := secureexec.ParseHash("7649b1b45dacd934cde8149f35f4fcc3937f6486fbac23ce976466719b3752bb")
	if err != nil {
		log.Fatal(err)
	}

	listener, err := launch.NewListenerName("demo")
	if err != nil {
		log.Fatal(err)
	}

	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("trabajador en ejecución en el listener de memoria compartida %s (opts=%+v)\n", listener, rt.SessionOptions{})
}
```

## Estructura del repositorio

Cada subsistema de `gosuda.org/hqq` está organizado en paquetes enfocados con límites explícitos:

```text
gosuda.org/hqq
├── standard.go / ring.go      # StandardLink (SPSC) y primitivas de anillo de memoria compartida de bajo nivel
├── advanced.go / slotpool.go  # Despachador Advanced (MPMC), conjunto de ranuras y tabla de finalización
├── shm_*.go / wait_*.go       # mmap/CreateFileMapping específicos del sistema operativo y futex/ulock/WaitOnAddress
├── c/                         # Biblioteca de transporte C11 (hqq.c) y runtime del servidor RPC (hqq_rpc.c)
├── bindgen/                   # Analizador de IDL de hqq-bindgen, verificador (V1-V41) y generador de código Go/C11
│   └── rt/                    # Runtime cliente de Go sin asignaciones para servicios generados
├── secureexec/                # Verificación de manifiestos SHA-256 / Ed25519 y ejecución sellada con memfd
├── launch/                    # Supervisor de procesos trabajadores con gestión de cordón --parent-stdin
├── cmd/hqq-manifest/          # Herramienta CLI para generar y firmar manifiestos de binarios de versión
└── spec/                      # Especificaciones de protocolo ejecutables y vectores de prueba de referencia (BINDGEN.md)
```

Para ver cómo HQQ impulsa reemplazos de producción directamente intercambiables para dos de las bibliotecas basadas en `cgo` más utilizadas del ecosistema de Go, consulte los paquetes complementarios construidos sobre esta pila:

- **[`gosuda.org/sqlite3`](https://gosuda.org/sqlite3)**: Controlador `database/sql` directamente intercambiable, compatible con `github.com/mattn/go-sqlite3`, que ejecuta SQLite nativo de C `3.54.1` sobre memoria compartida con captación previa de lotes en cadena.
- **[`gosuda.org/zstd`](https://gosuda.org/zstd)**: Reemplazo directamente intercambiable de `github.com/DataDog/zstd`, que ejecuta `libzstd 1.5.7` nativo de C en un trabajador aislado de memoria compartida con contextos sin asignaciones y procesamiento por lotes masivo.
- **Repositorio de GitHub**: [https://github.com/gosuda/HQQ](https://github.com/gosuda/HQQ)
