CParser:用纯Lua编写的具有有用扩展的C预处理器

2020-08-19 02:46:58

这个纯Lua模块实现了(1)一个符合标准的C预处理器,它带有几个有用的扩展,以及(2)一个解析器,它提供对C头文件或C程序文件中所有全局声明和定义的Lua友好描述。

驱动程序LCPP调用预处理器并输出预处理代码。虽然它可以用来替代普通预处理器,但作为额外的预处理步骤更有用(参见Option-ZPass,该选项在默认情况下处于打开状态)。模块cparser提供的函数cparser.cpp和cparser.cppTokenIterator会提供相同的功能。

驱动程序lcdecl分析C头文件和C程序文件,并输出声明和定义的简短描述。该程序对于理解由cparser函数cparser.secreationIterator生成的演示文稿非常有用。

此代码在此源代码树的根目录下的许可证文件中找到的MIT许可证下进行许可。

预处理文件inputfile.c,并将预处理代码写入文件outputfile.c或标准输出。

-werror导致所有警告都被视为错误。请注意,出现错误后无法恢复解析。解析器只抛出Lua错误。

-D sym[=val]将预处理程序符号sym定义为Value Value。val的默认值为1。注意,可以使用语法-Dsym(Args)=val定义类似函数的符号。

-i dir将目录dir添加到包含文件的搜索路径。请注意,没有默认搜索路径。当包含文件不是NOTFound时,INCLUDE指令将被忽略并发出警告(但也请参阅选项-ZPass)。因此,除非使用option-i指定搜索路径,否则所有include指令都将被忽略。

-i-标记系统包含路径的开始。如果包含文件带有尖括号(如#include<;stdio.h>;中所示),则只搜索-i-后面的-i选项指定的目录。因此,除非使用选项-I-后跟一个或多个选项-I,否则所有这些包括指令都将被忽略。

-dm不生成预处理文件,而是转储在分析结束时定义的所有宏。

-Zcppdef使用命令cpp-dm<;dev/nulland copy其预定义符号运行本地预处理器。当使用LCPP作为标准预处理器的完全替代时,这很有用。

-ZPass此选项默认启用(使用-Znopass禁用),表示LCPP的输出将由C预处理器和编译器处理。此选项触发以下行为:

当在提供的搜索路径中找不到包含的文件时,会将预处理器指令#include复制到输出文件中。

以双##为前缀的预处理器指令将使用单个#前缀逐字复制到输出文件中。此功能对于依赖于未解析的#include指令定义的符号的#if指令非常有用。

-std=(c|gnu)(89|99|11)此选项选择C方言。在预处理器的上下文中,这会影响LCPP预定义的符号,并可能启用可变宏定义语法的GCC扩展。

符号__STDC__和__STDC_VERSION__由Option-Zcppdef定义,或者采用适合目标C方言的值。

符号__GNUC__和__GNUC_MINOR__由Option-Zcppdef定义,或者如果目标方言以字符串gnu开头,则定义为值4和2。

这可以使用-D或-U选项进一步调整。默认方言是gnu99。

LCPP预处理器实现了几个有用的非标准特性,主要特性是多行宏。这里主要介绍其他功能,因为它们使多行宏更有用。

C标准规定#ifdirections后面的表达式是整型的常量表达式。但是,该处理器还处理字符串。字符串上唯一有效的操作是相等和排序比较。这对于为多行微处理器的某些参数值设置特殊情况非常有用,如下所示。

预处理器指令#def宏和#end宏可以用来定义一个类似函数的宏,它的主体跨越几行。#def宏指令包含宏名称和强制参数列表。宏的主体由以下所有行组成,直到匹配的#end宏。这提供了几个好处:

保留宏展开的行号。这确保编译器生成具有有意义的行枚举数的错误消息。

多行宏可以包含预处理器指令。条件指令在此上下文中非常有用。但是请注意,嵌套在多行宏中的预处理器定义(带有#DEFINE、#def宏或#UNDEF)仅在宏中有效。

标准字符串#和标记连接##运算符可以在多行宏的主体中自由使用。请注意,这些运算符仅适用于多行宏的参数,而不适用于普通的预处理器定义。这与这些运算符在普通预处理器宏中的标准行为一致。

#def宏DEFINE_VDOT(TNAME,TYPE)type TNAME##Vector_dot(type*a,type*b,int n){/*try cblas*/#if#type==";Float";return cblas_Sdot(n,a,1,b,1);#Elif#type==";Double";return cblas_DDOT(n,a,1,b,1);#Elf#type==";#Elif#type==";double";return cblas_DDOT(n,a,1,b,1);#ELSE int i;I++)s+=a[i]*b[i];返回s;#endif}#end宏DEFINE_VDOT(FLOAT,FLOAT);DEFINE_VDOT(DOUBLE,DOUBLE);DEFINE_VDOT(Int,INT);

详细信息-在将宏参数值替换到宏文本之前,宏参数值通常进行宏展开值。但是,当替换发生在串化或令牌连接运算符的上下文中时,不会发生这种宏扩展。这一切都是符合标准的。不同的是,当参数出现在嵌套的预处理器指令或多行宏中时,不会发生这种宏展开。

更多细节--仅当下一个非空格标记是宏参数时,字符串化操作符才有效。这提供了一种很好的方法来区分嵌套指令和出现在行首的字符串操作符。

更多细节--标准要求宏展开生成的令牌可以与下面的令牌组合,以组成新的宏调用。多行宏不允许这样做。如果多行宏的展开生成不完整的宏参数列表,则会发出错误信号。

C标准规定,仅用一个参数调用此宏是错误的。在第二个参数为空的情况下调用此宏--宏(msg,)--会在扩展中留下一个讨厌的逗号--printf(msg,)--并导致编译器语法错误。

该预处理器接受使用单个参数调用这样的宏。因此,参数__VA_ARGS__的值是所谓的负逗号,这意味着当此参数出现在宏定义中的逗号和大括号之间时,前面的逗号将被删除。

当宏的新调用出现在amacro的展开中时,该标准指定预处理器必须重新扫描Expansion,但不应递归展开宏。虽然这一限制既明智又有用,但很少有人愿意使用递归宏。作为实验,当用户使用#defrecsivemacro而不是#def宏定义多行宏时,此递归预防功能将被关闭。请注意,这可能会阻止预处理器终止,除非宏最终采用不递归调用宏的条件分支。

预处理和解析文件inputfile.c。解析器的输出是一系列表示代码中遇到的每个C定义或声明的Lua数据结构。程序ldecl以两种形式打印它们。第一种形式直接表示组成数据结构的LUA表。第二种形式重新构造了一段表示定义或利益声明的C代码。

这个程序对于使用由cparser模块提供的Lua函数的人来说非常有用,因为它提供了一种快速检查结果数据结构的方法。

程序lcdecl接受为程序lcpp记录的所有预处理选项。它还接受附加的选项-T typeName,并增加了选项-ZPass和-std=方言的含义。

-T typeName与LCPP类似,程序lcdecl只读取沿着-i选项指定的路径找到的包含文件。通常不希望读取所有包含文件,因为它们通常包含不直接有用的声明。这也意味着C解析器可能不知道在忽略的包含文件中找到的类型定义。尽管C语法足够明确,允许解析器猜测标识符是类型名而不是变量名,但这可能会导致混乱的错误消息。然后可以使用选项-T TypeName来告知解析器符号TypeName代表的是类型,而不是常数、变量或函数。

-Zpass与lcpp不同,程序lcdecl处理输入文件时默认关闭-ZPass选项。打开它只会消除潜在有用的警告消息。

-Ztag此选项使lcdecl将所有结构、联合和枚举视为标记类型,可能使用__ANON_XXXXX形式的合成标记。假设在解析的程序中的任何地方都不使用这样的名称。这对于某些代码转换应用程序很有用。

-std=(c|gnu)(89|99|11)方言选择选项还控制解析器是否识别由更高版本的C标准引入的关键字(例如,限制、_布尔、_复杂、_原子、_Pragma、内联)或由GCC编译器(例如,asm)引入的关键字。其中许多关键字都有双下划线分隔的变体,该变体在所有情况下都可识别(例如,__restrict__)。

Const int size=(3+2)*2;浮点arr[size];tyfinf struct symtable_s{const char*name;symval value;}symtable_t;void printSymbols(symtable_t*p,int n){do_thing(p,n);}。

+-|定义{WHERE=";test.c:2";,intval=10,type=qalified{t=Type{n=";int";},const=true},name=";size";,init={..}}|const int size=10+。,type=数组{t=类型{n=";浮点";},大小=10},名称=";arr";}|浮点数组[10]+-|类型定义{sclass=";[类型标签]";,其中=";test.c:4";,type=Struct{Pair{Pointer{t=Qualified{t=Type{n=";char";},常量=TRUE}}。},对{类型{n=";Symval";},";值";},n=";symtable_s";},name=";struct symtable_s";}|[tyetag]struct symtable_s{const char*name;Symval value;}+。,type=Type{_def={..},n=";struct symtable_s";},name=";symtable_t";}|tyfinf struct symtable_s symtable_t+-|定义{where=";test.c:5";,type=function{Pair{POINTING{t=Type{_dee。},对{类型{n=";int";},";n";},t=类型{n=";void";},name=";printSymbols";,init={..}}|void printSymbols(symtable_t*p,int n){..}+

调用此函数对文件文件名进行预处理,并将预处理后的代码写入指定的输出。可选参数outputfile可以是文件名或Lua文件描述符。当此参数为nil时,预处理代码将写入标准输出。可选参数选项是选项字符串数组。支持程序lcpp记录的所有选项。

参数选项是一个选项数组。支持为程序lcpp记录的所有选项。参数行是返回输入行的迭代器。Lua提供了许多这样的迭代器,包括io.lines(Filename)用于返回名为filename的文件的行,filedesc:Lines()用于从文件描述符filedesc返回行。您还可以使用string.gMatch(ome string,';[^\n]+';)从string ome string返回行。

令牌迭代器函数的每次连续调用通过返回两个字符串来描述预处理代码的令牌。第一个字符串表示令牌文本。第二个字符串遵循格式";filename:lineno";,并指示在哪一行找到该标记。文件名可以是参数前缀,也可以是包含文件的实际名称。生成所有令牌后,令牌迭代器函数返回nil。

宏定义表的每个命名条目都包含相应预处理器宏的定义。函数cparser.acroToString可用于根据此信息重新构建宏定义。

Ti,宏=cparser。CppTokenIterator(nil,io.line(';test/testacro.c';),';testacro.c';)对于令牌,ti中的位置对于符号为print(Token,location)end,_in对(宏)表示本地s=cparser。宏ToString(宏,符号)如果%s,则打印结束。

此函数返回一个字符串,表示在宏定义表宏中找到的预处理器宏名称的定义。如果没有定义这样的宏,则返回nil。请注意,宏定义表包含的命名条目不是宏定义,而是实现魔术符号(如__FILE__或__LINE__)的函数。

调用此函数对文件文件名进行预处理和解析,将跟踪写入指定的文件。可选参数outputfilecc可以是文件名或Lua文件描述符。当此参数为nil时,预处理代码将写入标准输出。可选参数选项是选项字符串数组。支持使用程序lcdecl记录的所有选项。

参数选项是一个选项数组。支持为程序lcdecl记录的所有选项。参数行是一个返回输入行的迭代器。Lua提供了许多这样的迭代器,包括io.lines(Filename)用于返回名为filename的文件的行,filedesc:Lines()用于从文件描述符filedesc返回行。您还可以使用string.gMatch(ome string,';[^\n]+';)从string ome string返回行。

每次连续调用声明迭代器函数都会返回一个表示声明、定义或某个预处理器事件的Luadata结构。这些数据结构的格式在函数cparser.declToString中描述。

符号表包含由解析文件定义或声明的所有C语言标识符的定义。类型名称由函数cparser.typeToString下记录的Type{}数据结构表示。常量、变量和函数由定义{}或声明{}数据结构表示,这些数据结构与声明迭代器返回的数据结构类似。

宏定义表包含预处理器宏的定义。有关详细信息,请参阅函数宏到字符串的文档。

此函数生成适合在C程序中声明类型为ty的变量nam的字符串。

参数ty是类型数据结构。参数nam应该是代表合法标识符的字符串。但是,为了计算适合标准Lua函数string.format的格式字符串,它的默认值是%s。

模块cparser用树表示每种类型,树的节点是由标记字段标记的Luatables。这些表配备了一种方便的元表方法,该方法首先显示标记,然后使用标准Luaconstruct显示其余字段,从而紧凑地打印它们。

类型{n=name}用于表示命名的类型名称。每个命名类型只有一个实例。名称可以由多个关键字组成,例如int或unsign long int,它们还可以包含定义标识符(例如size_t)或组合名称(例如struct foo或枚举bar)。此构造还可以包含一个field_def,该字段_def指向命名类型的定义(如果这样的定义是已知的)。

限定{t=basetype,...}用于表示basetype的限定变体。名为Const、Volatile或RestrictaReset的字段设置为true以表示适用的类型限定符。当函数参数中出现类型并且基类型为指针时,名为static的字段可能包含保证的数组大小。

指针{t=basetype}用于表示指向基本类型对象的指针。此构造还可以包含字段block=true以指示指针引用代码块(Apple编译器中的ac扩展)或字段ref=true以指示引用类型(受C++启发的C扩展)。

数组{t=basetype,size=s}用于表示类型为basetype的对象的数组。指定数组大小时,可选字段大小包含数组大小。大小通常是整数。但是,在某些情况下,解析器无法评估大小,例如,因为它依赖于C关键字sizeof(X)。在这种情况下,字段大小是包含大小的C表达式的字符串。

Struct{}和Union{}用于表示相应的Ctype。可选字段n包含结构标签。每个条目由位于连续整数索引处的一对{type,name}结构表示。这意味着结构类型ty的第三个条目的类型可以作为ty[3][1]访问,对应的名称是ty[3][2]。在Struct{}表的情况下,这些对可选地包含字段位字段以指示结构条目的abitfield大小。字段位字段通常包含一个小整数,但也可以包含表示C表达式的字符串(就像Array{}构造中的字段大小一样)。

枚举{}用于表示枚举类型。可选字段N可以包含枚举标签名称。枚举常量表示为位于连续整数索引处的对{name,value}。只有当C代码包含显式值时,才会给出第二对元素。它可以是整数或表达式字符串(就像Array{}中的字段大小一样)。

函数{t=rereturn type}用于表示返回rereturn类型的对象的函数。当函数不提供原型时,不带Proto的字段设置为true。否则,参数由位于整数索引中的对{type,name}描述。变量函数的原型以一对{省略=true}结束,以表示...。争论。

限定的{}、Function{}、Struct{}、Union{}和Enum{}表可能还有一个字段attr,其内容表示属性信息,如C11属性[[...]]、msvc-style属性__declspec(...)。或GNU ATTRIBUES__ATTRIBUTE__(...)。这由包含所有属性标记(在奇数索引上)及其位置(在偶数索引上)的数组表示。

将字符串%s解析为抽象类型名或变量声明,并返回类型对象,可能还返回变量名。当字符串不能解释为类型或声明时,或者当声明指定存储类时,此函数返回nil。

此函数生成描述声明迭代器返回的数据结构的字符串。实际上有三种数据结构。所有这些结构都有非常相似的字段,特别是总是包含定义或声明位置的字段。

TypeDef{name=n,sclass=s,type=ty}表示类型定义。当C程序包含tyfinf关键字时,字段sclass包含字符串,字段name包含新类型名称,字段type包含类型描述。当C程序定义标记的结构、联合或枚举类型时,字段sclass包含字符串";[tyetag]";,字段name包含标记类型名称(例如,";struct foo";),字段type包含类型。

.