我有很多预处理器宏定义,如下所示:
#define FOO 1
#define BAR 2
#define BAZ 3
在实际应用中,每个定义都对应于解释器虚拟机中的一条指令。宏在编号上也不是连续的,以留出空间供将来使用。可能有一个
#define FOO 41
,然后下一个是#define BAR 64
。我现在正在为该虚拟机开发调试器,并且需要有效地“逆向”这些前置宏。换句话说,我需要一个接受数字并返回宏名称的函数,例如输入2将返回
"BAR"
。当然,我可以自己使用
switch
创建一个函数:const char* instruction_by_id(int id) {
switch (id) {
case FOO:
return "FOO";
case BAR:
return "BAR";
case BAZ:
return "BAZ";
default:
return "???";
}
}
但是,这将是一个噩梦,因为重命名,删除或添加指令也将需要修改此功能。
是否可以使用另一个宏为我创建类似的函数,或者是否可以使用其他方法?如果没有,是否可以创建一个宏来执行此任务?
我在Windows 10上使用gcc 6.3。
最佳答案
您的方法错误。 如果您尚未阅读,请阅读SICP 。
我有很多预处理器宏定义,如下所示:
#define FOO 1
#define BAR 2
#define BAZ 3
请记住,可以生成来生成 C或C++代码,并且很容易指示build automation工具生成某些特定的C文件(使用GNU
make
或ninja您只需添加一些规则或配方即可)。例如,您可以使用一些其他预处理器(liek GPP或m4)或某些脚本-e.g。在
awk
或Python或Guile等中...,或编写自己的程序(在C,C++,Ocaml等中),以生成包含这些#define
-s的头文件。另一个脚本或程序(或相同的脚本,以不同的方式调用)可以生成instruction_by_id
的C代码至少从1980年代开始(从metaprogramming或yacc开始)就使用了这种基本的 RPCGEN技术(从更高级别但特定的东西生成一些或几个C文件)。 C preprocessor通过其
#include
伪指令来简化此操作(因为您甚至可以在某些函数体中包括行,等等。)。实际上,代码是数据(和证明)而数据是代码的想法甚至更古老(Church-Turing thesis,Curry-Howard correspondence,Halting problem)。 Gödel, Escher, Bach书非常有趣。例如,您可以决定拥有一个文本文件
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
和使用它的脚本,但是仅一次(在一行FOO
中)表示一个给定的“知识元素”(opcode.txt
与1的关系)。当然,您需要对此进行记录(至少,在Makefile
中带有注释)。来自更高级别的declarative形式化的元编程是一个非常强大的范例。自1960年代以来,在法国,J.Pitrat率先提出了这一点(他正在退休时写着有趣的blog)。在美国,J.MacCarthy和Lisp社区也是如此。
有关有趣的演讲,请参阅Liam Proven FOSDEM 2018 talk on The circuit less traveled
大型软件经常使用这种元编程方法。例如,GCC compiler大约有十二个C++代码生成器(总共发出超过一百万条C++行)。
查看这种方法的另一种方法是domain-specific languages的想法,它可能是compiled to C。如果使用提供dynamic loading的操作系统,则甚至可以编写一个发出C代码的程序,分叉一个过程以将其编译为某个插件,然后(在POSIX或Linux上,使用dlopen)加载该插件。有趣的是,计算机现在已经足够快,可以在交互式应用程序(以某种REPL形式)中启用这种方法:您可以发出几千行的C文件,将其编译为某些
.so
共享对象文件,然后将dlopen
编译为几分之一秒。您还可以使用GCCJIT或LLVM之类的JIT编译库在运行时生成代码。您可以在程序中嵌入解释器(如Lua或Guile)。顺便说一句,元编程方法是大多数开发人员(不仅是编译器行业的人们)都应了解基本compilation技术的原因之一;另一个原因是parsing问题很常见。因此,请阅读Dragon Book。
注意Greenspun's tenth rule。这不只是一个玩笑,实际上是关于大型软件的深刻事实。