How to do metaprogramming without changing the source files?

I just finished watching the metaprogramming episode and want to try it out. One of the first things that I want to do is implicitly add typedef before any struct definition (I code in straight C99). I don't want to change the source file but only add it before compiling the code, meaning after building the code, I don't want to have typedef anywhere in my source files. How can I do that? Should I just copy all the files into one giant separate file, then add typedef into that file, and finally compile that. Are there any other ways (because that sounds very inefficient to me)?


Edited by longtran2904 on

Your meta program could generate a file called typedefs.h that only contains the typedefs and you include that file manually in your source files.

So what if you want to do something more complex like adding new keywords (e.g defer)? I can't generate it into a new file. Also, if I typedef into a new file, any file that includes that file will have access to every type. I can generate it into multiple files and only include certain files but that's still too much manual work and the point of metaprogramming is to automate that for you. I was hoping for some other ways that I don't even need to include anything. I don't need to care or even notice it and it looks like a normal .cpp file even though it's not.


Edited by longtran2904 on
Replying to mrmixer (#25509)

If you need to modify the code but don't want to modify the original file than you'll need to make copies, modify and compile the copies. You'll need to correct the includes to use the modified files when it applies (e.g. not for system headers).

While that will work, it'll be annoying as the debug symbols will points to the copies and you'll have that mental overhead to handle (modifying the original file, not the copy).

Another way is to have your code into a "custom file type" and that generates C files (look at Metadesk). You never include your custom type only the generated C files. For example, I use "common_shaders.shader" to edit my shaders; that goes into a meta program that generates a "common_shaders.h" file that contains C string versions of the shaders.

/* common_shaders.shader */
[name: vs_lib]

vec4 color_unpack( unsigned int color ) {
    unsigned int mask_r = uint( 0x000000ff );
    unsigned int mask_g = uint( 0x0000ff00 );
    unsigned int mask_b = uint( 0x00ff0000 );
    unsigned int mask_a = uint( 0xff000000 );
    float r = float( ( color & mask_r ) >> 0 );
    float g = float( ( color & mask_g ) >> 8 );
    float b = float( ( color & mask_b ) >> 16 );
    float a = float( ( color & mask_a ) >> 24 );
    float r_255 = 1.0 / 255.0;
    vec4 result = vec4( r * r_255 , g * r_255, b * r_255, a * r_255 );
    return result;
}

/* common_shaders.h */
u8 vs_lib[ ] =
"vec4 color_unpack( unsigned int color ) {\n"
"    unsigned int mask_r = uint( 0x000000ff );\n"
"    unsigned int mask_g = uint( 0x0000ff00 );\n"
"    unsigned int mask_b = uint( 0x00ff0000 );\n"
"    unsigned int mask_a = uint( 0xff000000 );\n"
"    float r = float( ( color & mask_r ) >> 0 );\n"
"    float g = float( ( color & mask_g ) >> 8 );\n"
"    float b = float( ( color & mask_b ) >> 16 );\n"
"    float a = float( ( color & mask_a ) >> 24 );\n"
"    float r_255 = 1.0 / 255.0;\n"
"    vec4 result = vec4( r * r_255 , g * r_255, b * r_255, a * r_255 );\n"
"    return result;\n"
"}\n"
"\n";

If you're ok with using some Cpp you can have a defer statement without meta programing.

longtran2904
Also, if I typedef into a new file, any file that includes that file will have access to every type.

I don't really see an issue with that. In addition, you still need to have the definition of the struct to access its members.


Edited by Simon Anciaux on

While that will work, it'll be annoying as the debug symbols will points to the copies and you'll have that mental overhead to handle (modifying the original file, not the copy).

Quite easy workaround is to use #line directives:

#line 3 foo.cpp
void f() { ... }

Now stepping into f() will actually open foo.cpp file at line 4. Basically put #line after each place where you inserted multiple lines to have correct line number in original file.


Edited by Mārtiņš Možeiko on

Didn't thought about that. Thanks.


Replying to mmozeiko (#25512)

Each of my tokens has a column and line number. When parsing I just do a simple printf("%.*s", token.value.count, token.value.data) (like what Casey did). How can I align this to the correct column and line? Do you know any printf arguments or macros that can do that? Also, can you explain a little bit about the #line directives? What is the "3" for?


Replying to mmozeiko (#25512)

If you need correct alignment, you need to parse it and print it out. There's no magic solutions for that.

But if you're asking how to print N spaces in printf, then simplest solution is to abuse width specifier:

printf("%10s%.*s", "", token.value.count, token.value.data);

This will print 10 spaces before your token. Of course you can parametrize it with variable number:

printf("%*.s%.*s", 10, "", token.value.count, token.value.data);

As for how 3 works in #line - it specifies compiler parser to treat this line in source code as line number 3. So next line it will process (with void f()) will be line 4. Regardless of actual line number in generated code - it can be line 1000, but compiler will treat it as line 4 in file foo.cpp - both for error messages and debug info.


Replying to longtran2904 (#25516)

For some reason "%*.s" doesn't work with "\n".

Another thing that I want to add is the function caller's name and line number. Can someone point me in the right direction?


Edited by longtran2904 on
Replying to mmozeiko (#25517)

%*.s works just fine with any string. For example, for 4 space indent:

printf("%*.s%s", 4, "", "\n");

As for caller names & line numbers - that requires more work. Obvious solution would be to use __FILE__ and __LINE__ macros.

If you have function like this:

void f(int arg1, float arg2) { ... }

you change it to this:

void fun(int arg1, float arg2, const char* caller, int line) { ... }
#define fun(arg1, arg2) f(arg1, arg2, __FILE__, __LINE__)

Now everybody who will use fun(1, 2) to call function, will implicitly pass also __FILE__ and __LINE__ macros, so you'll know exact caller name and line where it was called.

Alternative is to use runtime functionality to lookup call stack and resolve debug symbols. On Windows you can do that with CaptureStackBackTrace function or just _ReturnAddress intrinsic, and then using dbghlp functions like SymFromAddr to lookup function name & line from address.


Replying to longtran2904 (#25518)

For some reason, this will print 10 empty spaces rather than lines.

printf("%*.s", 10, "\n");

Also, VS couldn't find the correct source file for the meta-generated executable. It keeps choosing the original file. Currently, I compile 2 files, one is the original file, and the other is the meta-generated file (I only test metaprogramming on a single file at this moment). It looks something like this (the compiler's arguments are the same and I build the executable to a separate folder):

cl ... Tokenizer.c ... Tokenizer.exe

Tokenizer.exe > MetaGenerated.c

cl ... Original.c ... Original.exe

cl ... MetaGenerated.c ... Meta.exe


Replying to mmozeiko (#25521)

For some reason, this will print 10 empty spaces rather than lines.

That is expected. *. for %s tells how much width of string to format. So width will be 9 spaces, and then string itself \n. *. does not make string to be repeated. It will just indicates width of field.

In my previous example with printf("%*.s%s", 4, "", "\n"); The first part %*.s makes it to print 4 space wide string "" (empty string) - so it just prints 4 spaces. And then %s makes it print \n. So this piece will print 4 spaces and \n at end.

Did you put #line directives? Otherwise it won't know what is "original" file.


Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#25522)

So to just print 10 new lines, I can only use a for loop? Are there any other printf arguments that I should know about?

Regarding the #line directives, does putting #line 3 "Original.c" in MetaGenerated.c make the Meta.exe's source file Original.c?


Edited by longtran2904 on
Replying to mmozeiko (#25523)

Yes, to repeat other character than space you need for loop.

#line directive will make compiler generate debug information about filename you specify in #line and not use actual filename. In your example debugger will have name "original.c" as filename for where to lookup functions.


Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#25524)

Thanks for replying! I want to get better at metaprogramming so your help is much appreciated. Some of the current features that I want to implement is:

  • Data structures
  • Defer
  • Function overloading
  • Default arguments
  • Named arguments
  • Code generation (like template)

Are there any other useful features that you have and would recommend me to implement?

Currently, my parser is very basic. It's just called GetToken and has a currentToken and a lastToken (very close to what handmade hero did). This only works for small and basic features. I don't know what should I do from here. How can I improve the parser to something more complete?


Edited by longtran2904 on
Replying to mmozeiko (#25528)