【问题标题】:'Reverse' a collection of C preprocessor macros easily轻松“反转”一组 C 预处理器宏
【发布时间】:2018-04-14 08:57:17
【问题描述】:

我有很多预处理宏定义,像这样:

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

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

我现在正在为此虚拟机开发调试器,并且需要有效地“反转”这些前驱宏。换句话说,我需要一个函数,它接受 number 并返回 macro name,例如输入 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。

【问题讨论】:

  • 只是检查一下,是否有必要使用预处理器宏而不是某种数据结构来执行此操作?
  • @DavidZ 我当然愿意接受不同的处理方式;如果有比预处理器宏更好的方法,那么我很乐意使用它。
  • 我真的很惊讶您从未学习过元编程方法(至少以解析器生成器或compiler compiler 为例)。你在哪里学过 C 语言?
  • 关注(当你有时间的时候)我回答中的所有链接。在某些时候,您需要了解所有这些。见norvig.com/21-days.html
  • @BasileStarynkevitch 会的,谢谢。 (在一个不相关的说明中,尽管我会让你知道你的简历中指向 GCC MELT 的链接已损坏。)

标签: c gcc c-preprocessor


【解决方案1】:

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

我有很多预处理宏定义,像这样:

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

请记住,可以生成 C 或 C++ 代码,并且很容易指示您的 build automation 工具生成一些特定的 C 文件(使用 GNU makeninja 你只需添加一些规则或配方)。

例如,您可以使用一些不同的预处理器(如GPPm4),或一些脚本 -e.g.在awkPythonGuile 等...,或编写您自己的程序(C、C++、Ocaml 等...),生成包含这些#define-s。而另一个脚本或程序(或相同的,调用方式不同)可以生成instruction_by_id的C代码

这种基本的 metaprogramming 技术(从更高级别但特定的东西生成一些或几个 C 文件)至少从 1980 年代开始就已经使用(例如,使用 yaccRPCGEN) . C preprocessor 通过它的 #include 指令促进了这一点(因为您甚至可以在某些函数体中包含行 inside 等...)。实际上,代码就是数据(和证明),数据就是代码的想法甚至更早(Church-Turing thesisCurry-Howard correspondenceHalting 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 与 1 的关系)(在一行opcode.txt 中)。当然,您需要记录下来(至少,在您的Makefile 中使用 cmets)。

来自更高级别的元编程,declarative 形式化,是一个非常强大的范例。在法国,自 1960 年代以来,J.Pitrat 开创了它(他今天正在写一个有趣的blog,同时已退休)。在美国,J.MacCarthyLisp 社区也有。

有关有趣的演讲,请参阅 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 编译库在运行时生成代码。您可以在程序中嵌入解释器(如 LuaGuile)。

顺便说一句,元编程方法是大多数开发人员(而不仅仅是编译器行业的人)应该了解基本compilation 技术的原因之一;另一个原因是parsing 问题非常普遍。所以请阅读Dragon Book

注意Greenspun's tenth rule。这不仅仅是一个玩笑,实际上是一个关于大型软件的深刻真理。

【讨论】:

  • 这真是太棒了。非常感谢您提供如此详细、全面的答案。至于您对我的问题的评论:我的 C 是自学的,而元编程方法正是我所寻找的。 (我在我的问题中提到了创建自定义宏的想法。)我已经用动态语言(主要是 Ruby)做了很多元编程,但从来没有用 C 语言,而且我没有想到我可以在任何地方#include(虽然这很有意义,因为我对#include 的理解是它基本上是复制粘贴)。再次感谢您!
【解决方案2】:

在类似的情况下,我求助于定义定义指令的文本文件格式,并编写程序来读取此文件并写出实际指令定义的 C 源代码和函数的 C 源代码,如您的指令_by_id ()。这样你只需要维护文本文件。

【讨论】:

    【解决方案3】:

    与一般代码生成一样棒,我很惊讶没有人提到(如果你放宽你的问题定义只是一点)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 与它们一起使用)。因此,使用 anonymous 枚举类型,像这样定义常量:

    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_FOOINSN_BAR等等。

    【讨论】:

    • 哇,这真的很酷!我以前从未遇到过 X 宏,但我很高兴我现在知道了它们,它们在这里提供了一个非常好的解决方案。感谢您添加如此详细的答案:)
    猜你喜欢
    • 2020-02-09
    • 2011-01-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-16
    • 2016-12-14
    • 2011-03-26
    • 2018-01-04
    相关资源
    最近更新 更多