A variant on the X macro is the "header file of macro calls", which I learned from the Clang source code. I'm very much a convert to this style of boilerplate-scrapping, and it's saved my bacon a few times.
Here's an example from Homebrew OS. The interrupt table is kind of complex, because it has to deal with machine exceptions/traps, hardware interrupts, and software-defined interrupts. For some traps, the CPU pushes extra information onto the stack (e.g. page fault pushes information about the memory access onto the stack), so the stack frame may not be the same in all cases. Some traps need special care (e.g. non-maskable interrupts or the machine-check exception).
To handle all the complexity, I define the interrupt table a header file which contains a big block of macro calls, called interrupt_table.inc:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | TRAP(0x00,__handle_de)
TRAP_SPC(0x01,__handle_db)
TRAP_IST2(0x02,__handle_nmi)
USER_TRAP(0x03,__handle_np)
USER_TRAP(0x04,__handle_of)
USER_TRAP(0x05,__handle_br)
TRAP(0x06,__handle_ud)
TRAP(0x07,__handle_nm)
TRAP_IST1(0x08,__handle_df)
TRAP(0x09,__handle_fpu_of)
TRAP_ERR(0x0a,__handle_ts)
TRAP_IST1(0x0b,__handle_np)
TRAP_IST1(0x0c,__handle_ss)
TRAP_IST1(0x0d,__handle_gp)
TRAP_SPC(0x0e,__handle_pf)
TRAP(0x0f,__handle_trap_0f)
TRAP(0x10,__handle_mp)
TRAP_ERR(0x11,__handle_ac)
TRAP_IST1(0x12,__handle_mc)
// ...and so on for all 256 possible interrrupts...
INTERRUPT(0xf8)
INTERRUPT(0xf9)
INTERRUPT(0xfa)
TRAP(0xfb,__handle_ipi_slow)
TRAP(0xfc,__handle_ipi_fast)
TRAP(0xfd,__handle_apic_error)
TRAP_SPC(0xfe,__handle_apic_spurious)
TRAP(0xff,handle_ipi_reschedule)
|
To use this, you do a bunch of #defines to say what all the cases mean, then #include the file.
So, for example, here is the code to set up the interrupt descriptor table (in C):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | #define TRAP_IST1(n, f) \
extern void f(); \
idt_set_trap(n, f, 0, 1);
#define TRAP_IST2(n, f) \
extern void f(); \
idt_set_trap(n, f, 0, 2);
#define TRAP(n, f) \
extern void f(); \
idt_set_trap(n, f, 0, 3);
#define TRAP_ERR TRAP
#define USER_TRAP TRAP
#define TRAP_SPC TRAP
#define USER_TRAP_SPC TRAP
#define INTERRUPT(n) \
extern void __irq_##n (); \
idt_set_int(n, __irq_##n, 0, 3);
#include "interrupt_table.inc"
|
And here is the code to automatically generate interrupt handler stubs (in assembler):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | #define TRAP_ERR(n, f) \
ENTRY(f) ; \
INT_HANDLER_BODY(n, handle_trap)
#define TRAP(n, f) \
ENTRY(f) ; \
pushq $0 ; \
INT_HANDLER_BODY(n, handle_trap)
#define INTERRUPT(irqn) \
ENTRY(__irq_##irqn) ; \
pushq $0 ; \
INT_HANDLER_BODY(irqn, handle_interrupt)
#define USER_TRAP TRAP
// We don't want automatically-generated stubs for these cases.
#define TRAP_SPC(n, f)
#define TRAP_IST1(n, f)
#define TRAP_IST2(n, f)
#define USER_TRAP_SPC(n, f)
#include "interrupt_table.inc"
|
The reason why this approach work well is that there are 256 cases which need to be kept consistent between different source files, even written in different languages. The "header file of macro calls" keeps it all in one place.