Back to all posts
5 min read

Macroaggression

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.

MacroApple clang 17, arm64GCC 15, aarch64
ma_timesidenticalidentical
ma_unlessidenticalidentical
ma_uptoidenticalsame count
ma_eachsame countsame count
ma_each with breaksame count1 fewer
ma_countidenticalsame count
ma_reverse_eachsame countidentical
ma_reverse_each with break8 moreidentical
ma_each_charidenticalidentical
ma_each_char with break2 more1 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.
  • break and continue behave as they do in a plain for.
  • 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.
  • v is a copy of the element. For a struct, assigning to v.x changes 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 -O0 the 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!

• Link to this post from your site• Share your thoughts via webmention• Join the IndieWeb conversation

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.

rubycrystalrust

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.

rubyrailsopen-source