Clang 之旅系列文章:
Clang 之旅--使用 Xcode 开发 Clang 插件
Clang 之旅--[翻译]添加自定义的 attribute
Clang 之旅--实现一个自定义检查规范的 Clang 插件
前言
这是 Clang 之旅系列的第二篇,自己想要完成的需求是:在编译阶段检查某个方法的参数与返回值的类型相同,如果类型不一致的话能抛出编译错误的提示。需要接触到 Clang 中关于 attribute 处理的代码,所以这篇先来翻译官方文档中添加自定义的 attribute 这一节,不得不说,虽然 Clang 的文档可以说是很标杆了,但是总有一种看了后面忘了前面的感觉,可能是 Clang 比较庞大,涉及专有词汇比较多的原因,所以我会偏向意译多一点,试图用更加易懂的表达组织语言,也是加深自己的记忆吧。
怎样添加 attribute
attribute 是一种可以附加到程序结构中的数据形式,允许开发人员传递信息给编译器来实现各种需求。例如,attribute 可以用来改变在程序构造时生成的代码,或者用来提供额外的信息给静态分析。本文档讲解如何添加一个自定义的 attribute 到 Clang 中。现有 attribute 列表的文档可以在这里找到。
attribute 基础知识
Clang 中的 attribute 涉及到三个阶段:解析 attribute 、从已解析的 attribute 转换成语法树上的 attribute、对 attribute 进行处理。
attribute 的解析可以采用多种语法形式,例如 GNU、C++ 11 和 Microsoft 形式,还由 attribute 提供的其他信息来确定。最终,解析好的 attribute 用一个 AttributeList
对象来表示。这些解析好的 attribute 会链成一个 attribute 链,加到声明或者定义上。attribtue 的解析是由 Clang 自动完成的,除了那些关键字 attribute。关键字的解析和 AttributeList
对象的生成必须由我们手动完成。
最后,Sema::ProcessDeclAttributeList()
带着 Decl
类型和 AttributeList
类型的参数被调用,此时解析好的 attribute 就会被转化成语法树上的 attribute。这个处理依赖于 attribute 的属性定义和语义要求。最后的结果就是语法树上的 attribute 对象可以从 Decl
对象获取到,也就是通过调用 Decl::getAttr
来获取。
语法树上的 attribute 的结构同样也受到 Attr.td 文件中的定义所限制。这个定义会自动生成 attribute 的实现所用到的功能,包括生成 clang::Attr
的子类、解析器所用到的信息和某些 attribute 自动进行的语义分析等等。
include/clang/Basic/Attr.td
添加新的 attribute 到 Clang 的第一个步骤就是把其定义添加到 include/clang/Basic/Attr.td。这个定义必须从 Attr
或者其子类继承。大多数 attribute 会直接从 InheritableAttr
继承,InheritableAttr
指定了这个 attribute 可以通过它所关联的 Decl
稍后进行重声明。如果这个 attribute 是作用于类型而不是声明,那么这种 attribute 应该从 TypeAttr
派生,并且通常不会被赋予 AST 表示(注意本文档并不讲解生成类型所用的 attribute)。一个继承于 IgnoredAttr
的 attribute 会被解析,但是会在被使用的时候产生一个 "被忽略的属性" 的警告,这种处理方法在某个属性支持别的前端而不支持 Clang 的情况下是很有用的。
这个定义能指定 attribute 的一些关键部分,比如 attribute 的名字、attribute 支持的拼写、attribute 的参数等等。Attr
类型中的大多数成员变量都不需要派生定义,缺省的就足够了。但是,每个 attribute 都需要至少指定 拼写列表、subject 列表和文档列表。
拼写
所有 attribute 都需要指定一个拼写列表,表示拼写 attribute 的方式。比如某个 attribute 可能会包含关键字拼写, C++11 拼写和 GNU 拼写。空的拼写列表也是允许的并且可能对隐式创建的 attribute 有用。以下是支持的拼写的表格:
拼写 | 描述 |
---|---|
GNU | 用 GNU 风格 __attribute__((attr)) 语法和位置拼写 |
CXX11 | 用 C++ 风格 [[attr]] 语法拼写。如果该 attribute 是由 Clang 所使用的,那么应该设置命名空间为 "clang" |
Declspec | 用 Microsoft 风格 __declspec(attr) 语法拼写 |
Keyword | 这个 attribute 用关键字的方式拼写,并且需要自定义解析 |
GCC | 指定两种拼写:首先是 GNU 风格拼写;然后是 C++ 风格拼写,命名空间为 gnu 。只能为支持 GCC 的 attribute 指定这个拼写。 |
Pragma | attribute 用 #pragma 的形式拼写,并且需要在预处理器中执行自定义的处理。如果该 attribute 是由 Clang 所使用的,那么应该设置命名空间为 "clang" 。需要注意这个拼写并不能被用于声明语句中。 |
Subjects
每个 attribute 都有一个或者多个 subject。如果 attribute 被使用到了一个不在 subject 列表上的 subject,就会自动显示诊断信息。 这个信息是警告还是错误是由 attribute 中的 SubjectList
决定的,默认的是警告。显示给用户的诊断信息将根据 subject 列表自动确定,但是也可以在 SubjectList
中指定自定义诊断参数。不符合 subject 列表导致的诊断信息要么是 diag::warn_attribute_wrong_decl_type
,要么是 diag::err_attribute_wrong_decl_type
。具体参数的枚举值可以从 include/clang/Sema/AttributeList.h 找到。如果先前未使用的 Decl
节点被添加到 SubjectList
中,则可能需要更新用于自动确定 utils/TableGen/ClangAttrEmitter.cpp 中的诊断参数的逻辑。
所有在 SubjectList 中的 subject 要么是在 DeclNodes.td
中定义的 Decl 节点,要么就是在 StmtNodes.td
中定义的 statement 节点。不过,可以生成 SubsetSubject
对象来创建更加复杂的 subject。每个这样的对象都有一个它所属的基本对象(必须是一个 Decl 或 Stmt 节点,而不是一个 SubsetSubject 节点),还有一些自定义代码在确定某个 attribute 是否属于该对象时被调用。例如,一个 NonBitField
SubsetSubject 关联到 FieldDecl
类,同时会测试给定的 FieldDecl 是否是一个位字段。当在 SubjectList 中指定了一个 SubsetSubject 时必须同时提供一个自定义的诊断信息参数。
attribute 的 subject 列表会在 HasCustomParsing
设为 1
的情况下自动进行诊断检查。
文档
所有的 attribute 都必须具有某种形式的文档。文档是通过每天运行的服务器端进程在公共服务器上生成的。通常来说,attribute 的文档是在 include/clang/Basic/AttrDocs.td 中单独定义的,以文档属性命名。
如果 attribute 不是通用的,或者是隐式创建的没有对应拼写的 attribuet,则可以将文档列表变量设置为 Undocumented
。否则,该 attribute 应将其文档添加到 AttrDocs.td。
文档属性是从 Documentation
tablegen 类型继承而来的,所有的派生类型都必须创建一个文档类别和设置文档本身内容。此外,它还可以为 attribute 指定一个自定义的标题,否则会选择默认的标题。
现在有四种预先定义好的文档类别:DocCatFunction
对应函数的 attribute,DocCatVariable
对应到变量的 attribute,DocCatType
对应类型的 attribute,DocCatStmt
对应声明的 attribute。自定义文档类别应该用于具有类似功能的 attribute 组。自定义类别非常适合用来为组中的 attribute 提供概述信息。
文档内容(包括 attribute 的内容或者类别的内容)是用 reStructuredText(RST)格式写的。
在编写该 attribute 的文档之后,应该对其在本地对其进行测试,以确保在服务器上生成文档不会有问题。本地测试需要重新构建 clang-tblgen。要生成 attribute 文档,请执行以下命令:
clang-tblgen -gen-attr-docs -I /path/to/clang/include /path/to/clang/include/clang/Basic/Attr.td -o /path/to/clang/docs/AttributeReference.rst
在本地进行测试时,不要对 AttributeReference.rst
提交更改。该文件是由服务器自动生成的,并且对该文件所做的任何更改都将被覆盖。
参数
attribute 可以选择指定可以传递给 attribute 的参数列表。attribute 的参数指定 attribute 的解析形式和语义形式。例如,如果 Args
是 [StringArgument<"Arg1">, IntArgument<"Arg2">]
,那么 __attribute__((myattribute("Hello", 3)))
就是一个合法的使用方式;这个 attribute 在解析时要求有两个参数:一个 string 类型一个 integer 类型。
每个参数都有个名字和一个用来指定这个参数是否为可选的标志。参数关联的 C++ 类型由参数定义类型确定。如果现有参数类型不足,则可以创建新类型,但需要修改 utils/TableGen/ClangAttrEmitter.cpp 才能正确支持该新类型。
其他属性
Attr
的定义还具有其他变量来控制 attribute 的行为。其中有很多是用于特殊用途的,超出了本文档的范围,但有一些还是值得提上一嘴的。
如果 attribute 的解析形式更加复杂或者和语义形式不同,则可以将 HasCustomParsing
变量设置为 1
,并且可以针对特殊情况修改 Parser::ParseGNUAttributeArgs() 中的解析代码。请注意,这仅适用于具有 GNU 拼写的 attribute;__declspec 拼写的 attribute 现在是忽略这个标志的,并由 Parser::ParseMicrosoftDeclSpec
负责解析。
请注意,把 HasCustomParsing
设置为 1
将不再使用通用的 attribute 处理逻辑,需要额外的处理来确保该 attribute 能使用。
如果该 attribute 不通过模板声明实例化,则将 Clone
成员变量设置为 0。默认情况下,所有的 attribute 都将通过模板进行实例化。
不需要 AST 节点的 attribute 应该将 ASTNode
变量设置为 0 以避免污染 AST。请注意,从 TypeAttr
或 IgnoredAttr
继承的类都不会自动生成 AST 节点。所有其他属性默认会生成一个 AST 节点。该 AST 节点是 attribute 的语义表示。
LangOpts
变量指定了 attribute 所需的语言选项列表。例如,所有的 CUDA-specific 的 attribuet 都将 LangOpts
字段指定为 [CUDA]
,并且当 CUDA 语言选项未启用时,会发出“attribute ignored”的警告诊断。由于语言选项不是自动生成的节点,因此必须手动创建新的语言选项,并应指定 LangOptions
类所使用的拼写。
可以基于 attribute 的拼写列表为该 attribute 生成自定义的存取器。例如,如果某个 attribute 有两种不同的拼写:'foo' 和 'bar',则可以创建访问器:[Accessor<"isFoo", [GNU<"Foo">]>, Accessor<"isBar",[GNU<"Bar">]>]
。这些存取器将在该 attribute 的语义形式上生成,不接受任何参数并返回一个布尔值。
不需要自定义语义分析的 attribute 应该将 SemaHandler
变量设为 0
。请注意,任何从 IgnoredAttr
继承的 attribute 都不会自动进行语义处理。所有其他 attribute 都使用默认的语义处理。没有语义处理的 attribute 都不会有解析好的 attribute Kind
枚举器。
指定 Target 的 attribute 可能会与不同 Target 的 attribute 共用一个拼写。例如,ARM 和 msp430 Target 都有一个拼写为 GNU<"interrupt">
的 attribute,但各自有不同的解析方式和语义要求。为了支持这个特性,继承自 TargetSpecificAttribute
的 attribute 可以指定 ParseKind
变量。这个变量在共用拼写的所有参数之间应该是相同的,并且对应于解析 attribute 的 Kind
的枚举器。这允许 attribute 共用一种解析类型,但具有不同的语义属性。例如,AttributeList::AT_Interrupt
是共用的解析类型,但 ARMInterruptAttr 和 MSP430InterruptAttr 是各自的语义属性。
默认情况下,当声明为 merging attribute 时,该 attributes 不会被复制。但是,如果在此合并阶段中可以复制某个 attribute,那么将 DuplicatesAllowedWhileMerging
变量设置为 1
,该 attribute 就会被合并。
默认情况下,attribute 的参数在上下文中被解析。如果应该在上下文中解析 attribute 的参数(类似于解析 sizeof
表达式的参数的方式),请将 ParseArgumentsAsUnevaluated
设置为 1
。
样板代码
声明 attribute 的所有的语义处理都在文件 lib/Sema/SemaDeclAttr.cpp 中,并且通常都从 ProcessDeclAttribute()
函数开始。如果这个 attribute 是一个“简单的” attribute,也就是说这个 attribute 除了自动生成的内容之外不需要自定义的语义处理,那么就添加 handleSimpleAttribute
函数到 switch 语句中。否则,编写一个新的 handleYourAttr()
函数,并将其添加到 switch 语句中。不要直接在 case
语句中实现处理逻辑。
除非 attribute 的定义中另有规定,否则将自动处理解析 attribute 的常见语义检查,包括诊断不属于给定 Decl
的解析的 attribute、确保传递正确的最小数量的参数等等。
如果 attribute 要加上额外的警告,那么在 include/clang/Basic/DiagnosticGroups.td 文件中定义一个 DiagGroup
。如果只有一个诊断信息的话,直接在 DiagnosticSemaKinds.td 文件中使用 InGroup
也是可以的。
所有为你自定义的 attribute 所生成的诊断信息,包括自动生成的(比如 subject 和参数个数),都应该有一个对应的测试用例。
语义处理
大多数 attribute 被实现为对编译器有一定的影响。例如,修改生成代码的方式,或为分析过程添加额外的语义检查等,将 attribute 的定义和转换添加到该 attribute 的语义表示中,剩下的就是实现 attribute 的自定义逻辑。
可以使用 hasAttr
方法来查询 clang::Decl
对象中是否有 attribute。可以使用 getAttr
来获取一个指向 attribute 的指针。
PS:我建了一个 Clang & LLVM 微信交流群,可以交流 Clang 相关问题、iOS 相关问题、发招聘信息,拒绝广告、拒绝刷屏。想要加入的可以加我微信拉你进群~