A Modern Web App in C
本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。
If you wanted a dynamic website in 1994, you wrote a C program, dropped it in a cgi-bin directory, and let Apache fork it per request. Thirty-two years later, I shipped a commercial web product using fundamentally the same technology.
如果你在 1994 年想要一个动态网站,你会编写一个 C 程序,把它放入 cgi-bin 目录,然后让 Apache 为每个请求派生(fork)一个进程。三十二年后,我使用本质上相同的技术发布了一个商业 Web 产品。
Everything about marketyour.games is modern. It’s a web product with user accounts, a search dashboard for content creators, folders for organizing them, payments via Stripe and transactional emails via Postmark. It follows progressive enhancement and doesn’t require any JavaScript.
marketyour.games 的一切都是现代化的。它是一个拥有用户账户、内容创作者搜索仪表板、用于组织内容的文件夹、通过 Stripe 进行支付以及通过 Postmark 发送事务性电子邮件的 Web 产品。它遵循渐进增强原则,不需要任何 JavaScript。
Everything is modern except the way it was made and how it is served. Here are my thoughts on writing a real product in C and serving it via CGI in 2026.
除了它的制作方式和托管方式外,一切都是现代的。以下是我关于在 2026 年用 C 语言编写真实产品并通过 CGI 提供服务的想法。
CGI’s decline was justified in 2004 but doesn’t hold up in 2026.
CGI 的衰落虽然在 2004 年是合理的,但在 2026 年已不再成立。
You can write a modern web application in C. I shipped one. One binary, a SQLite file, three dependencies and the entire thing fits in your head.
你可以用 C 语言编写现代 Web 应用程序。我已经发布了一个。一个二进制文件、一个 SQLite 文件、三个依赖项,整个系统都在你的掌控之中。
The stack is BCHS: OpenBSD, C, httpd, and SQLite.
技术栈是 BCHS:OpenBSD、C、httpd 和 SQLite。
There’s no framework. You write a C program that reads CGI environment variables and responds.
没有框架。你编写一个读取 CGI 环境变量并做出响应的 C 程序。
There’s no templating language. You write C that outputs the HTML directly.
没有模板语言。你编写直接输出 HTML 的 C 代码。
There’s no separate database server. You open the database file through SQLite’s C API.
没有独立的数据库服务器。你通过 SQLite 的 C API 打开数据库文件。
There’s no container. You build a statically linked binary and drop it in a folder.
没有容器。你构建一个静态链接的二进制文件并将其放入文件夹中。
Every request spawns a fresh process. Deploying a new version is replacing one file. That means no server restart and no downtime. New requests simply pick up the new binary.
每个请求都会生成一个全新的进程。部署新版本只需替换一个文件。这意味着无需重启服务器,也不会有停机时间。新的请求会自动使用新的二进制文件。
OpenBSD ships with sane defaults and gives you the tools to actually secure a C program like pledge and unveil. Httpd serves from a chroot by default, so your CGI binary runs sandboxed to its own corner of the filesystem.
OpenBSD 提供了合理的默认设置,并为你提供了诸如 pledge 和 unveil 之类的工具来真正保护 C 程序。Httpd 默认在 chroot 环境中提供服务,因此你的 CGI 二进制文件是在文件系统的一个独立角落中沙盒化运行的。
I lean on three additional libraries:
我依赖三个额外的库:
Only two of those are actual build dependencies: libcurl and libsodium.
其中只有两个是实际的构建依赖项:libcurl 和 libsodium。
Both are vendored and built as part of the project.
两者都被作为项目的一部分进行供应商化(vendored)并构建。
The result is a build with almost no moving parts. A clean build takes a few seconds and rebuilds complete in less than half a second.
结果是一个几乎没有活动部件的构建。干净的构建只需几秒钟,重新构建在不到半秒内即可完成。
Instead of serving static assets the traditional way, I route all traffic from httpd to the CGI binary. Every asset is compiled into the executable and served from memory.
我没有以传统方式提供静态资源,而是将所有流量从 httpd 路由到 CGI 二进制文件。每个资源都被编译进可执行文件中,并从内存中提供服务。
CGI isn’t a framework to learn. The 1.1 specification was published in 2004 and hasn’t changed since.
CGI 不是一个需要学习的框架。其 1.1 规范发布于 2004 年,此后从未改变。
There’s no API to internalize. The entire interface is a handful of fundamental HTTP concepts:
没有需要内化的 API。整个接口就是少数几个基本的 HTTP 概念:
If you understand HTTP, you already understand CGI.
如果你理解 HTTP,你就已经理解了 CGI。
It’s language and platform agnostic. You learn it once and the knowledge is good forever.
它与语言和平台无关。你学一次,知识终身受用。
A modern web application typically puts a web server like nginx in front and reverse-proxies to an application server written in another language like Ruby, Python, Go, or Rust. That always felt a bit strange to me: you essentially have two web servers, and one of them is also responsible for your business logic.
现代 Web 应用程序通常会在前端放置一个像 nginx 这样的 Web 服务器,并反向代理到用 Ruby、Python、Go 或 Rust 等其他语言编写的应用程序服务器。这对我来说总是感觉有点奇怪:你本质上拥有两个 Web 服务器,其中一个还负责你的业务逻辑。
CGI gives you a single web server that calls your business logic directly. It’s clear who does what.
CGI 为你提供了一个直接调用业务逻辑的单一 Web 服务器。谁负责什么一目了然。
It makes debugging simpler, too. Your application is an ordinary program you can run from a shell: pipe it a request, watch stderr, step through it with gdb.
这也简化了调试。你的应用程序是一个普通的程序,你可以从 shell 运行它:通过管道输入请求,观察 stderr,并使用 gdb 进行单步调试。
In a sense, CGI resembles serverless architecture: a supervisor invokes your code per request, hands it a bag of environment variables, and collects the result. CGI is serverless, minus the vendor (and surprise bills).
从某种意义上说,CGI 类似于无服务器架构:监督程序按请求调用你的代码,交给它一堆环境变量,并收集结果。CGI 就是无服务器,只是没有了供应商(以及意外账单)。
With the rise of autonomous agents and weekly attacks on package repositories, it feels reassuring to have very few dependencies.
随着自主代理的兴起和对包存储库的每周攻击,拥有极少的依赖项让人感到安心。
Most modern tech stacks come with a package manager and an ecosystem to go along with it. It’s convenient, but the dependency graph balloons. One or two direct dependencies can pull in hundreds of indirect ones, each maintained by somebody you’ve never heard of, with publish credentials protected by a password reused since 2015.
大多数现代技术栈都附带一个包管理器及其生态系统。这很方便,但依赖图会膨胀。一两个直接依赖项可能会引入数百个间接依赖项,每一个都由你从未听说过的人维护,且其发布凭据受到自 2015 年以来重复使用的密码保护。
My supply chain is a C compiler and three libraries. Compromising it would require compromising libcurl… a project the entire internet watches, not a package with eleven weekly downloads and an owner who left in 2019.
我的供应链是一个 C 编译器和三个库。破坏它需要破坏 libcurl……这是一个整个互联网都在关注的项目,而不是一个每周只有 11 次下载且所有者在 2019 年就离开的包。
I’m a minimalist at heart. I use classic Vim with zero plugins. That means no LSP, no autocomplete, no code formatters, and no AI.
我本质上是一个极简主义者。我使用没有任何插件的经典 Vim。这意味着没有 LSP、没有自动补全、没有代码格式化程序,也没有 AI。
Complex tech stacks demand complex tooling: they demand a language server to wade through the layers of abstraction. But this stack is simple enough a plain editor is all you need. The entire application is a directory of C files and a Makefile.
复杂的技术栈需要复杂的工具:它们需要语言服务器来梳理抽象层。但这个技术栈足够简单,你只需要一个普通的编辑器。整个应用程序就是一个 C 文件目录和一个 Makefile。
Open a file, read it, and you know what it does.
打开一个文件,阅读它,你就知道它做了什么。
When you do want tooling, it’s not far. Sanitizers, valgrind, gdb, ctags, cscope… these are mature (well, geriatric) tools that have been refined for decades and they’re a package install away on any system that can compile C.
当你确实需要工具时,它们触手可及。Sanitizers、valgrind、gdb、ctags、cscope……这些都是经过数十年打磨的成熟(甚至可以说是老古董)工具,在任何可以编译 C 的系统上,只需安装一个包即可使用。
The maturity goes further than the tools themselves: integration is built into many editors. Vim ships with native support for ctags, cscope and has a graphical gdb frontend (see :h Termdebug).
这种成熟度不仅限于工具本身:许多编辑器都内置了集成功能。Vim 自带对 ctags、cscope 的原生支持,并拥有一个图形化的 gdb 前端(参见 :h Termdebug)。
I was hesitant to write the entire frontend in C, but it wasn’t nearly as painful as I imagined.
我曾犹豫是否要用 C 语言编写整个前端,但这并没有我想象中那么痛苦。
All too many times have I:
我经历过太多次:
Using C means there’s no second language for your editor to half-detect, no mixed-syntax where highlighting falls apart, no plugin dance to get the two languages to cooperate.
使用 C 意味着你的编辑器不需要半吊子地检测第二种语言,没有语法混合导致高亮显示崩溃的情况,也不需要为了让两种语言协作而进行插件配置的折腾。
It was refreshing to just write C. It isn’t pretty, but neither is Tailwind.
只写 C 语言的感觉很清爽。它并不漂亮,但 Tailwind 也不漂亮。
I’ve always defaulted to Postgres for web applications. It’s a great database, but it comes with some operational burden. There’s a server process to babysit and frequent releases to upgrade to.
我一直默认将 Postgres 用于 Web 应用程序。它是一个很棒的数据库,但它带来了一些运维负担。有一个需要照看的服务器进程,还有频繁的升级版本。
SQLite has none of that. There’s no process, no configuration, no dependencies. Your application opens the database file directly. The operational burden is minimal.
SQLite 没有这些问题。没有进程、没有配置、没有依赖项。你的应用程序直接打开数据库文件。运维负担极小。
CGI is an interface between a web server and your application. There’s no framework or library. This means you get very little out of the box.
CGI 是 Web 服务器和应用程序之间的接口。没有框架或库。这意味着你开箱即用的功能很少。
You need to write your own utilities to handle things like:
你需要编写自己的实用程序来处理诸如以下事项:
I rolled my own in a single header file (cgi.h), but I’m not convinced that was the right solution. Libraries like kcgi exist, but adopting one means buying into its API and security model.
我用一个头文件 (cgi.h) 编写了自己的工具,但我并不确定这是正确的解决方案。虽然存在像 kcgi 这样的库,但采用它意味着必须接受其 API 和安全模型。
I’d like to see a set
我希望能看到一套