Когда на Rust уже всё переписали

от автора

Мем про переписывание всего на Rust в итоге стал индустриальным стандартом. Безопасность памяти и строгий компилятор реально решают кучу проблем. Но на практике регулярно всплывают задачи, где архитектурные рамки Раста только мешают и заставляют бороться с языком.

Писать системные сетевые сервисы на C в 2026ом году можно, но CVE на переполнение буфера вам выпишут быстрее, чем вы допишете свой Makefile.

Как говорится: Rust не позволит вам выстрелить себе в ногу. Zig позволит с радостью, но перед этим попросит явно передать аллокатор.

В двух последних проектах, в разработке которых я участвую, был выбран Zig. Я не буду продавать язык как идеальный (он объективно сырой), но ниже будет разбор реального опыта. 

Подопытные проекты

1. mtproto.zig

Высокопроизводительный MTProto прокси для Telegram. Главная задача этого демона кроется в бескомпромиссной мимикрии под обычный HTTPS трафик, потому что сегодня, так скажем, некоторые промежуточные узлы на магистралях обладают очень хрупкой душевной организацией. Если они видят незнакомый бинарный протокол, они пугаются, впадают в стресс и случайно роняют ваши TCP сессии. Мы глубоко уважаем ментальное здоровье сетевого оборудования, поэтому стараемся выглядеть для него максимально скучно и знакомо. Как дефолтный Nginx.

Специфика: строгий zero-allocation при парсинге, битовые манипуляции с TCP сессиями, фрагментация пакетов, реализация Fake TLS 1.3.

2. nullclaw

Инфраструктура автономных AI агентов. Суть в том, чтобы упаковать весь ИИ стек (провайдеры, каналы связи, векторную память, песочницы для кода) в минималистичный бинарник. Таргет запуска: дешевые edge устройства, роутеры и микрокомпьютеры, где лишние 10 мегабайт оперативки считаются непозволительной роскошью.

Специфика: модульная архитектура, горячая подмена провайдеров на лету, жесткий лимит RAM.

Сравнение лоб в лоб

Критерий

C

Rust

Zig

Память

Неявная. malloc вызывается внутри любых либ

Неявная. Глобальный аллокатор под капотом

Явное выделение. Аллокатор всегда прокидывается аргументом

Многопоточность

Pthreads, ручная синхронизация

Send/Sync, Borrow Checker. Строгий контроль

Атомики есть, защиты от гонок нет. Вся ответственность на вас

Ошибки

Коды возврата, errno

Тип Result<T, E>, оператор ?

Типы !T, блоки catch, оператор errdefer

Мета-код

Макросы препроцессора

Процедурные макросы

comptime. Выполнение обычного кода при компиляции

Сборка

CMake, Makefiles

Cargo

Своя система сборки build.zig, LLVM из коробки

Архитектура и память: почему не C

Взять и написать публичный сетевой сервис на чистом C сегодня, даже обложившись ИИ-ассистентами, — задачка та еще. Zig дает современные механизмы безопасности (проверки границ массивов, детекты переполнений), оставляя контроль над железом.

Но главный прикол Zig в другом. В языке нет скрытого выделения памяти. Ни одна стандартная функция не аллоцирует память сама по себе.

В mtproto.zig, например, нет потоков под коннекты. Сервер работает как единый однопоточный event loop на базе epoll. Мобильные клиенты любят держать пулы idle-сокетов сутками. Если бы мы выделяли даже по 256 KB стека на каждый тред (как это часто делают в Rust/Go), мы бы быстро улетели в OOM.

Вместо этого используются конечные автоматы (state machines) и предвыделенный на старте пул слотов для соединений. Никаких аллокаций в горячем цикле.

[ Классический подход ]
Новый клиент -> Выделяем тред -> malloc() под буферы -> Читаем -> free() -> Убиваем тред

[ Подход mtproto.zig: Epoll + State Machine ]
Старт сервера -> Предвыделяем массив слотов в памяти один раз
Новый клиент -> Берем свободный слот (O(1)) -> Читаем асинхронно
Клиент отвалился -> Возвращаем слот в массив

Никакой фрагментации кучи и никакого сбора мусора. Когда TCP соединение закрывается, мы просто сбрасываем состояние слота в начальное.

Никакой фрагментации кучи и никакого сбора мусора. Когда TCP соединение закрывается, мы просто сбрасываем состояние слота в начальное.

Факапы

Zig далек от идеала. Экосистема местами собрана из палок и изоленты, а компилятор все еще не 1.0.

99% CPU и каскадный отказ из-за логгера

Дефолтный логгер Зиг пишет напрямую в stdout/stderr. Под нагрузкой сотни коннектов одновременно сгенерировали log.debug сообщения. Системный вызов write в консоль является блокирующим. Наш единственный поток, который должен был молниеносно тасовать TCP пакеты, встал в очередь на вывод логов в терминал. Возник классический каскадный отказ: клиенты отваливаются, генерят еще больше логов об ошибках, и весь event loop стопорится.

Пришлось полностью пересматривать логирование на горячем пути. Огромный плюс сборки Zig с профилем ReleaseFast заключается в том, что вызовы log.debug не просто игнорируются в рантайме

Пришлось полностью пересматривать логирование на горячем пути. Огромный плюс сборки Zig с профилем ReleaseFast заключается в том, что вызовы log.debug не просто игнорируются в рантайме

Почему не Rust

Компромисс Раста в том, что язык агрессивно диктует вам архитектуру. Он круто работает с деревьями и строгим владением, но на графах или плагинных системах начинается возня.

В nullclaw нужна была легкая архитектура для подмены AI провайдеров на лету. В Rust динамическая диспетчеризация через dyn Trait тянет за собой fat pointers и заставляет обмазывать стейт конструкциями вроде Arc<Mutex<Box<dyn Provider>>>. Если добавить туда Tokio для асинхронных HTTP запросов, бинарник распухнет. Для роутера с 32 МБ оперативки это критично.

В Zig мы используем старые добрые таблицы виртуальных функций (vtable). Как в ядре Linux:

const AiProvider = struct {
    ptr: anyopaque, // Сырой указатель на стейт плагина
    vtable:
const VTable, // Таблица функций

    pub const VTable = struct {
        generate_response: const fn(ptr: anyopaque, prompt: []const u8) anyerror![]const u8,
        deinit: const fn(ptr: anyopaque) void,
    };

    pub fn ask(self: AiProvider, prompt: []const u8) ![]const u8 {
        return self.vtable.generate_response(self.ptr, prompt);
    }
};

Смотрим на суровые факты того, как этот подход сказывается на железе:

Время холодного старта нашего бинарника на Zig в итоге составило микроскопические 2 миллисекунды.

Время холодного старта нашего бинарника на Zig в итоге составило микроскопические 2 миллисекунды.

Comptime: нормальное метапрограммирование

Фича, которую все хайпят в Zig, — это comptime (выполнение кода при сборке).

Вернемся к прокси. Чтобы не травмировать психику транзитного оборудования, TCP-сессия начинается с фейкового TLS-рукопожатия. Железка должна видеть побайтово корректный ServerHello, иначе она запаникует и сбросит пакет RST.

Вместо того чтобы динамически собирать структуру пакета в рантайме или тащить тяжелые либы, мы эксплуатируем особенности компилятора:

// Выполняется ТОЛЬКО во время сборки.
// Результат намертво зашивается в секцию .rodata бинарника
const NGINX_HELLO_BYTES = comptime blk: {
    // В compile-time мы можем собирать сложную логику,
    // рассчитывать длины расширений и структуру пакета:
    var template: [128]u8 = undefined;
    fillFakeTlsExtensions(&template);
   
    // Строгая валидация до того, как скомпилируется билд.
    std.debug.assert(template.len == EXPECTED_TLS_SIZE);
    break :blk template;
};

В рантайме серверу не нужно выделять память и склеивать расширения. Он просто берет уже готовый массив байт и патчит фиксированные смещения (сессионные ID ключи), которые известны заранее. В Rust для подобных вещей пришлось бы городить отдельный крейт с процедурными макросами, а в Zig это просто обычный код внутри функции.

Итог

Zig — это не замена Rust. Переписывать на нем большой корпоративный бэкенд не стоит. Zig — это современная и адекватная замена C.

Если у вас сложная бизнес-логика, куча микросервисов и команда из десяти человек: берите Rust. Он докажет на этапе компиляции, что никто не отстрелит серверу ногу. Придется спорить с борроу-чекером, но приложение будет стабильным.

Если вам нужен сетевой демон, парсер или системная утилита с жестким лимитом по памяти: Zig отличный инструмент. Без скрытых аллокаций, без тяжелого рантайма и без макросов из семидесятых.

ссылка на оригинал статьи https://habr.com/ru/articles/1022260/