写一个足够健壮的C++宏编译器

本笔记原文写于19年,22年预计进行翻新
自己实现简易的扫描器(去注释,分行,分词)
分析语法,为每行语句建立一个简单的语法树(不复杂的情况时,都不必是个树就能知其义...)
由遍历语法树得知该语句的语义,匹配对应语义到相应逻辑执行(比如define、if、聚合、字符转义、类型转换等)
最后得到执行的结果保存

题目描述
用Python2.7编写一个宏处理程序,能够解析#ifdef/#ifndef/#else/#endif/#define/#undef这些宏指令,将其转为Python内置类型并存储,并可导出为只含有当前宏定义的cpp源文件

假定有cpp源码文件仅有如下内容
  1. 注释: // 及 /**/
  2. 空白字符,制表符,换行等
  3. 只包含 #ifdef/#ifndef/#else/#endif/#define/#undef 这几个宏指令的应用
  4. #define 定义仅有如下定义情况:#define identifier token-stringopt
  5. 其中 token-stringopt 只有兼容如下几种C/C++指定基本类型常量表示内容:整型,浮点,布尔(true, false),字符(忽略宽字符),字符串,及各上述基本类型组成的聚合
  6. 聚合:结构体 或者 数组初始化,均可用聚合初始化形式{} 表示,聚合类型可以多维(嵌套)
  7. 除如上指定内容外,不再有其他内容。不考虑字符映射及三元组序列,不考虑行拼接

技术指标
  1. 能读取.cpp文件中的宏定义,并接收可变化的预定义宏串,并同时根据两者,解析出当前所有的可用的宏定义。
  2. 可用宏定义可转为Python字典模式输出。 其中宏名转为字符串作为字典key, 若有与宏对应的常量定义转为对应python数据类型后作为字典的value。 类型对应关系见后表。若无任何常量则value为None
  3. 可用宏定义可输出成为只含有宏定义的源文件。
  4. 遵循CPP宏及C常量类型的定义标准,确保相同常量变换后数值上保持一致。 (由于python转换过程中可能会损失掉C的具体类型信息,比如char可能最终变为 int,具体表示方法也会有所变化,比如16进制最终表示为10进制, 故多次转换后能保持最终常量的值相等即可)
  5. 只允许使用Python内置模块(如sys、math)和string模块,不允许使用其他标准模块及任何第三方开发库(包括但不限于re),不能使用 evel/excec 懒人解析。
  6. 全部字符串都是ANSI编码

使用说明
Python代码文件名为PyMacroParser.py,包含一个class,类名为PyMacroParser,类中包含以下方法:
  • load(f) 从指定文件中读取CPP宏定义,存为python内部数据,以备进一步解析使用。 f为文件路径,文件操作失败抛出异常;无返回值。若在初步解析中遇到宏定义格式错误 或 常量类型数据定义错误应该抛出异常。
  • preDefine(s) 输入一堆预定义宏名串,宏名与宏名之间以 ; 分割。 比如串"mcname1;mcname2"相当于把#define mcname1 #define mcname2加在了CPP宏数据的最前面。而空串 "" 表示没有任何预定义宏。显然,预定义宏会影响对CPP文件数据内的可用宏解析。
  • preDefine函数可被反复调用,每次调用自动清理掉之前的预定义宏序列。 preDefine与load的CPP宏定义数据,一起决定最终可用的宏。
  • dumpDict() 返回一个dict, 结合类中存储的CPP宏定义与预定义的宏序列,解析输出所有的可用宏到一个字典,其中宏名转为字符串后作为字典的key, 若有与宏名对应的常量转为python数据对象,无常量则存为None, 注意不要返回类中内置的对象的引用。 解析过程若遇到宏定义格式错误 或 常量类型数据定义错误应该抛出异常;
  • dump( f) 结合类中的CPP宏定义数据与预定义宏序列,解析输出所有可用宏存储到新的CPP源文件,f为CPP文件路径,文件若存在则覆盖,文件操作失败抛出异常。 若遇到宏定义格式错误 或 常量类型数据定义错误应该抛出异常。 注意,转换后的常量数据表示应与Python对应类型兼容, 所以常量类型的长度存储信息可能丢失(例如 short 转为 int; float 转为 double 等), 允许特别表示方法信息丢失(例如原本16进制 统一变成10进制表示等)。 导出宏的顺序不做特别要求。

技术实现
在实现之前,我做了需求分析-可行性研究,第一遍先是简单的验证设计思路:先删除注释,再处理分支,最后实现define等具体操作。
但是第一次写,由于没有经验,对一些实现不太自信,用自己的方法实现后又一直不能通过测试,就对自己产生了怀疑。所以debug的过程中较为痛苦,但在痛苦下,学会更多知识,下次再写相似的就很轻松了。
最后成功通过测试了,还是非常开心的,也对自己更加自信了。
以下是技术上的主要难点:

编码问题
源文件是gb2312,所以为了处理中文,需要#coding=gb2312且sys.setdefaultencoding('gb2312')【但是貌似用UTF-8也能过】否则很容易报错'ascii' codec can't encode characters,就处理不了非ascii编码【需要reload(sys)才能使用setdefaultencoding api,避免出现module has no attribute error】
然后因为是python2,编码问题上产生了很多坑,默认读进来是encode的,比较时最好用同样编码的字符(如gb2312),unicode不便于直接比较

注释问题
在""和''中的//, /* */是无效的,不应删除。
我开始是一个字符一个字符复制过去,while 循环判断字符,写入到BytesIO中。后来直接用list存字符了
这里提醒我了:每个符号都要仔细考虑什么情况下是有效的,什么情况下是无效的。

总结一下实现时可能的情况:
遍历字符,判断该字符
在引号内:
开始匹配结束的引号,结束前全部直接写入
在引号外:
  1. 标记过//注释,一直读到\n为止都不写入
  2. 标记过/注释,剩下的都不写入,直到匹配到/为止
其他:
根据不同符号加不同标记
根据情况写入,指针前进

分支处理
#ifdef/#ifndef/#else/#endif 等分支的处理还是比较简单的,但是要避免自己编写了重复的代码。

有固定的格式,将匹配的key在字典里find,然后执行分支即可
若存在,则继续执行(可能有嵌套),执行后跳转到#endif(即略过#else)。若不存在,则跳转#else(如果没有就直接跳转#endif),直到#endif
跳转的实现:用stack,在局部域中遇到一个if即入栈,需要一个endif才能出栈,若栈空,readline得到的#else才是该if对应的else
或者简单一点,一个counter,遇到if+1,endif-1,counter=0时才能跳转。很成功。
ifdef True: 直接执行,else跳转
False:先跳转,后执行

原子类型转换
这个对照MSDN上关于各类型的说明进行实现即可,
比如int要去除ul、i64之类的后缀再做转换,float要去除f/l后缀,再考虑16进制、8进制、正负号之类。

聚合转换
聚合是可以嵌套的,因此实现时用到了划分+归并算法:
for循环,以逗号为单位划分,设置一个大括号的counter,遇{+1,遇}-1,counter为1才会划分。记录startIndex和endIndex,用截断获得子字符串,每个子字符串都是一个list或atom type,将子字符串递归。然后就可以对每个递归的子字符串多路归并,递归函数返回该子字符串对应的类型,加入到父级list中。最后返回得到的就是一个完整的聚合。

转义处理
盲点:\other和\xhh,\ooo。这些都是转义,后者是先转数值再对应ascii码。
引号判定:不能是\'和\"这样的转义引号,为了确认是\"而不是\\",要统计前面的\是否是偶数个,是偶数个则不是转义引号。如果是"在' '内,则也不会产生引号的效果。
符号判定:如果符号在 ' " 这样的引号作用域内,则无效。

异常处理
raise。文件操作失败抛出异常
宏定义格式错误 或 常量类型数据定义错误 抛出异常
格式错误,导致下标越界
抛出异常后不需要向下继续执行

调试测试
有很多很有趣的测试用例,我同样放在项目文件夹下了。比如:

模块说明
最后成功实现并通过了测试,代码托管在GitHub:
?我没找到,好像是遗失了

以下是模块说明文档:

构造函数

def init(self)
构造函数,设置默认编码为gb2312, 定义类的实例变量的默认值。

对外的接口方法

def load(self, f)
打开文件,进行删除注释,分行处理等操作,保留lineList 。
def preDefine(self, s)
输入一堆预定义宏名串,宏名与宏名之间以";" 分割,而空串"" 表示没有任何预定义宏。preDefine函数可被反复调用,每次调用自动清理掉之前的预定义宏序列。preDefine会去除宏名前后的空格,以防非法宏名。
def dumpDict(self)
返回一个dict, 结合类中存储的CPP宏定义数据与预定义的宏序列,解析输出所有的可用宏到一个字典。
def dump(self, f)
结合类中的CPP宏定义数据与预定义宏序列,解析输出所有可用宏存储到新的CPP源文件。

私有方法

def _deleteComments(self, sourceStr)
删除注释。分为//和/**/两种注释。
def _charListToLineList(self, charList)
去除不必要的空白行和每行的首尾空格,将原本由字符构成的list转为以\n分行的lineList。
def _isValidQueto(self, pos, sourceStr)
判断引号是否有效。所有引号都需要确认是否是有效的。因为符号可能在''中,可能是转义引号,前面有奇数个\就是转义引号了。
def _processLine(self)
对删去注释后的代码进行逐行处理。用迭代器实现, 去除第一个#和末尾的; ,最后split空白符,将宏,宏名,值分开。处理包括if跳转,undef等宏实现。
def _processDefine(self, splitLine)
处理define,调用processDefineTuple,得到返回值并将key-value存入字典,如果没有value就是None。
def _processDefineTuple(self, tupleStr)
处理define的value,移除每个字符串的首尾空格,如果是tuple返回tuple,不是的话交给_processDefineOther处理并返回对应的类型。 聚合的处理使用了分治法,递归实现,需要判断'{' '} ',' 三个符号。
def _processDefineOther(self, defValue)
若非聚合,根据value对应的类型做类型转换。
def _parseInterger(self, value)
将value转为整型。
def _parseFloat(self, value)
将value转为浮点型。
def _parseEscapeString(self, defValue)
对字符串中的转义字符做转换。
def _dumpToCpp(self, value)
将Python内类型转回C/CPP的表示。对于tuple,采用递归转换。
def _escapeStringDump(self, value)
将python字符串回转到c++时调用。需要注意的是,python和c++符号有所不同,需要把"前的\加回去

程序执行流程
init 构造函数
load 加载文件 -> _deleteComments 删除注释 -> _charListToLineList 字符列表转行列表
[可选] preDefine 预定义宏
dumpDict 导出字典 -> processLine 逐行处理 -> _processDefine 处理宏定义 -> _processDefineTuple 处理聚合定义 -> _processDefineOther 处理其他类型定义 -> _parseInterger -> _parseFloat -> _parseEscapeString 最后将宏名和值加入字典
dump 导出源文件 -> dumpDict 先导出字典 -> _dumpToCpp 转cpp类型 -> _dumpEscapeString 字符串转回cpp。最后写入文件











comments powered by Disqus