---
id: 43974d116e135fd9d3478f658dcf71c6
author: Lemon Mint
title: 'HQQ: Изолиране на C библиотеки от Go чрез споделена памет, подравнена по кеш линия'
description: Среда за IPC без cgo, IDL компилатор (hqq-bindgen) и верифициран супервайзър на процеси, които изпълняват нативни C11 библиотеки в изолирани работни процеси чрез споделени кръгови буфери в паметта.
language: bg
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` е времево-нулева (zero-cgo) среда за междупроцесна комуникация и инструментариум за генериране на код за Go и C11. Тя изолира нативни C библиотеки в отделни работни процеси на операционната система и ги свързва с Go извикващите през споделени пръстени в паметта, подредени по граници на кеш линии и картирани чрез mmap. Проектът съчетава два транспортни слоя (`StandardLink` за изолирани сесии от тип SPSC с един производител и един консуматор и `Advanced` за MPMC диспечиране към множество работници), компилатора на IDL `hqq-bindgen` и надзорник на процеси с проверка за целостност (`secureexec` и `launch`).

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

## Цената на споделянето на адресното пространство с C

Всеки системен инженер на Go рано или късно се изправя пред същото разклонение. Вие разработвате Go услуга, която се компилира за секунди с `CGO_ENABLED=0`, се разпространява като единичен статичен двоичен файл, се кроскомпилира от лаптоп към Linux или Windows цели без sysroot и работи в безопасна към паметта купчина с автоматично управление. След това се нуждаете от зряла C библиотека: SQLite за вградено релационно съхранение, Zstandard (`libzstd`) за компресия с речници или хардуерно ускорена медийна или криптографска библиотека.

Активирането на `cgo`, за да се свърже тази C библиотека в Go процеса, решава непосредствения проблем със свързването, но въвежда три структурни уязвимости:

1. **Свързване на инструментариума и кросскомпилацията**: `CGO_ENABLED=1` изисква C компилатор, заглавни файлове на целевата libc и платформен линкер за всяка целева операционна система и архитектура. Изграждането на артефакти за `linux/arm64` или `windows/arm64` от работна станция с `darwin/arm64` превръща една команда `go build` в матрица от кросскомпилатори и sysroot среди.
2. **Режийни разходи за планиране на изпълнението**: Всяко извикване на `cgo` преминава границата на Go средата на изпълнение, превключва от разширяем стек на горутина към системен стек и закрепва нишка на операционната система (`M`). При фино гранулирани извиквания разходът за преход (~50 до 150 ns на извикване) и конкуренцията за планировчика бързо се натрупват.
3. **Колапс на домейна на грешките**: C кодът работи в същото виртуално адресно пространство като Go средата на изпълнение. Единствен запис извън границите, използване след освобождаване, препълване на стека или `abort()` във функция на C повреждат метаданните на Go купчината или незабавно прекратяват целия сървърен процес, заобикаляйки `recover()` и повличайки след себе си всяка заявка по HTTP или RPC, която е в ход.

Когато екипите се опитват да избегнат `cgo`, често срещаните алтернативи налагат друг компромис. Чисто Go механични преводи на C кодови бази запазват `CGO_ENABLED=0`, но обикновено изостават след горестоящите C издания, поддържат емулиран слой за виртуална памет и отделят значителна памет при натоварване. Преместването на C библиотеката в помощен процес през loopback TCP или Unix домейн сокети възстановява изолацията на грешките, но всяко извикване заплаща за два превключвания на контекста в ядрото, копирания в буферите на сокетите и сериализация за предаване.

Създадохме HQQ, след като си зададохме директен въпрос: какво ще стане, ако C библиотеката живее в отделен процес на операционната система със собствено виртуално адресно пространство, а Go и C обменят кадри директно през споделени кеш линии на процесора, без да навлизат в ядрото по бързия път?

| Модел на интеграция                | `CGO_ENABLED=0` | Изолация на грешки          | Системни извиквания на бързия път | Динамични алокации на паметта за извикване | Вярност спрямо upstream C11 |
| :--------------------------------- | :-------------- | :-------------------------- | --------------------------------: | ----------------------------------------: | :-------------------------- |
| **`cgo` вграждане в процеса**      | Не              | Няма (споделена купчина)    |                                 0 |                                   0 до 1+ | Точно upstream C            |
| **Подпроцесен RPC през сокет/тръба** | Да              | Граница на процеса          |                           2 до 4 |             Множество (рамкиране + I/O)   | Точно upstream C            |
| **Механичен превод от C към Go**   | Да              | Go в процеса                |                                 0 |                     Високо при сложни извиквания | Емулирана C среда           |
| **HQQ споделена памет за IPC**     | Да              | Граница на процеса          |          0 (адаптивно въртене)    |                 0 (`StandardLink`)        | Точно upstream C            |

## Архитектурен преглед: четири слоя през границите на процесите

HQQ разделя изолацията на процесите на четири комбинируеми слоя, така че механиката на пръстените в споделената памет, генерирането на типизирани RPC кодове, проверката на целостта на двоичните файлове и надзора върху дъщерните процеси остават независими.

```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. Транспорт през споделена памет (`gosuda.org/hqq` и `c/`)

Базовият пакет `gosuda.org/hqq` и неговият C11 аналог в `c/` реализират два режима на транспорт през споделена памет върху POSIX `mmap` (Linux и macOS) и Win32 `CreateFileMappingW` (Windows). Всеки споделен сегмент започва с проверен суперблок `HQQ_STL3`, който фиксира версията на протокола, геометрията на буферите и PID на крайните точки, преди някой индекс на пръстен да бъде прочетен.

Всички горещи контролни думи и заглавки на слотове са подравнени по граници от 64 байта (кеш линия), така че производителите и консументите на ядра никога да не конкурират за една и съща кеш линия поради фалшиво споделяне.

```text
Оформление на слот в StandardLink (подравнен по кеш линия от 64 байта)
Отместване в байтове:  0x00              0x08              0x10                          0x40
              +-------------------+-------------------+-----------------------------+
Линия на заглавката: | seq (u64 atomic)  | len (u32) | flags | reserved (zero-padded)      |
              +-------------------+-------------------+-----------------------------+
Област на полезния товар: | payload bytes [0 .. BufferSize - 1] (directly encoded / decoded)    |
              +---------------------------------------------------------------------+
```

| Поле      | Отместване |       Размер | Семантика                                                                                                                  |
| :-------- | ---------: | -----------: | :------------------------------------------------------------------------------------------------------------------------- |
| `seq`     | `0x00`     |          8 B | Монотонен брояч на последователността. Нечетните стойности маркират записване в ход; четните публикуват потвърден кадър чрез store-release. |
| `len`     | `0x08`     |          4 B | Дължина в байтове на полезния товар, записан в слота (`0 <= len <= BufferSize`).                                           |
| `flags`   | `0x0C`     |          4 B | Битове за управление на кадъра (продължение, състояние на грешка или известие за спиране).                                 |
| `payload` | `0x40`     | `BufferSize` | Непрекъсната област на полезния товар, която се чете и записва на място, без междинни копия.                              |

Извикващите избират между два транспортни слоя в зависимост от структурата на конкурентността:

- **`StandardLink` (сесия SPSC 1:1)**: Свързва клиентска и сървърна сесия през специализирани слотове за заявка и отговор, защитени с seqlock (`SeqLock`). На бързия път записващият сериализира директно в картирания слот и потвърждава с атомарен store-release; четящият кратко върти в потребителското пространство, преди да премине към примитивите за събуждане на операционната система (`futex` на Linux, `__ulock_wait` на macOS, `WaitOnAddress` на Windows). Едно кръгово пътуване на единичен кадър завършва за ~200 ns с `0 allocs/op`.
- **`Advanced` (MPMC пръстен за диспечиране)**: Мултиплексира `M` едновременни извикващи от Go през `N` работни нишки на C, използвайки неблокиращ многопроизводителен многоконсуматорен пръстен за подаване, общ пул от слотове и индексирана таблица за завършване. Извикващите резервират слот, публикуват индекса на слота в пръстена и чакат на своя запис за завършване, докато работните нишки извличат и завършват заявките в произволен ред.

И двата слоя проследяват непрекъснато жизнеspособността на партньора. Всяка крайна точка актуализира атомарна монотонна времева марка чрез `Heartbeat()` и проверява `PeerAlive(timeout)` по време на чакане. Ако работен процес се срине по средата на извикване, партньорът незабавно открива остарялия сърдечен ритъм или мъртвия PID, прекратява блокираните чакания с `ErrPeerGone` и поддържа атомарно `Takeover` на сесията, без да оставя заклещени файлове в споделената памет.

### 2. RPC без алокации и обработки с проверка на поколенията (`gosuda.org/hqq/bindgen`)

Ръчното писане на рамкиране за споделена памет за C библиотека с десетки функции е отегчително и податливо на грешки. `hqq-bindgen` е компилатор на схеми, който чете файл на езика за дефиниране на интерфейси (`.idl`) (или създава такъв директно от C заглавен файл `.h` чрез `hqq-bindgen init`) и генерира Go клиентски пакет без cgo заедно със скелет на C11 сървър.

Най-опасната грешка при всяко C свързване е излагането на сурови C указатели (`sqlite3*`, `sqlite3_stmt*`, `ZSTD_CCtx*`) през езиковата граница. Ако извикващият подаде указател след освобождаването му, подаде указател, принадлежащ на друга сесия, или повреди целочислен адрес, C библиотеката дereferencира невалидна памет.

`hqq-bindgen` забранява сурови указатели в мрежовия трафик. Вместо това IDL моделира състоятелните C ресурси като типизирани декларации `handle`:

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

В мрежата всяка обработка е 64-битово цяло число, което съдържа 32-битов индекс в таблицата и 32-битов ненулев брояч на поколение: `(uint64_t)generation << 32 | index`. C средата за изпълнение (`c/src/hqq_rpc.c`) поддържа таблица на обработките за всяка сесия с три инварианта:

1. **Проверка на поколението**: Всяко търсене проверява дали слотът е зает, дали записаното поколение съвпада с горните 32 бита на обработката и дали типовият етикет на обработката съвпада с типа на параметъра (`Db` срещу `Stmt`). Повторното използване на затворена обработка незабавно довежда до `HQQRPC_E_BAD_HANDLE` преди извикване на C библиотеката.
2. **Йерархично притежание (`@owned_by`)**: Тъй като `Stmt` е анотиран с `@owned_by(Db)`, затварянето на обработка `Db` автоматично обхожда всички живи дъщерни обработки `Stmt`, притежавани от този `Db`, и изпълнява техните деструктори в обратен ред на алокацията (`sqlite3_finalize` преди `sqlite3_close_v2`).
3. **Почистване при прекратяване на сесията**: Ако Go клиент се разкачи или се срине, без да затвори обработките си, деструкторът на сесията на C сървъра обхожда цялата таблица на обработките на сесията в обратен ред, като гарантира отсъствие на изтекли C алокации след рестартиране на клиента.

Преди сесията да обработи първото си RPC извикване, клиентът и сървърът обменят 32-байтов SHA-256 дайджест на интерфейса, изчислен върху канонизираното IDL (префикс `HQQ-BINDGEN-IDL-V1\n`). Ако Go клиентът и C работният процес са компилирани от несъвместими IDL схеми, ръкостискането отхвърля връзката при инициализацията.

### 3. Изпълнение на двоични файлове без възможност за манипулация (`gosuda.org/hqq/secureexec`)

Преместването на C код в работен двоичен файл повдига въпрос за веригата на доставка: как Go процесът знае, че работният двоичен файл на диска не е бил променен или заменен между проверката и изпълнението?

Извикването на `crypto/sha256` върху път до файл, последвано от `os/exec.Command(path).Start()`, оставя прозорец за време на проверка и време на използване (TOCTOU), в който друг процес може да презапише или подмени файла, преди ядрото да го изпълни. `gosuda.org/hqq/secureexec` затваря този прозорец:

- На Linux `secureexec` отваря двоичния файл, прехвърля съдържанието му в анонимен файлов дескриптор в паметта, създаден чрез `memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING)`, като същевременно изчислява SHA-256 хеша му, проверява хеша спрямо фиксиран 32-байтов дайджест или спрямо манифест на издание, подписан с Ed25519 (`cmd/hqq-manifest`), прилага `fcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)`, така че никой процес в системата да не може да променя страниците, и стартира дъщерния процес от запечатания дескриптор.
- На macOS и Windows `secureexec` материализира проверените двоични файлове в директории за кеш със защитени права, индексирани по SHA-256 дайджест, и проверява целостта по размер на файла и дайджест преди стартиране.

### 4. Надзор на процесите чрез родителска връзка (`gosuda.org/hqq/launch`)

Когато Go приложение стартира фонов C работен процес, работният процес никога не трябва да оцелява като сирачески процес след родителя си, дори когато Go родителят бъде прекратен със `SIGKILL` (`kill -9`) и не може да изпълни `defer` процедурите за почистване.

`gosuda.org/hqq/launch` налага обвързване на жизнения цикъл през анонимна тръба на операционната система, подадена към стандартния вход на работния процес с флага `--parent-stdin`. Go родителят държи записващия край на тръбата и никога не пише в нея; C работният процес наблюдава четящия край в отделна нишка на връзката. Ако Go процесът завърши нормално, предизвика паника или бъде убит от ядрото на операционната система (`SIGKILL` или убиецът при липса на памет), ядрото незабавно затваря записващия дескриптор. C работният процес получава `EOF` върху тръбата в рамките на микросекунди, прекъсва всички активни извиквания, изпълнява деструкторите на обработките на сесията, премахва връзките към файловете в споделената памет и приключва коректно.

## Бърз старт: извикване на генерирана услуга

С `hqq-bindgen` и `launch` извикването на изолиран C работен процес от Go изглежда като обикновен типизиран Go клиент, като същевременно се запазва `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() {
	// Създаване на контекст с време на изчакване от 5 секунди
	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("worker running on shared-memory listener %s (opts=%+v)\n", listener, rt.SessionOptions{})
}
```

## Структура на хранилището

Всяка подсистема в `gosuda.org/hqq` е организирана в тематични пакети с ясни граници:

```text
gosuda.org/hqq
├── standard.go / ring.go      # StandardLink (SPSC) и примитиви за пръстен в споделена памет от ниско ниво
├── advanced.go / slotpool.go  # Advanced (MPMC) диспечер, пул от слотове и таблица за завършване
├── shm_*.go / wait_*.go       # Специфични за ОС mmap/CreateFileMapping и futex/ulock/WaitOnAddress
├── c/                         # C11 транспортна библиотека (hqq.c) и RPC сървърна среда за изпълнение (hqq_rpc.c)
├── bindgen/                   # Анализатор на IDL за hqq-bindgen, верификатор (V1-V41) и генератор на код за Go/C11
│   └── rt/                    # Клиентска среда за Go без алокации за генерирани услуги
├── secureexec/                # Проверка с SHA-256 / манифест с Ed25519 и изпълнение със запечатана memfd
├── launch/                    # Надзорник на работни процеси с управление на родителската връзка --parent-stdin
├── cmd/hqq-manifest/          # CLI инструмент за генериране и подписване на манифести на двоични издания
└── spec/                      # Изпълними спецификации на протокола и златни тестови вектори (BINDGEN.md)
```

За да видите как HQQ захранва директни производствени замени за две от най-широко използваните `cgo` библиотеки в Go екосистемата, разгледайте придружаващите пакети, изградени върху този стек:

- **[`gosuda.org/sqlite3`](https://gosuda.org/sqlite3)**: Директен драйвер за `database/sql`, съвместим с `github.com/mattn/go-sqlite3`, който изпълнява нативен C SQLite `3.54.1` през споделена памет с конвейерно предварително извличане на пакети.
- **[`gosuda.org/zstd`](https://gosuda.org/zstd)**: Директна замяна на `github.com/DataDog/zstd`, която изпълнява нативен C `libzstd 1.5.7` в изолиран работен процес със споделена памет, с контексти без алокации и групово пакетиране.
- **GitHub хранилище**: [https://github.com/gosuda/HQQ](https://github.com/gosuda/HQQ)
