[Перевод] Современное веб-приложение на C: CGI, SQLite и один бинарник

Если в 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 году. В нём меньше подвижных частей, чем во всём, что я выпускал раньше. Я ещё вернусь к этому стеку.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.