Мем про переписывание всего на 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 отличный инструмент. Без скрытых аллокаций, без тяжелого рантайма и без макросов из семидесятых.

Комментарии (17)


  1. Astrowalk
    11.04.2026 13:45

    Классика – неэффективное решение на Rust сравнивают с эффективным на другом языке.

    > Компромисс Раста в том, что язык агрессивно диктует вам архитектуру

    Бред.


  1. sceptizator
    11.04.2026 13:45

    Пугать в наше время переполнением буфера можно только тех, кто не может почитать man mprotect и догадаться выровнять и обернуть буфер двумя защитными страницами памяти.

    А еще авторов zig (и ржавчины тоже) можно отправить почитать про memory pools and resource management, в такие проекты как APR, postgresql (там они называются Context, Resource Owners) и т.д.

    Хотя зачем... как тогда отделять С от .. как там было сказано - "адекватных" :) его замен?


    1. d3d14
      11.04.2026 13:45

      Защита от переполнения буфера - это не выравнивание и не оборачивание защитными страницами, а контроль размера записываемых в него данных.


      1. sceptizator
        11.04.2026 13:45

        И каким образом это делать в общем случае, контроль этот? Встроенные в язык и его runtime проверки это очень хорошо, но память в процессе общая, при желании кто угодно может писать куда угодно даже в случае Java, если только нет защиты памяти на запись на уровне OS (которую впрочем так-же легко выключить, как и включить).

        Cуперзащищеннный на уровне языка и runtime буфер может быть поврежден просто вызовом сторонней функции, в новой версии которой, недавно полученной по dnf update вдруг внезапно появился вызов sprintf()


        1. d3d14
          11.04.2026 13:45

          Ну если вы не знаете какие функции у вас в программе куда пишут, то я даже не знаю...


          1. sceptizator
            11.04.2026 13:45

            Наивно думать, что С программисты не контролируют память и свои буферы и прочие целочисленные переполнения. Большинство CVE построены на том, что пробивается лишь одна из функций, часто библиотечная, и дальше вся система контроля падает как карточный домик. Это справедливо для любого runtime, в любой среде.

            Отличие подхода Zig, Rust и подобных от С лишь в том, что они принуждают контролировать диапазоны значений даже там, где это реально и не нужно.

            К примеру изначальный код на С (изначально со всеми нужными проверками) будет в теории дописан иначе (второй кусок). Примеры ниже утрированные и наверняка там будет оптимизация, излишние проверки выбросит компилятор, как минимум когда-нибудь, но сути это не меняет - то что для Rust, Zig нормик, любой программист на C, мягко говоря, не поймет и заставит переписать подобный PR оптимально.

            // "Неправильный" подход
            
            #define N 12
            char data[N] = "hello world";
            
            for (int i = 0; i < N; i++) {
                if (data[i] == 'e') {
                    data[i] = 'E';
                }
            }
            
            
            // А теперь мы покажем как на самом деле надо программировать
            
            #define N 12
            char data[N] = "hello world";
            
            for (int i = 0; i < N; 
                IIF(i < MAX_INT; 
                    i++; 
                    panic("integer overflow"
                ) 
            ) { // да, но i никогда не cможет быть более N=12)
                if (
                    IIF(
                        data != NULL && i >= 0 && i < N; 
                        data[i] == 'e'; 
                        panic("buffer index out of range")
                    )
                ) {
                  // да,  но i никогда не cможет быть <0 и > N=12), data всегда != NULL, но нет, еще раз
                    IIF(
                        data != NULL && i >= 0 && i < N; 
                        data[i] = 'E';
                        panic ("buffer index out of range")
                    )
                }
            }
            


            1. AndreiVorobev
              11.04.2026 13:45

              • Люк, используй итераторы

              • И не используй знаковые типы для индексации

              • Проверки целочисленного переполнения by default есть только в дебаг версии. В gcc можно с тем же успехом -fsanitize добавить, только оно по умолчанию отключено. (Кстати, а в ваших проектах есть -fwrapv, или живёте в мире UB?)

              • Если data не является сырым указателем то он имеет NonNull тип.

              • Если вы готовы зуб отдать, гарантируя что инварианты не будут нарушены - напишите unsafe блок, с get_unchecked


              1. sceptizator
                11.04.2026 13:45

                • Итераторы для С? внимательно слушаю

                • Знаковые типы для индексации? а иначе что? вот серьезно? Я могу привести примеры как раз обратного, что безнаковый size_t это опасное заблуждение.

                • Включать проверку на целочисленное переполнение для C по-умолчанию не верно, к примеру алгоритмы расчета MD5 на целочисленном переполнении и построены (для самого было сюрпризом). UB не UB, но из песни слов не выкинуть, они реализованы именно так

                • Ммм, честно говоря, не совсем понятно о чем речь.

                • Зачем? Я просто продолжу и дальше пользовать C, не расходуя зубы и нервы, все ходы известны и записаны. Его, если не ошибаюсь, уже 30-ть лет все пытаются заменить - C++, Java, C#, D, C--, Vala, Scala, Rust, Zig (да да, на этот раз точно получится), а он зараза, все никак не заменяется :) Парадокс? Отнюдь, там фундаментальная закономерность.

                  RustOS и ZigOS кстати еще не написали? JavaOS уже была, C#.OS тоже (Singularity)


                1. AndreiVorobev
                  11.04.2026 13:45

                  Как я понял, вторая часть вашего сниппета - это утрированный пример того, как выглядит аналог ржавому коду, если явно все проверки прописать. Так вот,

                  • В расте итераторы есть и активно используются.

                  • Как минимум - избавитесь от проверки i >= 0.

                  • Да, значительная часть сишного кода такое не переживет. И да - реализации многих алгоритмов полагаются на то, что при переполнении будет выполнено приведение по модулю. И если они используют беззнаковый тип, то это не UB и 100% соответствие стандарту си.

                  • Cистема типов и инварианты раста гарантируют, что data (из вашего примера) никогда не будет null. Соответственно - никаких rt проверок.

                  • Просто так? Ничего против C или плюсов не имею. Но распробовав раст, возвращаться к ним не хочется.

                  Как минимум Redox написали…

                  И кстати, из любопытства погонял компилятор, так вот. Если первый пример кода на rust 1:1 переписать, то, при включенных оптимизациях

                  • если N - константа, то цикл размотается вообще без проверок

                  • если N - rt-переменная, то компилятор вынесет все проверки перед телом цикла

                  Но все это - такое себе. Компиляторы, конечно, нынче умные, но я не люблю на них в таких вопросах сильно полагаться. Здесь соптимизировал, там нет. И сиди гадай - где что.


            1. d3d14
              11.04.2026 13:45

              #define N 12
              char data[N] = "hello world";
              
              for (int i = 0; i < N; i++) {
                  if (data[i] == 'e') {
                      data[i] = 'E';
                  }
              }

              и что здесь неправильного?


  1. apevzner
    11.04.2026 13:45

    Вот и выросло поколение программистов, для которых написать корректный код, который не вываливается за границы массива - что-то из области научной фантастики…


    1. Tikani
      11.04.2026 13:45

      Ну так языки-альтернативы С/С++ появились как результат признания факта, что количество ошибок работы с памятью слабо коррелирует с квалификацией программиста. Потому что помимо пролома буфера есть use-after-free, double free, работа с неициализированными переменными в редко исполняющихся ветках программы и т.д. Да, какая-то часть таких ошибок предотвращается полезными привычками и дисциплиной самого программиста + разным тулингом в виде санитайзеров и статических анализаторов, но это не убирает главную причину таких ошибок - снижение внимательности и концентрации. Да и эволюционно человеческие мозги заточены на то, чтобы эффективно работать только с тем, что непосредственно перед глазами. Если в языке нет явного выражения владения и заимствования, а только неявное, в виде специфических "соглашений " и паттернов, то в какой-то момент кодовая база перестанет "влазить" в окно фокуса внимания разработчика и он так или иначе накосячит.

      Так что не очень понятно, откуда такой хейт к языкам, которые зашивают корректную работу с памятью в синтаксис и семантику.

      Самое забавное, что в индустрии есть двойные стандарты: в программистском сообществе только языки со строгой статической типизацией признаются подходящими для промышленной разработки ПО, хотя учитывая моду на "квалификационный снобизм", вообще не было бы неожиданностью, если звучали заявления что языки с динамической типизацией для настоящих профессионалов, а неучи просто не могут держать весь код в голове.


      1. Siemargl
        11.04.2026 13:45

        Так что не очень понятно, откуда такой хейт к языкам, которые зашивают корректную работу с памятью в синтаксис и семантику.

        Какой хейт? Эта функциональность начиналась ещё с Паскаля. А может и раньше, не помню.

        только языки со строгой статической типизацией признаются подходящими для промышленной разработки

        Потому что у других проблемы с производительностью.


        1. sceptizator
          11.04.2026 13:45

          Потому что у других проблемы с производительностью.

          Производительность в современном корпоративном мире чаще всего вообще не аргумент. Про нее могут начать думать лишь владельцы кластеров при числе нод более, условно, 100 штук, там это экономически обосновано.

          И то - тот-же Facebook до сих пор пишет себе на PHP, и отказываться от него не собирается. Хотя Google с Yandex вполне себе использует C++ для ключевых сервисов, потенциальные пробои буферов, целочисленные переполнения, UB и утечки памяти их почему-то не смущают.


          1. Siemargl
            11.04.2026 13:45

            Информация прямо от совета директоров?

            Или знакомые СТО рассказали?

            Не обязательно впадать в крайности - плюсы или пхп, достаточно промежуточных нормальных вариантов - от Явы до Го. Это я про Веб, на дескопе тем более другой расклад


      1. sceptizator
        11.04.2026 13:45

        Потому что помимо пролома буфера есть use-after-free, double free, работа с неициализированными переменными в редко исполняющихся ветках программы

        Неинициализированные переменные элементарно ловятся линтером или даже штатно диагностикой clang/gcc, упоминать про такое это не серъезно.

        use-afte-free, double free - это тяжелое наследие работы с HEAP через malloc/free - я уже выше упоминал APR Pools или Contexts, они были как раз специально придуманы против этого, и в т.ч. с целью получить бенефиты от идей Garbage Collector в части скорости выделения памяти, и получить правильное освобождение памяти при завершении Запроса, Сессии, Процесса.

        Про них правда мало кто знает, а напрасно. Взять любой проект с претензией на многодневный runtime, т.е. если это не короткоживущая консольная утилита, и поискать там вызовы free/malloc/new/delete, GC или ARC - то... все, пожалуй сразу пиши пропало, эти люди скорее всего просто пока еще не умеют писать действительно отказоустойчивый и защищенный код, бывают исключения конечно, да и перезапускать сервисы раз в день или даже каждый час тоже, наверное, можно.


  1. qr-kot
    11.04.2026 13:45

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

    Что мешает сделать ровно это же на С?

    Если клиенты не отваливаются и их становится больше предвыделенного массива слотов -- тогда что?