C で書く現代的な Web アプリケーション

Corey Prophitt は、完全に C で書き、CGI 経由で配信する商用 Web 製品をリリースした。データベースは SQLite、配布物はバイナリひとつ。BCHS スタックが肩代わりしてくれた部分と、自分で書かねばならなかった部分をまとめる。

日本語
コピー

1994 年、動的なウェブサイトを作ろうと思ったら、C のプログラムを書いて cgi-bin ディレクトリに置き、Apache にリクエストのたびに fork させるしかなかった。それから 32 年後、私は同じ技術で商用 Web プロダクトをリリースしている。

外から見た marketyour.games は現代的だ。プログレッシブエンハンスメントな Web プロダクトで、ユーザーアカウントがあり、コンテンツクリエイター向けの検索パネルがあり、コンテンツを整理するフォルダがあり、決済は Stripe、トランザクションメールは Postmark を通している。

だが中身はまったく現代的ではない。2026 年に純粋な C で実際のプロダクトを書き、CGI で配信するという経験から思ったことを以下に書く。

土台

CGI は Web サーバーとアプリケーションの間のインターフェースだ。依存すべきフレームワークもライブラリもない。ただし、その上に積み上げるための堅牢な土台は必要になる。

私が好んで使っている土台は BCHS スタック、つまり OpenBSD、C、httpd、SQLite だ。

フレームワークもテンプレート言語もデータベースサーバーもコンテナもない。ワークフローはごく単純だ。

  • C のプログラムを書いて、CGI の環境変数を読む。
  • レスポンスは stdout に直接書き、ログメッセージは stderr に書く。
  • データベース接続が必要なら、SQLite の C API でデータベースファイルを直接開く。
  • 静的リンクしたバイナリをビルドし、cgi-bin ディレクトリに置く。

リクエストごとに新しいプロセスが起動するので、新バージョンのデプロイは古いファイルを差し替えるだけだ。サーバーの再起動もダウンタイムもない。新しいリクエストは自然と新しいバイナリを使う。

OpenBSD は妥当なデフォルト設定を備えていて、pledge や unveil のようなツールも提供してくれるので、C のプログラムを本当にロックダウンできる。httpd はデフォルトで chroot 内から配信するため、CGI バイナリはサンドボックスの中で動き、ファイルシステム上で自分に割り当てられた一角に閉じ込められる。他に 3 つのライブラリにも依存している。

  • libcurl — Stripe や Postmark への外向き Web リクエストに使う。
  • libsodium — パスワードハッシュ、cookie の暗号化、HMAC の検証に使う。
  • libcjson — Stripe や Postmark が返す JSON のパースに使う。

実際のビルド依存は libcurl と libsodium だけだ。

libcjson と SQLite は vendor 形式でプロジェクトに同梱してビルドしている。libcjson は C ファイル 1 つだけで外部依存がなく、SQLite はそのために公式の amalgamation(単一ファイルのソース)を提供しているから、これが成り立つ。

デプロイを単純にするため、すべてのアセットはビルド時にバイナリへ埋め込まれる。出来上がるビルドには可動部分がほとんどない。クリーンビルドは数秒、完全なリビルドも 1 秒かからない。

利点

フレームワークではなく、土台。

CGI はまた一つ覚えなければならないフレームワークではない。1.1 仕様は 2004 年に公開されて以来、一度も変わっていない。覚えるべき API はない。インターフェースの全体は、いくつかの基本的な HTTP の概念にすぎない。

  • リクエストヘッダー。
  • リクエストメソッド。
  • クエリ文字列。
  • リクエストボディのデータ。

HTTP を知っていれば CGI もわかる。言語にもプラットフォームにも依存しない。一度覚えればずっと使える。

関心の分離が明確。

現代の Web アプリケーションは普通、手前に nginx などの Web サーバーを置き、Ruby、Python、Go、Rust などで書いたアプリケーションサーバーへリバースプロキシする。私はこれが少し奇妙に感じていた。実際には Web サーバーが 2 つあって、そのうち片方がビジネスロジックまで担っているのだ。CGI は 2 つ目のサーバーを図から消してくれる。nginx(または httpd)のような Web サーバーが 1 つあり、それがビジネスロジックを直接呼び出す。何が何を担うかは一目瞭然だ。

デバッグも簡単になる。アプリケーションは普通のプログラムなので、シェルから直接実行できる。リクエストをパイプで流し込み、stderr を眺め、gdb でステップ実行する、といったことがそのままできる。CGI は serverless アーキテクチャによく似ている。supervisor がリクエストを受け取り、あなたのコードを呼び出し、環境変数の袋を渡して、結果を回収する。現代の serverless アプリケーションと同じで、違うのはベンダーがいないこと(そして予想外の請求もないこと)だけだ。

サプライチェーンのリスクが極めて低い。

自律エージェントの台頭と、パッケージリポジトリを狙った毎週の攻撃を思うと、依存がごく少ないというのは心強い。

現代の技術スタックのほとんどはパッケージマネージャーとそのエコシステムを伴う。便利ではあるが、依存グラフはとてつもなく膨らむ。直接依存が 1 つか 2 つあるだけで、間接依存が何百も引き込まれる。そのどれも聞いたことのないメンテナが管理していて、リリース資格情報は 2015 年から使っているパスワードで守られている。

私のサプライチェーンは C コンパイラ 1 つとライブラリ 3 つだ。これを破るにはまず libcurl を破る必要がある……インターネット全体が見張っているプロジェクトであって、週に 11 回ダウンロードされ、メンテナが 2019 年に逃げ出したパッケージではない。

ツールが単純。

私は根っからのミニマリストだ。プラグインなしの素の Vim を使っている。つまり LSP も自動補完もコードフォーマッターも AI もない。

複雑な技術スタックには複雑なツールが要る。何層もの抽象を抜けてくる言語サーバーが必要になる。だがこのスタックは十分に単純で、普通のエディタで事足りる。アプリケーション全体が C ファイルの詰まったディレクトリ 1 つと Makefile 1 つだ。

ファイルを開いて一度読めば、何をしているか分かる。

良いツールはすぐそこにある。

本当にツールが必要になったとき、それは遠くにはない。Sanitizers、valgrind、gdb、ctags、cscope……どれも成熟した(まあ、年季の入った)ツールで、何十年もかけて磨かれてきた。C がコンパイルできるシステムなら、パッケージを一つ入れるだけで使える。

成熟しているのはツールだけではない。統合は多くのエディタにネイティブで組み込まれている。たとえば vim には ctags と cscope のサポートが標準で付いており、グラフィカルな gdb フロントエンドもある(https://marketyour.games)。 を参照)。

C で HTML を書くのはそんなに悪くない。

最初はフロントエンド全体を C で書くのは気が進まなかったが、実際にやってみると想像していたよりずっと楽だった。

何度もこんなことがあった:

  • Go の html/template のシンタックスハイライトを効かせるために vim と格闘する。
  • Go テンプレートを認識させつつ HTML の検出を壊さないように helix と格闘する。
  • askama を使ったプロジェクトの jinja テンプレートを認識させるために rustrover と格闘する。

C を使えば、エディタが第二の言語を中途半端に認識する必要もないし、混在した構文でハイライトが崩れることもないし、二つの言語を連携させるプラグインで苦労することもない。見た目は良くないが、仕事はする。

SQLite は清流だ。

Web アプリを作るとき、私はいつもデフォルトで Postgres を選ぶ。良いデータベースだが、運用の負担が付いてくる。サーバープロセスを面倒見なければならないし、新しいバージョンへのアップグレードも頻繁に必要だ。SQLite にはそれが一切ない。プロセスもなければ、設定も依存もない。アプリが直接データベースファイルを開く。運用の負担はほぼゼロだ。

良くないところ

CGI はあくまでインターフェースなので、そのまま使えるものはほとんどない。結果として、以下のことは自分でツールを書いて処理しなければならない:

  • Cookie のパースと生成。
  • クエリパラメータのパースと検証。
  • フォームデータのパースと検証。
  • ページに出力する際の入力のエスケープ。

私は一つのヘッダファイル(cgi.h)に自分で一式を書いたが、これが正しいやり方なのかは分からない。kcgi のようなライブラリは既にあるが、採用すればその API とセキュリティモデルを受け入れることになる。stb 系のライブラリのように、開発者がすぐ使える共有ツール群があればいいのだが。

最後に

CGI は古いが、忘れられてはいない。

2000年代初頭に CGI を捨てた理由は、今ではほとんど成り立たない。プロセスの fork はとうに昔ほど高コストではなくなったし、現代のカーネルはこの点で多大な最適化を行っている。

仮に fork のコストが本当に問題になったとしても、FastCGI への移行は十分に簡単だ。

C でモダンな Web アプリを書くことは完全に可能だ——marketyour.games がその一例だ。開発体験はかなり快適で、モダンな技術スタックでは得られない感覚がある。私はまたこの組み合わせを使うだろう。

出典: prophitt.me← ホームへ戻る