Clang 插件

Clang 插件允许在编译期间运行额外的用户定义操作。本文档将提供一个关于如何编写和运行 Clang 插件的基本演练。

简介

Clang 插件在代码上运行 FrontendActions。有关如何使用 RecursiveASTVisitor 编写 FrontendAction,请参见 FrontendAction 教程。在本教程中,我们将演示如何编写一个简单的 clang 插件。

编写一个 PluginASTAction

与编写普通的 FrontendActions 的主要区别在于,您可以处理插件命令行选项。 PluginASTAction 基类声明了一个 ParseArgs 方法,您必须在插件中实现该方法。

bool ParseArgs(const CompilerInstance &CI,
               const std::vector<std::string>& args) {
  for (unsigned i = 0, e = args.size(); i != e; ++i) {
    if (args[i] == "-some-arg") {
      // Handle the command line argument.
    }
  }
  return true;
}

注册插件

插件是在运行时由编译器从动态库加载的。要在库中注册插件,请使用 FrontendPluginRegistry::Add<>

static FrontendPluginRegistry::Add<MyPlugin> X("my-plugin-name", "my plugin description");

定义编译指示

插件还可以通过声明一个 PragmaHandler 并使用 PragmaHandlerRegistry::Add<> 注册它来定义编译指示。

// Define a pragma handler for #pragma example_pragma
class ExamplePragmaHandler : public PragmaHandler {
public:
  ExamplePragmaHandler() : PragmaHandler("example_pragma") { }
  void HandlePragma(Preprocessor &PP, PragmaIntroducer Introducer,
                    Token &PragmaTok) {
    // Handle the pragma
  }
};

static PragmaHandlerRegistry::Add<ExamplePragmaHandler> Y("example_pragma","example pragma description");

定义属性

插件可以通过声明一个 ParsedAttrInfo 并使用 ParsedAttrInfoRegister::Add<> 注册它来定义属性。

class ExampleAttrInfo : public ParsedAttrInfo {
public:
  ExampleAttrInfo() {
    Spellings.push_back({ParsedAttr::AS_GNU,"example"});
  }
  AttrHandling handleDeclAttribute(Sema &S, Decl *D,
                                   const ParsedAttr &Attr) const override {
    // Handle the attribute
    return AttributeApplied;
  }
};

static ParsedAttrInfoRegistry::Add<ExampleAttrInfo> Z("example_attr","example attribute description");

插件属性必须定义的 ParsedAttrInfo 成员是

  • Spellings,必须使用属性的每个 拼写 来填充,每个拼写都包含一个属性语法以及该语法如何拼写属性名称。如果语法允许范围,则拼写必须是“scope::attr”(如果存在范围)或“::attr”(如果不存在)。

根据属性的不同,可能需要定义的 ParsedAttrInfo 成员是

  • NumArgsOptArgs,它们设置属性的必需参数和可选参数的数量。

  • diagAppertainsToDecl,它检查属性是否已在正确类型的声明上使用,如果未使用,则发出诊断。

  • handleDeclAttribute,它是将属性应用于声明的函数。它负责检查属性的参数是否有效,并且通常通过向 Decl 添加一个 Attr 来应用属性。它返回 AttributeApplied(表示属性已成功应用)或 AttributeNotApplied(如果未应用)。

  • diagAppertainsToStmt,它检查属性是否已在正确类型的语句上使用,如果未使用,则发出诊断。

  • handleStmtAttribute,它是将属性应用于语句的函数。它负责检查属性的参数是否有效,并且通常通过向 Stmt 添加一个 Attr 来应用属性。它返回 AttributeApplied(表示属性已成功应用)或 AttributeNotApplied(如果未应用)。

  • diagLangOpts,它检查属性是否允许用于当前语言模式,如果未允许,则发出诊断。

  • existsInTarget,它检查属性是否允许用于给定的目标。

要查看属性插件的实际示例,请参见 Attribute.cpp 示例

综合起来

让我们来看一个打印顶级函数名的插件示例。此示例已检入 clang 存储库;请查看 PrintFunctionNames.cpp 的最新版本

运行插件

使用编译器驱动程序

Clang 驱动程序接受 -fplugin 选项来加载插件。Clang 插件可以通过 fplugin-arg-<插件名称>-<参数> 选项从编译器驱动程序命令行接收参数。使用此方法,插件名称本身不能包含连字符,但传递给插件的参数可以包含连字符。

$ export BD=/path/to/build/directory
$ make -C $BD CallSuperAttr
$ clang++ -fplugin=$BD/lib/CallSuperAttr.so \
          -fplugin-arg-call_super_plugin-help \
          test.cpp

如果您的插件名称包含连字符,请重命名插件或使用下面列出的 cc1 命令行选项。

使用 cc1 命令行

要运行插件,必须通过 -load 命令行选项加载包含插件注册表的动态库。这将加载所有已注册的插件,您可以通过指定 -plugin 选项来选择要运行的插件。可以使用 -plugin-arg-<插件名称> 传递插件的其他参数。

请注意,这些选项必须到达 clang 的 cc1 进程。有两种方法可以做到这一点:

  • 通过使用 -cc1 选项直接调用解析过程;这样做有缺点,即不会配置默认的标题搜索路径,因此您需要在命令行上指定完整的系统路径配置。

  • 照常使用 clang,但将所有传递给 cc1 进程的参数都加上 -Xclang 前缀。

例如,要通过 clang 在源文件上运行 print-function-names 插件,首先构建插件,然后从源代码树中使用插件调用 clang

$ export BD=/path/to/build/directory
$ (cd $BD && make PrintFunctionNames )
$ clang++ -D_GNU_SOURCE -D_DEBUG -D__STDC_CONSTANT_MACROS \
          -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -D_GNU_SOURCE \
          -I$BD/tools/clang/include -Itools/clang/include -I$BD/include -Iinclude \
          tools/clang/tools/clang-check/ClangCheck.cpp -fsyntax-only \
          -Xclang -load -Xclang $BD/lib/PrintFunctionNames.so -Xclang \
          -plugin -Xclang print-fns

另请参阅 print-function-name 插件示例的 README

使用 clang 命令行

在 clang 命令行上使用 -fplugin=plugin 会将插件作为参数传递给 cc1 命令行上的 -load。如果插件类实现了 getActionType 方法,则该插件将自动运行。例如,要在主 AST 操作之后自动运行插件(即与使用 -add-plugin 相同):

// Automatically run the plugin after the main AST action
PluginASTAction::ActionType getActionType() override {
  return AddAfterMainAction;
}

-clear-ast-before-backend 的交互

为了降低编译器的峰值内存使用量,建议插件在主要操作(通常是代码生成)之前运行。这是因为,如果任何插件在代码生成操作之后运行,就会自动关闭 -clear-ast-before-backend-clear-ast-before-backend 通过在生成 IR 之后、运行 IR 优化之前清除 Clang AST 来降低峰值内存使用量。将 CmdlineBeforeMainActionAddBeforeMainAction 作为 getActionType 来运行插件,同时仍然可以从 -clear-ast-before-backend 中获益。插件必须确保不要修改 AST,否则应该在主操作之后运行。