---
id: 43974d116e135fd9d3478f658dcf71c6
author: Lemon Mint
title: 'HQQ: Izolowanie bibliotek C od Go za pomocą pamięci współdzielonej w obrębie linii pamięci podręcznej'
description: Środowisko uruchomieniowe IPC bez cgo, kompilator IDL (hqq-bindgen) oraz zweryfikowany nadzorca procesów, które wykonują natywne biblioteki C11 w izolowanych procesach roboczych za pośrednictwem pierścieni w pamięci dzielonej.
language: pl
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` to środowisko uruchomieniowe komunikacji międzyprocesowej oraz zestaw narzędzi do generowania kodu dla Go i C11, działające bez zależności cgo. Izoluje natywne biblioteki C w oddzielnych procesach roboczych systemu operacyjnego i łączy je z wywołującymi kodu Go za pośrednictwem zmapowanych w pamięci, wyrównanych do linii pamięci podręcznej buforów pierścieniowych we wspólnej pamięci. Projekt łączy dwa poziomy transportu (`StandardLink` dla dedykowanych sesji jednego producenta i jednego konsumenta typu SPSC oraz `Advanced` dla wielowątkowego rozdzielania zadań typu MPMC), kompilator IDL `hqq-bindgen` oraz nadzorcę procesów odpornego na manipulacje (`secureexec` i `launch`).

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

## Koszt współdzielenia przestrzeni adresowej z C

Każdy inżynier systemów w Go prędzej czy później staje przed tym samym rozwidleniem dróg. Budujesz usługę w Go, która kompiluje się w ciągu kilku sekund przy `CGO_ENABLED=0`, jest dostarczana jako pojedynczy statyczny plik binarny, jest kompilowana krzyżowo z laptopa na systemy Linux lub Windows bez sysroot i działa w bezpiecznym pod względem pamięci stercie z automatycznym zarządzaniem pamięcią. Następnie potrzebujesz dojrzałej biblioteki C: SQLite do osadzonego relacyjnego przechowywania danych, Zstandard (`libzstd`) do kompresji ze słownikiem lub przyspieszanej sprzętowo biblioteki multimedialnej bądź kryptograficznej.

Włączenie `cgo` w celu podlinkowania tej biblioteki C do procesu Go rozwiązuje bieżący problem wiązania, ale wprowadza trzy strukturalne zobowiązania:

1. **Sprzężenie narzędzi i kompilacji krzyżowej**: `CGO_ENABLED=1` wymaga kompilatora C, nagłówków libc docelowego systemu oraz linkera platformy dla każdego docelowego systemu operacyjnego i architektury. Zbudowanie artefaktów `linux/arm64` lub `windows/arm64` ze stacji roboczej `darwin/arm64` zamienia pojedyncze polecenie `go build` w macierz kompilatorów krzyżowych i sysrootów.
2. **Narzut planowania w środowisku uruchomieniowym**: Każde wywołanie `cgo` przekracza granicę środowiska uruchomieniowego Go, przełącza się z rosnącego stosu goroutine na stos systemowy i przypina wątek systemu operacyjnego (`M`). W przypadku drobnoziarnistych wywołań koszt przejścia (~50 do 150 ns na wywołanie) oraz rywalizacja harmonogramu szybko się kumulują.
3. **Załamanie domeny awarii**: Kod C działa w tej samej wirtualnej przestrzeni adresowej co środowisko uruchomieniowe Go. Pojedynczy zapis poza granicami, użycie zwolnionej pamięci, przepełnienie stosu lub wywołanie `abort()` wewnątrz funkcji C uszkadza metadane sterty Go lub natychmiast kończy cały proces serwera, omijając `recover()` i powodując przerwanie wszystkich żądań HTTP lub RPC będących w toku.

Gdy zespoły próbują unikać `cgo`, typowe alternatywy wymuszają inny kompromis. Czysto Go'owe, mechaniczne translacje baz kodu C zachowują `CGO_ENABLED=0`, ale często nie nadążają za wydaniami upstream w C, wprowadzają emulowaną warstwę pamięci wirtualnej i intensywnie alokują pamięć pod obciążeniem. Przeniesienie biblioteki C do procesu pomocniczego za pośrednictwem TCP na pętli zwrotnej lub gniazd domeny Unix przywraca izolację awarii, lecz każde wywołanie ponosi koszt dwóch przełączeń kontekstu jądra, kopiowania buforów gniazd oraz serializacji na przewodzie.

HQQ powstało po postawieniu bezpośredniego pytania: co, jeśli biblioteka C znajdowałaby się w oddzielnym procesie systemu operacyjnego z własną wirtualną przestrzenią adresową, a Go i C wymieniały ramki bezpośrednio przez współdzielone linie pamięci podręcznej procesora, nie wchodząc do jądra na ścieżce szybkiej?

| Model integracji                   | `CGO_ENABLED=0` | Izolacja awarii        | Wywołania systemowe na ścieżce szybkiej | Alokacje sterty na wywołanie | Wierność wobec upstream C11 |
| :--------------------------------- | :-------------- | :--------------------- | --------------------------------------: | ---------------------------: | :-------------------------- |
| **Łączenie w procesie przez `cgo`** | Nie             | Brak (wspólna sterta)  |                                       0 |                    0 do 1+ | Dokładnie upstream C        |
| **Podproces RPC przez gniazda / potoki** | Tak        | Granica procesu        |                                     2 do 4 |  Wiele (ramkowanie + I/O)    | Dokładnie upstream C        |
| **Mechaniczna translacja C na Go** | Tak             | Go w obrębie procesu   |                                       0 |          Wysoka przy złożonych wywołaniach | Emulowane środowisko C |
| **HQQ, IPC we współdzielonej pamięci** | Tak         | Granica procesu        |             0 (adaptacyjne oczekiwanie) |        0 (`StandardLink`)    | Dokładnie upstream C        |

## Przegląd architektury: cztery warstwy ponad granicami procesów

HQQ rozdziela izolację procesów na cztery komponowalne warstwy, tak aby mechanika pierścieni we współdzielonej pamięci, typowane generowanie kodu RPC, weryfikacja integralności plików binarnych oraz nadzór nad procesami potomnymi pozostawały niezależne.

```text
+-------------------------------------------------------------------+
| Proces aplikacji Go (CGO_ENABLED=0)                               |
|                                                                   |
|  +-----------------------+     +-------------------------------+  |
|  | Wygenerowany klient Go|     | Nadzorca procesów             |  |
|  | (gosuda.org/hqq/      |     | (gosuda.org/hqq/launch +      |  |
|  |  bindgen/rt)          |     |  gosuda.org/hqq/secureexec)   |  |
|  +-----------+-----------+     +---------------+---------------+  |
|              |                                 |                  |
|  +-----------v-----------+                     | Anonimowy potok  |
|  | Warstwa transportu    |                     | (linia życia)    |
|  | (StandardLink /       |                     | (--parent-stdin) |
|  |  Advanced MPMC)       |                     |                  |
+--------------+---------------------------------+------------------+
               |                                 |
===============|========= Pamięć współdzielona ==|===================
               |     (mmap / CreateFileMapping)  |
+--------------v---------------------------------v------------------+
| Proces roboczy C11 (izolowana przestrzeń adresowa)                |
|                                                                   |
|  +-----------------------+     +-------------------------------+  |
|  | Wygenerowany serwer C11|--->| Tabela uchwytów z weryfikacją |  |
|  | i shimy wsadowe       |     | generacji (hqq_rpc.c)         |  |
|  +-----------+-----------+     +---------------+---------------+  |
|              |                                 |                  |
|              +----------------+----------------+                  |
|                               |                                   |
|                     +---------v---------+                         |
|                     | Natywna biblioteka|                         |
|                     | C (SQLite, Zstd...)|                        |
|                     +-------------------+                         |
+-------------------------------------------------------------------+
```

### 1. Transport we współdzielonej pamięci (`gosuda.org/hqq` i `c/`)

Pakiet bazowy `gosuda.org/hqq` oraz jego odpowiednik w C11, znajdujący się w `c/`, implementują dwa tryby transportu we współdzielonej pamięci oparte na POSIX `mmap` (Linux i macOS) oraz Win32 `CreateFileMappingW` (Windows). Każdy segment współdzielony rozpoczyna się od zweryfikowanego superbloku `HQQ_STL3`, który ustala wersję protokołu, geometrię buforów oraz PID-y punktów końcowych, zanim zostanie odczytany jakikolwiek indeks pierścienia.

Wszystkie gorące słowa kontrolne i nagłówki slotów są wyrównane do granic linii pamięci podręcznej o rozmiarze 64 bajtów, dzięki czemu rdzenie producenta i konsumenta nigdy nie rywalizują o tę samą linię pamięci podręcznej w wyniku fałszywego współdzielenia.

```text
Układ slotu StandardLink (wyrównany do linii pamięci podręcznej 64 bajtów)
Przesunięcie bajtowe: 0x00                0x08                0x10                          0x40
              +-------------------+-------------------+-----------------------------+
Linia nagłówka: | seq (u64 atomic)  | len (u32) | flags | zarezerwowane (wypełnione zerami) |
              +-------------------+-------------------+-----------------------------+
Obszar ładunku: | bajty ładunku [0 .. BufferSize - 1] (kodowane / dekodowane bezpośrednio) |
              +---------------------------------------------------------------------+
```

| Pole      | Przesunięcie |    Rozmiar | Znaczenie                                                                                                                       |
| :-------- | -----------: | ---------: | :------------------------------------------------------------------------------------------------------------------------------ |
| `seq`     | `0x00`       |        8 B | Monotoniczny licznik sekwencji. Wartości nieparzyste oznaczają zapis w toku; wartości parzyste publikują zatwierdzoną ramkę poprzez store-release. |
| `len`     | `0x08`       |        4 B | Długość ładunku w bajtach zapisanego w slocie (`0 <= len <= BufferSize`).                                                       |
| `flags`   | `0x0C`       |        4 B | Bity kontrolne ramki (kontynuacja, status błędu lub informacja o zamknięciu).                                                    |
| `payload` | `0x40`       | `BufferSize` | Ciągły obszar ładunku odczytywany i zapisywany w miejscu, bez pośrednich kopii.                                               |

Wywołujący wybierają jeden z dwóch poziomów transportu w zależności od struktury współbieżności:

- **`StandardLink` (sesja SPSC 1:1)**: Paruje sesję klienta i serwera poprzez dedykowane sloty żądań i odpowiedzi, chronione blokadą sekwencyjną (`SeqLock`). Na ścieżce szybkiej zapisujący serializuje dane bezpośrednio do zmapowanego slotu i zatwierdza je atomowym store-release; czytający krótko oczekuje w przestrzeni użytkownika, zanim sięgnie po prymitywy wybudzania systemu operacyjnego (`futex` w Linuksie, `__ulock_wait` w macOS, `WaitOnAddress` w Windows). Pojedyncza ramka w obie strony kończy się w ~200 ns przy `0 allocs/op`.
- **`Advanced` (pierścień rozdzielania MPMC)**: Multipleksuje `M` współbieżnych wywołujących z Go na `N` wątków roboczych C, wykorzystując bezblokadowy wielowątkowy pierścień zgłoszeń typu MPMC, wspólną pulę slotów oraz indeksowaną tabelę ukończeń. Wywołujący rezerwuje slot, publikuje jego indeks w pierścieniu i czeka na swój wpis ukończenia, podczas gdy wątki robocze pobierają żądania i realizują je w dowolnej kolejności.

Oba poziomy stale śledzą żywotność partnera. Każdy punkt końcowy aktualizuje atomowy monotoniczny znacznik czasu za pomocą `Heartbeat()` i sprawdza `PeerAlive(timeout)` podczas oczekiwania. Jeśli proces roboczy ulegnie awarii w trakcie wywołania, partner natychmiast wykrywa nieaktualny sygnał życia lub martwy PID, przerywa blokujące oczekiwania błędem `ErrPeerGone` i umożliwia atomowe przejęcie sesji (`Takeover`) bez pozostawiania uszkodzonych plików pamięci współdzielonej.

### 2. RPC bez alokacji i uchwyty z weryfikacją generacji (`gosuda.org/hqq/bindgen`)

Ręczne pisanie ramkowania pamięci współdzielonej dla biblioteki C z kilkudziesięcioma funkcjami jest żmudne i podatne na błędy. `hqq-bindgen` to kompilator schematów, który odczytuje plik w języku definicji interfejsów (`.idl`) (lub tworzy go bezpośrednio z nagłówka C `.h` poleceniem `hqq-bindgen init`) i generuje pakiet klienta Go bez zależności cgo wraz ze szkieletem serwera C11.

Najbardziej niebezpiecznym błędem w każdym wiązaniu z C jest udostępnianie surowych wskaźników C (`sqlite3*`, `sqlite3_stmt*`, `ZSTD_CCtx*`) przez granicę języków. Jeśli wywołujący przekaże wskaźnik po jego zwolnieniu, przekaże wskaźnik należący do innej sesji lub uszkodzi adres w postaci liczby całkowitej, biblioteka C odwoła się do nieprawidłowej pamięci.

`hqq-bindgen` zabrania surowych wskaźników w przesyłanych danych. Zamiast tego IDL modeluje stanowe zasoby C jako typowane deklaracje `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>;
}
```

Na przewodzie każdy uchwyt jest 64-bitową liczbą całkowitą łączącą 32-bitowy indeks tabeli i 32-bitowy niezerowy licznik generacji: `(uint64_t)generation << 32 | index`. Środowisko uruchomieniowe C (`c/src/hqq_rpc.c`) utrzymuje tabelę uchwytów dla każdej sesji, spełniającą trzy niezmienniki:

1. **Weryfikacja generacji**: Każde wyszukanie sprawdza, czy slot jest zajęty, czy jego zapisana generacja odpowiada górnym 32 bitom uchwytu oraz czy znacznik typu uchwytu odpowiada typowi parametru (`Db` vs. `Stmt`). Ponowne użycie zamkniętego uchwytu kończy się natychmiast błędem `HQQRPC_E_BAD_HANDLE`, zanim zostanie wywołana biblioteka C.
2. **Hierarchiczna własność (`@owned_by`)**: Ponieważ `Stmt` jest oznaczony `@owned_by(Db)`, zamknięcie uchwytu `Db` automatycznie przechodzi przez wszystkie żywe potomne uchwyty `Stmt` należące do tego `Db` i uruchamia ich destruktory w odwrotnej kolejności alokacji (`sqlite3_finalize` przed `sqlite3_close_v2`).
3. **Czyszczenie przy zakończeniu sesji**: Jeśli klient Go rozłączy się lub ulegnie awarii bez zamknięcia swoich uchwytów, destruktor sesji po stronie serwera C przegląda całą tabelę uchwytów sesji w odwrotnej kolejności, gwarantując brak wycieków alokacji C przy ponownym uruchamianiu klienta.

Zanim sesja przetworzy pierwsze wywołanie RPC, klient i serwer wymieniają 32-bajtowy skrót SHA-256 interfejsu, obliczony na kanonicznej postaci IDL (prefiks `HQQ-BINDGEN-IDL-V1\n`). Jeśli klient Go i proces roboczy C zostały skompilowane z niezgodnych schematów IDL, uzgadnianie odrzuca połączenie już na etapie inicjalizacji.

### 3. Wykonywanie plików binarnych odpornych na manipulacje (`gosuda.org/hqq/secureexec`)

Przeniesienie kodu C do pliku binarnego procesu roboczego rodzi pytanie o łańcuch dostaw: skąd proces Go może wiedzieć, że plik binarny procesu roboczego na dysku nie został zmodyfikowany ani podmieniony między weryfikacją a wykonaniem?

Wywołanie `crypto/sha256` na ścieżce pliku, a następnie `os/exec.Command(path).Start()` pozostawia okno typu Time-of-Check to Time-of-Use (TOCTOU), w którym inny proces może nadpisać lub podmienić plik, zanim jądro go wykona. `gosuda.org/hqq/secureexec` zamyka to okno:

- W Linuksie `secureexec` otwiera plik binarny, strumieniuje jego zawartość do anonimowego deskryptora pliku w pamięci utworzonego przez `memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING)` i jednocześnie oblicza jego skrót SHA-256, weryfikuje skrót względem przypiętego 32-bajtowego skrótu lub manifestu wydania podpisanego algorytmem Ed25519 (`cmd/hqq-manifest`), stosuje `fcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)`, aby żaden proces w systemie nie mógł modyfikować stron pamięci, i uruchamia proces potomny z zapieczętowanego deskryptora.
- W macOS i Windows `secureexec` materializuje zweryfikowane pliki binarne w katalogach pamięci podręcznej z ograniczonymi uprawnieniami, indeksowanych skrótem SHA-256, i weryfikuje rozmiar pliku oraz integralność skrótu przed uruchomieniem.

### 4. Nadzór nad procesem z linią życia rodzica (`gosuda.org/hqq/launch`)

Gdy aplikacja Go uruchamia proces roboczy C działający w tle, proces roboczy nigdy nie może przetrwać swojego rodzica jako proces osierocony, nawet jeśli proces nadrzędny Go zostanie zakończony sygnałem `SIGKILL` (`kill -9`) i nie będzie mógł wykonać procedur porządkowych `defer`.

`gosuda.org/hqq/launch` wymusza powiązanie czasu życia za pośrednictwem anonimowego potoku systemu operacyjnego, przekazanego na standardowe wejście procesu roboczego z flagą `--parent-stdin`. Rodzic Go trzyma koniec zapisu potoku i nigdy do niego nie pisze; proces roboczy C monitoruje koniec odczytu w dedykowanym wątku linii życia. Jeśli proces Go zakończy się normalnie, wpadnie w panikę lub zostanie zabity przez jądro systemu (`SIGKILL` lub mechanizm OOM killer), jądro natychmiast zamyka deskryptor zapisu. Proces roboczy C otrzymuje `EOF` na potoku w ciągu mikrosekund, przerywa wszystkie aktywne wywołania, uruchamia destruktory uchwytów sesji, usuwa pliki pamięci współdzielonej i czysto kończy działanie.

## Szybki start: wywołanie wygenerowanej usługi

Z `hqq-bindgen` i `launch` wywołanie izolowanego procesu roboczego C z poziomu Go wygląda jak zwykły, typowany klient Go, przy zachowaniu `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("worker running on shared-memory listener %s (opts=%+v)\n", listener, rt.SessionOptions{})
}
```

## Struktura repozytorium

Każdy podsystem w `gosuda.org/hqq` jest zorganizowany w wyspecjalizowane pakiety o wyraźnych granicach:

```text
gosuda.org/hqq
├── standard.go / ring.go      # StandardLink (SPSC) i niskopoziomowe prymitywy pierścieni pamięci współdzielonej
├── advanced.go / slotpool.go  # Dyspozytor Advanced (MPMC), pula slotów i tabela ukończeń
├── shm_*.go / wait_*.go       # Specyficzne dla systemu mmap/CreateFileMapping oraz futex/ulock/WaitOnAddress
├── c/                         # Biblioteka transportu C11 (hqq.c) i środowisko serwera RPC (hqq_rpc.c)
├── bindgen/                   # Parser IDL hqq-bindgen, weryfikator (V1-V41) oraz generator kodu Go/C11
│   └── rt/                    # Środowisko klienta Go bez alokacji dla wygenerowanych usług
├── secureexec/                # Weryfikacja manifestów SHA-256 / Ed25519 i wykonanie z zapieczętowanym memfd
├── launch/                    # Nadzorca procesów roboczych z zarządzaniem linią życia --parent-stdin
├── cmd/hqq-manifest/          # Narzędzie CLI do generowania i podpisywania manifestów plików binarnych wydania
└── spec/                      # Wykonywalne specyfikacje protokołu i wzorcowe wektory testowe (BINDGEN.md)
```

Aby zobaczyć, jak HQQ zapewnia gotowe do produkcji, bezpośrednio podstawialne zamienniki dwóch najszerzej używanych bibliotek `cgo` w ekosystemie Go, zapoznaj się z pakietami towarzyszącymi zbudowanymi na tym stosie:

- **[`gosuda.org/sqlite3`](https://gosuda.org/sqlite3)**: Bezpośrednio podstawialny sterownik `database/sql`, zgodny z `github.com/mattn/go-sqlite3`, uruchamiający natywną bibliotekę C SQLite `3.54.1` we współdzielonej pamięci z potokowym wstępnym pobieraniem partii danych.
- **[`gosuda.org/zstd`](https://gosuda.org/zstd)**: Bezpośrednio podstawialny zamiennik dla `github.com/DataDog/zstd`, uruchamiający natywną bibliotekę `libzstd 1.5.7` w izolowanym procesie roboczym ze współdzieloną pamięcią, z kontekstami bez alokacji i grupowaniem wsadowym.
- **Repozytorium GitHub**: [https://github.com/gosuda/HQQ](https://github.com/gosuda/HQQ)
