Введение

Если в 1994 году вам нужен был динамический сайт, вы писали программу на C, клали её в каталог cgi-bin и позволяли Apache форкать её на каждый запрос. Тридцать два года спустя я выпустил коммерческий веб-продукт на, по сути, той же технологии.

Всё в marketyour.games современное. Это веб-продукт с пользовательскими аккаунтами, дашбордом поиска для контент-мейкеров, папками для их организации, оплатой через Stripe и транзакционными письмами через Postmark. Он сделан по принципу прогрессивного улучшения и вообще не требует JavaScript. Современно всё, кроме способа, которым он написан и которым раздаётся.

Здесь мои мысли о том, каково писать настоящий продукт на C и отдавать его через CGI в 2026 году.

Коротко

Закат CGI в 2004 году был оправдан, но в 2026-м он уже не выдерживает критики. Современное веб-приложение на C написать можно. Я выпустил одно. Один бинарник, один файл SQLite, три зависимости, и всё приложение помещается в голове.

Как это сделано

Стек называется BCHS: OpenBSD, C, httpd и SQLite.

Фреймворка нет. Вы пишете программу на C, которая читает переменные окружения CGI и формирует ответ.

Шаблонизатора нет. Вы пишете на C код, который выводит HTML напрямую.

Отдельного сервера БД нет. Файл базы открывается через C API SQLite.

Контейнера нет. Вы собираете статически слинкованный бинарник и кладёте его в каталог.

Каждый запрос порождает новый процесс. Выкатка новой версии — это замена одного файла: перезапуск сервера не нужен, простоя нет, новые запросы просто подхватывают новый бинарник.

OpenBSD из коробки даёт разумные настройки по умолчанию и инструменты, чтобы по-настоящему защитить программу на C, например pledge и unveil. Httpd по умолчанию работает в chroot, так что CGI-бинарник сидит в песочнице в своём уголке файловой системы.

Я использую ещё три библиотеки:

  • libcurl делает все исходящие запросы к Stripe и Postmark.

  • libsodium отвечает за генерацию случайных идентификаторов, хеширование паролей и проверку HMAC для вебхуков Stripe.

  • libcjson разбирает JSON от Stripe и Postmark.

Реальных зависимостей сборки из них только две: libcurl и libsodium. libcjson — один C-файл без внешних зависимостей. SQLite поставляется в виде официальной «амальгамы» (один C-файл). Обе эти библиотеки вендорятся и собираются вместе с проектом.

В итоге сборка почти без подвижных частей. Чистая сборка занимает несколько секунд, пересборка — меньше полусекунды.

Статику я не раздаю традиционным способом. Весь трафик httpd направляет в CGI-бинарник, а каждый ассет вкомпилирован в исполняемый файл и отдаётся из памяти.

Что мне понравилось

Основы вместо фреймворков. CGI не фреймворк, его не нужно учить. Спецификация 1.1 опубликована в 2004 году и с тех пор не менялась. Никакого API запоминать не надо. Весь интерфейс — это горстка базовых понятий HTTP: заголовки запроса, методы, строка запроса, тело. Если вы понимаете HTTP, вы уже понимаете CGI. Он не привязан ни к языку, ни к платформе. Выучил один раз — и знание годится навсегда.

Чёткое разделение ответственности. Современное веб-приложение обычно ставит впереди веб-сервер вроде nginx и проксирует запросы на сервер приложения на Ruby, Python, Go или Rust. Мне это всегда казалось странным: по сути у вас два веб-сервера, и один из них ещё и отвечает за бизнес-логику. CGI даёт один веб-сервер, который напрямую вызывает вашу бизнес-логику. Понятно, кто за что отвечает.

Отлаживать тоже проще. Ваше приложение — обычная программа, которую можно запустить из шелла: скормить ей запрос через пайп, посмотреть stderr, пройтись по ней в gdb. В каком-то смысле CGI похож на serverless: супервизор вызывает ваш код на каждый запрос, передаёт ему набор переменных окружения и забирает результат. CGI — это serverless минус вендор (и неожиданные счета).

Минимальный риск цепочки поставок. На фоне роста автономных агентов и еженедельных атак на репозитории пакетов приятно иметь очень мало зависимостей. К большинству современных стеков прилагаются менеджер пакетов и экосистема. Это удобно, но граф зависимостей раздувается: одна-две прямые зависимости тянут сотни косвенных, и каждую поддерживает кто-то, о ком вы никогда не слышали, а доступ к публикации у него защищён паролем, который он не менял с 2015 года. Моя цепочка поставок — это компилятор C и три библиотеки. Чтобы её скомпрометировать, нужно взломать libcurl, за которым следит весь интернет, а не пакет с одиннадцатью скачиваниями в неделю и владельцем, ушедшим в 2019 году.

Простой инструментарий. Я минималист по натуре. Я пользуюсь классическим Vim без единого плагина: ни LSP, ни автодополнения, ни форматтеров, ни ИИ. Сложные технологические стеки требуют сложных инструментов, например языкового сервера, чтобы продираться сквозь слои абстракции. Здесь стек настолько прост, что хватает обычного редактора. Всё приложение — каталог C-файлов и Makefile. Открываешь файл, читаешь — и знаешь, что он делает.

Хороший инструментарий под рукой. Если инструменты всё-таки нужны, они недалеко. Санитайзеры, valgrind, gdb, ctags, cscope — зрелые (ну, скорее, геронтологические) инструменты, которые десятилетиями шлифовались и ставятся одним пакетом в любой системе, где можно скомпилировать C. Зрелость не ограничивается самими инструментами: интеграция встроена во многие редакторы. В Vim есть нативная поддержка ctags и cscope и графический фронтенд для gdb (см. :h Termdebug).

Писать HTML и CSS на C не так уж плохо. Я сомневался, стоит ли писать весь фронтенд на C, но это оказалось далеко не так мучительно, как я себе представлял. Сколько раз я:

  • боролся с vim, чтобы он подсвечивал html/template из Go;

  • боролся с helix, чтобы он определял шаблоны Go и не ломал определение HTML;

  • боролся с RustRover, чтобы он распознавал шаблоны jinja в проекте на askama.

С C нет второго языка, который редактор определяет наполовину, нет смешанного синтаксиса, в котором разваливается подсветка, нет танцев с плагинами, чтобы два языка уживались. Было освежающе просто писать на C. Выглядит это некрасиво, но Tailwind тоже не красавец.

SQLite — глоток свежего воздуха. Для веб-приложений я всегда по умолчанию брал Postgres. Отличная база, но она приносит эксплуатационную нагрузку: серверный процесс, за которым надо присматривать, и частые релизы, на которые надо обновляться. У SQLite этого нет. Нет процесса, нет конфигурации, нет зависимостей. Приложение открывает файл базы напрямую. Эксплуатационная нагрузка минимальна.

Что бы я изменил

CGI — это интерфейс между веб-сервером и вашим приложением. Ни фреймворка, ни библиотеки тут нет, поэтому из коробки вы получаете очень мало. Придётся самому писать утилиты для:

  • разбора и создания cookie;

  • разбора и валидации параметров запроса;

  • разбора и валидации данных форм;

  • экранирования вводимых данных при выводе.

Я написал их сам в одном заголовочном файле (cgi.h), но не уверен, что это было правильное решение. Есть библиотеки вроде kcgi, но, взяв одну из них, вы покупаете её API и модель безопасности. Мне бы хотелось набор общих утилит, которые разработчики могли бы просто подключать (как библиотеки stb). Возможно, я опубликую cgi.h для начала.

Заключение

CGI стар, но не забыт. Большинство причин, по которым мы отказались от него в начале 2000-х, сегодня не работают. Форк процесса уже не так дорог: современные ядра сильно его оптимизируют. А если стоимость форка когда-нибудь станет проблемой, до FastCGI добраться достаточно просто.

Писать современное веб-приложение на C вполне реально: marketyour.games — коммерческий продукт, работающий на одном бинарнике по модели обслуживания, стандартизованной в 2004 году. В нём меньше подвижных частей, чем во всём, что я выпускал раньше. Я ещё вернусь к этому стеку.

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


  1. Charlz_Klug
    13.10.2026 11:44

    Не совсем понял для чего этот сайт. Маленькие белые буквы на чёрном фоне читаются плохо. Хорошо, что есть светлый режим. Вашу идею поддерживаю. Веб стал раздутым.


  1. showmelove
    13.10.2026 11:44

    Не очень понятно, зачем под это брать C, если все тоже самое можно сделать на Go и без nginx и без контейнеров, 1 бинарник под любую OS, проще и быстрее, без устаревшей CGI модели? Если язык был создан для обслуживания таких сценариев как этот и все имеет из коробки самого языка? Где тоже, не нужны фреймворки, внешние зависимости, вот это все? Я не понял идею.


  1. savostin
    13.10.2026 11:44

    А чо не на ассемблере?