我有很多的预处理器宏定义,如下所示:
#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。
答案 0 :(得分:3)
你的方法错了。 如果您还没有阅读,请阅读SICP 。
我有很多预处理器宏定义,如下所示:
#define FOO 1 #define BAR 2 #define BAZ 3
请记住,可以生成C或C ++代码,并且可以很容易地指示您的build automation工具生成某个特定的C文件(使用{ {3}}或GNU make
您只需添加一些规则或配方。
例如,您可以使用一些不同的预处理器(liek ninja或GPP)或某些脚本-e.g.在m4或awk
或Python等等,或编写您自己的程序(在C,C ++,Ocaml等中......),以生成包含这些#define
- s的头文件。另一个脚本或程序(或同一个脚本或程序,以不同方式调用)可以生成instruction_by_id
这种基本的 Guile技术(从更高级别但特定的某些方面生成一些或几个C文件)至少从20世纪80年代开始使用(例如使用metaprogramming或yacc)。 RPCGEN使用#include
指令促进了这一点(因为你甚至可以在里面包含行某些函数体等等)。实际上,代码是数据(和证明)和数据是代码的想法甚至更早(C preprocessor,Church-Turing thesis,Curry-Howard correspondence)。 Halting problem这本书非常有趣......
例如,您可以决定使用文本文件opcodes.txt
(甚至包含某些内容的Gödel, Escher, Bach数据库....)
# ignore lines starting with an hashsign
FOO 1
BAR 2
并且有两个小的awk
或Python脚本(或两个微小的C专用程序),一个生成#define
- s(转换为opcode-defines.h
),另一个生成instruction_by_id
的主体1}}(进入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
与1的关系)表达一次(在opcode.txt
的单行中)。当然,您需要记录(至少在Makefile
中添加评论)。
来自某些更高级别sqlite形式化的元编程是一种非常强大的范例。在法国,J.Pitrat开创了它(自从20世纪60年代以来,他正在撰写一篇有趣的declarative,同时正在退休。在美国,blog和J.MacCarthy社区也是。
有关有趣的演讲,请参阅Liam Proven Lisp
大型软件经常使用这种元编程方法。例如,FOSDEM 2018 talk on The circuit less traveled有大约十几个C ++代码生成器(总共它们发出了超过一百万个C ++代码行)。
另一种看待这种方法的方法是GCC compiler可能是domain-specific languages的想法。如果您使用提供compiled to C的操作系统,您甚至可以编写一个发出C代码的程序,分支进程将其编译成一些插件,然后加载该插件(在POSIX或Linux上,dynamic loading) 。有趣的是,计算机现在足够快,可以在交互式应用程序中实现这种方法(在某种dlopen中):你可以发出几千行的C文件,将其编译成一些.so
共享目标文件,以及dlopen
,只需几分之一秒。您还可以使用JIT编译库(如REPL或LLVM)在运行时生成代码。您可以将口译员(如GCCJIT或Lua)嵌入您的计划中。
请注意Dragon Book。它不仅仅是一个笑话,实际上是关于大型软件的深刻真理。
答案 1 :(得分:1)
在类似的情况下,我采用定义文本文件格式来定义指令,并编写程序来读取该文件,并写出实际指令定义的C源代码和函数的C源代码,如instruction_by_id ()。这样您只需要维护文本文件。
答案 2 :(得分:1)
与通用代码生成一样棒,我很惊讶没有人提到(如果你放松你的问题定义只是一点)C 预处理器完全能够生成必要的代码,使用一种称为 X macros 的技术。事实上,我见过的每个简单的 C 语言字节码 VM 都使用这种方法。
该技术的工作原理如下。首先,有一个包含权威指令列表的文件(称为 insns.h
),
INSN(FOO, 1)
INSN(BAR, 2)
INSN(BAZ, 3)
或者其他一些包含相同内容的标头中的宏,
#define INSNS \
INSN(FOO, 1) \
INSN(BAR, 2) \
INSN(BAZ, 3)
以您更方便的为准。 (我将在下面使用第一个选项。)请注意,INSN
未在任何地方定义。 (传统上它会被称为 X
,因此是该技术的名称。)无论您想在何处循环指令,定义 INSN
以生成您想要的代码,包括 insns.h
,然后再次取消定义 INSN
。
在你的反汇编程序中,写
const char *instruction_by_id(int id) {
switch (id) {
#define INSN(NAME, VALUE) \
case NAME: return #NAME;
#include "insns.h" /* or just INSNS if you use a macro */
#undef INSN
default: return "???";
}
}
使用前缀 stringification operator #
将名称作为标识符转换为名称作为字符串文字。
你显然不能这样定义常量,因为宏不能在 C 预处理器中定义其他宏。但是,如果您不坚持指令常量是预处理器常量,那么 C 语言中有一个完全不同的常量工具:枚举。无论您是否使用枚举类型,从编译器的角度来看,其中定义的枚举器都是常规整数常量(尽管不是预处理器 - 例如,您不能将 #ifdef
与它们一起使用)。因此,使用匿名枚举类型,像这样定义常量:
enum {
#define INSN(NAME, VALUE) \
NAME = VALUE,
#include "insns.h" /* or just INSNS if you use a macro */
#undef INSN
NINSNS /* C89 doesn’t allow trailing commas in enumerations (but C99+ does), and you may find this constant useful in any case */
};
如果要静态初始化由字节码索引的数组,无论是否使用 X 宏,都必须使用 C99 designated initializers {[FOO] = foovalue, [BAR] = barvalue, /* ... */}
。但是,如果你不坚持给你的指令分配自定义代码,你可以从上面去掉VALUE
,让枚举自动分配连续的代码,然后数组可以简单地按顺序初始化,{foovalue, barvalue, /* ... */}
。作为奖励,上面的 NINSNS
然后变成等于指令的数量和任何此类数组的大小,这就是我称之为的原因。
您可以在此处使用更多技巧。例如,如果某些指令具有多种数据类型的变体,则指令列表 X 宏可以调用类型列表 X 宏来自动生成变体。 (将 X 宏列表存储在大宏中而不是包含文件中的第二个有点丑陋的选项在这里可能更方便。)INSN
宏可能需要额外的参数,例如模式名称,在代码列表,但用于在反汇编程序中调用适当的解码例程。您可以使用 token pasting operator ##
为常量的名称添加前缀,如在 INSN_ ## NAME
中生成 INSN_FOO
、INSN_BAR
、等。< /em> 等等。