Macroaggression

In Matz Told Me Why I promised a post about each library behind that story. This is the first. macroaggression is the library behind the /r/rails post that vanished in 2024, and a slice of it, renamed happy_c.h, was the pull request matz closed.
It gives C Ruby’s iterators.
The same code in Ruby and C¶
scores = [42, 77, 50, 91, 13]
total = 0
scores.each { |v| total += v }
passed = scores.select { |v| v >= 50 }
passed.each { |v| puts v }
puts total
int scores[] = {42, 77, 50, 91, 13}, n = 5, passed[5];
int total = 0;
ma_each(v, scores, n) total += v;
int kept = ma_select(v, scores, n, passed, v >= 50);
ma_each(v, passed, kept) printf("%d\n", v);
printf("%d\n", total);
Both print the passing scores and the total. Each method lives in its own header, 26 of them, and you include only the ones you use.
Why¶
It exists for readers. A Rubyist opening C spends the first minutes decoding for (int i = 0; i < n; i++) and checking whether the bound says < or <=. ma_upto(i, 1, 10) answers that before you ask. CRuby and spinel’s output are both C, and the people who could help with either think in each and times.
Mostly the spelling changes¶
The closest comparison is WinDev. Its language, WLangage, takes French or English keywords, and the editor translates code from one to the other. The program stays the same whichever language you typed it in. macroaggression lets you type C in Ruby.
It adds no types either. C already has them, and the macros only derive a loop variable’s type from the arguments you pass.
That is a claim about machine code, so I checked it. At -O2 I compiled the macros below next to the pointer loop you would write by hand, such as for (const int *p = a, *e = a + n; p < e; p++), and counted the instructions. “Same count” means the same number of instructions.
| Macro | Apple clang 17, arm64 | GCC 15, aarch64 |
|---|---|---|
ma_times | identical | identical |
ma_unless | identical | identical |
ma_upto | identical | same count |
ma_each | same count | same count |
ma_each with break | same count | 1 fewer |
ma_count | identical | same count |
ma_reverse_each | same count | identical |
ma_reverse_each with break | 8 more | identical |
ma_each_char | identical | identical |
ma_each_char with break | 2 more | 1 fewer |
GCC never needed more instructions than my hand-written loops. Clang still pays extra in two loops that break.
The table looked worse before I wrote this post. ma_each nests three for statements, because each for can declare only one base type and break leaves only the innermost one. The first version carried the way out through a guard pointer that started at zero, and that made the compiler add a NULL check on the array before the loop. A run flag replaced the pointer, and the check went away.
Steve Bourne got there first¶
Steve Bourne’s shell shipped with Seventh Edition Unix in 1979. He had worked on an ALGOL 68 compiler at Cambridge, and he made his C look like it: mac.h defines IF, THEN, FI, DO and OD, which expand into plain C. The dialect got a name, Bournegol.
On 23 March 1984, Larry Bassel was fixing a bug in that shell while Landon Curt Noll wrestled with BSD’s finger. They started the International Obfuscated C Code Contest that day. Macro dialects of C are older than the contest they inspired.
The prefix¶
The library had its ma_ prefix before I opened that pull request. For spinel I stripped it and renamed the slice happy_c.h, because handing matz a dependency on a library he had never seen felt impolite. The stripped version is the one he turned down, for name capture, which is the problem the prefix exists to prevent.
POSIX already uses two of the bare names: times() in <sys/times.h> and select() in <sys/select.h>. A function-like macro named times breaks every later call to the real function. So the prefix is the default, and one test includes both system headers next to the library and calls the real times(). The bare names are opt-in, for code that owns its namespace:
#define MACROAGGRESSION_BARE
#include <macroaggression.h>
each(v, scores, n) total += v;
The fine print¶
- It is GNU C, so it needs GCC or Clang. It leans on statement expressions and GNU type inference (
__typeof__,__auto_type). The tests compile each header alone under every standard from c99 to c23, with-Wall -Wextra -Wshadow -Werror. breakandcontinuebehave as they do in a plainfor.- The loop variable takes its type from the arguments after C’s usual conversions, so mixed signs bite:
ma_upto(j, -1, 10u)runs zero times, where Ruby’s-1.upto(10)runs twelve. vis a copy of the element. For a struct, assigning tov.xchanges the copy, where Ruby would change the object.- Arguments are evaluated once, except conditions and block expressions, which run on every pass.
- The table is for
-O2. At-O0the macros cost about twice the instructions of a hand-written loop, and a debugger steps over a whole macro as one line.
It is BSD 2-Clause. The next post is CrystalMeet.
🔗Interstellar Communications
No transmissions detected yet.Be the first to establish contact!
Related Posts
CrystalMeet
CrystalMeet turned Ruby into Crystal with a pile of regular expressions. It half worked, and the other half lives in Ruby's runtime, where no regex can reach. /r/rails deleted it in 2024. Its scenarios are landing in spinel now.
Matz Told Me Why
In one week, matz closed my pull request to spinel and the /r/rails moderators banned me for good. Matz explained his decision in five sentences. In two years of removing my posts, the moderators never explained one.
The Observability Trap: Why I Built Lapsoss to Break Vendor Chains
How the observability industry's vendor lock-in tactics led to building Lapsoss and the Liberation Stack - community-owned tools that put developers back in control