事实上,这个世界并无几份 GNU m4 教程。前端
这个文档系列是我第一次认真学习 GNU m4 并进行了一些实践以后的一些总结。因为我在撰写此文的过程当中充满着像 m4 展开一个又一个宏通常的耐心,所以这篇文章会比较长。在这个信息碎片化的时代,彷佛没有不少人愿意去看很长的文章,你们更喜欢干货。为了节省你们的时间,必须声明,这个文档系列没有干货,它是写给我本身或者那些像我本身的人看的。编程
书名是『宏』,它被做者展开为这本书的所有内容。药瓶上的标签是『宏』,将药片从瓶中倾倒出来,就是这个宏的展开结果。被用的最多的『宏』,应该是 Internet 的超级连接。每当你点击一个超级连接,就至关于将这个宏展开为网页中的内容。生活中,相似的例子还有不少,只要你给某种具体的事物贴上了一个标签,那么这个标签就至关于宏。segmentfault
人类很是喜欢给事物贴标签,尽管不管他们贴与不贴,那些事物自己依然是存在的。在编程中,若是你想给一段代码贴标签,最简单最直接的办法就是使用宏。那些还在用汇编语言编程的人,他们是离不开宏的,由于汇编语言自己就是将一大堆标签贴在了更大的一堆机器代码上。若是所用的编程语言不提供宏功能,能够用这种编程语言为一段代码制做一个标签——函数,不过这种标签就不是宏了,并且要付出一些性能上的代价,由于标签的展开过程被推迟到程序的运行过程。缓存
C 语言自诞生后,只用了 5 年就让汇编语言归隐山林了,这可能要归功于 Unix 的成功以及 Dennis Ritchie 的忽悠。Steve Johnson——yacc, lint, spell 以及 PCC(Portable C Compiler)的做者说:『Dennis Ritchie 告诉全部人,C 函数的调用开销真的很小很小。因而人人都开始编写小函数,搞模块化。然而几年后,咱们发如今 PDF-11 中函数的调用开销依然很是大,而 VAX 机器上的代码每每在 CALL 指令上花费掉 50% 的运行时间。Dennis 对咱们撒了谎!但为时已晚,咱们已经欲罢不能……』安全
现代的编程语言,几乎都赞同用函数来取代宏。拥护者们每每会给出一些堂而皇之的理由是,诸如没必要额外实现一个宏处理器,函数比宏更安全而且更容易调试。事实上,他们的理由仅仅是迎合现实而已。若是将这些人扔进时空裂缝让他们穿越到 Ken Thompson 编写 Unix 系统的时代,让他们也在一台废弃的 PDP-7 型号的计算机上写程序。在这种内存只有 8KB 的计算机上,那些堂而皇之的理由近乎与科幻小说等价。函数之因此可以取代宏,仅仅是由于 CPU 的计算速度比过去更快了,内存比之前更大了,牺牲一些程序性能,让编程工做更容易一些,这样比较合算而已。编程语言的性能与机器的性能彷佛老是成反比的。编程语言
宏被不少人主观的弃用了,得益于现代编程语言的表达能力,他们彷佛几乎不须要用宏,因而他们做出结论:宏过期了。事实上,宏会永远居于众编程语言之上的,由于前者老是可以生成后者。编程专家老是会告诉咱们,要慎用宏。胆子小的程序猿看到宏就躲得远远的,以致于他们总以为那些使用宏的代码是糟糕的,是不安全的。事实上,在编程中,若能恰如其分的使用宏,可让代码更加简洁易读,特别是对 C 语言这种表现力不足的语言。模块化
例以下面 C 代码中的宏:函数
#define DEF_PAIR_OF(dtype) \ typedef struct pair_of_##dtype { \ dtype first; \ dtype second; \ } pair_of_##dtype##_t DEF_PAIR_OF(int); DEF_PAIR_OF(double); DEF_PAIR_OF(MyStruct);
是否是有点 C++ 模板的意味?像 C 标准库提供的 qsort
函数所接受的回调函数,也能够用相似的方法半自动生成。有关 C 语言宏的基本规则与技巧,可参考『宏定义的黑魔法 - 宏菜鸟起飞手册』。即便是表达能力很强的现代编程语言,在处理复杂问题上,也没法避免代码自身的频繁重复,妥善的使用宏老是能够消除这种重复,甚至能够创造一些 DSL(领域专用语言)。性能
在代码中适当的运用宏,创造优雅易读的代码,这样或许更能体现编程是一种艺术。虽然有些编程语言未提供宏功能,可是咱们老是会有 GNU m4 这种通用的宏处理器可用。学习
m4 是一种宏处理器,它扫描用户输入的文本并将其输出,期间若是遇到宏就将其展开后输出。宏有两种,一种是内建的,另外一种是用户定义的,它们能接受任意数量的参数。除了作展开宏的工做以外,m4 内建的宏可以加载文件,执行 Shell 命令,作整数运算,操纵文本,造成递归等等。m4 可用做编译器的前端,或者单纯做为宏处理器来用。
全部的 Unix 系统都会提供 m4 宏处理器,由于它是 POSIX 标准的一部分。一般只有不多一部分人知道它的存在,这些发现了 m4 的人每每会在某个方面成为专家。这不是我说的,这是 m4 手册说的。
其实,手册里的原话翻译过来应该是:一般只有不多一部分人知道它的存在,发现它的人每每会成为它的忠实用户。(这里要多谢 @mohu3g 的正本清源之举)
有些人对 m4 很是着迷,他们先是用 m4 解决一些简单的问题,而后解决了一个比一个更大的问题,直至掌握如何编写一个复杂的 m4 宏集。若痴迷于此,每每会对一些简单的问题写出复杂的 m4 脚本,而后耗费不少时间去调试,反而不如直接手动解决问题更有效。因此,对于程序猿中的强迫症患者,要对 m4 有所警戒,它可能会危及你的健康。这也不是我说的,是 m4 手册说的。
上文提到『m4 是一种宏处理器,它扫描用户输入的文本并将其输出,期间若是遇到宏就将其展开后输出』,其实更正式的说,应该是:m4 从文本输入流中获取文本并将其发送到文本输出流,期间若是遇到宏就将其展开后发送到文本输出流。
在 Brian Kernighan 与 Dennis Ritchie 合著的《C Programming Language》中将流(Stream)定义为『与磁盘或其它外围设备关联的数据的源或目的地』。基于这个定义,m4 的输入流就是与磁盘或其它外围设备关联的数据的源,其输出流就是与磁盘或其它外围设备关联的数据的源或目的地,只不过 m4 但愿它的输入流与输出流的内容是文本。若是你不那么较真,能够将流理解为文件,对于 m4 而言,就是文本文件,可是下文会坚持使用流的概念。
m4 使用流的概念并不是巧合,若是说巧合,也只是由于它的做者刚好也是 Dennis Ritchie。
m4 是如何从输入流中获取文本并将其发送到输出流的?确定不是简单的读取文本就了事,由于 m4 有一个任务是『遇到宏就将其展开』。这意味着 m4 在从输入流中读取文本的过程当中至少须要检测所读取的某段文本是否是宏。也就是说,从 m4 的角度来看,它首先要将输入流所提供的文本分为两类:宏与非宏。若是 m4 读取的是一段文本是非宏,它基本上会将它们直接发送到输出流。之因此说是『基本上』,是由于非宏的文本会被进一步分类处理,其中细节后文会讲。若是 m4 读取的文本片断是宏,m4 就会将它展开,而后将展开结果发送到输出流。
m4 的工做过程具备必定程度的即时性,它不须要将输入流中所有信息都读取出来,而后再进行处理,而是扮演了一种过滤器的角色。从用户的角度来看,文本流入 m4,而后又流出。
从图灵的角度来看 m4,输入流与输出流能够衔接起来构成一条无限延伸的纸带,m4 是这条纸带的读写头,因此 m4 是一种图灵机。事实上,m4 的确是一种图灵机。所以 m4 的计算能力与任何一种编程语言等同,区别只体如今编程效率以及所编写的程序的运行效率方面。感受基于 m4 来说解计算机原理仍是挺不错的。
m4 既然是图灵机,它至少须要有一个『状态寄存器』,不然它没法判断当前从输入流中读取的文本是宏仍是非宏。为了提升文本处理效率,还应该有一个缓存空间,使得 m4 在这一空间中高效工做。现代的 CPU,没有缓存的应该很罕见。
m4 缓存的容量为 512KB。当它满了的时候,m4 会将自动将其中的内容妥善的保存到一份临时文件中备用。因此,只要你的磁盘或其它外围设备的容量足够,就不要担忧 m4 没法处理大文件。
注意,m4 缓存,这个概念是我瞎杜撰的。GNU m4 官方文档没这个概念,官方的概念是转移(Diversion)。
相似 CPU 的多级缓存,m4 的缓存空间也是划分了级别的。符合 POSIX 标准的 m4,可将缓存空间划分为 10 种级别,编号依次为 0, 1, 2, ..., 9
。GNU m4 对缓存空间的级别数量不做限制。
m4 默认在 0 号缓存中工做,它在这个缓存对文本进行处理,而后将其发送到输出流。使用 m4 内建的宏 divert
,能够从当前缓存切换到其余缓存。例如:
divert(3)
就从当前的缓存切换到 3 号缓存了,而后 m4 就在 3 号缓存中对输入流中的文本进行处理。若是不继续使用 divert
进行缓存切换,m4 会一直在 3 号缓存中工做,直到输入流终结。最后,m4 会将各个缓存中的文本汇总到 0 号缓存中。
缓存的汇总过程是按照缓存级别进行的。m4 会根据缓存级别的编号的增序进行汇总。例如,它老是先将 1 号缓存的内容汇总到 0 号缓存中,而后将 2 号缓存的内容汇总到 0 号缓存中,以此类推,最后将 0 号缓存中的内容依序发送到输出流中。
划分了级别的缓存,像是一道一道分水岭,使得文本流像河流同样拥有支流,不一样的支流最终又聚集到一块儿,奔流到海……是否是有些气势恢宏的感受,然而你也应该考虑到这样的现实:百川东到海,什么时候复西归?也就是说,文本流经 m4 的过程也像河流入海同样的不可逆。这是宏最大的弱点。在程序中滥用宏,形同过分开采水资源。
软件领域有一门学科,叫逆向工程,研究如何借助反汇编技术重现某个程序的原有逻辑。具体技术我不是很了解,可是幸亏有这门学科,不然个人显卡很难在新版本的 Linux 内核上工做。由于 Nvidia 官方的 Linux 驱动自某个版本以后就宣布再也不支持我这种型号的显卡了,而 Nvidia 官方驱动已经被大神实施逆向工程产生了 Nouveau 驱动,然后者又被集成到了 Linux 内核中。
彷佛跑题了,我想表达的是,逆向工程当然可以在必定程度上复原某个程序的源码,但它却永远没法基于宏的展开结果重现宏的定义,只有宏的做者才知道当初究竟发生了什么。
这时,你应该有一个问题。若是你真的想学习 m4,那就必需要有这个问题——m4 为何要对缓存划分级别?回顾一下上文,各个缓存的汇总过程是遵循特定次序的。有了这种分级的缓存汇总机制,你就有能力借助缓存来控制文本的支流,决定哪条支流先汇入 0 号缓存。你能够说这样你有机会扮演大禹,可是我以为这更像铁路调度员所作的事。对于铁路调度员而言,文本流是他要调度的一组列车。
更有趣的是,m4 也提供了暗黑缓存,它的编号是 -1
。GNU m4 对暗黑缓存也不限制数量,只要它们的编号是负数就能够。
暗黑缓存,彷佛有点恐怖,实际上你能够将它们理解为地下河。也就是流过暗黑缓存的文本,m4 会将它们汇总到 0 号缓存,汇总过程按照暗黑缓存编号的递减次序进行的,可是 m4 不会将暗黑缓存汇总的内容发送到输出流。这没什么很差理解的,现实中没有什么东西是负数的。
在 m4 的应用中,暗黑缓存的主要做用就是做为宏定义的空间。若是在 0 号缓存定义一个宏,例如:
divert(0) define(say_hello_world, Hello World!)
定义了一个名为 say_hello_world
的 m4 宏。宏定义语句『展开』为一个长度为 0 的字符串,而后发送到输出流。长度为 0 的字符串,就是空文本,即便它被发送到输出流,对输出流不会产生任何影响,可是 say_hello_world
宏以前,也就是 divert(0)
以后存在一个换行符,m4 会将这个换行符发送到输出流。除非你本来就但愿输出流中须要这个换行符,不然你就在输出流中引入了一个额外的换行符,一般状况下,它不是你想要的结果。为了更好的说明这一点,能够看下面的示例:
divert(0) define(say_hello_world, Hello World!) say_hello_world
这个示例就是在上述代码中又增长了一行文本,它表示调用了上一行所定义的 say_hello_world
宏。假设示例代码保存在 hello.m4 文件中,而后执行如下命令:
$ m4 hello.m4
此时,hello.m4 就是 m4 的输入流。m4 从输入流中读取文本,处理文本,而后将处理结果发送到输出流。此时,输出流是系统的标准输出设备(stdout),也就是当前的终端屏幕。
执行上述命令后,咱们指望的结果一般是:
$ m4 hello.m4 say_hello_world
然而,m4 输出的倒是:
$ m4 hello.m4 Hello World!
Hello World!
前面出现了两处空行,一处是 divert
语句后面的换行符致使的,另处是 say_hello_world
宏定义语句后面的换行符致使的。
若是将 say_hello_world
宏定义语句放在暗黑缓存中,能够解决一半问题。例如:
divert(-1) define(say_hello_world, Hello World!) divert(0) say_hello_world
再次执行 m4 命令,可得:
$ m4 hello.m4 Hello World!
如今 Hello World!
前面只有 1 处空行了,它是 divert(0)
后面的换行符致使的。要消除它,有两种方法。第一种方法就是 divert(0)
后面不换行,例如:
divert(-1) define(say_hello_world, Hello World!) divert(0)say_hello_world
另外一种方法是使用 m4 内建的 dnl
宏,它会从将它被调用的位置到后面的第一个换行符之间的文本(包括换行符自己)一并删除,例如:
divert(-1) define(say_hello_world, Hello World!) divert(0)dnl say_hello_world
这两种方法输出的结果是相同的。为了让文本具备更好的可读性,一般用 dnl
来作这样的事。
(1) 对于如下 m4 代码
divert(-1) define(say, ) define(hello, HELLO) define(world, WORLD!) divert(0)dnl say hello world
推测一下 m4 的处理结果,而后执行 m4 命令检验所作的推测是否正确。
(2) 对于如下 m4 代码
divert(2) define(say, ) define(hello, HELLO) divert(1) define(world, WORLD!) divert(0)dnl say hello world
推测一下 m4 的处理结果,而后执行 m4 命令检验所作的推测是否正确。