---
id: 43974d116e135fd9d3478f658dcf71c6
author: Lemon Mint
title: 'HQQ: изоляция библиотек на C от Go посредством разделяемой памяти на уровне строк кэша'
description: Среда выполнения IPC без cgo, компилятор IDL (hqq-bindgen) и верифицированный супервизор процессов, которые выполняют нативные библиотеки C11 в изолированных рабочих процессах через кольцевые буферы в разделяемой памяти.
language: ru
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` — это среда межпроцессного взаимодействия без использования cgo и инструментарий генерации кода для Go и C11. Она изолирует нативные C-библиотеки внутри отдельных рабочих процессов ОС и подключает их к вызывающим Go-программам через отображаемые в память кольцевые буферы в разделяемой памяти, выровненные по границам кэш-линий. Проект объединяет два транспортных уровня (`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 нс на вызов) и конкуренция за планировщик быстро накапливаются.
3. **Коллапс домена отказа**: Код на C выполняется в том же виртуальном адресном пространстве, что и среда выполнения Go. Единственная запись за границами буфера, обращение к освобождённой памяти, переполнение стека или `abort()` внутри C-функции повреждает метаданные кучи Go или немедленно завершает весь серверный процесс, минуя `recover()` и обрывая все выполняющиеся HTTP- или RPC-запросы.

Когда команды пытаются избежать `cgo`, распространённые альтернативы требуют другого компромисса. Чисто Go-трансляции C-кодовой базы сохраняют `CGO_ENABLED=0`, но часто отстают от upstream-релизов 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-кода, проверка целостности бинарных файлов и supervision дочерних процессов оставались независимыми.

```text
+-------------------------------------------------------------------+
| Процесс Go-приложения (CGO_ENABLED=0)                             |
|                                                                   |
|  +-----------------------+     +-------------------------------+  |
|  | Сгенерированный       |     | Супервизор процессов          |  |
|  | Go-клиент             |     | (gosuda.org/hqq/launch +      |  |
|  | (gosuda.org/hqq/      |     |  gosuda.org/hqq/secureexec)   |  |
|  |  bindgen/rt)          |     |                               |  |
|  +-----------+-----------+     +---------------+---------------+  |
|              |                                 |                  |
|  +-----------v-----------+                     | Анонимный канал  |
|  | Транспортный уровень  |                     | Lifeline         |
|  | (StandardLink /       |                     | (--parent-stdin) |
|  |  Advanced MPMC)       |                     |                  |
+--------------+---------------------------------+------------------+
               |                                 |
===============|========= Разделяемая память ====|===================
               |     (mmap / CreateFileMapping)  |
+--------------v---------------------------------v------------------+
| Рабочий процесс C11 (изолированное адресное пространство)         |
|                                                                   |
|  +-----------------------+     +-------------------------------+  |
|  | Сгенерированный       |---->| Таблица дескрипторов с        |  |
|  | сервер C11 и         |     | проверкой поколений           |  |
|  | пакетные обёртки      |     | (hqq_rpc.c)                   |  |
|  +-----------+-----------+     +---------------+---------------+  |
|              |                                 |                  |
|              +----------------+----------------+                  |
|                               |                                   |
|                     +---------v---------+                         |
|                     | Нативная C-       |                         |
|                     | библиотека        |                         |
|                     | (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 (дополнено нулями) |
              +-------------------+-------------------+-----------------------------+
Область полезной нагрузки: | байты payload [0 .. BufferSize - 1] (кодируются и декодируются напрямую) |
              +---------------------------------------------------------------------+
```

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

Вызывающая сторона выбирает один из двух транспортных уровней в зависимости от структуры конкурентности:

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

Оба уровня непрерывно отслеживают доступность пиров. Каждая конечная точка обновляет атомарную монотонную метку времени через `Heartbeat()` и проверяет `PeerAlive(timeout)` во время ожидания. Если рабочий процесс аварийно завершается в ходе вызова, пир немедленно обнаруживает устаревший heartbeat или мёртвый 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-библиотека разыменовывает недопустимую память.

`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 отслеживает конец для чтения в выделенном потоке lifeline. Если процесс Go завершается штатно, возникает паника или он уничтожается ядром ОС (`SIGKILL` или OOM killer), ядро немедленно закрывает дескриптор записи. Рабочий процесс 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()

	// Разбираем закреплённый SHA-256 дайджест рабочего процесса
	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)
