Static template generator for C
git clone git://soophie.de/ductape
1
CC=cc2
CFLAGS=-Wall -Wextra -Werror -pedantic3
LIBS=4
TARGET=ctl4
TARGET=ductape6
build: src/main.c7
$(CC) -o $(TARGET) $(CFLAGS) $(LIBS) $^9
install: build10
cp $(TARGET) /usr/local/bin/11
mkdir -p /usr/local/include/so12
cp src/ctl.h /usr/local/include/so/11
cp src/ductape.h /usr/local/include/12
rm ./$(TARGET)14
clean:
1
# so/ctl1
# ductape3
Static template generator for C5
## Installation7
```8
git clone git@soophie.de:/srv/git/ductape9
cd ductape10
sudo make install11
```11
## Usage13
## License13
```html14
% #include "macro.h" %15
This project is licensed under the **GNU General Public License v3.0 (GPLv3)**.16
% TEMPLDEF(17
sample,18
char *message;19
) %17
**What this means:** You are free to use, study, and modify this framework. However, if you distribute a program that uses this framework (including selling it), you **must** provide the full source code of your program under the same GPLv3 license.21
<h1>% STRING($(message)) %</h1>22
<ul>23
% for (size_t i = 0; i < 10; i++) { %24
<li>% NUMBER(i) %</li>25
% } %26
</ul>28
% ENDTEMPL %29
```31
```c32
#define SO_STR_IMPL33
#include <so/str.h>35
#define CTL_OUT (str_t *) ctl->out37
#define CTL_WRITE($str) { \38
str_append(CTL_OUT, $str); \39
}41
#define $($var) ctx->$var43
#define STRING CTL_WRITE45
#define NUMBER($num) { str_appendf(CTL_OUT, "%%d", $num); }46
```48
```c49
#include <stdio.h>51
#define SO_CTL_IMPL52
#include <so/ctl.h>53
#define SO_STR_IMPL54
#include <so/str.h>56
#include "../sample.h"58
int main(void) {59
str_t out = STR_NULL;60
ctl_t *ctl = ctl_init(&out);61
sample_t data = {62
.message = "It works!",63
};64
sample_render(ctl, &data, "./sample.so");65
printf("%s", str_get(&out));66
str_free(&out);67
ctl_cleanup(ctl);68
return 0;69
}70
```72
```bash73
ctl -h sample.html > sample.h74
ctl -o sample.html > sample.so75
```19
See the [LICENSE](?license) file for the full legal text.
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 \39
} $name##_t; \40
\41
typedef void (*$name##_fn_t)(void *, $name##_t *); \42
\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) { \63
fprintf(stderr, "%s\n", err); \64
dlclose(handle); \65
return 1; \66
} \67
fn(out, ($name##_t *) ctx); \68
dlclose(handle); \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, \92
.data = $data, \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
8
// @include <so/str.h>9
#include <so/str.h>11
#define CTL_BUILDPATH "CTL_BUILDPATH"12
#define CTL_HEADERPATH "CTL_HEADERPATH"11
#define DUC_BUILDPATH "DUC_BUILDPATH"12
#define DUC_HEADERPATH "DUC_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"14
#define DUC_SYM_SIG_HASH "__DUC_SIG_HASH_"15
#define DUC_SYM_UID "__DUC_UID_"16
#define DUC_SYM_SPLIT "%"17
#define DUC_SYM_SPLIT_ESCAPE DUC_SYM_SPLIT DUC_SYM_SPLIT18
#define DUC_SYM_WRITE "DUC_WRITE"19
#define DUC_SYM_TEMPLDEF "TEMPLDEF"20
#define DUC_SYM_ENDTEMPL "ENDTEMPL"21
#define DUC_SYM_TEMPLDEF_HEADER "__TEMPLDEF_HEADER"23
#define CTL_TMP_C_PATH ".ctl_tmp.c"24
#define CTL_TMP_SO_PATH ".ctl_tmp.so"23
#define DUC_TMP_C_PATH ".duc_tmp.c"24
#define DUC_TMP_SO_PATH ".duc_tmp.so"26
#define CTL_CMD_COMPILE "cc -shared -fPIC -o %s %s"26
#define DUC_CMD_COMPILE "cc -shared -fPIC -o %s %s"28
// @enum ctl_err_t28
// @enum duc_err_t29
typedef enum {30
CTL_ERR_OK,31
CTL_ERR_NO_ARGS,32
CTL_ERR_NO_FILE,33
CTL_ERR_NO_WRITE,34
CTL_ERR_UNCLOSED_CODE,35
CTL_ERR_NO_COMPILE,36
__CTL_ERR__COUNT,37
} ctl_err_t;30
DUC_ERR_OK,31
DUC_ERR_NO_ARGS,32
DUC_ERR_NO_FILE,33
DUC_ERR_NO_WRITE,34
DUC_ERR_UNCLOSED_CODE,35
DUC_ERR_NO_COMPILE,36
__DUC_ERR__COUNT,37
} duc_err_t;39
// @enum ctl_mode_t39
// @enum duc_mode_t40
typedef enum {41
CTL_MODE_HEADER,42
CTL_MODE_MODULE,43
__CTL_MODE__COUNT,44
} ctl_mode_t;41
DUC_MODE_HEADER,42
DUC_MODE_MODULE,43
__DUC_MODE__COUNT,44
} duc_mode_t;46
// @func ctl_error_str47
void ctl_error_str(ctl_err_t err) {46
// @func duc_error_str47
void duc_error_str(duc_err_t err) {48
switch (err) {49
case CTL_ERR_OK:49
case DUC_ERR_OK:50
break;51
case CTL_ERR_NO_ARGS:52
fprintf(stderr, "Usage: ctl -h <templ_path> (HEADER), ctl -o <templ_path> (MODULE)\n");51
case DUC_ERR_NO_ARGS:52
fprintf(stderr, "Usage: ductape <files...>\n");53
break;54
case CTL_ERR_NO_FILE:54
case DUC_ERR_NO_FILE:55
fprintf(stderr, "Error: No file found!\n");56
break;57
case CTL_ERR_NO_WRITE:57
case DUC_ERR_NO_WRITE:58
fprintf(stderr, "Error: Can't write to file!\n");59
break;60
case CTL_ERR_UNCLOSED_CODE:60
case DUC_ERR_UNCLOSED_CODE:61
fprintf(stderr, "Error: Unclosed code block!\n");62
break;63
case CTL_ERR_NO_COMPILE:63
case DUC_ERR_NO_COMPILE:64
fprintf(stderr, "Error: Failed to compile!\n");65
break;66
case __CTL_ERR__COUNT:66
case __DUC_ERR__COUNT:67
break;68
}69
}71
// @func ctl_sig_hash72
size_t ctl_sig_hash(char *str, size_t len) {71
// @func duc_sig_hash72
size_t duc_sig_hash(char *str, size_t len) {73
size_t hash = 5381;74
size_t i = 0;75
while (i < len) {
84
return hash;85
}87
// @func ctl_render88
ctl_err_t ctl_render(ctl_mode_t mode, str_t *out, char *content, char **uid) {87
// @func duc_render88
duc_err_t duc_render(duc_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) {95
if (content[i] == *DUC_SYM_SPLIT) {96
// ignore escaped DUC_SYM_SPLIT97
if (i + 1 < len && content[i + 1] == *DUC_SYM_SPLIT) {98
i += 1;99
continue;100
}101
if (is_code) {102
for (size_t j = 0; j < i - set; j++) {103
if (strncmp(content + set + j, CTL_SYM_TEMPLDEF, strlen(CTL_SYM_TEMPLDEF)) == 0) {103
if (strncmp(content + set + j, DUC_SYM_TEMPLDEF, strlen(DUC_SYM_TEMPLDEF)) == 0) {104
is_templ = 1;105
hash = ctl_sig_hash(content + set, i - set);105
hash = duc_sig_hash(content + set, i - set);106
bool is_arg = false;107
bool is_uid = false;108
size_t uid_set = 0;
116
is_arg = false;117
uid_set = k;118
}119
if (is_uid && (!isalpha(c) && !isdigit(c))) {119
if (is_uid && (!isalpha(c) && !isdigit(c) && c != '_')) {120
is_uid = false;121
*uid = strndup(content + set + uid_set, k - uid_set);122
break;123
}124
}125
}126
if (strncmp(content + set + j, CTL_SYM_ENDTEMPL, strlen(CTL_SYM_ENDTEMPL)) == 0) {126
if (strncmp(content + set + j, DUC_SYM_ENDTEMPL, strlen(DUC_SYM_ENDTEMPL)) == 0) {127
is_templ = 0;128
}129
}130
if (mode == CTL_MODE_MODULE) {130
if (mode == DUC_MODE_MODULE) {131
str_append_len(out, content + set, i - set);132
str_append(out, "\n");133
}134
if (mode == CTL_MODE_HEADER && is_templ == 1) {134
if (mode == DUC_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");139
str_repall(&templdef, CTL_SYM_TEMPLDEF, CTL_SYM_TEMPLDEF_HEADER);139
str_repall(&templdef, DUC_SYM_TEMPLDEF, DUC_SYM_TEMPLDEF_HEADER);140
str_concat(out, templdef);141
str_free(&templdef);142
}
157
rem_len -= hex_len;158
str_t hex = STR_NULL;159
str_append_len(&hex, content + set + idx, hex_len);160
str_repall(&hex, CTL_SYM_SPLIT_ESCAPE, CTL_SYM_SPLIT);160
str_repall(&hex, DUC_SYM_SPLIT_ESCAPE, DUC_SYM_SPLIT);161
str_to_hex(&hex);162
if (mode == CTL_MODE_MODULE && is_templ == 1) {163
str_append(out, CTL_SYM_WRITE "(\"");162
if (mode == DUC_MODE_MODULE && is_templ == 1) {163
str_append(out, DUC_SYM_WRITE "(\"");164
str_concat(out, hex);165
str_append(out, "\")\n");166
}
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);177
str_append(&meta, "#include <ductape.h>\n");178
str_appendf(&meta, "const char " DUC_SYM_UID "%s[] = \"%s\";\n", *uid, *uid);179
str_appendf(&meta, "const size_t " DUC_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;186
return DUC_ERR_UNCLOSED_CODE;187
}188
else {189
const size_t STRING_LIMIT = 4095;
201
rem_len -= hex_len;202
str_t hex = STR_NULL;203
str_append_len(&hex, content + set + idx, hex_len);204
str_repall(&hex, CTL_SYM_SPLIT_ESCAPE, CTL_SYM_SPLIT);204
str_repall(&hex, DUC_SYM_SPLIT_ESCAPE, DUC_SYM_SPLIT);205
str_to_hex(&hex);206
if (mode == CTL_MODE_MODULE && is_templ == 1) {207
str_append(out, CTL_SYM_WRITE "(\"");206
if (mode == DUC_MODE_MODULE && is_templ == 1) {207
str_append(out, DUC_SYM_WRITE "(\"");208
str_concat(out, hex);209
str_append(out, "\")\n");210
}
212
idx += hex_len;213
}214
}215
str_repall(out, CTL_SYM_SPLIT_ESCAPE, CTL_SYM_SPLIT);216
return CTL_ERR_OK;215
str_repall(out, DUC_SYM_SPLIT_ESCAPE, DUC_SYM_SPLIT);216
return DUC_ERR_OK;217
}219
// @func ctl_file_read220
ctl_err_t ctl_file_read(char **content, const char *filename) {219
// @func duc_file_read220
duc_err_t duc_file_read(char **content, const char *filename) {221
FILE *fp = fopen(filename, "r");222
if (fp == NULL) {223
return CTL_ERR_NO_FILE;223
return DUC_ERR_NO_FILE;224
}225
fseek(fp, 0, SEEK_END);226
size_t len = ftell(fp);
229
fread(*content, sizeof(char), len, fp);230
(*content)[len] = '\0';231
fclose(fp);232
return CTL_ERR_OK;232
return DUC_ERR_OK;233
}235
// @func ctl_file_read_binary236
ctl_err_t ctl_file_read_binary(char **binary, size_t *len, const char *filename) {235
// @func duc_file_read_binary236
duc_err_t duc_file_read_binary(char **binary, size_t *len, const char *filename) {237
FILE *fp = fopen(filename, "rb");238
if (fp == NULL) {239
return CTL_ERR_NO_FILE;239
return DUC_ERR_NO_FILE;240
}241
fseek(fp, 0, SEEK_END);242
*len = ftell(fp);
244
*binary = calloc(*len, sizeof(char));245
fread(*binary, sizeof(char), *len, fp);246
fclose(fp);247
return CTL_ERR_OK;247
return DUC_ERR_OK;248
}250
// @func ctl_file_write251
ctl_err_t ctl_file_write(const char *content, size_t len, const char *filename) {250
// @func duc_file_write251
duc_err_t duc_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;254
return DUC_ERR_NO_FILE;255
}256
if (fwrite(content, sizeof(char), len, fp) != len) {257
return CTL_ERR_NO_WRITE;257
return DUC_ERR_NO_WRITE;258
}259
fclose(fp);260
return CTL_ERR_OK;260
return DUC_ERR_OK;261
}263
// @func ctl_compile_file264
ctl_err_t ctl_compile_file(ctl_mode_t mode, char *filepath, char **uid) {265
ctl_err_t err;263
// @func duc_compile_file264
duc_err_t duc_compile_file(duc_mode_t mode, char *filepath, char **uid) {265
duc_err_t err;266
char *content = NULL;267
err = ctl_file_read(&content, filepath);268
if (err != CTL_ERR_OK) {269
ctl_error_str(err);267
err = duc_file_read(&content, filepath);268
if (err != DUC_ERR_OK) {269
duc_error_str(err);270
return err;271
}272
str_t template = STR_NULL;273
err = ctl_render(mode, &template, content, uid);273
err = duc_render(mode, &template, content, uid);274
free(content);275
if (err != CTL_ERR_OK || *uid == NULL) {275
if (err != DUC_ERR_OK || *uid == NULL) {276
str_free(&template);277
ctl_error_str(err);277
duc_error_str(err);278
return err;279
}280
switch (mode) {281
case CTL_MODE_HEADER: {282
char *path = getenv(CTL_HEADERPATH);281
case DUC_MODE_HEADER: {282
char *path = getenv(DUC_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));288
duc_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;291
return DUC_ERR_OK;292
}293
case CTL_MODE_MODULE: {294
err = ctl_file_write(str_get(&template), str_len(&template), CTL_TMP_C_PATH);293
case DUC_MODE_MODULE: {294
err = duc_file_write(str_get(&template), str_len(&template), DUC_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);297
snprintf(cmd, sizeof(cmd), DUC_CMD_COMPILE, DUC_TMP_SO_PATH, DUC_TMP_C_PATH);298
int rc = system(cmd);299
remove(CTL_TMP_C_PATH);299
remove(DUC_TMP_C_PATH);300
if (rc != 0) {301
err = CTL_ERR_NO_COMPILE;302
ctl_error_str(err);301
err = DUC_ERR_NO_COMPILE;302
duc_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);307
err = duc_file_read_binary(&binary, &len, DUC_TMP_SO_PATH);308
remove(DUC_TMP_SO_PATH);309
if (err != DUC_ERR_OK) {310
duc_error_str(err);311
return err;312
}313
char *path = getenv(CTL_BUILDPATH);313
char *path = getenv(DUC_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));319
duc_file_write(binary, len, (char *) str_get(&filename));320
str_free(&filename);321
free(binary);322
return CTL_ERR_OK;322
return DUC_ERR_OK;323
}324
case __CTL_MODE__COUNT:324
case __DUC_MODE__COUNT:325
break;326
}327
return CTL_ERR_OK;327
return DUC_ERR_OK;328
}330
// @func main331
int main(int argc, char **argv) {332
if (argc <= 1) {333
return CTL_ERR_NO_ARGS;333
return DUC_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;335
char *headerpath = getenv(DUC_HEADERPATH);336
char *buildpath = getenv(DUC_BUILDPATH);337
fprintf(stdout, "[ductape] Compiling templates...\n ." DUC_HEADERPATH " = '%s'\n ." DUC_BUILDPATH " = '%s'\n", headerpath ? headerpath : "./", buildpath ? buildpath : "./");338
duc_err_t err = DUC_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;341
duc_err_t header_err = DUC_ERR_OK;342
char *uid = NULL;343
if ((header_err = ctl_compile_file(CTL_MODE_HEADER, filepath, &uid)) != CTL_ERR_OK) {343
if ((header_err = duc_compile_file(DUC_MODE_HEADER, filepath, &uid)) != DUC_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) {350
duc_err_t module_err = DUC_ERR_OK;351
if ((module_err = duc_compile_file(DUC_MODE_MODULE, filepath, &uid)) != DUC_ERR_OK) {352
err = module_err;353
}354
fprintf(stdout, "-- compile '%s' | %s [%s]\n", uid, filepath, header_err == CTL_ERR_OK && module_err == CTL_ERR_OK ? "OK" : "FAILED");354
fprintf(stdout, "-- compile '%s' | %s [%s]\n", uid, filepath, header_err == DUC_ERR_OK && module_err == DUC_ERR_OK ? "OK" : "FAILED");355
if (uid) {356
free(uid);357
}