Skip to content

Instantly share code, notes, and snippets.

@koutoftimer
Last active February 8, 2026 07:43
Show Gist options
  • Select an option

  • Save koutoftimer/79427d8127f42f3829de77e5f86dfb83 to your computer and use it in GitHub Desktop.

Select an option

Save koutoftimer/79427d8127f42f3829de77e5f86dfb83 to your computer and use it in GitHub Desktop.
Minimal templating in C

What is it?

Proof of concept.

We can create arbitrary templates like index.template.h and translate them with ctrans into valid C code that we can later call as shown in run.c

$ make
gcc ctrans.c -o ctrans
./ctrans index.template.h > index.h
gcc -I. run.c
./a.out
. . first line         >end
astart  end
\tsecond line    <<
 end
\tsecond line    <<
 end
\tsecond line    <<
 end
\tsecond line    <<
 end
\tsecond line    <<
 end
\tsecond line    <<
 end
\tsecond line    <<
 end
\tsecond line    <<
 end
\tsecond line    <<
 end
\tsecond line    <<
John Doe
// initially generated with https://aistudio.google.com/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <sys/stat.h>
char const* filename = NULL;
void print(char c) {
if (c == '\"') printf("\\\"");
else if (c == '\r') printf("\\r");
else if (c == '\n') printf("\\n");
else if (c == '\\') printf("\\\\");
else printf("%c", c);
}
void process(char const* text) {
char const* cursor = text;
char const* start_tag;
while ((start_tag = strstr(cursor, "{{")) != NULL) {
// Print text before tag
if (start_tag != cursor) {
printf(" sb_append(sb, \"");
while (cursor < start_tag) {
print(*cursor++);
}
printf("\");\n");
}
// Find end of tag
const char* end_tag = strstr(start_tag, "}}");
if (!end_tag) break;
// Extract and print code
printf(" ");
const char* code_ptr = start_tag + 2;
while (code_ptr < end_tag) {
printf("%c", *code_ptr++);
}
cursor = end_tag + 2;
}
// Print remaining line content
if (*cursor != '\0') {
printf(" sb_append(sb, \"");
while (*cursor) {
print(*cursor++);
}
printf("\");\n");
}
}
int main(int argc, char** argv) {
FILE* in;
struct stat st;
char *buffer;
if (argc == 2) {
filename = argv[1];
in = fopen(filename, "r");
if (!in) {
perror("Can't open specified file");
return EXIT_FAILURE;
}
if (fstat(fileno(in), &st)) {
perror("Can't get file stat");
return EXIT_FAILURE;
}
buffer = malloc(st.st_size + 1);
if (fread(buffer, 1, st.st_size, in) == 0) {
perror("Can't read specified file");
return EXIT_FAILURE;
};
buffer[st.st_size] = '\0';
}
printf("#include <string-builder.h>\n\n");
printf("struct Context;\n\n");
printf("void render_template(struct Context* ctx, struct StringBuilder* sb);\n\n");
printf("#ifdef IMPLEMENTATION\n\n");
printf("void render_template(struct Context* ctx, struct StringBuilder* sb) {\n");
process(buffer);
printf("}\n\n");
printf("#endif // IMPLEMENTATION\n");
free(buffer);
return EXIT_SUCCESS;
}
. . first line >end
astart {{ for (int i = 0; i < 10; ++i) { }} end
\tsecond line <<
{{ } sb_append(sb, ctx->user->name); }}
run: a.out
./a.out
ctrans: ctrans.c
gcc ctrans.c -o ctrans
index.h: index.template.h ctrans
./ctrans index.template.h > index.h
a.out: index.h
gcc -I. run.c
#include <stdio.h>
struct User {
char const* name;
};
struct Context {
struct User *user;
};
#define IMPLEMENTATION
#define render_template index_page_template
#include "index.h"
#undef render_template
int main() {
struct StringBuilder sb = {0};
struct User user = {"John Doe"};
struct Context ctx = {&user};
index_page_template(&ctx, &sb);
sb.data[sb.len] = '\0';
puts(sb.data);
}
#ifndef _STRING_BUILDER_H_
#define _STRING_BUILDER_H_
struct StringBuilder;
void sb_finalize(struct StringBuilder* sb);
#define PRINTF_LIKE __attribute__((format(printf, 2, 3)))
PRINTF_LIKE void sb_appendf(struct StringBuilder* sb, char const* fmt, ...);
#define sb_append(sb, text) sb_appendf((sb), "%s", (text))
#ifdef IMPLEMENTATION
#include <assert.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
struct StringBuilder {
char data[1024 * 1024];
size_t len;
};
void
sb_finalize(struct StringBuilder* sb)
{
assert(sb->len < sizeof(sb->data) && "Too small internal buffer");
sb->data[sb->len] = '\0';
}
void
sb_appendf(struct StringBuilder* sb, char const* fmt, ...)
{
va_list args;
va_start(args, fmt);
size_t free_space = sizeof(sb->data) - sb->len - 1;
sb->len += vsnprintf(sb->data + sb->len, free_space, fmt, args);
va_end(args);
}
#endif // _STRING_BUILDER_H_
@oduortoni
Copy link

Or we could just give special meaning to the $ within the template. If it is inside the {{}}, it is the context and we replace it. It is a simple string replacement. No more magic. And no differentiating between underscores and dundersocres.

At this point it does seem as though, you introduced a problem because you did not like the ctx->, and now the whole implementation is being complicated based off a problem that should not even exist in the first place.

@koutoftimer
Copy link
Author

@koutoftimer
Copy link
Author

The drawback is that the string mode can't output local variable char *text as a string. And I'm very willing to accept this small inconvenience in the favor of epic simplicity.

@oduortoni
Copy link

The drawback is that the string mode can't output local variable char *text as a string. And I'm very willing to accept this small inconvenience in the favor of epic simplicity.

Drawback of what exactly? The complex macro or the simple $ sign string replacement.

@koutoftimer
Copy link
Author

koutoftimer commented Feb 7, 2026

@oduortoni

The complex macro or the simple $ sign string replacement.

#define $ (*ctx) is not a complex macro. It is a macro that enforces specific
syntax. Difference between $user.name and $.user.name is barely noticeable,
yet later is completely valid C code that simplifies the logic of translation
tool.

Drawback of what exactly?

{{ for (int i = 0; i < $.user.friends.len; ++i) { }}
    {{ char *name = get_friend_name($.user.friends.ids[i]); }}
    <li>{{ "%s", name }}</li>
{{ } }}

In situation like this, you can't use String mode because it doens't start
with the $. so you have to use Format mode.

@koutoftimer
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment