2026-08-10
Compiled templates for C
Update: this project became tpl2c in version 0.4.
Plenty of C programs write web pages: the status page of a server, the settings of a router, a CGI tool, the debug page of a game server. I like C for this. It is fast, small, and it runs everywhere. But it usually writes HTML with printf, and that breaks easily.
The usual way
Most of the time it looks like this:
printf("<tr><td>%s</td><td>%d</td></tr>\n", item->name, item->stock);
This breaks as soon as item->name contains a <. At best the page is broken; at worst it runs someone else's script. Escaping every value by hand fixes it until I forget one. And the HTML is spread across format strings: moving a column means editing C.
Weight
The usual alternative is a web stack in another language, and its runtime alone is heavy. I measured a script that prints one escaped line, <p>Hello, Ann!</p>, in a few runtimes, on Debian 13: the peak memory of the whole process, as the kernel reports it, median of five runs.
| Runtime | Peak memory |
|---|---|
| Python 3.13 | 11 MB |
| Ruby 3.3, ERB | 19 MB |
| Python 3.13, Jinja2 | 22 MB |
| PHP 8.4 | 28 MB |
| Node.js 22 | 43 MB |
| Java 21 | 49 MB |
That is for one line. A framework, its dependencies and a database driver come on top. I want the C version of that page to need about one megabyte, most of it the C library.
The idea
A PHP page reads like the page it produces. I want that in C, without an interpreter. A template is the page with C in it:
<?c const char *who = hc_context; ?>
<p>Hello, <?= who ? who : "world" ?>!</p>
A translator turns it into a C function at build time:
void hc_template_hello(hc_out *hc_o, void *hc_context)
{
const char *who = hc_context;
hc_write(hc_o, "<p>Hello, ", 10);
hc_put_str_esc(hc_o, HC_HTML, (who ? who : "world"));
hc_write(hc_o, "!</p>\n", 6);
}
Code between the tags is copied as it is, so the compiler checks it. Text outside the tags becomes one write with a length known at compile time, which ends up as a memcpy. A value printed with <?= is escaped by default; printing it raw needs a different tag.
The translator also writes #line directives, so compiler errors point to the template, not to the generated file.
Goals
- C89. The translator, the runtime and the generated code build with any C89 compiler, old ones and small targets included.
- No allocation while rendering. The caller passes a buffer, its size and a function to flush it.
- No global state. A page gets its output and its context as arguments, so it works in one process, in prefork workers or in threads.
- One C file per template.
makerebuilds only what changed. Routing and HTTP stay in the program. - Small. About a thousand lines for the translator, about the same for the runtime.
Templates are trusted code, like PHP files, so there is no sandbox. The translator does not parse C; that is the compiler's job. No layout inheritance, and no compiling at request time.
Similar ideas
There are already projects and frameworks that take the same approach.
KLone, by KoanLogic, is the closest: pages written in C between <% %> tags are compiled, with the web server, into a single binary for small devices.
Omar Polo wrote a template compiler for gotwebd, the web front end of Game of Trees, after the same frustration with printf. It turns templates into C, escapes by default and uses #line, like yacc.
htmlcc compiles HTML templates to C with no allocation, and lets the program decide how the output is written. The translator is a Python program, and the templates use a Jinja-like syntax.
onion, an HTTP library for C, builds Django-like templates into the program with its otemplate tool.
In C++, CppCMS compiles its templates to C++ with cppcms_tmpl_cc, and Drogon turns its views, C++ inside HTML, into C++ files with drogon_ctl.
Next
This is not my first try. My first idea was to let the C preprocessor do the parsing, with two macros and the template included as a file:
#define TEXT(s) hc_write(hc_o, s, sizeof(s) - 1);
#define VALUE(e) hc_put_str_esc(hc_o, HC_HTML, (e));
void hello(hc_out *hc_o, const char *who)
{
#include "hello.tpl"
}
with hello.tpl holding:
TEXT("<p>Hello, ") VALUE(who) TEXT("!</p>\n")
It works, and it needs no other tool. But the template is no longer the page. The preprocessor reads its input as C tokens, so every piece of text has to sit in a string literal, and an apostrophe in plain text is an unterminated character constant. I gave up on it quickly.
The first step this time is the translator itself: pages and partials in, C functions out, and a test that renders real pages and compares them with the expected output.