轻松“反转”C预处理器宏的集合

我有很多预处理器宏定义,如下所示:

#define FOO 1 #define BAR 2 #define BAZ 3 

在实际应用中,每个定义对应于解释器虚拟机中的指令。 这些宏在编号方面也不是连续的,以便为将来的指令留出空间; 可能有#define FOO 41 ,然后下一个是#define BAR 64

我现在正在为这个虚拟机开发一个调试器,需要有效地“反转”这些预制宏。 换句话说,我需要一个获取数字并返回宏名称的函数,例如2的输入返回"BAR"

当然,我可以自己创建一个使用switch的function:

 const char* instruction_by_id(int id) { switch (id) { case FOO: return "FOO"; case BAR: return "BAR"; case BAZ: return "BAZ"; default: return "???"; } } 

但是,这将是一个难以维护的噩梦,因为重命名,删除或添加指令也需要修改此function。

是否有另一个宏可以用来为我创建这样的函数,还是有其他方法? 如果没有,是否可以创建宏来执行此任务?

我在Windows 10上使用gcc 6.3。

你有错误的方法。 如果您还没有阅读,请阅读SICP

我有很多预处理器宏定义,如下所示:

 #define FOO 1 #define BAR 2 #define BAZ 3 

请记住, 可以生成C或C ++代码 ,并且很容易指示构建自动化工具生成某个特定的C文件(使用GNU make或ninja,您只需添加一些规则或配方)。

例如,你可以使用一些不同的预处理器(liek GPP或m4 ),或awk或Python或Guile等的一些脚本-eg,或者编写你自己的程序(用C,C ++,Ocaml等… ), 生成包含这些#define -s的头文件。 另一个脚本或程序(或相同的脚本或程序,以不同方式调用)可以生成instruction_by_id的C代码

至少在20世纪80年代(例如使用yacc或RPCGEN ),已经使用了这种基本的元编程技术 (从更高级别但特定的更高级别生成一些或几个C文件)。 C预处理器通过其#include指令来实现这一点(因为你甚至可以一些函数体内包含行等等)。 实际上,代码是数据(和certificate)和数据是代码的想法甚至更老( Church-Turing论文 , Curry-Howard对应 , Halting问题 )。 哥德尔,埃舍尔,巴赫的书非常有趣……

例如,你可以决定有一个文本文件opcodes.txt (甚至一些包含东西的sqlite数据库….)

 # ignore lines starting with an hashsign FOO 1 BAR 2 

并且有两个小的awk或Python脚本(或两个微小的C专用程序),一个生成#define -s(进入opcode-defines.h ),另一个生成instruction_by_id (进入opcode-instr.inc )。 然后你需要调整你的Makefile来生成这些,并把#include "opcode-defines.h"放在一些全局头文件中,并且有

  const char* instruction_by_id(int id) { switch (id) { #include "opcode-instr.inc" default: return "???"; } } 

这将是一场噩梦,

这种元编程方法并非如此。 你只需要维护opcodes.txt和使用它的脚本,但是你只需要一次(在一行opcode.txt )表达给定的“知识元素”( FOO与1的关系)。 当然你需要记录(至少在你的Makefile有注释)。

从一些更高级别的声明式forms化中进行元编程是一种非常强大的范例。 在法国,自20世纪60年代以来,J.Pitrat开创了这一领域(他今天正在撰写一篇有趣的博客 ,同时正在退休)。 在美国, J.MacCarthy和Lisp社区也是如此。

对于一个有趣的谈话,请参阅Liam Proven FOSDEM 2018关于电路较少旅行的谈话

大型软件经常使用这种元编程方法。 例如, GCC编译器有大约十几个C ++代码生成器(总共它们发出了超过一百万个C ++行)。

查看这种方法的另一种方法是可以编译为C 的特定于域的语言 。 如果您使用提供动态加载的操作系统,您甚至可以编写一个发出C代码的程序,分支进程将其编译成一些插件,然后加载该插件(在POSIX或Linux上,使用dlopen )。 有趣的是,计算机现在足够快,可以在交互式应用程序中实现这种方法(在某种REPL中 ):你可以发出几千行的C文件,将其编译成一些.so共享对象文件,然后执行,在几分之一秒内。 您还可以使用JIT编译库(如GCCJIT或LLVM)在运行时生成代码。 您可以将解释器(如Lua或Guile )嵌入到您的程序中。

BTW,元编程方法是大多数开发人员(而不仅仅是编译器业务人员)应该知道基本编译技术的原因之一; 另一个原因是解析问题很常见。 所以阅读龙书

注意Greenspun的第十条规则 。 它不仅仅是一个笑话,实际上是关于大型软件的深刻真理。

在类似的情况下,我使用定义文本文件格式来定义指令,并编写程序来读取该文件,并写出实际指令定义的C源和函数的C源,如instruction_by_id()。 这样您只需要维护文本文件。