基于LibTooling的C宏展开后AST解析断言错误求助
解决LibTooling中ParseAST触发"Cannot reenter the main file!"断言错误
问题背景
我正在开发一个基于LibTooling的插桩工具,核心需求:
- 展开C宏(含嵌套函数式宏)并保留注释
- 将预处理后的代码传递给
RecursiveASTVisitor<MyASTVisitor>完成语句改写
目前通过继承clang::ASTFrontendAction实现了类似clang -E的宏展开输出,未自定义clang::PPCallbacks。但手动调用clang::ParseAST()解析预处理后的代码时,触发Preprocessor断言错误:
assert(NumEnteredSourceFiles == 0 && "Cannot reenter the main file!")
此时NumEnteredSourceFiles的值为2。
相关代码片段
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> { // 实现各类语句改写逻辑 }; class MyASTConsumer : public ASTConsumer { public: void HandleTranslationUnit(ASTContext &Ctx) override { MyASTVisitor visitor; visitor.TraverseDecl(Ctx.getTranslationUnitDecl()); } }; class MyFrontendAction : public ASTFrontendAction { public: std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) override { return std::make_unique<MyASTConsumer>(); } void ExecuteAction() override { auto &CI = getCompilerInstance(); auto &PP = CI.getPreprocessor(); // 模拟clang -E输出宏展开后的代码 PP.EnterMainSourceFile(); Token Tok; llvm::raw_ostream &OS = llvm::outs(); while (PP.Lex(Tok)) { PP.PrintToken(OS, Tok, true); OS << '\n'; } // 尝试解析AST,触发断言错误 ParseAST(PP, *getASTConsumer(), CI.getASTContext()); } }; int main(int argc, const char **argv) { CommonOptionsParser OptionsParser(argc, argv, MyToolCategory); ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList()); return Tool.run(newFrontendActionFactory<MyFrontendAction>().get()); }
测试用C文件
// 测试注释 #define ADD(a, b) ((a)+(b)) #define MUL(a, b) ((a)*(b)) #define COMPUTE(a, b) ADD(MUL(a, b), 10) int main() { int x = COMPUTE(2, 3); // 计算2*3+10 return x; }
工具错误输出
Assertion failed: (NumEnteredSourceFiles == 0 && "Cannot reenter the main file!"), function EnterMainSourceFile, file Preprocessor.cpp, line 410.
问题原因
断言触发的直接原因是:手动调用PP.EnterMainSourceFile()遍历输出预处理Token时,Preprocessor内部的NumEnteredSourceFiles计数器加1;后续调用ParseAST()时,其内部会再次调用EnterMainSourceFile(),导致计数器变为2,触发断言检查。
修复方案
方案1:使用PPCallbacks同步捕获预处理输出(推荐)
通过自定义PPCallbacks,在AST解析流程中同步捕获宏展开后的Token(含注释),既完成输出需求,又避免重复进入主文件的问题。
- 自定义PPCallbacks类:
#include "clang/Lex/PPCallbacks.h" #include "clang/Lex/Preprocessor.h" class PreprocessOutputCallbacks : public clang::PPCallbacks { private: llvm::raw_ostream &OS; clang::Preprocessor &PP; public: PreprocessOutputCallbacks(llvm::raw_ostream &OS, clang::Preprocessor &PP) : OS(OS), PP(PP) {} void HandleToken(clang::Preprocessor &PPC, clang::Token &Tok) override { // 输出Token并保留注释 PP.PrintToken(OS, Tok, true); if (!Tok.is(clang::tok::eof)) OS << '\n'; } };
- 修改FrontendAction,注册回调并移除手动遍历逻辑:
class MyFrontendAction : public ASTFrontendAction { public: std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) override { return std::make_unique<MyASTConsumer>(); } bool BeginSourceFileAction(CompilerInstance &CI) override { auto &PP = CI.getPreprocessor(); // 开启注释保留功能 PP.SetCommentRetentionState(clang::Preprocessor::CRS_All); // 注册自定义回调 PP.addPPCallbacks(std::make_unique<PreprocessOutputCallbacks>(llvm::outs(), PP)); return true; } // 移除自定义ExecuteAction,使用默认逻辑自动调用ParseAST() };
方案2:拆分预处理与AST解析为独立流程
如果需要先获取完整预处理代码再做AST解析,可分两步执行:
- 第一步:用LibTooling执行预处理,将结果保存到临时文件/内存;
- 第二步:创建新的
ClangTool实例,以预处理后的文件为输入,运行AST解析与改写逻辑。
这种方式适合需要对预处理代码做额外修改再解析的场景,但需额外处理文件IO。
内容的提问来源于stack exchange,提问作者johnco3
相关产品推荐
相关产品推荐

