ASTImporter
:合并ClangAST
ASTImporter
类是Clang
的核心库AST
库的一部分.它导入一个ASTContext
的节点到另一个ASTContext
中.
这里,假设你对ClangAST
有基本了解.如果你想了解有关AST
结构的更多信息,见ClangAST
简介.匹配ClangAST
在此.
ASTContext
包含长期有
的,可在文件
的整个分析语义
过程中引用的AST
节点(如类型
和声明
).有时,最好使用多个ASTContext
.
如,想在同一个Clang
工具中,解析多个
不同的文件.如果可像解析
每个文件产生一个AST
一样的,查看生成
的AST
集,就会很方便
.
ASTImporter
提供了可从一个ASTContext
复制声明
或类型
到另一个ASTContext
的方法.从中导入
的环境
叫"从"
环境或源环境
;
而"到"
环境或目标
环境为导入进
的环境.
ASTImporter
库的现有用户
,是交叉翻译单元(CTU)
静态分析和LLDB
式解析器.如果在另一个
,(TU)
翻译单元中找到函数
定义,则CTU
静态分析导入
函数定义.
这样,分析
就可突破单个TU
限制.LLDB
的式
命令解析
用户定义的式,为其创建一个ASTContext
,然后从从调试信息(DWARF
等)的AST
导入中获得缺失定义
.
导入一个AST
节点,会复制该节点
到目标ASTContext
中.为什么必须复制
节点,而不能插入
该节点指针到目标环境
中呢?一个原因是"from"
环境可能比"to"
环境更长久.
此外,如果节点
有相同地址,ClangAST
会认为节点(或节点的某些属性
)是等效的!
导入算法必须确保
不同翻译单元
中,结构等效
的节点,不会在合并的AST
中重复.如,如果在两个翻译单元
中,包含向量模板(#include
)的定义
,则合并的AST
应该只有一个代表模板
的节点
.
此外,必须发现(ODR)
一个定义规则的违规行为
.如,如果两个翻译单元
中,有相同名字的类定义
,但其中一个定义
包含不同数量
的字段.
因此,要查找
现有定义,然后检查
这些节点上的结构等价性
.以下伪代码
演示了导入机制
的底层:
//导入的伪代码(!):
ErrorOrDecl Import(Decl *FromD) {
Decl *ToDecl = nullptr;
Found声明List = 用FromD相同名,在`to`环境中查找所有声明
for (auto FoundDecl : Found声明List) {
if (StructurallyEquivalent声明(FoundDecl, FromD)) {
ToDecl = FoundDecl;
Mark FromD as imported;
break;
} else {
Report ODR violation;
return error;
}
}
if (Found声明List is empty) {
导入依赖声明及to声明的类型
ToDecl = 在`to`环境创建新AST;
Mark FromD as imported;
}
return ToDecl;
}
如果两个AST
节点在结构
上是等效的,则它们是等效的.
1,内置
类型和引用
相同类型,如int
和int
在结构
上是等价的,
2,函数类型
及其所有参数
在结构上有等效类型
,
3,记录类型
及其所有字段
(按其定义顺序
)有相同的标识名
和结构
上等效的类型,
4,变量或函数声明
,且有相同标识名
,且它们的类型
在结构
上是等效的.
可把定义结构
等价类似
地扩展到模板
.
创建一个使用ASTImporter
类的工具!首先,从虚文件构建两个AST
;虚文件的内容是从串字面
合成的:
std::unique_ptr<ASTUnit> ToUnit = buildASTFromCode("", "to.cc"); //空文件
std::unique_ptr<ASTUnit> FromUnit = buildASTFromCode(
R"(
class MyClass {
int m1;
int m2;
};
)",
"from.cc");
第一个AST
对应("to")
为空的目标环境
,第二个AST
对应源("from")
环境.接着,定义一个匹配"from"
环境中的MyClass
的匹配器:
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);
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
不包含成员
声明.
要想得到成员,所以,用ImportDefinition
复制MyClass
的整个定义到"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
也包含成员
了.
如果把导入器
设置为执行"正常"
(非最小
)导入,则可省去调用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"); //把`待匹配节点`绑定到`串键`.
auto MatchRes = match(MB, Unit->getASTContext());
//至少应该有一个匹配.
assert(MatchRes.size() >= 1);
//取第一个`匹配`及绑定节点.
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"(
//主模板
template
struct X {};
//显式特化
template<>
struct X { int i; };
)" ,
"to.cc");
ToUnit->enableSourceFileDiagnostics();
std::unique_ptr<ASTUnit> FromUnit = buildASTFromCode(
R"(
//主模板
template
struct X {};
//显式特化
template<>
struct X { int i2; };
//字段不匹配:^^
)" ,
"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:`警告:`"X" `类型在不同的翻译单元中有不兼容的定义`[-Wodr]`
`构X<int>{inti;`
^
`to.cc:7:27:`注意:此处的`字段名`叫`"i"`
`构X<int>{inti;`
^
`from.cc:7:27:`注意:此处的`字段名`叫`"i2"`
`structX<int>{inti2;`
^
注意,因为这些诊断,必须在ASTUnit
对象上调用enableSourceFileDiagnostics
.
因为无法
导入指定的(From)
声明,因此返回值
中出现错误.AST
不包含冲突定义
,因此只剩下原始AST
.
如果在导入给定节点
前,有必须先导入的依赖节点
,则把与依赖
关系关联的导入错误
传播到依赖节点
.修改前例并导入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));
//检查是否也按错误标记`ClassTemplateSpecializationDecl`.
auto *FromSpec = getFirstDecl<ClassTemplateSpecializationDecl>(
classTemplateSpecializationDecl(hasName("X")), FromUnit);
assert(Importer.getImportDeclErrorIfAny(FromSpec));
//顺便,也为`FieldDecl`设置错误.
assert(Importer.getImportDeclErrorIfAny(From));
return 1;
}
AST
可能会在导入依赖节点
时,发现错误.但是,那时,已创建了依赖项
.这时,不会从"to"
环境中删除
现有的错误节点
,而是关联一个错误
到该节点
.
用另一个Y类
来扩展前例.此类
在"to"
环境中有前向
定义,但在"from"
环境中定义它.要想导入
定义,但它包含一个类型与"to"
环境中的类型
冲突的成员
:
std::unique_ptr<ASTUnit> ToUnit = buildASTFromCode(
R"(
//主模板
template
struct X {};
//显式特化
template<>
struct X { int i; };
class Y;
)" ,
"to.cc");
ToUnit->enableSourceFileDiagnostics();
std::unique_ptr<ASTUnit> FromUnit = buildASTFromCode(
R"(
//主模板
template
struct X {};
//显式特化
template<>
struct X { int i2; };
//字段不匹配:^^
class Y { void f() { X xi; } };
)" ,
"from.cc");
FromUnit->enableSourceFileDiagnostics();
auto Matcher = cxxRecordDecl(hasName("Y"));
auto *From = getFirstDecl<CXXRecordDecl>(Matcher, FromUnit);
auto *To = getFirstDecl<CXXRecordDecl>(Matcher, ToUnit);
这一次,为ASTImporterSharedState
创建一个拥有"to"
环境关联错误
的shared_ptr
.注意,可能会有几个不同的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));
//...但是已创建`节点`.
auto *ToYDef = getFirstDecl<CXXRecordDecl>(
cxxRecordDecl(hasName("Y"), isDefinition()), ToUnit);
ToYDef->dump();
//在共享状态下,已为`"ToYDef"`设置了错误.
Optional<ASTImportError> OptErr =
ImporterState->getImportDeclErrorIfAny(ToYDef);
assert(OptErr);
return 1;
}
如果看一下AST
,则可见创建了带定义的Decl
,但缺少字段
.
不会删除
错误节点,因为当错误识别
时,再删除
节点为时已晚,可能会有对AST
中已有节点
的其他引用
.
这与ClangAST
的整体设计原则一致:ClangAST
节点(类型,声明,语句,式等)
一般按创建后
不变设计.
因此,ASTImporter
库的用户
,应总是在目标环境
中,检查
待检查节点
是否有相关错误
.建议跳过有关联错误
节点的处理
.
-ast-merge
的Clang
前端操作-ast-merge
命令行开关,可用来从给定
的表示源环境
的序化AST
文件合并.有此开关
时,会把源环境
的每个顶级AST
节点都合并
到目标环境
中.
如果合并
成功,则为声明
调用ASTConsumer::HandleTopLevelDecl
.这导致可在扩展的AST
上执行原始前端操作
.
考虑以下三个文件
:
//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
可检查函数的原型
和它的定义
是否合并
到同一个再声明链
中.更重要的是,还合并了第三个
原型声明到链
中.
函数的合并
方式是,如果原型
引用相同类型
,则添加原型
到再声明
链中,但只能有一个定义
.
前两个声明
是bar.ast
,第三个
声明是main.ast
.
现在,从合并的AST
创建一个目标文件
:
$ clang -cc1 -ast-merge bar.ast -ast-merge main.ast /dev/null -emit-obj -o main.o
Next, we may call the linker and execute the created binary file.
$ 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
$