我着实不喜欢写C的预处理部分实现。
在上个月闲的没事参考chibicc再次尝试写可自举的C编译器时,发现它也实现了C的预处理器,我完全不想接触这个部分。
考虑到安装gcc基本会带一个cpp(The C Preprocessor)程序,它可用于进行预处理,于是我计划将其通过子进程调用的方式集成到我的C编译器中,这样我就可以不关注预处理部分了。
考虑到cpp默认是配合gcc使用的,预先配置了各种GNU C相关的宏。因此,为了能将它集成到我自己实现的编译器,需要去除相关的GNU C特定的功能和宏定义。
首先阅读cpp的手册,了解到-undef参数:
-undef
Do not predefine any system-specific or GCC-specific macros. The standard predefined macros remain defined.
以及-nostdinc:
-nostdinc
–no-standard-includes
Do not search the standard system directories for header files. Only the directories explicitly specified with
-I, -iquote, -isystem, and/or -idirafter options (and the directory of the current file, if appropriate) are
searched.
再从chibicc的预处理模块中抄出来它定义的宏:
1 | |
因为有些代码会使用位于函数参数上的VLA引用已声明参数的功能,比如regex.h中的regmatch_t __pmatch[__nmatch]
1 | |
我没实现这个功能就会导致语法报错。所以临时定义一下__STDC_NO_VLA__来避免这种声明格式。
只要C的语法解析正确,此时是可以编译过一部分应用的。如果想要报错时可以输出正确行号而不是展开后的行号,需要去掉-P参数,再解析cpp生成的linemarker。规则为Preprocessor Output。实践下来只需要解析linenum和filename即可。
但是在测试CPython时,会发现epoll相关的测试用例通过不了。经过检查发现:虽然我实现的attribute支持packed属性,但是只要引入系统头文件后__attribute__关键字就会消失,而epoll的epoll_event结构体实际上会标注__attribute__((packed))参数,导致传递的结构体和预期不符,从而产生无效结果。
经过排查后发现引入系统头文件后__attribute__关键字会消失这一行为主要来源于/usr/include/sys/cdefs.h头文件:
1 | |
而这个文件经过弯弯绕绕后会被stdio.h进行include,导致任何include了stdio.h的程序都会让__attibute__关键字在宏展开后消失。
那么我们应该怎么在不进行大面积修改的情况下解决这个问题呢?
首先我尝试了define __GNUC__,奈何其有太多GNU拓展我无法实现,只能放弃。在重新观察/usr/include/sys/cdefs.h时发现有__TINYC__宏的存在。
这个宏原本是tinycc用来标识自己的,但是在25年10月的[PATCH] cdefs: allow __attribute__ on tcc添加了一个补丁,允许tinycc使用gnulib时不再忽略__attribute__。
于是我们可以通过定义__TINYC__来假装自己是tinycc的方式以避免gnulib的头文件忽略__attribute__进而无法正常调用使用了__attribute__((packed))的epoll API。
但是现在又会产生一些新的问题,比如定义
1 | |
展开的结果为
1 | |
由于之前__attribute__被忽略,所以__attribute__ ((__malloc__))与__attribute__ ((__alloc_align__ (1)))会被忽略,但是现在不能忽略了,得着手看看这几个功能是否可以展开为空,最次再在编译器中兼容。
观察__attribute_malloc__的定义,可以看到
1 | |
其中__glibc_has_attribute(x)会被展开为__has_attribute(x)。
__has_attribute是内置的宏定义,根据gcc的源码libcpp/init.cc#L449我们可以看见所有的内置宏,针对以__has开头的类函数宏,通过定义一个内容为空的宏来让他们失效,如:
1 | |
或许定义成#define __has_attribute(...) 0更好,但是我没有继续深入。
这样就可以尽量控制cpp的输出在可控范围内。
此时频繁修改创建cpp子进程的源码,重新编译感觉很麻烦.所以我将一部分非系统相关的宏移动到一个头文件,再使用-include参数来让每个源码文件开头都导入此目录来达到相同的操作,不同的是此时修改宏定义不需要重新编译编译器了。
目前来说,已经可以稳定借用cpp做宏展开并编译一些程序了。如果对具体代码感兴趣可以看dangjinghao/dcc。