You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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(含注释),既完成输出需求,又避免重复进入主文件的问题。

  1. 自定义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';
  }
};
  1. 修改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解析,可分两步执行:

  1. 第一步:用LibTooling执行预处理,将结果保存到临时文件/内存;
  2. 第二步:创建新的ClangTool实例,以预处理后的文件为输入,运行AST解析与改写逻辑。

这种方式适合需要对预处理代码做额外修改再解析的场景,但需额外处理文件IO。

内容的提问来源于stack exchange,提问作者johnco3

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 04:46:19