Atlas blue cedar Cedro Español, English

Cedro is a C language extension that works as a pre-processor with four features:

  1. The backstitch @ operator.
  2. Deferred resource release.
  3. Block macros.
  4. Binary inclusion.

To activate it, the source file must contain this line: #pragma Cedro 1.0
Otherwise, the file is copied directly to the output.

Usage: cedro [options] file.c [file2.c … ] The result goes to stdout, can be used without an intermediate file like this: cedro file.c | cc -x c - -o file It is what the cedrocc program does: cedrocc -o file file.c With cedrocc, the following options are the defaults: --discard-comments --insert-line-directives --apply-macros Applies the macros: backstitch, defer, etc. (default) --escape-ucn Escapes non-ASCII in identifiers as UCN. --no-apply-macros Does not apply the macros. --no-escape-ucn Does not escape non-ASCII in identifiers. (default) --discard-comments Discards the comments. --discard-space Discards all whitespace. --no-discard-comments Does not discard comments. (default) --no-discard-space Does not discard whitespace. (default) --insert-line-directives Inserts #line directives. --no-insert-line-directives Does not insert #line directives. (default) --print-markers Prints the markers. --no-print-markers Does not print the markers. (default) --enable-core-dump Enables core dump on crash. --no-enable-core-dump Does not enable core dump on crash. (default) --benchmark Runs a performance benchmark. --version Shows version: 1.0 The corresponding “pragma” is: #pragma Cedro 1.0

The option --escape-ucn encodes Unicode® characters outside of the ASCII range, when they appear as part of an identifier, as C99 universal character names (“C99 standard”, page 65, “6.4.3 Universal character names”), which can be useful for older compilers without UTF-8 support such as GCC before version 10.

For API documentation, see doc/api/index.html after running make doc that requires having Doxygen installed.

Backstitch operator: @ #backstitch-operator

Threads a value through a sequence of function calls, as first parameter for each of them.

It is an explicit version of what other programming languages do to implement member functions, and the result is a usual pattern in C libraries.

object @ f(a), g(b); f(object, a); g(object, b);
&object @ f(a), g(b); f(&object, a); g(&object, b);
object.field @ f(a), g(b); f(object.field, a); g(object.field, b);
int x = (object @ f(a), g(b)); int x = (f(object, a), g(object, b));

This is the C comma operator, it’s the same as

f(object, a); int x = g(object, b);
object @prefix_... f(a), g(b); prefix_f(object, a); prefix_g(object, b);
object @..._suffix f(a), g(b); f_suffix(object, a); g_suffix(object, b);
graphics_context @nvg... BeginPath(), Rect(100,100, 120,30), Circle(120,120, 5), PathWinding(NVG_HOLE), FillColor(nvgRGBA(255,192,0,255)), Fill();
nvgBeginPath(graphics_context); nvgRect(graphics_context, 100,100, 120,30); nvgCircle(graphics_context, 120,120, 5); nvgPathWinding(graphics_context, NVG_HOLE); nvgFillColor(graphics_context, nvgRGBA(255,192,0,255)); nvgFill(graphics_context);

For each comma-separated segment, if it starts with any of the tokens “[”, “++”, “--”, “.”, “->”, “=”, “+=”, “-=”, “*=”, “/=”, “%=”, “<<=”, “>>=”, “&=”, “^=”, “|=”, or there is nothing that looks like a function call, the insertion point is the beginning of the segment:

number_array @ [3]=44, [2]=11; number_array[3]=44; number_array[2]=11;
*number_array++ = @ 1, 2; *number_array++ = 1; *number_array++ = 2;
figure_center_point @ .x=44, .y=11; figure_center_point.x=44; figure_center_point.y=11;

The object part can be left empty, which is useful for things like adding prefixes or suffixes to enumerations:

typedef enum { @TOKEN_... SPACE, WORD, NUMBER } TokenType; typedef enum { TOKEN_SPACE, TOKEN_WORD, TOKEN_NUMBER } TokenType;

Note: the @ symbol is not recognized when written as \u0040, but it gets converted to @ in the output. This can be used to escape it when chaining Cedro with another pre-processor that uses it.

Looking for prior implementations of this idea I’ve found magma (2014), where it is called doto. It is a macro for the cmacro pre-processor which has the inconvenient of needing the Common Lisp SBCL compiler.

Functional languages often have a similar operator although it threads the result of the first function as first parameter of the next one etc. instead of the same value for all functions. For instance, the equivalent of f₃(f₂(f₁(x))):

Ada 2005 introduced a feature called prefixed-view notation that is more similar to C++ as the exact function being called can not be determined without knowing which methods are implemented for the object type.

Deferred resource release: #deferred-resource-release

Moves the clean-up code for a variable to the end of its scope including the exit points break, continue, goto, return.

In C, resources must be released back to the system explicitly once they are no longer needed, which usualy happens quite far from the place where they were allocated. As time passes and changes accumulate in the program, it’s easy to forget releasing them in all cases or to attempt releasing a resource twice.

Other programming languages have mechanisms for automatic resource release: C++ for instance, uses functions called destructors that get run implicitly when exiting a variable’s scope.

The programming language Go introduced an explicit notation called «defer» that fits better the style of C. The first difference is that in Go, all releases happen when exiting the function, while with Cedro the releases happen when exiting each block, like the destructors in C++ do.

There are more differences, such as for instance that in Go it can be used to modify the return value of the function, and that Cedro does not even try to handle longjmp(), exit(), thrd_exit() etc. because it could only apply the deferred actions in the current function, not in any function that called this one. See “A defer mechanism for C” (published academic paper as PDF in the SAC’21 conference) for a compiler-level implementation that does handle longjmp() and stack unwinding.

In Cedro, the release function is marked with the C keyword auto which is not needed in standard C code because it is the default. If you want to use Cedro with standard C code that already uses auto, you can first replace it with signed as it has the same effect.

In this example, there is a text store and a file that must be released back to the system:

#include <stdio.h> #include <stdlib.h> #include <errno.h> #pragma Cedro 1.0 int repeat_letter(char letter, size_t count, char* file_name) { char* text = malloc(count + 1); if (!text) return ENOMEM; auto free(text); for (size_t i = 0; i < count; ++i) { text[i] = letter; } text[count] = 0; if (file_name) { FILE* file = fopen(file_name, "w"); if (!file) return errno; auto fclose(file); fwrite(text, sizeof(char), count, file); fputc('\n', file); } printf("Repeated %lu times: %s\n", count, text); return 0; } int main(void) { return repeat_letter('A', 6, "aaaaaa.txt"); }

#include <stdio.h> #include <stdlib.h> #include <errno.h> int repeat_letter(char letter, size_t count, char* file_name) { char* text = malloc(count + 1); if (!text) return ENOMEM; for (size_t i = 0; i < count; ++i) { text[i] = letter; } text[count] = 0; if (file_name) { FILE* file = fopen(file_name, "w"); if (!file) { free(text); return errno; } fwrite(text, sizeof(char), count, file); fputc('\n', file); fclose(file); } printf("Repeated %lu times: %s\n", count, text); free(text); return 0; } int main(void) { return repeat_letter('A', 6, "aaaaaa.txt"); }

Compiling it with GCC or clang, on the left running explicitly the compiler, and on the right using cedrocc which has the same effect:

$ cedro repeat.c | cc -o repeat -x c - $ ./repeat Repeated 6 times: AAAAAA $ cat aaaaaa.txt AAAAAA $ valgrind --leak-check=yes ./repeat … ==8795== HEAP SUMMARY: ==8795== in use at exit: 0 bytes in 0 blocks ==8795== total heap usage: 4 allocs, 4 frees, 5,599 bytes allocated ==8795== ==8795== All heap blocks were freed -- no leaks are possible $ cedrocc -o repeat repeat.c $ ./repeat Repeated 6 times: AAAAAA $ cat aaaaaa.txt AAAAAA $ valgrind --leak-check=yes ./repeat … ==8795== HEAP SUMMARY: ==8795== in use at exit: 0 bytes in 0 blocks ==8795== total heap usage: 4 allocs, 4 frees, 5,599 bytes allocated ==8795== ==8795== All heap blocks were freed -- no leaks are possible

In this example adapted from “Proposal for C2x, WG14 ​n2542, Defer Mechanism for C” p. 40, the released resources are spin locks:

/* Adapted from example in n2542.pdf#40 */ #pragma Cedro 1.0 int f1(void) { puts("g called"); if (bad1()) { return 1; } spin_lock(&lock1); auto spin_unlock(&lock1); if (bad2()) { return 1; } spin_lock(&lock2); auto spin_unlock(&lock2); if (bad()) { return 1; } /* Access data protected by the spinlock then force a panic */ completed += 1; unforced(completed); return 0; } /* Adapted from example in n2542.pdf#40 */ int f1(void) { puts("g called"); if (bad1()) { return 1; } spin_lock(&lock1); if (bad2()) { spin_unlock(&lock1); return 1; } spin_lock(&lock2); if (bad()) { spin_unlock(&lock2); spin_unlock(&lock1); return 1; } /* Access data protected by the spinlock then force a panic */ completed += 1; unforced(completed); spin_unlock(&lock2); spin_unlock(&lock1); return 0; }

Andrew Kelley compared resource management between C and his Zig programming language in a 2019 presentation titled “The Road to Zig 1.0" at 29:21″, and here I’ve re-created his C example using Cedro to produce the function as he showed it, except that Cedro does not know that the end for loop never returns so it adds unnecessary resource release code after it.

/* -*- coding: utf-8 c-basic-offset: 4 tab-width: 4 indent-tabs-mode: nil -*- */ // Example retrofitted from C example by Andrew Kelley: // https://www.youtube.com/watch?v=Gv2I7qTux7g&t=1761s #pragma Cedro 1.0 int main(int argc, char **argv) { struct SoundIo *soundio = soundio_create(); if (!soundio) { fprintf(stderr, "out of memory\n"); return 1; } auto soundio_destroy(soundio); int err; if ((err = soundio_connect(soundio))) { fprintf(stderr, "unable to connect: %s\n", soundio_strerror(err)); return 1; } soundio_flush_events(soundio); int default_output_index = soundio_default_output_device_index(soundio); if (default_output_index < 0) { fprintf(stderr, "No output device\n"); return 1; } struct SoundIoDevice *device = soundio_get_output_device(soundio, default_output_index); if (!device) { fprintf(stderr, "out of memory\n"); return 1; } auto soundio_device_unref(device); struct SoundIoOutStream *outstream = soundio_outstream_create(device); if (!outstream) { fprintf(stderr, "out of memory\n"); return 1; } auto soundio_outstream_destroy(outstream); outstream->format = SoundIoFormatFloat32NE; outstream->write_callback = write_callback; if ((err = soundio_outstream_open(outstream))) { fprintf(stderr, "unable to open device: %s" "\n", soundio_strerror(err)); return 1; } if ((err = soundio_outstream_start(outstream))) { fprintf(stderr, "unable to start device: %s\n", soundio_strerror(err)); return 1; } for (;;) soundio_wait_events(soundio); } /* -*- coding: utf-8 c-basic-offset: 4 tab-width: 4 indent-tabs-mode: nil -*- */ // Example retrofitted from C example by Andrew Kelley: // https://www.youtube.com/watch?v=Gv2I7qTux7g&t=1761s int main(int argc, char **argv) { struct SoundIo *soundio = soundio_create(); if (!soundio) { fprintf(stderr, "out of memory\n"); return 1; } int err; if ((err = soundio_connect(soundio))) { fprintf(stderr, "unable to connect: %s\n", soundio_strerror(err)); soundio_destroy(soundio); return 1; } soundio_flush_events(soundio); int default_output_index = soundio_default_output_device_index(soundio); if (default_output_index < 0) { fprintf(stderr, "No output device\n"); soundio_destroy(soundio); return 1; } struct SoundIoDevice *device = soundio_get_output_device(soundio, default_output_index); if (!device) { fprintf(stderr, "out of memory\n"); soundio_destroy(soundio); return 1; } struct SoundIoOutStream *outstream = soundio_outstream_create(device); if (!outstream) { fprintf(stderr, "out of memory\n"); soundio_device_unref(device); soundio_destroy(soundio); return 1; } outstream->format = SoundIoFormatFloat32NE; outstream->write_callback = write_callback; if ((err = soundio_outstream_open(outstream))) { fprintf(stderr, "unable to open device: %s" "\n", soundio_strerror(err)); soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); return 1; } if ((err = soundio_outstream_start(outstream))) { fprintf(stderr, "unable to start device: %s\n", soundio_strerror(err)); soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); return 1; } for (;;) soundio_wait_events(soundio); soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); }

However, his Zig example had the unfair advantage of returning error values instead of printing error messages which takes more space. The following is a C function that matches the Zig version more closely:

/* -*- coding: utf-8 c-basic-offset: 4 tab-width: 4 indent-tabs-mode: nil -*- */ // Example retrofitted from Zig example by Andrew Kelley: // https://www.youtube.com/watch?v=Gv2I7qTux7g&t=1761s #pragma Cedro 1.0 int main(int argc, char **argv) { struct SoundIo *soundio = soundio_create(); if (!soundio) { return SoundIoErrorNoMem; } auto soundio_destroy(soundio); int err; if ((err = soundio_connect(soundio))) return err; soundio_flush_events(soundio); const int default_output_index = soundio_default_output_device_index(soundio); if (default_output_index < 0) return SoundIoErrorNoSuchDevice; const struct SoundIoDevice *device = soundio_get_output_device(soundio, default_output_index); if (!device) return SoundIoErrorNoMem; auto soundio_device_unref(device); const struct SoundIoOutStream *outstream = soundio_outstream_create(device); if (!outstream) return SoundIoErrorNoMem; auto soundio_outstream_destroy(outstream); outstream->format = SoundIoFormatFloat32NE; outstream->write_callback = write_callback; if ((err = soundio_outstream_open(outstream))) return err; if ((err = soundio_outstream_start(outstream))) return err; while (true) soundio_wait_events(soundio); } /* -*- coding: utf-8 c-basic-offset: 4 tab-width: 4 indent-tabs-mode: nil -*- */ // Example retrofitted from Zig example by Andrew Kelley: // https://www.youtube.com/watch?v=Gv2I7qTux7g&t=1761s int main(int argc, char **argv) { struct SoundIo *soundio = soundio_create(); if (!soundio) { return SoundIoErrorNoMem; } int err; if ((err = soundio_connect(soundio))) { soundio_destroy(soundio); return err; } soundio_flush_events(soundio); const int default_output_index = soundio_default_output_device_index(soundio); if (default_output_index < 0) { soundio_destroy(soundio); return SoundIoErrorNoSuchDevice; } const struct SoundIoDevice *device = soundio_get_output_device(soundio, default_output_index); if (!device) { soundio_destroy(soundio); return SoundIoErrorNoMem; } const struct SoundIoOutStream *outstream = soundio_outstream_create(device); if (!outstream) { soundio_device_unref(device); soundio_destroy(soundio); return SoundIoErrorNoMem; } outstream->format = SoundIoFormatFloat32NE; outstream->write_callback = write_callback; if ((err = soundio_outstream_open(outstream))) { soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); return err; } if ((err = soundio_outstream_start(outstream))) { soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); return err; } while (true) soundio_wait_events(soundio); soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); }

The Cedro version is much closer, but his point still stands because the plain C version needs a lot of repeated code and is more fragile. And of course Zig has many other great features.

Apart from the already mentioned «A defer mechanism for C», there are macros that use a for loop as for (allocation and initialization; condition; release) { actions } [1] or other techniques [2].

[1] “P99 Scope-bound resource management with for-statements” from the same author (2010), “Would it be possible to create a scoped_lock implementation in C?” (2016), ”C compatible scoped locks“ (2021), “Modern C and What We Can Learn From It - Luca Sas [ ACCU 2021 ] 00:17:18”, 2021
[2] “Would it be possible to create a scoped_lock implementation in C?” (2016), “libdefer: Go-style defer for C” (2016), “A Defer statement for C” (2020), “Go-like defer for C that works with most optimization flag combinations under GCC/Clang” (2021)

Compilers like GCC and clang have non-standard features to do this like the __cleanup__ variable attribute.

Block macros: #block-macros

Formats a multi-line macro into a single line.

C macros must be written all in one line, but some times you need to split them in several pseudo-lines and it gets tedious and error-prone to maintain all the newline escapes (“\”).

By adding braces (“{” or “}”) right after #define we can have Cedro do that for us:

#define { macro(A, B, C) f_##A(B, C); /// Specialized version of f() for type A. #define } #define macro(A, B, C) \ f_##A(B, C); /** Specialized version of f() for type A. */ \ // End #define

Preprocessor directives are still not allowed inside macros, so you can not use #if, #include, etc.

Note: the directive must start exactly with “#define {” or “#define }”, with no more or less space between “#define” and the brace “{” or “}”.

Binary inclusion: #binary-include

Inserts a file as a byte array.

#include <stdint.h> const uint8_t image #include {images/cedro-32x32.png} ; #include <stdint.h> const uint8_t image [1480] = { // cedro-32x32.png 0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,… 0x00,0x00,0x00,0x20,0x00,0x00,0x00,… ⋮ };

The file name is relative to the current directory, not the C file, because usually binary files do not go next to the source code.

Note: the directive must start exactly with “#include {”, with no more or less space between “#include” and the brace “{”.

This feature is an old idea and there are several implementations, for instance xxd (as xxd -i, man page) which I used many years ago and has it since 1994.

More recently, the include_bytes!() macro has been very useful to me in my Rust programs, so I added it to Cedro.