ASTImporter:合并 Clang AST

ASTImporter 类是 Clang 核心库(AST 库)的一部分。它将 ASTContext 中的节点导入到另一个 ASTContext 中。

在本文档中,我们假设您对 Clang AST 有基本了解。如果您想了解有关 AST 结构的更多信息,请参阅 Clang AST 简介。有关 匹配 Clang AST匹配器参考 的知识也很有用。

简介

ASTContext 保存着长期存在的 AST 节点(如类型和声明),这些节点可以在整个文件的语义分析过程中被引用。在某些情况下,最好使用多个 ASTContext。例如,我们希望在同一个 Clang 工具中解析多个不同的文件。如果我们可以将生成的 AST 集视为从一起解析每个文件得到的单个 AST,那将会很方便。 ASTImporter 提供了一种方法,可以将类型或声明从一个 ASTContext 复制到另一个 ASTContext。我们将从其导入的上下文称为 **“from”上下文** 或 *源上下文*;我们将导入到的上下文称为 **“to”上下文** 或 *目标上下文*。

ASTImporter 库的现有客户端包括跨翻译单元 (CTU) 静态分析和 LLDB 表达式解析器。CTU 静态分析会导入函数的定义,如果其定义位于另一个翻译单元 (TU) 中。这样,分析就可以突破单个 TU 的限制。LLDB 的 expr 命令会解析用户定义的表达式,为其创建一个 ASTContext,然后从我们从调试信息(DWARF 等)中获得的 AST 中导入缺少的定义。

导入算法

导入一个 AST 节点会将该节点复制到目标 ASTContext 中。为什么我们必须复制节点?将该节点的指针插入目标上下文是否不够?一个原因是“from”上下文可能比“to”上下文存活时间更长。此外,Clang AST 认为节点(或节点的某些属性)在具有相同地址时是等效的!

导入算法必须确保在不同翻译单元中结构上等效的节点不会在合并的 AST 中重复。例如,如果我们在两个翻译单元中包含了向量模板的定义 (#include <vector>),那么它们的合并 AST 应该只有一个节点来表示该模板。此外,我们还必须发现 *一个定义规则* (ODR) 违规。例如,如果在两个翻译单元中都存在同名的类定义,但其中一个定义包含不同数量的字段。因此,我们查找现有的定义,然后检查这些节点的结构等效性。以下伪代码演示了导入机制的基本原理

// Pseudo-code(!) of import:
ErrorOrDecl Import(Decl *FromD) {
  Decl *ToDecl = nullptr;
  FoundDeclsList = Look up all Decls in the "to" Ctx with the same name of FromD;
  for (auto FoundDecl : FoundDeclsList) {
    if (StructurallyEquivalentDecls(FoundDecl, FromD)) {
      ToDecl = FoundDecl;
      Mark FromD as imported;
      break;
    } else {
      Report ODR violation;
      return error;
    }
  }
  if (FoundDeclsList is empty) {
    Import dependent declarations and types of ToDecl;
    ToDecl = create a new AST node in "to" Ctx;
    Mark FromD as imported;
  }
  return ToDecl;
}

两个 AST 节点在结构上是等效的,如果它们是

  • 内置类型并引用相同的类型,例如 intint 在结构上是等效的,

  • 函数类型,并且其所有参数都具有结构上等效的类型,

  • 记录类型,并且其所有字段按定义顺序具有相同的标识符名称和结构上等效的类型,

  • 变量或函数声明,并且它们具有相同的标识符名称,它们的类型在结构上是等效的。

我们可以类似地将结构等效性的定义扩展到模板。

如果 A 和 B 是 AST 节点,并且 *A 依赖于 B*,那么我们说 A 是 B 的 **依赖项**,B 是 A 的 **依赖关系**。单词“依赖项”和“依赖关系”是英国英语中的名词。不幸的是,在美国英语中,形容词“dependent”用于表示这两种含义。在本文档中,“dependent”形容词始终指依赖关系,即示例中的 B 节点。

API

让我们创建一个使用 ASTImporter 类的工具!首先,我们从虚拟文件构建两个 AST;虚拟文件的内容是从字符串文字中合成的

std::unique_ptr<ASTUnit> ToUnit = buildASTFromCode(
    "", "to.cc"); // empty file
std::unique_ptr<ASTUnit> FromUnit = buildASTFromCode(
    R"(
    class MyClass {
      int m1;
      int m2;
    };
    )",
    "from.cc");

第一个 AST 对应于目标(“to”)上下文 - 它是空的 - 第二个用于源(“from”)上下文。接下来,我们定义一个匹配器来匹配“from”上下文中的 MyClass

auto Matcher = cxxRecordDecl(hasName("MyClass"));
auto *From = getFirstDecl<CXXRecordDecl>(Matcher, FromUnit);

现在我们创建 Importer 并进行导入

ASTImporter Importer(ToUnit->getASTContext(), ToUnit->getFileManager(),
                     FromUnit->getASTContext(), FromUnit->getFileManager(),
                     /*MinimalImport=*/true);
llvm::Expected<Decl *> ImportedOrErr = Importer.Import(From);

Import 调用以 llvm::Expected 形式返回,因此,我们必须检查是否有任何错误。有关详细信息,请参阅 错误处理 文档。

if (!ImportedOrErr) {
  llvm::Error Err = ImportedOrErr.takeError();
  llvm::errs() << "ERROR: " << Err << "\n";
  consumeError(std::move(Err));
  return 1;
}

如果没有错误,我们就可以获取底层值。在本例中,我们将打印“to”上下文的 AST。

Decl *Imported = *ImportedOrErr;
Imported->getTranslationUnitDecl()->dump();

由于我们在导入器的构造函数中设置了 **最小导入**,因此 AST 将不包含成员的声明(在我们运行测试工具时)。

TranslationUnitDecl 0x68b9a8 <<invalid sloc>> <invalid sloc>
`-CXXRecordDecl 0x6c7e30 <line:2:7, col:13> col:13 class MyClass definition
  `-DefinitionData pass_in_registers standard_layout trivially_copyable trivial literal
    |-DefaultConstructor exists trivial needs_implicit
    |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param
    |-MoveConstructor exists simple trivial needs_implicit
    |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param
    |-MoveAssignment exists simple trivial needs_implicit
    `-Destructor simple irrelevant trivial needs_implicit

我们希望也获取成员,因此,我们使用 ImportDefinitionMyClass 的整个定义复制到“to”上下文。然后我们再次转储 AST。

if (llvm::Error Err = Importer.ImportDefinition(From)) {
  llvm::errs() << "ERROR: " << Err << "\n";
  consumeError(std::move(Err));
  return 1;
}
llvm::errs() << "Imported definition.\n";
Imported->getTranslationUnitDecl()->dump();

这次 AST 将包含成员。

TranslationUnitDecl 0x68b9a8 <<invalid sloc>> <invalid sloc>
`-CXXRecordDecl 0x6c7e30 <line:2:7, col:13> col:13 class MyClass definition
  |-DefinitionData pass_in_registers standard_layout trivially_copyable trivial literal
  | |-DefaultConstructor exists trivial needs_implicit
  | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param
  | |-MoveConstructor exists simple trivial needs_implicit
  | |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param
  | |-MoveAssignment exists simple trivial needs_implicit
  | `-Destructor simple irrelevant trivial needs_implicit
  |-CXXRecordDecl 0x6c7f48 <col:7, col:13> col:13 implicit class MyClass
  |-FieldDecl 0x6c7ff0 <line:3:9, col:13> col:13 m1 'int'
  `-FieldDecl 0x6c8058 <line:4:9, col:13> col:13 m2 'int'

如果我们设置导入器进行“正常”(非最小)导入,我们可以省去对 ImportDefinition 的调用。

ASTImporter Importer( ....  /*MinimalImport=*/false);

使用 **正常导入**,所有依赖的声明都会被正常导入。但是,使用最小导入,依赖的声明会在没有定义的情况下被导入,如果我们之后需要,我们必须为每个依赖的声明单独导入其定义。

将所有这些放在一起,下面是工具源代码的样子

#include "clang/AST/ASTImporter.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Tooling/Tooling.h"

using namespace clang;
using namespace tooling;
using namespace ast_matchers;

template <typename Node, typename Matcher>
Node *getFirstDecl(Matcher M, const std::unique_ptr<ASTUnit> &Unit) {
  auto MB = M.bind("bindStr"); // Bind the to-be-matched node to a string key.
  auto MatchRes = match(MB, Unit->getASTContext());
  // We should have at least one match.
  assert(MatchRes.size() >= 1);
  // Get the first matched and bound node.
  Node *Result =
      const_cast<Node *>(MatchRes[0].template getNodeAs<Node>("bindStr"));
  assert(Result);
  return Result;
}

int main() {
  std::unique_ptr<ASTUnit> ToUnit = buildASTFromCode(
      "", "to.cc");
  std::unique_ptr<ASTUnit> FromUnit = buildASTFromCode(
      R"(
      class MyClass {
        int m1;
        int m2;
      };
      )",
      "from.cc");
  auto Matcher = cxxRecordDecl(hasName("MyClass"));
  auto *From = getFirstDecl<CXXRecordDecl>(Matcher, FromUnit);

  ASTImporter Importer(ToUnit->getASTContext(), ToUnit->getFileManager(),
                       FromUnit->getASTContext(), FromUnit->getFileManager(),
                       /*MinimalImport=*/true);
  llvm::Expected<Decl *> ImportedOrErr = Importer.Import(From);
  if (!ImportedOrErr) {
    llvm::Error Err = ImportedOrErr.takeError();
    llvm::errs() << "ERROR: " << Err << "\n";
    consumeError(std::move(Err));
    return 1;
  }
  Decl *Imported = *ImportedOrErr;
  Imported->getTranslationUnitDecl()->dump();

  if (llvm::Error Err = Importer.ImportDefinition(From)) {
    llvm::errs() << "ERROR: " << Err << "\n";
    consumeError(std::move(Err));
    return 1;
  }
  llvm::errs() << "Imported definition.\n";
  Imported->getTranslationUnitDecl()->dump();

  return 0;
};

我们可以在例如 clang/tools 下扩展 CMakeLists.txt,其中包含构建和链接指令

add_clang_executable(astimporter-demo ASTImporterDemo.cpp)
clang_target_link_libraries(astimporter-demo
  PRIVATE
  LLVMSupport
  clangAST
  clangASTMatchers
  clangBasic
  clangFrontend
  clangSerialization
  clangTooling
  )

然后我们可以构建并执行新工具。

$ ninja astimporter-demo && ./bin/astimporter-demo

导入过程中的错误

通常,源上下文或目标上下文会包含声明的定义。但是,在某些情况下,这两个上下文都可能为给定符号提供定义。如果这些定义不同,那么我们就发生了名称冲突,在 C++ 中称为 ODR(一个定义规则)违规。让我们修改之前编写的工具,尝试导入具有冲突定义的 ClassTemplateSpecializationDecl

int main() {
  std::unique_ptr<ASTUnit> ToUnit = buildASTFromCode(
      R"(
      // primary template
      template <typename T>
      struct X {};
      // explicit specialization
      template<>
      struct X<int> { int i; };
      )",
      "to.cc");
  ToUnit->enableSourceFileDiagnostics();
  std::unique_ptr<ASTUnit> FromUnit = buildASTFromCode(
      R"(
      // primary template
      template <typename T>
      struct X {};
      // explicit specialization
      template<>
      struct X<int> { int i2; };
      // field mismatch:  ^^
      )",
      "from.cc");
  FromUnit->enableSourceFileDiagnostics();
  auto Matcher = classTemplateSpecializationDecl(hasName("X"));
  auto *From = getFirstDecl<ClassTemplateSpecializationDecl>(Matcher, FromUnit);
  auto *To = getFirstDecl<ClassTemplateSpecializationDecl>(Matcher, ToUnit);

  ASTImporter Importer(ToUnit->getASTContext(), ToUnit->getFileManager(),
                       FromUnit->getASTContext(), FromUnit->getFileManager(),
                       /*MinimalImport=*/false);
  llvm::Expected<Decl *> ImportedOrErr = Importer.Import(From);
  if (!ImportedOrErr) {
    llvm::Error Err = ImportedOrErr.takeError();
    llvm::errs() << "ERROR: " << Err << "\n";
    consumeError(std::move(Err));
    To->getTranslationUnitDecl()->dump();
    return 1;
  }
  return 0;
};

当我们运行工具时,我们会收到以下警告

to.cc:7:14: warning: type 'X<int>' has incompatible definitions in different translation units [-Wodr]
      struct X<int> { int i; };
             ^
to.cc:7:27: note: field has name 'i' here
      struct X<int> { int i; };
                          ^
from.cc:7:27: note: field has name 'i2' here
      struct X<int> { int i2; };
                        ^

注意,由于这些诊断信息,我们必须在 ASTUnit 对象上调用 enableSourceFileDiagnostics

由于我们无法导入指定的声明 (From),因此在返回值中会发生错误。AST 不包含冲突的定义,因此我们保留了原始 AST。

ERROR: NameConflict
TranslationUnitDecl 0xe54a48 <<invalid sloc>> <invalid sloc>
|-ClassTemplateDecl 0xe91020 <to.cc:3:7, line:4:17> col:14 X
| |-TemplateTypeParmDecl 0xe90ed0 <line:3:17, col:26> col:26 typename depth 0 index 0 T
| |-CXXRecordDecl 0xe90f90 <line:4:7, col:17> col:14 struct X definition
| | |-DefinitionData empty aggregate standard_layout trivially_copyable pod trivial literal has_constexpr_non_copy_move_ctor can_const_default_init
| | | |-DefaultConstructor exists trivial constexpr needs_implicit defaulted_is_constexpr
| | | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param
| | | |-MoveConstructor exists simple trivial needs_implicit
| | | |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param
| | | |-MoveAssignment exists simple trivial needs_implicit
| | | `-Destructor simple irrelevant trivial needs_implicit
| | `-CXXRecordDecl 0xe91270 <col:7, col:14> col:14 implicit struct X
| `-ClassTemplateSpecialization 0xe91340 'X'
`-ClassTemplateSpecializationDecl 0xe91340 <line:6:7, line:7:30> col:14 struct X definition
  |-DefinitionData pass_in_registers aggregate standard_layout trivially_copyable pod trivial literal
  | |-DefaultConstructor exists trivial needs_implicit
  | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param
  | |-MoveConstructor exists simple trivial needs_implicit
  | |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param
  | |-MoveAssignment exists simple trivial needs_implicit
  | `-Destructor simple irrelevant trivial needs_implicit
  |-TemplateArgument type 'int'
  |-CXXRecordDecl 0xe91558 <col:7, col:14> col:14 implicit struct X
  `-FieldDecl 0xe91600 <col:23, col:27> col:27 i 'int'

错误传播

如果在导入给定节点之前,我们必须导入依赖节点,那么与依赖关系相关的导入错误将传播到依赖节点。让我们修改之前的示例,导入 FieldDecl 而不是 ClassTemplateSpecializationDecl

auto Matcher = fieldDecl(hasName("i2"));
auto *From = getFirstDecl<FieldDecl>(Matcher, FromUnit);

在本例中,我们可以看到错误与专业化 (getImportDeclErrorIfAny) 也相关联,不仅仅是与字段相关联

llvm::Expected<Decl *> ImportedOrErr = Importer.Import(From);
if (!ImportedOrErr) {
  llvm::Error Err = ImportedOrErr.takeError();
  consumeError(std::move(Err));

  // check that the ClassTemplateSpecializationDecl is also marked as
  // erroneous.
  auto *FromSpec = getFirstDecl<ClassTemplateSpecializationDecl>(
      classTemplateSpecializationDecl(hasName("X")), FromUnit);
  assert(Importer.getImportDeclErrorIfAny(FromSpec));
  // Btw, the error is also set for the FieldDecl.
  assert(Importer.getImportDeclErrorIfAny(From));
  return 1;
}

污染的 AST

我们可能会在导入依赖节点期间识别出错误。但是,到那时,我们已经创建了依赖项。在这些情况下,我们不会从“to”上下文中删除现有的错误节点,而是会将错误与该节点关联起来。让我们扩展之前的示例,添加另一个类 Y。该类在“to”上下文中具有前向声明,但其定义位于“from”上下文中。我们希望导入定义,但它包含一个成员,其类型与“to”上下文中的类型发生冲突

std::unique_ptr<ASTUnit> ToUnit = buildASTFromCode(
    R"(
    // primary template
    template <typename T>
    struct X {};
    // explicit specialization
    template<>
    struct X<int> { int i; };

    class Y;
    )",
    "to.cc");
ToUnit->enableSourceFileDiagnostics();
std::unique_ptr<ASTUnit> FromUnit = buildASTFromCode(
    R"(
    // primary template
    template <typename T>
    struct X {};
    // explicit specialization
    template<>
    struct X<int> { int i2; };
    // field mismatch:  ^^

    class Y { void f() { X<int> xi; } };
    )",
    "from.cc");
FromUnit->enableSourceFileDiagnostics();
auto Matcher = cxxRecordDecl(hasName("Y"));
auto *From = getFirstDecl<CXXRecordDecl>(Matcher, FromUnit);
auto *To = getFirstDecl<CXXRecordDecl>(Matcher, ToUnit);

这次,我们为 ASTImporterSharedState 创建了一个 shared_ptr,它拥有与“to”上下文相关的错误。注意,可能会有几个不同的 ASTImporter 对象,它们从不同的“from”上下文导入到同一个“to”上下文;它们应该共享同一个 ASTImporterSharedState。(还要注意,我们必须包含相应的 ASTImporterSharedState.h 头文件。)

auto ImporterState = std::make_shared<ASTImporterSharedState>();
ASTImporter Importer(ToUnit->getASTContext(), ToUnit->getFileManager(),
                     FromUnit->getASTContext(), FromUnit->getFileManager(),
                     /*MinimalImport=*/false, ImporterState);
llvm::Expected<Decl *> ImportedOrErr = Importer.Import(From);
if (!ImportedOrErr) {
  llvm::Error Err = ImportedOrErr.takeError();
  consumeError(std::move(Err));

  // ... but the node had been created.
  auto *ToYDef = getFirstDecl<CXXRecordDecl>(
      cxxRecordDecl(hasName("Y"), isDefinition()), ToUnit);
  ToYDef->dump();
  // An error is set for "ToYDef" in the shared state.
  Optional<ASTImportError> OptErr =
      ImporterState->getImportDeclErrorIfAny(ToYDef);
  assert(OptErr);

  return 1;
}

如果我们查看 AST,那么我们会发现创建了带有定义的 Decl,但缺少字段。

|-CXXRecordDecl 0xf66678 <line:9:7, col:13> col:13 class Y
`-CXXRecordDecl 0xf66730 prev 0xf66678 <:10:7, col:13> col:13 class Y definition
  |-DefinitionData pass_in_registers empty aggregate standard_layout trivially_copyable pod trivial literal has_constexpr_non_copy_move_ctor can_const_default_init
  | |-DefaultConstructor exists trivial constexpr needs_implicit defaulted_is_constexpr
  | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param
  | |-MoveConstructor exists simple trivial needs_implicit
  | |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param
  | |-MoveAssignment exists simple trivial needs_implicit
  | `-Destructor simple irrelevant trivial needs_implicit
  `-CXXRecordDecl 0xf66828 <col:7, col:13> col:13 implicit class Y

我们不会移除错误节点,因为当我们识别到错误时,已经太迟了,无法移除该节点,因为 AST 中可能还有其他对该节点的引用。这与 Clang AST 的整体 设计原则 相一致:Clang AST 节点(类型、声明、语句、表达式等)在创建后通常被设计为 **不可变的**。因此,ASTImporter 库的客户端应该始终检查它们在目标上下文中检查的节点是否存在任何关联错误。我们建议跳过对那些与错误关联的节点的处理。

使用 -ast-merge Clang 前端动作

-ast-merge <pch-file> 命令行开关可用于从给定的序列化 AST 文件进行合并。该文件表示源上下文。当存在此开关时,源上下文的每个顶层 AST 节点都会被合并到目标上下文中。如果合并成功,则会为 Decl 调用 ASTConsumer::HandleTopLevelDecl。这会导致我们可以在扩展的 AST 上执行原始前端动作。

C 语言示例

让我们考虑以下三个文件

// bar.h
#ifndef BAR_H
#define BAR_H
int bar();
#endif /* BAR_H */

// bar.c
#include "bar.h"
int bar() {
  return 41;
}

// main.c
#include "bar.h"
int main() {
    return bar();
}

让我们为这两个源文件生成 AST 文件

$ clang -cc1 -emit-pch -o bar.ast bar.c
$ clang -cc1 -emit-pch -o main.ast main.c

然后,让我们检查如果我们只考虑 bar() 函数,合并后的 AST 会是什么样子

$ clang -cc1 -ast-merge bar.ast -ast-merge main.ast /dev/null -ast-dump
TranslationUnitDecl 0x12b0738 <<invalid sloc>> <invalid sloc>
|-FunctionDecl 0x12b1470 </path/bar.h:4:1, col:9> col:5 used bar 'int ()'
|-FunctionDecl 0x12b1538 prev 0x12b1470 </path/bar.c:3:1, line:5:1> line:3:5 used bar 'int ()'
| `-CompoundStmt 0x12b1608 <col:11, line:5:1>
|   `-ReturnStmt 0x12b15f8 <line:4:3, col:10>
|     `-IntegerLiteral 0x12b15d8 <col:10> 'int' 41
|-FunctionDecl 0x12b1648 prev 0x12b1538 </path/bar.h:4:1, col:9> col:5 used bar 'int ()'

我们可以检查到函数的原型和定义被合并到同一个重声明链中。更重要的是,还有一个第三个原型声明被合并到链中。函数以一种将原型添加到重声明链中的方式进行合并,如果它们引用了相同类型,但我们只能有一个定义。前两个声明来自 bar.ast,第三个声明来自 main.ast

现在,让我们从合并的 AST 创建一个目标文件

$ clang -cc1 -ast-merge bar.ast -ast-merge main.ast /dev/null -emit-obj -o main.o

接下来,我们可以调用链接器并执行创建的二进制文件。

$ clang -o a.out main.o
$ ./a.out
$ echo $?
41
$

C++ 语言示例

在 C++ 的情况下,AST 文件的生成方式以及我们调用前端的方式略有不同。假设我们有以下三个文件

// foo.h
#ifndef FOO_H
#define FOO_H
struct foo {
    virtual int fun();
};
#endif /* FOO_H */

// foo.cpp
#include "foo.h"
int foo::fun() {
  return 42;
}

// main.cpp
#include "foo.h"
int main() {
    return foo().fun();
}

我们将生成 AST 文件,合并它们,创建可执行文件,然后运行它

$ clang++ -x c++-header -o foo.ast foo.cpp
$ clang++ -x c++-header -o main.ast main.cpp
$ clang++ -cc1 -x c++ -ast-merge foo.ast -ast-merge main.ast /dev/null -ast-dump
$ clang++ -cc1 -x c++ -ast-merge foo.ast -ast-merge main.ast /dev/null -emit-obj -o main.o
$ clang++ -o a.out main.o
$ ./a.out
$ echo $?
42
$