Regis VidalRead between the cache lines

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.

RuntimePeak memory
Python 3.1311 MB
Ruby 3.3, ERB19 MB
Python 3.13, Jinja222 MB
PHP 8.428 MB
Node.js 2243 MB
Java 2149 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

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.