Show HN: C programming language extension: Cedro pre-processor
sentido-labs.com
sentido-labs.com
Hey, that's me! I wrote cmacro. I'm glad someone has built a newer and better alternative (cmacro is a big mess inside and has some significant parser errors :P).
Related to the backstitch operator, I've wondered about implementing a transform to turn code like:
type_t value = allocate();
operate(value);
operate(value);
destroy(value);
Into: type_t value = allocate();
type_t tmp1 = operate(value);
type_t tmp2 = operate(tmp1);
destroy(tmp2);
To implement something like linear types[1] in C, such that each instance of type_t must be used once and only once. But I imagine this would require too much integration with the type system and code analysis.I wish binary inclusion was provided by more languages or build systems. For small files it's a very suitable alternative to the difficulties of packaging resources when distributing an application or library.
Great project! C is a very useful language and this type of source-to-source compilers are huge force multipliers.
[0]: https://github.com/eudoxia0/cmacro
[1]: https://en.wikipedia.org/wiki/Substructural_type_system#Line...
When it occasionally comes up as a need, I use the bintosrc command available at https://github.com/jochenleidner/ltools
Thanks for the nice words, my program is much more modest than cmacro :-)
Yes, I think that the transform you describe would require the processor to know about types, which is something I avoided here. I experimented early with writing a function that would track the types of things and enable smarter macros, but it became obvious that it was going to be a choice between writing a full compiler frontend and a hacky thing that worked only in trivial cases, so I discarded it.
The decision was to keep it as a naïve macro processor, just with a minimum understanding of C syntax, and leave out anything that requires taking into account the semantics. I have other macros that do things like expanding «obj[a..b]» to «(Slice){.start_p=&obj[a], .end_p=&obj[b] }» but they are not worth it, because to work smoothly they would need to know the type of «obj». If absolutely needed, I think that «_Generic» could be used here but it starts complicating things beyond the point of diminishing returns.
Even parsing C "correctly" is a huge problem, since the syntax is ambiguous and the ambiguity can only be resolved by keeping a symbol table that tracks what kind of thing a symbol is.
So there's a huge pragmatic force to keep a strict separation between a type-unaware preprocessor and a full-blown compiler frontend.
(I considered using clang to dump the C AST when building cmacro, but concluded it would be too unwieldy.)
type_t value = allocate();
operate(value) |> operate |> destroy;
Functional languages like OCaml, F#, and Haskell have it built in.That way you get to write something like:
typedef struct cookie_batch {
bool valid;
bool baked;
unsigned short ingredient_count;
char **ingredients;
} cookie_batch;
.
.
.
void bake(cookie_batch *batch) {
if(!batch->valid) return;
batch->baked = true;
}
.
.
.
cookie_batch batch = { .valid = true };
add_ingredients(&batch);
bake(&batch);
serve(&batch);
cleanup(&batch);
So you don't need any complex error handling in client code.If you haven’t watched it, I can recommend it.
$call value_ops, val, operate, operate
subcode: value_ops(v, @ops)
type_t $(v) = allocate();
$(for:op in $(ops))
type_t tmp$(_i) = operate($(v));
$(set-1:v=tmp$(_i))
destroy($(v))
MyDef is a general purpose preprocessor.EDIT: maybe this is more fit what was intended
type_t var;
$(for:op in allocate, operate, operate, destroy)
var = $(op)(var);From the linked document:
> Cedro is a C language extension that works as a pre-processor with four features:
> - The backstitch @ operator.
> - Deferred resource release.
> - Block macros.
> - Binary inclusion.
The source code archive and GitHub link can be found at: https://sentido-labs.com/en/library/
I have no intention of building this up to a new language: if you can switch to a new compiler, there are better alternatives. This is for cases where all you have is a C99 (or close to it) compiler or you need to produce plain C code for compilation on a different target machine which might only have C89.
That leaves the @ operator… which seems useful for specific cases (e.g. graphics rendering), but I can honestly say I don't really feel a need for that in the codebases I worked with.
But. Let me offer some suggestions for extensions! :)
An improved preprocessor would be massively useful if it had features like:
- enumerating struct members in preprocessor macros (something like:)
#define MACRO2(typename, fieldname, arg1, arg2) ... arg1 arg2 ...
#define MACRO(typename, arg1, arg2) something { #each-member(MACRO2, typename, arg1, arg2) }
struct bla {
int a, b;
}
MACRO(struct bla, x, y)
/* => something { MACRO2(struct bla, a, x, y) MACRO2(struct bla, b, x, y) } */
- same, for function parameters- and last but not least, macros consuming { } blocks, e.g.
#define MACRO(...)#{name} something something #{name} something #{name}
MACRO(1,2,3) {
this block is consumed by the macro and placed in 2nd and 3rd #{name} (first one just signals the macro consumes a {} block
}
...oh and, almost forgot, "extensible" macros: #define MACRO(...) abc
#ifdef MACRO
#extend MACRO(...) def MACRO(...) ghi
#else
#define MACRO(...) def stub ghi
#endif
(also, macros that can define other macros. Like _Pragma() => #pragma, _Define() => #define)If it isn't — the number of people who understand m4 well enough to be able to maintain something like this can probably be counted on a few hands.
It's not an easy problem. Generating some kind of macro-usable list like you suggest is probably the way to go, essentially making the entire thing a 2-pass process. (That said, the C preprocessor doesn't have a concept of "lists", perhaps that's the thing to actually add here.)
I considered something along these lines: my main use case would be to produce string versions of enum values, like Rust’s «#[derive(Debug)]».
The problem is that, without type information, it would be too fragile, so I decided to leave it out.
Still, I’ll consider what you propose, maybe there is some form of purely syntactical macro that could cover these use cases.
typeof() is being considered for ISO C2x and has been around for ages in GCC/clang. _Generic is already in C11. With a struct member iterator, these 2 could/should be useful to branch out into type specific handling for each field.
Admittedly those are somewhat limited, particularly when working with typedefs, but that's a bit of a fundamental problem in C :(
While the original C preprocessor can be a pain (but is still useful to have), additional pre-processors can add language features without abandoning the C standard and one's tool chain. One issue is that other developers need to either learn the dialect accepted by the preprocessor or make sense of its output.
Here I think some good decisions have been made:
Pros:
+ safer code
+ shorter/more concise code
Cons:
- learning curve of the pre-processor
Objective C and C++ started out as pre-processors, of course... ;)
As an extra related work on the deferred resource release: Dlang uses scope guards https://tour.dlang.org/tour/en/gems/scope-guards
But as I type this comment I realize that other syntax parsing tools like IDEs would be happier if the unprocessed code is still valid C.
Yes, that’s the reason. The code is still close enough to standard C to work well with Emacs for instance. It would be a deal breaker for me to require a special editing mode.
e.g. Instead of writing...
```
ANSELBbits.ANSB3 = 0; // RB3 is digital (pinMode)
TRISBbits.TRISB3 = 0; // RB3 is output (pinMode)
LATBbits.LATB3 = 1; // RB3 is set to high (digitalWrite)
```
we would be able to write the macro like below, where `ANSEL[RB3] --> ANSELBbits.ANSB3`
```
pinMode(RB3, OUTPUT);
digitalWrite(RB3, HIGH);
```
Since «ANSELBbits.ANSB3 = 0» sets the write to digital, would it not belong in «digitalWrite()» instead?
Would it work if the generated code was like this?
TRISBbits.TRISB3 = 0; // RB3 is output (pinMode)
ANSELBbits.ANSB3 = 0; // RB3 is digital (digitalWrite)
LATBbits.LATB3 = 1; // RB3 is set to high (digitalWrite)
EDIT: Assuming I got the meaning right, you can get quite close with standard C macros, but since I’m here demoing Cedro we could as well use it: #pragma Cedro 1.0
enum { OUTPUT = 0, INPUT = 1 };
#define { pinMode(REGISTER, DIRECTION)
TRISBbits.TRIS##REGISTER = DIRECTION
#define }
enum { LOW = 0, HIGH = 1, DIGITAL = 0, ANALOG = 1 };
#define { digitalWrite(REGISTER, LEVEL)
ANSELBbits.ANS##REGISTER = DIGITAL;
LATBbits.LAT##REGISTER = LEVEL
#define }
#define { analogWrite(REGISTER, LEVEL)
ANSELBbits.ANS##REGISTER = ANALOG;
LATBbits.LAT##REGISTER = LEVEL;
#define }
int main(void)
{
TRISBbits.TRISB3 = 0; // RB3 is output (pinMode)
ANSELBbits.ANSB3 = 0; // RB3 is digital (digitalWrite)
LATBbits.LATB3 = 1; // RB3 is set to high (digitalWrite)
pinMode(B3, OUTPUT);
digitalWrite(B3, HIGH);
}
If you put that in a file («regtest.c») and run it through Cedro and the standard pre-processor, you get this: $ ../bin/cedrocc -E regtest.c
# 1 "<stdin>"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "<stdin>"
# 3 "regtest.c"
enum { OUTPUT = 0, INPUT = 0 };
# 9 "regtest.c"
enum { LOW = 0, HIGH = 1, DIGITAL = 0, ANALOG = 0 };
# 16 "regtest.c"
# 21 "regtest.c"
int main(void)
{
TRISBbits.TRISB3 = 0;
ANSELBbits.ANSB3 = 0;
LATBbits.LATB3 = 1;
TRISBbits.TRISB3 = OUTPUT;
ANSELBbits.ANSB3 = DIGITAL; LATBbits.LATB3 = HIGH;
}
The only part that uses Cedro is the block #defines for «pinMode()» and «digitalWrite()».EDIT2: I suspect that what you really need is a «macro» that checks that the register has been last set to the correct direction (INPUT/OUTPUT) and mode (DIGITAL/ANALOG) before calling the «…Write()»/«…Read()» functions. For instance, if B3 was last set to ANALOG and you call «digitalWrite(B3, …)», it would insert «ANSELBbits.ANSB3 = DIGITAL», but not if it was last set to DIGITAL, and so on for all function variants: «digitalWrite()», «digitalRead()», «analogWrite()», «analogRead()».
You could write such a function in «src/macros/pic32.c», list it in «src/macros.h», and Cedro would run it.
__cleanup__ might be a C extension, but it's pretty widely supported. If you really wanted to have it be a code block instead of a function, in plain C, then you could possibly also use clang or GCC's lambda-ish extensions.
To me that has always been a lame excuse for a poor standard library. You can always build your own, even if the standard library has a solution that works for 99% of cases - and that makes 99% of code more readable to 100% of people.
I love C (let's not get into a C++ rant here..) so this is just a comment about the culture, like the parent post. The fact that people who don't like to re-invent the wheel have not stuck with C, and maybe gone to join the Perl, Python, Java... communities made this issue worse over the years.
In theory, this could still be done now (e.g. a standard build system), but I think most "systems language" innovation action now firmly happens in the Rust camp (which may be a good thing, because the world needs more safe code).
UTF-8 saves space and is just as easy (and fast!) to process as UTF-16, as both are variable length encodings. UTF-8 is always unambiguous and self-synchronizing. The byte order stays always the same. The only complication is potentially multiple ways to encode a character, but that can be solved by just declaring all but the shortest encoding illegal.
Hopefully we'll only have UTF-8 in the long run, both externally and (by default) internally.
C string functions are mostly compatible with UTF-8, as long as individual non-7-bit characters are not processed or the string indexed, etc.
[1]: https://scattered-thoughts.net/writing/against-sql/#innovation-at-the-implementation-level-is-gated
(But hey, the defer proposal is here and might actually become a thing... right?)