diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..81717c6 --- /dev/null +++ b/DOCS.md @@ -0,0 +1,40 @@ +# Docs + +This file is meant to explain how it's working. I'll try to keep it up to date. + +## General idea + +Meta programming is really useful, in a lots of ways, that's why I'm trying to implement this in C, in the most pleasant way. +Header-only libraries are also a must imo, so let's try to implement meta programming within a single header! The same header for both compilation and runtime btw. + +## Compilation time + +`cmeta.h` contains the code to parse structs, lexing is handled by `stb_c_lexer` (for now). +The `bool process_file(const char *file_path)` function calls `gcc -E ` (so we handle macros etc), then parses the result. It then generates all `Struct_Info`s in a special place in the header, delimited by two `// AUTO GENERATED CODE //`. +If anything has failed, nothing will be generated between the two comments, so cmeta remains intact. + +This struct: +```c +typedef struct { + int int_field; +} Foo_Struct; +``` + +Will be converted to this: +```c +// cmeta.h + +// AUTO GENERATED CODE // +static Struct_Info foo_struct_info = { + .name = "Foo_Struct", + .fields_count = 1, + .fields = (Field_Info[1]) { + { .type = "int", .name = "int_field" }, + }, +}; +// AUTO GENERATED CODE // +``` + +## Runtime + +`cmeta.h` now contains both type infos and useful functions to interact with them. diff --git a/example/01_simple_struct/Makefile b/example/01_simple_struct/Makefile new file mode 100644 index 0000000..897c5f9 --- /dev/null +++ b/example/01_simple_struct/Makefile @@ -0,0 +1,8 @@ +all: cmeta main + +cmeta: main.c cmeta.h cmeta.c + gcc cmeta.c -I../../third_party/ -o cmeta + +main: cmeta.h main.c + ./cmeta + gcc main.c -o main diff --git a/example/01_simple_struct/cmeta.c b/example/01_simple_struct/cmeta.c new file mode 100644 index 0000000..62f22fe --- /dev/null +++ b/example/01_simple_struct/cmeta.c @@ -0,0 +1,8 @@ +#define CMETA_COMPTIME +#include "cmeta.h" + +int main(void) { + if (!process_file("./main.c")) return 1; + + return 0; +} \ No newline at end of file diff --git a/example/01_simple_struct/cmeta.h b/example/01_simple_struct/cmeta.h new file mode 100644 index 0000000..3ec5ee7 --- /dev/null +++ b/example/01_simple_struct/cmeta.h @@ -0,0 +1,413 @@ +#ifndef CMETA_H +#define CMETA_H + +#include + +typedef struct { + const char* type; + const char* name; +} Field_Info; + +typedef struct { + const char* name; + size_t fields_count; + Field_Info *fields; +} Struct_Info; + +// AUTO GENERATED CODE // +// AUTO GENERATED CODE // + +#ifdef CMETA_COMPTIME + +#include +#include +#include +#include +#include + +#define STB_C_LEXER_IMPLEMENTATION +#include "stb_c_lexer.h" + +stb_lexer lexer = {0}; + +typedef struct { + char* type; + char* name; +} Parsed_Field_Info; + +typedef struct { + Parsed_Field_Info* items; + size_t count; + size_t capacity; +} Parsed_Field_Infos; + +typedef struct { + char* name; + size_t fields_count; + Parsed_Field_Info *fields; +} Parsed_Struct_Info; + +typedef struct { + Parsed_Struct_Info* items; + size_t count; + size_t capacity; +} Parsed_Struct_Infos; + +typedef struct { + char* data; + size_t capacity; + size_t length; +} String_Builder; + +void sb_append(String_Builder* sb, const char* data) { + size_t data_len = strlen(data); + size_t total_len = sb->length + data_len; + + if (total_len + 1 > sb->capacity) { + size_t new_capacity = sb->capacity == 0 ? 64 : sb->capacity * 2; + while (new_capacity < total_len + 1) { + new_capacity *= 2; + } + sb->data = (char*) realloc(sb->data, new_capacity); + sb->capacity = new_capacity; + } + + memcpy(sb->data + sb->length, data, data_len); + sb->length = total_len; + sb->data[sb->length] = '\0'; +} + +void sb_append_ch(String_Builder* sb, char ch) { + char buf[2] = {ch, '\0'}; + sb_append(sb, buf); +} + +#define da_append(da, item) \ + do { \ + if ((da)->count + 1 > (da)->capacity) { \ + size_t new_capacity = (da)->capacity == 0 ? 64 : (da)->capacity * 2; \ + while ((da)->count + 1 > new_capacity) { \ + new_capacity *= 2; \ + } \ + (da)->items = realloc((da)->items, new_capacity * sizeof(*(da)->items)); \ + } \ + (da)->items[(da)->count++] = (item); \ + } while (0) \ + +bool lexer_expect_keyword(const char* expected) { + stb_c_lexer_get_token(&lexer); + + if (lexer.token != CLEX_id) { + // TODO: map lexer.token to readable name + fprintf(stderr, "ERROR: expected `%s` but got `%ld`\n", expected, lexer.token); + return false; + } + + if (strcmp(lexer.string, expected) != 0) { + fprintf(stderr, "ERROR: expected `%s` but got `%s`\n", expected, lexer.string); + return false; + } + + return true; +} + +bool lexer_expect(long expected, const char* expected_str) { + stb_c_lexer_get_token(&lexer); + + if (lexer.token != expected) { + // TODO: map lexer.token to readable name + if(expected_str != NULL) { + fprintf(stderr, "ERROR: expected %s but got `%ld`\n", expected_str, lexer.token); + } else { + fprintf(stderr, "ERROR: expected `%ld` but got `%ld`\n", expected, lexer.token); + } + return false; + } + + return true; +} + +long lexer_peek() { + char* mark = lexer.parse_point; + if (!stb_c_lexer_get_token(&lexer)) { + lexer.parse_point = mark; + return CLEX_eof; + } + + long token = lexer.token; + lexer.parse_point = mark; + + return token; +} + +// parses typedef struct { FIELDS } TYPE_NAME +bool parse_struct(Parsed_Struct_Info* info) { + bool result = false; + char* name = NULL; + + Parsed_Field_Infos fields = {0}; + String_Builder field = {0}; + + if (!lexer_expect_keyword("typedef")) goto fail; + if (!lexer_expect_keyword("struct")) goto fail; + if (!lexer_expect('{', NULL)) goto fail; + + while (true) { + char* mark = lexer.parse_point; + if (!stb_c_lexer_get_token(&lexer)) { + fprintf(stderr, "ERROR: expected struct fields but got EOF\n"); + goto fail; + } + + if (lexer.token == '}') break; + lexer.parse_point = mark; + + field.length = 0; + while (stb_c_lexer_get_token(&lexer) && lexer.token != ';') { + if (lexer.token <= 255) { + // TODO: parse arrays + if(lexer.token == '[') goto fail; + + sb_append_ch(&field, (char)lexer.token); + sb_append_ch(&field, ' '); + } else { + // TODO: parse unions + if(strcmp(lexer.string, "union") == 0) goto fail; + + // TODO: parse attributes + sb_append(&field, lexer.string); + sb_append_ch(&field, ' '); + } + } + field.data[field.length - 1] = '\0'; + + char* last_space = strrchr(field.data, ' '); + char* field_name = strdup(last_space + 1); + + field.data[last_space - field.data] = '\0'; + + char* field_type = strdup(field.data); + + da_append(&fields, ((Parsed_Field_Info) { + .type = field_type, + .name = field_name, + })); + } + + if (!lexer_expect(CLEX_id, "type name")) goto fail; + name = strdup(lexer.string); + + info->name = name; + info->fields_count = fields.count; + info->fields = (Parsed_Field_Info*)calloc(info->fields_count, sizeof(Parsed_Field_Info)); + for(size_t i = 0; i < info->fields_count; i += 1) { + info->fields[i].type = fields.items[i].type; + info->fields[i].name = fields.items[i].name; + } + + result = true; +fail: + free(field.data); + + if(!result) { + free(name); + for(size_t i = 0; i < fields.count; i += 1) { + free(fields.items[i].name); + free(fields.items[i].type); + } + free(fields.items); + } + + return result; +} + +void print_struct(Parsed_Struct_Info info) { + printf("struct_name = %s\n", info.name); + printf("fields[%zu] = [\n", info.fields_count); + for (size_t i = 0; i < info.fields_count; i += 1) { + printf(" { type = %s, name = %s },\n", info.fields[i].type, info.fields[i].name); + } + printf("]\n"); +} + +#define gen(...) do { \ + fprintf(stream, __VA_ARGS__); \ + fprintf(stream, " // %s:%d\n", __FILE__, __LINE__); \ + } while(0) + +char* to_lowercase(char* str) { + size_t len = strlen(str); + for(size_t i = 0; i < len; i += 1) { + str[i] = tolower(str[i]); + } + return str; +} + +void generate_struct_info(FILE* stream, Parsed_Struct_Info info) { + char* lowercase_name = to_lowercase(strdup(info.name)); + + gen("static Struct_Info %s_info = {", lowercase_name); + gen(" .name = \"%s\",", info.name); + gen(" .fields_count = %zu,", info.fields_count); + gen(" .fields = (Field_Info[%zu]) {", info.fields_count); + for (size_t i = 0; i < info.fields_count; i += 1) { + gen(" { .type = \"%s\", .name = \"%s\" },", info.fields[i].type, info.fields[i].name); + } + gen(" },"); + gen("};"); + + free(lowercase_name); +} + +bool read_entire_file(const char* file_path, char** content) { + bool result = false; + FILE* file = fopen(file_path, "rb"); + if(file == NULL) goto fail; + + if(fseek(file, 0, SEEK_END) < 0) goto fail; + + long length = ftell(file); + if(length < 0) goto fail; + + if(fseek(file, 0, SEEK_SET) < 0) goto fail; + + *content = (char*) malloc((length + 1) * sizeof(char)); + fread(*content, 1, length, file); + + // TODO: will not set errno + if (ferror(file)) goto fail; + + (*content)[length] = '\0'; + + result = true; +fail: + if (!result) { + free(*content); + fprintf(stderr, "ERROR: Could not read `%s`: %s\n", file_path, strerror(errno)); + } + if (file) fclose(file); + return result; +} + +bool generate_output_file(const char* output_path, Parsed_Struct_Infos struct_infos) { + const char* GENERATION_MARK = "// AUTO GENERATED CODE //\n"; + const size_t GENERATION_MARK_LEN = strlen(GENERATION_MARK); + + bool result = false; + + char* header_content; + if (!read_entire_file(__FILE__, &header_content)) goto fail; + + // 1. find BEGIN an END + char* generate_begin = strstr(header_content, GENERATION_MARK); + if (generate_begin == NULL) { + fprintf(stderr, "ERROR: could not found generation mark in cmeta.h\n"); + goto fail; + } + + char* generate_end = strstr(generate_begin + GENERATION_MARK_LEN, GENERATION_MARK); + if (generate_end == NULL) { + fprintf(stderr, "ERROR: could not found generation mark in cmeta.h\n"); + goto fail; + } + + FILE* output_file = fopen(output_path, "wb"); + if (!output_file) { + fprintf(stderr, "ERROR: could not write to %s: %s\n", output_path, strerror(errno)); + goto fail; + } + + FILE* stream = output_file; + + // write up to the generation mark, including it + fwrite(header_content, generate_begin + GENERATION_MARK_LEN - header_content, 1, stream); + + for (size_t i = 0; i < struct_infos.count; i += 1) { + generate_struct_info(stream, struct_infos.items[i]); + } + + fwrite(generate_end, strlen(generate_end), 1, stream); + + result = true; +fail: + free(header_content); + if (output_file) fclose(output_file); + + return result; +} + +bool preprocess_file(const char* file_path, String_Builder* result) { + char command[256] = {0}; + sprintf(command, "gcc -E %s", file_path); + + FILE* fp = popen(command, "r"); + if (fp == NULL) { + fprintf(stderr, "ERROR: Failed to run command: %s\n", strerror(errno)); + return false; + } + + char line[512]; + size_t line_num = 0; + char file_name[512]; + + size_t cursor = 0; + result->length = 0; + + bool collecting_content = false; + + while (fgets(line, sizeof(line), fp) != NULL) { + if (sscanf(line, "# %zu \"%s\"", &line_num, file_name) == 2) { + // remove trailing " + file_name[strlen(file_name) - 1] = '\0'; + + if (strcmp(file_name, file_path) == 0) { + collecting_content = true; + continue; + } + + // TODO: read original file at line_num, to check for comments (e.g annotations) + } else if(collecting_content) { + sb_append(result, line); + cursor += strlen(line); + } + } + + pclose(fp); + + return true; +} + +bool process_file(const char* input_file) { + // read input file + String_Builder input_content = {0}; + if (!preprocess_file(input_file, &input_content)) return false; + + // init lexer + char string_store[1024] = {0}; + stb_c_lexer_init(&lexer, input_content.data, input_content.data + input_content.length, string_store, sizeof(string_store) / sizeof(char)); + + // find and parse all structs + Parsed_Struct_Infos struct_infos = {0}; + + while (true) { + char* mark = lexer.parse_point; + + if (!stb_c_lexer_get_token(&lexer)) break; + if (lexer.token == CLEX_id && strcmp(lexer.string, "typedef") == 0) { + lexer.parse_point = mark; + + Parsed_Struct_Info struct_info = {0}; + if (parse_struct(&struct_info)) { + da_append(&struct_infos, struct_info); + } + } + } + + if (!generate_output_file(__FILE__, struct_infos)) return false; + + return true; +} + +#endif // CMETA_COMPTIME + +#endif // CMETA_H diff --git a/example/01_simple_struct/main.c b/example/01_simple_struct/main.c new file mode 100644 index 0000000..3c429a7 --- /dev/null +++ b/example/01_simple_struct/main.c @@ -0,0 +1,19 @@ +#include +#include "cmeta.h" + +typedef struct { + int int_field; +} Foo_Struct; + +int main(void) { + printf("Foo_Struct infos:\n"); + printf(" name = %s\n", foo_struct_info.name); + printf(" fields_count = %zu\n", foo_struct_info.fields_count); + + for (size_t i = 0; i < foo_struct_info.fields_count; i += 1) { + Field_Info field = foo_struct_info.fields[i]; + printf(" .%s: %s\n", field.name, field.type); + } + + return 0; +}