Static template generator for C
git clone git://soophie.de/ductape
1
// Copyright (C) 2026 Sophie Sitzler2
//3
// This program is free software: you can redistribute it and/or modify4
// it under the terms of the GNU General Public License as published by5
// the Free Software Foundation, either version 3 of the License, or6
// (at your option) any later version.7
//8
// This program is distributed in the hope that it will be useful,9
// but WITHOUT ANY WARRANTY; without even the implied warranty of10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11
// GNU General Public License for more details.12
//13
// You should have received a copy of the GNU General Public License14
// along with this program. If not, see <https://www.gnu.org/licenses/>.16
// @file ctl.h18
#ifndef _SO_CTL_H19
#define _SO_CTL_H21
#include <stdio.h>22
#include <stdlib.h>24
// @type ctl_render_fn_t25
typedef int (*ctl_render_fn_t)(void *, void *, const char *);27
// @struct ctl_templref_t28
typedef struct {29
ctl_render_fn_t fn;30
void *data;31
const char *dl_path;32
} ctl_templref_t;34
// @macro __TEMPLDEF_MACRO35
// @internal36
#define __TEMPLDEF_MACRO($name, $params) \37
typedef struct { \38
$params \
40
\41
typedef void (*$name##_fn_t)(void *, $name##_t *); \42
\22
int $name##_render(void *out, void *ctx, const char *dl_path) { \43
int $name##_render(void *out, void *ctx) { \44
char *path = getenv("CTL_BUILDPATH"); \45
if (path == NULL) { \46
path = "./"; \47
} \48
char dl_path[strlen(path) + strlen(__CTL_UID_##$name) + 4]; \49
snprintf(dl_path, sizeof(dl_path), "%s%s.so", path, __CTL_UID_##$name); \50
void *handle = dlopen(dl_path, RTLD_NOW); \51
if (!handle) { \52
fprintf(stderr, "%s\n", dlerror()); \53
return 1; \54
} \55
const size_t *hash = (size_t *) dlsym(handle, "__CTL_SIG_HASH_" #$name); \56
if (__CTL_SIG_HASH_##$name != *hash) { \57
fprintf(stderr, "Invalid signature hash! (%llx != %llx)\n", __CTL_SIG_HASH_##$name, *hash); \58
return 1; \59
} \60
$name##_fn_t fn = ($name##_fn_t) dlsym(handle, "ctl_" #$name); \61
char *err = dlerror(); \62
if (err) { \
69
return 0; \70
}72
// @macro __TEMPLDEF_HEADER73
// @internal74
#define __TEMPLDEF_HEADER($name, $params) \75
__TEMPLDEF_MACRO($name, $params) \76
\77
void ctl_##$name(void *out, $name##_t *ctx);79
// @macro TEMPLDEF80
#define TEMPLDEF($name, $params) \81
__TEMPLDEF_MACRO($name, $params) \82
\83
void ctl_##$name(void *out, $name##_t *ctx) {85
// @macro ENDTEMPL86
#define ENDTEMPL }88
// @macro TEMPLBIND89
#define TEMPLBIND($name, $data, $dl_path) \90
(ctl_templref_t) { \91
.fn = $name##_render, \
93
.dl_path = $dl_path, \94
},96
// @macro INCLUDE97
#define INCLUDE($name) ctx->$name.fn(ctl, ctx->$name.data, ctx->$name.dl_path);99
#endif // _SO_CTL_H
1
// @file main.c3
#include <stdio.h>4
#include <stdlib.h>3
#include <getopt.h>5
#include <ctype.h>7
#define SO_STR_IMPL8
// @include <so/str.h>9
#include <so/str.h>11
#define CTL_BUILDPATH "CTL_BUILDPATH"12
#define CTL_HEADERPATH "CTL_HEADERPATH"14
#define CTL_SYM_SIG_HASH "__CTL_SIG_HASH_"15
#define CTL_SYM_UID "__CTL_UID_"16
#define CTL_SYM_SPLIT "%"17
#define CTL_SYM_SPLIT_ESCAPE CTL_SYM_SPLIT CTL_SYM_SPLIT18
#define CTL_SYM_WRITE "CTL_WRITE"19
#define CTL_SYM_TEMPLDEF "TEMPLDEF"20
#define CTL_SYM_ENDTEMPL "ENDTEMPL"21
#define CTL_SYM_TEMPLDEF_HEADER "__TEMPLDEF_HEADER"23
#define CTL_TMP_C_PATH ".ctl_tmp.c"24
#define CTL_TMP_SO_PATH ".ctl_tmp.so"26
#define CTL_CMD_COMPILE "cc -shared -fPIC -o %s %s"28
// @enum ctl_err_t29
typedef enum {30
CTL_ERR_OK,31
CTL_ERR_NO_ARGS,
33
CTL_ERR_NO_WRITE,34
CTL_ERR_UNCLOSED_CODE,35
CTL_ERR_NO_COMPILE,36
__CTL_ERR__COUNT,37
} ctl_err_t;39
// @enum ctl_mode_t40
typedef enum {41
CTL_MODE_HEADER,42
CTL_MODE_MODULE,43
__CTL_MODE__COUNT,44
} ctl_mode_t;46
// @func ctl_error_str47
void ctl_error_str(ctl_err_t err) {48
switch (err) {49
case CTL_ERR_OK:
63
case CTL_ERR_NO_COMPILE:64
fprintf(stderr, "Error: Failed to compile!\n");65
break;66
case __CTL_ERR__COUNT:67
break;68
}69
}47
ctl_err_t ctl_render(ctl_mode_t mode, str_t *template, char *content) {48
int len = strlen(content);49
int set = 0;50
int is_code = 0;51
int is_templ = 0;52
str_append(template, "#include <dlfcn.h>\n");53
str_append(template, "#include <so/ctl.h>\n");54
for (int i = 0; i < len; i++) {55
if (content[i] == '%') {56
// ignore escaped %57
if (i + 1 < len && content[i + 1] == '%') {71
// @func ctl_sig_hash72
size_t ctl_sig_hash(char *str, size_t len) {73
size_t hash = 5381;74
size_t i = 0;75
while (i < len) {76
unsigned char c = (unsigned) str[i];77
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {78
i++;79
continue;80
}81
hash = ((hash << 5) + hash) + c;82
i++;83
}84
return hash;85
}87
// @func ctl_render88
ctl_err_t ctl_render(ctl_mode_t mode, str_t *out, char *content, char **uid) {89
size_t len = strlen(content);90
size_t set = 0;91
size_t is_code = 0;92
size_t is_templ = 0;93
size_t hash = 0;94
for (size_t i = 0; i < len; i++) {95
if (content[i] == *CTL_SYM_SPLIT) {96
// ignore escaped CTL_SYM_SPLIT97
if (i + 1 < len && content[i + 1] == *CTL_SYM_SPLIT) {98
i += 1;99
continue;100
}101
if (is_code) {62
for (int j = 0; j < i - set; j++) {63
if (strncmp(content + set + j, "TEMPLDEF", 8) == 0) {102
for (size_t j = 0; j < i - set; j++) {103
if (strncmp(content + set + j, CTL_SYM_TEMPLDEF, strlen(CTL_SYM_TEMPLDEF)) == 0) {104
is_templ = 1;105
hash = ctl_sig_hash(content + set, i - set);106
bool is_arg = false;107
bool is_uid = false;108
size_t uid_set = 0;109
for (size_t k = j; k < i - set; k++) {110
char c = content[set + k];111
if (c == '(') {112
is_arg = true;113
}114
if (is_arg && isalpha(c)) {115
is_uid = true;116
is_arg = false;117
uid_set = k;118
}119
if (is_uid && (!isalpha(c) && !isdigit(c))) {120
is_uid = false;121
*uid = strndup(content + set + uid_set, k - uid_set);122
break;123
}124
}125
}66
if (strncmp(content + set + j, "ENDTEMPL", 8) == 0) {126
if (strncmp(content + set + j, CTL_SYM_ENDTEMPL, strlen(CTL_SYM_ENDTEMPL)) == 0) {127
is_templ = 0;128
}129
}130
if (mode == CTL_MODE_MODULE) {71
str_append_len(template, content + set, i - set);72
str_append(template, "\n");131
str_append_len(out, content + set, i - set);132
str_append(out, "\n");133
}134
if (mode == CTL_MODE_HEADER && is_templ == 1) {135
is_templ = 0;136
str_t templdef = STR_NULL;137
str_append_len(&templdef, content + set, i - set);138
str_append(&templdef, "\n");79
str_repall(&templdef, "TEMPLDEF", "__TEMPLDEF_HEADER");80
str_concat(template, templdef);139
str_repall(&templdef, CTL_SYM_TEMPLDEF, CTL_SYM_TEMPLDEF_HEADER);140
str_concat(out, templdef);141
str_free(&templdef);142
}143
}144
else {85
const int STRING_LIMIT = 4095;86
int str_len = i - set;87
int rem_len = str_len;88
int hex_len = 0;89
int idx = 0;145
const size_t STRING_LIMIT = 4095;146
size_t str_len = i - set;147
size_t rem_len = str_len;148
size_t hex_len = 0;149
size_t idx = 0;150
while (rem_len > 0) {151
if (rem_len > STRING_LIMIT) {152
hex_len = STRING_LIMIT;
157
rem_len -= hex_len;158
str_t hex = STR_NULL;159
str_append_len(&hex, content + set + idx, hex_len);100
str_repall(&hex, "%%", "%");160
str_repall(&hex, CTL_SYM_SPLIT_ESCAPE, CTL_SYM_SPLIT);161
str_to_hex(&hex);162
if (mode == CTL_MODE_MODULE && is_templ == 1) {103
str_append(template, "CTL_WRITE(\"");104
str_concat(template, hex);105
str_append(template, "\")\n");163
str_append(out, CTL_SYM_WRITE "(\"");164
str_concat(out, hex);165
str_append(out, "\")\n");166
}167
str_free(&hex);168
idx += hex_len;
172
set = i + 1;173
}174
}175
str_t meta = STR_NULL;176
str_append(&meta, "#include <dlfcn.h>\n");177
str_append(&meta, "#include <so/ctl.h>\n");178
str_appendf(&meta, "const char " CTL_SYM_UID "%s[] = \"%s\";\n", *uid, *uid);179
str_appendf(&meta, "const size_t " CTL_SYM_SIG_HASH "%s = %ld;\n", *uid, hash);180
str_concat(&meta, *out);181
str_free(out);182
*out = STR_NULL;183
str_concat(out, meta);184
str_free(&meta);185
if (is_code) {186
return CTL_ERR_UNCLOSED_CODE;187
}188
else {119
const int STRING_LIMIT = 4095;120
int str_len = len - set;121
int rem_len = str_len;122
int hex_len = 0;123
int idx = 0;189
const size_t STRING_LIMIT = 4095;190
size_t str_len = len - set;191
size_t rem_len = str_len;192
size_t hex_len = 0;193
size_t idx = 0;194
while (rem_len > 0) {195
if (rem_len > STRING_LIMIT) {196
hex_len = STRING_LIMIT;
201
rem_len -= hex_len;202
str_t hex = STR_NULL;203
str_append_len(&hex, content + set + idx, hex_len);134
str_repall(&hex, "%%", "%");204
str_repall(&hex, CTL_SYM_SPLIT_ESCAPE, CTL_SYM_SPLIT);205
str_to_hex(&hex);206
if (mode == CTL_MODE_MODULE && is_templ == 1) {137
str_append(template, "CTL_WRITE(\"");138
str_concat(template, hex);139
str_append(template, "\")\n");207
str_append(out, CTL_SYM_WRITE "(\"");208
str_concat(out, hex);209
str_append(out, "\")\n");210
}211
str_free(&hex);212
idx += hex_len;213
}214
}145
str_repall(template, "%%", "%");215
str_repall(out, CTL_SYM_SPLIT_ESCAPE, CTL_SYM_SPLIT);216
return CTL_ERR_OK;217
}219
// @func ctl_file_read220
ctl_err_t ctl_file_read(char **content, const char *filename) {221
FILE *fp = fopen(filename, "r");222
if (fp == NULL) {223
return CTL_ERR_NO_FILE;224
}225
fseek(fp, 0, SEEK_END);155
int len = ftell(fp);226
size_t len = ftell(fp);227
fseek(fp, 0, SEEK_SET);228
*content = calloc(len + 1, sizeof(char));229
fread(*content, sizeof(char), len, fp);
232
return CTL_ERR_OK;233
}235
// @func ctl_file_read_binary236
ctl_err_t ctl_file_read_binary(char **binary, size_t *len, const char *filename) {237
FILE *fp = fopen(filename, "rb");238
if (fp == NULL) {
247
return CTL_ERR_OK;248
}178
ctl_err_t ctl_file_write(const char *content, const char *filename) {250
// @func ctl_file_write251
ctl_err_t ctl_file_write(const char *content, size_t len, const char *filename) {252
FILE *fp = fopen(filename, "w");253
if (fp == NULL) {254
return CTL_ERR_NO_FILE;255
}183
size_t len = strlen(content);256
if (fwrite(content, sizeof(char), len, fp) != len) {257
return CTL_ERR_NO_WRITE;258
}
260
return CTL_ERR_OK;261
}191
int main(int argc, char **argv) {263
// @func ctl_compile_file264
ctl_err_t ctl_compile_file(ctl_mode_t mode, char *filepath, char **uid) {265
ctl_err_t err;193
opterr = 0;194
ctl_mode_t mode;195
char *path = NULL;196
int opt;197
while ((opt = getopt(argc, argv, "h:o:")) != -1) {198
switch (opt) {199
case 'h':200
mode = CTL_MODE_HEADER;201
path = optarg;202
break;203
case 'o':204
mode = CTL_MODE_MODULE;205
path = optarg;206
break;207
default:208
err = CTL_ERR_NO_ARGS;209
ctl_error_str(err);210
return err;211
}212
}213
if (path == NULL) {214
err = CTL_ERR_NO_ARGS;215
ctl_error_str(err);216
return err;217
}266
char *content = NULL;219
err = ctl_file_read(&content, path);267
err = ctl_file_read(&content, filepath);268
if (err != CTL_ERR_OK) {269
ctl_error_str(err);270
return err;271
}272
str_t template = STR_NULL;225
err = ctl_render(mode, &template, content);273
err = ctl_render(mode, &template, content, uid);274
free(content);227
if (err != CTL_ERR_OK) {275
if (err != CTL_ERR_OK || *uid == NULL) {276
str_free(&template);277
ctl_error_str(err);278
return err;279
}232
if (mode == CTL_MODE_HEADER) {233
fprintf(stdout, "%s", str_get(&template));234
str_free(&template);235
return CTL_ERR_OK;280
switch (mode) {281
case CTL_MODE_HEADER: {282
char *path = getenv(CTL_HEADERPATH);283
if (path == NULL) {284
path = "./";285
}286
str_t filename = STR_NULL;287
str_appendf(&filename, "%s%s.h", path, *uid);288
ctl_file_write((char *) str_get(&template), str_len(&template), (char *) str_get(&filename));289
str_free(&filename);290
str_free(&template);291
return CTL_ERR_OK;292
}293
case CTL_MODE_MODULE: {294
err = ctl_file_write(str_get(&template), str_len(&template), CTL_TMP_C_PATH);295
str_free(&template);296
char cmd[500];297
snprintf(cmd, sizeof(cmd), CTL_CMD_COMPILE, CTL_TMP_SO_PATH, CTL_TMP_C_PATH);298
int rc = system(cmd);299
remove(CTL_TMP_C_PATH);300
if (rc != 0) {301
err = CTL_ERR_NO_COMPILE;302
ctl_error_str(err);303
return err;304
}305
char *binary = NULL;306
size_t len = 0;307
err = ctl_file_read_binary(&binary, &len, CTL_TMP_SO_PATH);308
remove(CTL_TMP_SO_PATH);309
if (err != CTL_ERR_OK) {310
ctl_error_str(err);311
return err;312
}313
char *path = getenv(CTL_BUILDPATH);314
if (path == NULL) {315
path = "./";316
}317
str_t filename = STR_NULL;318
str_appendf(&filename, "%s%s.so", path, *uid);319
ctl_file_write(binary, len, (char *) str_get(&filename));320
str_free(&filename);321
free(binary);322
return CTL_ERR_OK;323
}324
case __CTL_MODE__COUNT:325
break;326
}237
if (mode == CTL_MODE_MODULE) {238
err = ctl_file_write(str_get(&template), CTL_TMP_C_PATH);239
str_free(&template);240
char cmd[500];241
snprintf(cmd, sizeof(cmd), "cc -shared -fPIC -o %s %s", CTL_TMP_SO_PATH, CTL_TMP_C_PATH);242
int rc = system(cmd);243
remove(CTL_TMP_C_PATH);244
if (rc != 0) {245
err = CTL_ERR_NO_COMPILE;246
ctl_error_str(err);247
return err;327
return CTL_ERR_OK;328
}330
// @func main331
int main(int argc, char **argv) {332
if (argc <= 1) {333
return CTL_ERR_NO_ARGS;334
}335
char *headerpath = getenv(CTL_HEADERPATH);336
char *buildpath = getenv(CTL_BUILDPATH);337
fprintf(stdout, "[ctl] Compiling templates...\n .HEADERPATH = '%s'\n .BUILDPATH = '%s'\n", headerpath ? headerpath : "./", buildpath ? buildpath : "./");338
ctl_err_t err = CTL_ERR_OK;339
for (size_t i = 1; i < (size_t) argc; i++) {340
char *filepath = argv[i];341
ctl_err_t header_err = CTL_ERR_OK;342
char *uid = NULL;343
if ((header_err = ctl_compile_file(CTL_MODE_HEADER, filepath, &uid)) != CTL_ERR_OK) {344
err = header_err;345
}346
if (uid) {347
free(uid);348
}349
uid = NULL;350
ctl_err_t module_err = CTL_ERR_OK;351
if ((module_err = ctl_compile_file(CTL_MODE_MODULE, filepath, &uid)) != CTL_ERR_OK) {352
err = module_err;353
}249
char *binary = NULL;250
size_t len = 0;251
err = ctl_file_read_binary(&binary, &len, CTL_TMP_SO_PATH);252
remove(CTL_TMP_SO_PATH);253
if (err != CTL_ERR_OK) {254
ctl_error_str(err);255
return err;354
fprintf(stdout, "-- compile '%s' | %s [%s]\n", uid, filepath, header_err == CTL_ERR_OK && module_err == CTL_ERR_OK ? "OK" : "FAILED");355
if (uid) {356
free(uid);357
}257
fwrite(binary, sizeof(char), len, stdout);258
free(binary);259
return CTL_ERR_OK;358
}359
return err;360
}