2512C++,clangd支持模块

原文

Clangd中的C++20模块支持

作者:许传奇

这篇博客的目的是鼓励更多的人来贡献Clangd中的C++20模块支持.我发现,抱怨C++20模块的智能提示的人数,与ClangdC++20模块支持的贡献者人数,及ClangdC++20模块支持的复杂度,这三者并不成比例.

所以我推测大多数人可能没有理解ClangdC++20模块不难.所以写了这篇博客,希望说明,与编译器模块支持的复杂度不同,在clangd(及类似的工具,如clangtidy或其他语言服务器)中不难支持C++20模块.

此外也在此推荐下Clice,虽然当前还没有支持模块,但看上去前景不错,这里.

C++20模块前置背景

C++20模块前,可直接编译所有的编译单元.几乎所有C++工具(构建系统,静态分析工具等等)都依赖这一前提.

然而在引入C++20模块后,该前提就错误了.这是C++20模块会对工具链造成巨大影响的原因,也是当前C++20模块应用进度相对较慢的原因.

以一个最简单的C++20模块示例:

cpp 复制代码
//`a.cppm`
export module a;
//`b.cppm`
export module b;
import a;

为了编译b.cppm,现在"需要"使用两条编译命令:

cpp 复制代码
$ clang++ -std=c++20 a.cppm --precompile -o a.pcm
$ clang++ -std=c++20 b.cppm -fmodule-file=a=a.pcm -fsyntax-only

对该极简单的情况,编译器可能用一条语句处理,但这不具备可扩展性,除非把构建系统塞到编译器里,但这里不讲它.

这里的a.pcmBMI*,是编译器a.cppm需要对外暴露信息序化后的结果.所有直接或间接``import a;编译单元,都需要a.cppmBMI才可编译它.

BMI二进制模块接口.

工具链支持模块要求步骤

除了编译器外的工具链,为了支持模块需要完成:

1,对任意包含导入编译单元,需要计算出其直接和间接导入模块单元,这里称这些模块单元要求模块单元.

并根据此信息计算出编译单元有向无环图依赖图.

2,取这些要求模块单元对应的BMI,当不存在此BMI时,工具需要根据有向无环依赖图调用编译器以生成要求的BMI.

3,对一个编译单元,当其存在导入时,需根据以上两条信息,拼接出编译此编译单元要求的编译命令.

特别地,当构建了一个项目,并生成了编译数据库后,对其中的每一个编译单元,实际上既有编译此单元要求的编译命令,又有编译此单元所需的全部BMI,所以此时其实是可以用很多工具的.
编辑数据库

因此可在不显式支持clangtidy时,集成clangtidy到使用模块的项目中的原因.此外,因此,建议构建系统搞个特别的叫"buildBMIsonly"的模式.

这样可让用户在只需要clangtidy此场景里只构建项目里所有的BMI而不需要编译整个项目.

Clangd支持模块要求步骤

同上,当构建一个项目并生成编译数据库后,将该编译数据库导入Clangd,可能会发现此时的Clangd突然可在使用模块的项目中工作了.

但这并不理想,因为:

1,这要求Clangd的版本和构建时使用的Clang必须是完全一致的.暗示了不能使用GCCMSVC构建工具.这和Clangd设计理念不符.

2,Clangd语言服务器需要及时反馈.但clangd如果不显式支持模块的话,当修改了模块接口后,对应的BMI是不会自动更新的,也无法看到对应的修改的,除非重构一遍项目.此用户体验不好.

这里的第二点是clangd显式支持C++20模块更迫切的主要原因.其他工具clangtidy是可接受先构建后分析工作流的,但对clangd而言,不能即时看到更改的用户体验差异会非常大.

Clangd``工作原理

高级来说,Clangd的核心逻辑在clangtoolsextra/clangd/TUScheduler.cpp中,其中有详细的注释,大家感兴趣的可阅读.

其中Clangd会为打开的每一个选项卡页创建一个工作者,该工作者在一个单独的线程中.每个工作者会调用ClangAPI编译其打开的页,取AST.

后续事件来临时(如鼠标悬浮,要求跳转等),工作者会先检查之前的AST是否依然可用,如果已过时了,工作者重新编译,然后根据AST响应各种事件的请求.

因此,Clangd支持C++20模块的工作主要集中在编译部分和检测编译结果是否过时两部分.

对利用AST的部分,在不要求新功能,只要求clangd可继续工作时,暂时可不用关心.新功能的示例之一即编写import A时,Clangd自动提示后面可能的模块名,还没有支持这部分工作.

编译

如前,在引入C++20模块前,每个编译单元都是独立的,只需要对应的编译命令(当然,及对应的文件)就可编译.
Clangd编译数据库取每个编译单元编译命令,然后编译当前页以取AST.这部分逻辑也主要在TUScheduler.cpp中.

特别地,为了加速编译,Clangd工作者为其打开的选项卡页使用PCH(预编译头技术)提前编译头文件,并将编译结果保存起来,叫(Preamble)预制件.

好处是当打开一个选项卡页,并修改该页的内容时,(如果没有修改头文件区域),重新编译当前制表符内容时可避免重新编译头文件区域的内容.

这是很好的优化手段,大大加快了Clangd的处理速度.

当然也有坏处,因为每个预制件属于不同ASTWorker,所以打开新的选项卡页时,Clangd依旧会花费额外的时间,编译该选项卡页预制件,这会浪费时间.

此外因为每个选项卡页都有自己的预制件,可以预见当打开大量选项卡页Clangd会花费更多内存.

从该角度,可知,对充分使用C++20模块的项目,在完成热身后,将大大减少Clangd消耗的内存.

检测编译结果是否过时

预制件设计的另一个意义是,它按两部分划分选项卡页的内容,一个是当前页的依赖,另一个是该页的内容自身.

此时检测编译结果是否过时,则自然变成了检测当前页内容是否过时,及依赖的内容是否过时两部分.检测当前页是否过时很简单.

只需要检测依赖的内容,及预制件是否过时即可.

这部分在isPreambleCompatible中实现,这里.

Clangd中支持C++20模块

高级现状

当前Clangd中已基本支持C++20模块,只需要传入--experimental-modules-support选项即可打开.

本地功能符合期望,但社区里还是有很多抱怨,很多问题也不好复现.这也是写这篇博客的主要动机,大家可在自己的环境里简单检查下,发现clangd漏洞或顺手开发的新特征也可贡献到社区.

如前,使用C++20模块需要传入编译数据库.编译数据库中需要包含当前项目中所有TU编译选项.

社区常见的几个错误报告或为没有传入编译数据库,或为编译数据库没有包含必须的编译单元及其编译命令.

如,如果你的项目使用了import std;则需要在传入的编译数据库中看到提供std module编译单元.

测试与调试

因为Clangd编译数据库复用构建系统构建的BMI文件,大家测试ClangdC++20模块能力时,可在生成编译数据库后删除所有BMI,避免假阳性现象的出现.

当大家当前使用clangdC++20模块特征时,如果有问题,可先使用--log=verbose观察Clangd输出,可能会有发现.

若发现Clangd编译模块单元报错时,需要检查是否可用同版本Clang``编译器构建你的项目.

如果可以,但是Clangd编译模块单元时依然报错甚至崩溃,可试用--debug-modules-builder选项.

否则,Clangd会在退出时,删除所有其构建的BMI文件,这不利于复现Clangd报错或崩溃.

在打开该选项后,Clangd不会删除其构建的BMI文件.结合--log=verbose中显示的clangd构建编译数据库所使用的编译命令,就可在终端中复现Clangd的报错了.

另根据推测,在#include导入大量混用的项目中,ClangdClang的编译结果不一致的原因,可能是Clangd自带的PCHC++20模块间的兼容问题.

PCH,Clang头模块C++20模块间理论上是兼容的,但缺乏足够的测试和实践,在早期开发ClangdC++20模块支持时,遇见的不少问题就是Clangd自带的PCHC++20模块二者交互时产生的问题.

当前在Clangd中,没有看到可关闭ClangdPCH功能的选项,之后可能可以增加一个(避免在使用导入TU中构建PCH).

支持思路

如前,在clangd中支持C++20模块,需要打开任意一个选项卡页时,找到其直接和间接导入的所有模块单元RequiredModuleUnits,然后找到或构建这些要求模块单元所对应的BMI,然后拼接编译选项即可.

第一个问题是,在源码当中,只能看到import a;import b;import c;此语句,并不能确定当前TUimport a;所指的a模块是由哪一个模块单元提供的.

为此,对每个选项卡页,会在看到导入扫描整个项目,以取从模块名模块单元的映射.会按缓存全局维护一个模块名模块单元的映射.

然后可递归地得到当前TU,直接依赖间接依赖模块单元RequiredModuleUnits.然后可自下而上地对要求模块单元中的模块单元执行GetOrBuildBMI逻辑.

并按缓存维护一个全局的模块名BMI文件的映射.

同时因为导入#include都可以说,是为当前TU引入依赖,导入也一般,会放在TU最前面,所以当前TU要求的BMI自然变成了当前TU预制件.

PCH不同,每个选项卡页只存一份到全局BMI文件的引用,避免了相同BMI文件的反复构建和冗余存储.

所以也需要在isPreambleCompatible中管理当前TU导入模块是否依然可复用.

可用编译器提供的能力来检测BMI是否过时.

以上,即是Clangd中支持C++20模块的全部高级逻辑了.其中真正最复杂的两个逻辑:"如何构建BMI"和"如何检测BMI是否过时"实际上都是Clang完成的,Clangd中只需要管理,如何调用BMI及如何管理BMI生命期了.

实现

本节说明ClangdC++20模块实现细节.

可用如下三个抽象来理解ClangdC++20模块支持的入口

1,项目模块,查询项目中的模块信息

2,PrerequisiteModules,一个文件要求的模块信息,也是预制件中要求的模块信息.

3,ModulesBuilder,构建模块文件(.pcm)

项目模块

cpp 复制代码
class ProjectModules {
public:
  using CommandMangler =
      llvm::unique_function<void(tooling::CompileCommand &, PathRef) const>;
  virtual std::vector<std::string> getRequiredModules(PathRef File) = 0;
  virtual std::string getModuleNameForSource(PathRef File) = 0;
  virtual std::string getSourceForModuleName(llvm::StringRef ModuleName, PathRef RequiredSrcFile) = 0;
  virtual void setCommandMangler(CommandMangler Mangler) {}
  virtual ~ProjectModules() = default;
};

项目模块用来查询项目中模块的信息.其中setCommandMangler(CommandMangler Mangler)用来处理clang处理不了的命令,一般来使用GCC编译的场景下.

其他接口来查看文件直接导入模块,查看一个文件的模块名及查看一个模块名对应的文件.

因为其生态位类似编译数据库,即为clangd提供项目的信息,所以接口上设计需要从GlobalCompilationDatabase项目模块.

ScanningAllProjectModules

cpp 复制代码
class ScanningAllProjectModules : public ProjectModules {
public:
  ScanningAllProjectModules(
      std::shared_ptr<const clang::tooling::CompilationDatabase> CDB,
      const ThreadsafeFS &TFS)
      : Scanner(CDB, TFS) {}
  ~ScanningAllProjectModules() override = default;
  std::vector<std::string> getRequiredModules(PathRef File) override;
  void setCommandMangler(CommandMangler Mangler) override;
     //并非故意使用`RequiredSourceFile`.
//细节见`ModuleDependencyScanner`的注释.
  std::string getSourceForModuleName(llvm::StringRef ModuleName, PathRef RequiredSourceFile) override;
  std::string getModuleNameForSource(PathRef File) override;
private:
  ModuleDependencyScanner Scanner;
  CommandMangler Mangler;
};
std::unique_ptr<ProjectModules> scanningProjectModules(
    std::shared_ptr<const clang::tooling::CompilationDatabase> CDB,
    const ThreadsafeFS &TFS) {
  return std::make_unique<ScanningAllProjectModules>(CDB, TFS);
}

ScanningAllProjectModules则实现了前述的扫描编译数据库所有文件的逻辑.实现底层调用了与clang-scan-deps中扫描模块相同的API.

CachingProjectModules

cpp 复制代码
class CachingProjectModules : public ProjectModules {
public:
  CachingProjectModules(std::unique_ptr<ProjectModules> MDB, ModuleNameToSourceCache &Cache);
  std::vector<std::string> getRequiredModules(PathRef File) override;
  std::string getModuleNameForSource(PathRef File) override;
  std::string getSourceForModuleName(llvm::StringRef ModuleName, PathRef RequiredSrcFile) override;
private:
  std::unique_ptr<ProjectModules> MDB;
  ModuleNameToSourceCache &Cache;
};

CachingProjectModules用来全局缓存模块名模块单元的映射.避免反复扫描所有文件造成浪费.

PrerequisiteModules

cpp 复制代码
class PrerequisiteModules {
public:
//在本`PrerequisiteModules`中,
//先更改命令,来加载记录的模块文件.
  virtual void
  adjustHeaderSearchOptions(HeaderSearchOptions &Options) const = 0;
//无论`构建模块文件`是否是最新的.
//注意,这只能在`构建模块文件`之后使用.
  virtual bool
  canReuse(const CompilerInvocation &CI,
       llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem>) const = 0;
  virtual ~PrerequisiteModules() = default;
};

PrerequisiteModules用来表示一个TU需要的模块信息.这里只输出两个接口,canReuse()用来查询这些BMI是否已过时,void adjustHeaderSearchOptions(HeaderSearchOptions &Options)则用来将BMI信息直接拼接到编译命令中.这里专门避免了泄露BMI信息.

如前,PrerequisiteModules的语义天然地属于一个TU前置信息,所以在预制数据中放置它.

cpp 复制代码
struct PreambleData {
  PreambleData(PrecompiledPreamble Preamble) : Preamble(std::move(Preamble)) {}
//本预制件所基于的解析输入版本.
  std::string Version;
  tooling::CompileCommand CompileCommand;
     //构建预制件时使用的目标选项.
//变更目标可能导致,反序化预制件时崩溃,
//这使得消费者(而无需重新解析`CompileCommand`)可用相同目标.  
  std::unique_ptr<TargetOptions> TargetOpts = nullptr;
  PrecompiledPreamble Preamble;
  std::vector<Diag> Diags;
     //像补全代码和转向定义此流程需要`#include`信息,
//而它们的编译操作跳过预制件范围.
  IncludeStructure Includes;

//在`#included``头文件`中捕捉`#include`映射信息.
  std::shared_ptr<const include_cleaner::PragmaIncludes> Pragmas;
+
//本预制件要求的模块文件的信息.
+ std::unique_ptr<PrerequisiteModules> RequiredModules;
 //宏定义在主文件的预制件节.
//用户关心的是`头文件`与主文件,而不是预制件与非预制件.
//应按主文件实例对待这些,如补全代码.  
  MainFileMacros Macros;

//在主文件的预制件节定义`Pragma`标记.
  std::vector<PragmaMark> Marks;

//构建预制件时执行的`FS`操作缓存.
//在重用预制件时,可消耗该缓存以节省`IO`.
  std::shared_ptr<PreambleFileStatusCache> StatCache;
 //主文件是否存在(可能不完整的)`include`护卫.
//需要"手动"将这些信息传播到`后续的解析`中.
  bool MainIsIncludeGuarded = false;
};

ModulesBuilder

cpp 复制代码
class ModulesBuilder {
public:
  ModulesBuilder(const GlobalCompilationDatabase &CDB);
  ~ModulesBuilder();
  ModulesBuilder(const ModulesBuilder &) = delete;
  ModulesBuilder(ModulesBuilder &&) = delete;
  ModulesBuilder &operator=(const ModulesBuilder &) = delete;
  ModulesBuilder &operator=(ModulesBuilder &&) = delete;
  std::unique_ptr<PrerequisiteModules>
  buildPrerequisiteModulesFor(PathRef File, const ThreadsafeFS &TFS);
private:
  class ModulesBuilderImpl;
  std::unique_ptr<ModulesBuilderImpl> Impl;
};

ModulesBuilderclangd实现C++20模块核心逻辑.主要逻辑都在ModulesBuilder.cpp中.

cpp 复制代码
std::unique_ptr<PrerequisiteModules>
ModulesBuilder::buildPrerequisiteModulesFor(PathRef File, const ThreadsafeFS &TFS) {
  std::unique_ptr<ProjectModules> MDB = Impl->getCDB().getProjectModules(File);
  if (!MDB) {
    elog("Failed to get Project Modules information for {0}", File);
    return std::make_unique<FailedPrerequisiteModules>();
  }
  CachingProjectModules CachedMDB(std::move(MDB), Impl->getProjectModulesCache());
  std::vector<std::string> RequiredModuleNames =
      CachedMDB.getRequiredModules(File);
  if (RequiredModuleNames.empty())
    return std::make_unique<ReusablePrerequisiteModules>();
  auto RequiredModules = std::make_unique<ReusablePrerequisiteModules>();
  for (llvm::StringRef RequiredModuleName : RequiredModuleNames) {
//如果出错,提前返回.
    if (llvm::Error Err = Impl->getOrBuildModuleFile( File, RequiredModuleName, TFS, CachedMDB, *RequiredModules.get())) {
      elog("Failed to build module {0}; due to {1}", RequiredModuleName, toString(std::move(Err)));
      return std::make_unique<FailedPrerequisiteModules>();
    }
  }
  return std::move(RequiredModules);
}

ModulesBuilder::buildPrerequisiteModulesFor会去调用ModulesBuilderImpl::getOrBuildModuleFile.

cpp 复制代码
llvm::Error ModulesBuilder::ModulesBuilderImpl::getOrBuildModuleFile(
    PathRef RequiredSource, StringRef ModuleName, const ThreadsafeFS &TFS,
    CachingProjectModules &MDB, ReusablePrerequisiteModules &BuiltModuleFiles) {
  if (BuiltModuleFiles.isModuleUnitBuilt(ModuleName))
    return llvm::Error::success();
  std::string ModuleUnitFileName =
      MDB.getSourceForModuleName(ModuleName, RequiredSource);
     //有可能遇见的是第三方模块(源码不在项目中,
//如`大多数`项目的`标准模块`可能是`第三方模块`),
//或是`项目模块`的实现有问题.
//`FIXME`:应该如何处理第三方模块?如果想忽略第三方模块,这里应该返回`真`而不是`假`.
//当前只是选出.
  if (ModuleUnitFileName.empty())
    return llvm::createStringError(
        llvm::formatv("Don't get the module unit for module {0}", ModuleName));
//尽量先从编译`数据库`里取`预构建的模块文件`.
//这帮助避免构建`编译器`已构建的模块文件.
  getPrebuiltModuleFile(ModuleName, ModuleUnitFileName, TFS, BuiltModuleFiles);
//按拓扑顺序取要求的模块.
  auto ReqModuleNames = getAllRequiredModules(RequiredSource, MDB, ModuleName);
  for (llvm::StringRef ReqModuleName : ReqModuleNames) {
    if (BuiltModuleFiles.isModuleUnitBuilt(ReqModuleName))
      continue;
    if (auto Cached = Cache.getModule(ReqModuleName)) {
      if (IsModuleFileUpToDate(Cached->getModuleFilePath(), BuiltModuleFiles, TFS.view(std::nullopt))) {
        log("Reusing module {0} from {1}", ReqModuleName, Cached->getModuleFilePath());
        BuiltModuleFiles.addModuleFile(std::move(Cached));
        continue;
      }
      Cache.remove(ReqModuleName);
    }
    std::string ReqFileName =
        MDB.getSourceForModuleName(ReqModuleName, RequiredSource);
    llvm::Expected<std::shared_ptr<BuiltModuleFile>> MF = buildModuleFile(
        ReqModuleName, ReqFileName, getCDB(), TFS, BuiltModuleFiles);
    if (llvm::Error Err = MF.takeError())
      return Err;
    log("Built module {0} to {1}", ReqModuleName, (*MF)->getModuleFilePath());
    Cache.add(ReqModuleName, *MF);
    BuiltModuleFiles.addModuleFile(std::move(*MF));
  }
  return llvm::Error::success();
}

删除已构建模块的情况,构建模块的逻辑一方面是先从构建命令中,取之前编译好的BMI(即如前的由构建系统构建的BMI),然后递归的扫描所有依赖的文件以取所有直接或间接导入模块单元.

之后若无法在缓存中,找到对应的BMI,则用buildModuleFile接口构建.

cpp 复制代码
//为`"模块名"`的模块构建一个模块文件.
//在`\param BuiltModuleFiles`中保存`构建模块文件`的信息.
llvm::Expected<std::shared_ptr<BuiltModuleFile>>
buildModuleFile(llvm::StringRef ModuleName, PathRef ModuleUnitFileName,
    const GlobalCompilationDatabase &CDB, const ThreadsafeFS &TFS,
    const ReusablePrerequisiteModules &BuiltModuleFiles) {
//如果有问题,尽早试便宜的操作,这样可便宜地解决.
  auto Cmd = CDB.getCompileCommand(ModuleUnitFileName);
  if (!Cmd)
    return llvm::createStringError(
        llvm::formatv("No compile command for {0}", ModuleUnitFileName));
  llvm::SmallString<256> ModuleFilesPrefix =
      getUniqueModuleFilesPath(ModuleUnitFileName);
  Cmd->Output = getModuleFilePath(ModuleName, ModuleFilesPrefix);
  ParseInputs Inputs;
  Inputs.TFS = &TFS;
  Inputs.CompileCommand = std::move(*Cmd);
  IgnoreDiagnostics IgnoreDiags;
  auto CI = buildCompilerInvocation(Inputs, IgnoreDiags);
  if (!CI)
    return llvm::createStringError("Failed to build compiler invocation");
  auto FS = Inputs.TFS->view(Inputs.CompileCommand.Directory);
  auto Buf = FS->getBufferForFile(Inputs.CompileCommand.Filename);
  if (!Buf)
    return llvm::createStringError("Failed to create buffer");
//在`Clang`的驱动中,将抑制`GMF`中`ODR`违规的检查.
//详见`Clang.cpp`中的`RenderModulesOptions`实现.
  CI->getLangOpts().SkipODRCheckInGMF = true;
//哈希`输入文件`内容,并在`BMI`文件中存储哈希值.
//这样当想重用`BMI`文件时,
//可检查这些文件是否仍有效.
  CI->getHeaderSearchOpts().ValidateASTInputFilesContent = true;
  BuiltModuleFiles.adjustHeaderSearchOptions(CI->getHeaderSearchOpts());
  CI->getFrontendOpts().OutputFile = Inputs.CompileCommand.Output;
  auto Clang =
      prepareCompilerInstance(std::move(CI), /*预制件=*/nullptr, std::move(*Buf), std::move(FS), IgnoreDiags);
  if (!Clang)
    return llvm::createStringError("Failed to prepare compiler instance");
  GenerateReducedModuleInterfaceAction Action;
  Clang->ExecuteAction(Action);
  if (Clang->getDiagnostics().hasErrorOccurred()) {
    std::string Cmds;
    for (const auto &Arg : Inputs.CompileCommand.CommandLine) {
      if (!Cmds.empty())
        Cmds += " ";
      Cmds += Arg;
    }
    clangd::vlog("Failed to compile {0} with command: {1}", ModuleUnitFileName, Cmds);
    std::string BuiltModuleFilesStr = BuiltModuleFiles.getAsString();
    if (!BuiltModuleFilesStr.empty())
      clangd::vlog("The actual used module files built by clangd is {0}", BuiltModuleFilesStr);
    return llvm::createStringError(
        llvm::formatv("Failed to compile {0}. Use '-log=verbose' to view "
      "detailed failure reasons. It is helpful to use "
      "'-debugmodulesbuilder' flag to keep the clangd's "
      "built module files to reproduce the failure for "
      "debugging. Remember to remove them after debugging.", ModuleUnitFileName));
  }
  return BuiltModuleFile::make(ModuleName, Inputs.CompileCommand.Output);
}

这段代码的核心逻辑即是从编译数据库中取编译选项,然后调用Clang,API编译.

其中

cpp 复制代码
BuiltModuleFiles.adjustHeaderSearchOptions(CI->getHeaderSearchOpts());

使用clangd构建的BMI替换编译数据库中提供的BMI.用户可理解为替换/增加编译命令中的-fmodule=<module-name>=<BMI-path>选项.

cpp 复制代码
CI->getHeaderSearchOpts().ValidateASTInputFilesContent = true;

这是允许编译器中的一致性检查功能.编译器在写BMI时会记录所有的输入文件(模块单元,即包含的文件).

然后当ValidateASTInputFilesContent选项为真时,当导入BMI时,编译器会检查此前纪录的所有文件哈希值是否依然相等.

重用

接上文,用编译器的能力,可试导入的方式测试是否依然可复用BMI.

cpp 复制代码
bool ReusablePrerequisiteModules::canReuse(
    const CompilerInvocation &CI,
    llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS) const {
  if (RequiredModules.empty())
    return true;
  llvm::SmallVector<llvm::StringRef> BMIPaths;
  for (auto &MF : RequiredModules)
    BMIPaths.push_back(MF->getModuleFilePath());
  return IsModuleFilesUpToDate(BMIPaths, *this, VFS);
}
bool IsModuleFileUpToDate(PathRef ModuleFilePath,
  const PrerequisiteModules &RequisiteModules,
  llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS) {
  HeaderSearchOptions HSOpts;
  RequisiteModules.adjustHeaderSearchOptions(HSOpts);
  HSOpts.ForceCheckCXX20ModulesInputFiles = true;
  HSOpts.ValidateASTInputFilesContent = true;
  clang::clangd::IgnoreDiagnostics IgnoreDiags;
  DiagnosticOptions DiagOpts;
  IntrusiveRefCntPtr<DiagnosticsEngine> Diags =
      CompilerInstance::createDiagnostics(*VFS, DiagOpts, &IgnoreDiags, /*`ShouldOwnClient=`*/false);
  LangOptions LangOpts;
  LangOpts.SkipODRCheckInGMF = true;
  FileManager FileMgr(FileSystemOptions(), VFS);
  SourceManager SourceMgr(*Diags, FileMgr);
  HeaderSearch HeaderInfo(HSOpts, SourceMgr, *Diags, LangOpts, /*目标=*/nullptr);
  PreprocessorOptions PPOpts;
  TrivialModuleLoader ModuleLoader;
  Preprocessor PP(PPOpts, *Diags, LangOpts, SourceMgr, HeaderInfo, ModuleLoader);
  IntrusiveRefCntPtr<ModuleCache> ModCache = createCrossProcessModuleCache();
  PCHContainerOperations PCHOperations;
  CodeGenOptions CodeGenOpts;
  ASTReader Reader(PP, *ModCache, /*`ASTContext=`*/nullptr, PCHOperations.getRawReader(), CodeGenOpts, {});
//不需要任何监听器.
//默认,它会使用验证`监听器`.
  Reader.setListener(nullptr);
  if (Reader.ReadAST(ModuleFilePath, serialization::MK_MainFile, SourceLocation(), ASTReader::ARR_None) != ASTReader::Success)
    return false;
  bool UpToDate = true;
  Reader.getModuleManager().visit([&](serialization::ModuleFile &MF) -> bool {
    Reader.visitInputFiles(
        MF, /*`IncludeSystem=`*/false, /*抱怨=*/false,
        [&](const serialization::InputFile &IF, bool isSystem) {
          if (!IF.getFile() || IF.isOutOfDate())
            UpToDate = false;
        });
    return !UpToDate;
  });
  return UpToDate;
}
bool IsModuleFilesUpToDate(
    llvm::SmallVector<PathRef> ModuleFilePaths,
    const PrerequisiteModules &RequisiteModules,
    llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS) {
  return llvm::all_of(
      ModuleFilePaths, [&RequisiteModules, VFS](auto ModuleFilePath) {
        return IsModuleFileUpToDate(ModuleFilePath, RequisiteModules, VFS);
      });
}

该做法最大的缺点是加载BMI的成本过高.这里应该有充足的优化空间.

未来待办

至此,已介绍完了当前ClangdC++20模块的支持原理和情况.大家在遇见问题时可先根据自己的情况试修复.除了修复漏洞外,其他能加入的优化和特征:

重用优化

如上,当前的ModulesBMIReuse的实现依赖编译器加载BMI后再给出结果.太慢了.可能可在Clangd中,记录每个BMI涉及到的文件,然后重用直接比较这些文件.

扫描优化

现在虽然有扫描缓存,但对超大规模项目可能会因为长时扫描有比较长的热身时间.可试一些方式优化.

后缀优化

一个简单可行的优化是,在找模块名模块单元的映射时,可只扫描带特殊后缀(.cppm,.ixx)的文件.

LazyScanning

另一个优化避免全局扫描,而是根据需要查询模块名,来按需扫描,如当需要a模块时,当发现一个模块单元声明了a模块,就可停止扫描返回该结果了.

该优化可和后缀优化一起使用.

BMI持久化

现在clangd为了避免资源泄漏,会在关闭制表符时减少此制表符要求的BMI的引用.当一个BMI的引用归0时,就会删掉该BMI.

同时直接关闭clangd时,也会删除clangd构建的所有BMI.

这导致关闭clangd再打开clangd后,可能会等待较长的时间构建许多BMI.可设计一些策略,在避免资源泄漏时,将BMI持久地存储下来.

模块的恰当编译数据库

在前文,我有意地避免提及当前clangdC++20模块支持的两个缺陷,这两个缺陷需要更好的编译数据库来支持.相关工作有p2977r2.这里

重复的模块名

考虑如下项目:

cpp 复制代码
//`a.cppm`
export module a;
//`a.v1.cppm`
export module a;
//`main.cc`
import a;
int main() { return 0; }
//==
add_executable(exe main.cc a.cppm)
add_library(a a.v1.cppm)
//(`假cmake`)

这是个正确的项目吗?该项目在不同TU中包含了重复的模块.该项目是正确的.因为ISOCPP的规定是,在链接的程序中,禁止有重名的模块.

未链接在一起,则重名并不影响正确性.

clangd中,错误的假设了整个项目不包含重名的模块.在当前的设计中,clangd也没法处理不同模块单元声明相同模块.

因为clangd没有足够的信息来判断,对特定的TU,哪一个模块单元才是正确的.

clangd需要构建系统提供更细粒度编译信息才可做出正确的判断.

相同模块单元的不同编译选项

cpp 复制代码
//`a.cc``编译时`用`std=c++23`
import std;
...
//`b.cc``编译时`用`std=c++26`
import std;
...

本例中,最好,需要将为std模块提供-std=c++23-std=c++26两个版本的BMI.但当前缺乏足够的信息.

除了依赖构建系统提供更好的编译数据库外,对它clangd可能可做一些更有效率的猜测.

BMI构建库

如上,clangd支持模块的逻辑,其实和很多其他工具支持模块要求的逻辑是高度类似的.此时,按一个单独的库抽象这些逻辑会好很多.

不然clangtidy等工具可能每个都得类似处理.

总结

本文介绍了ClangdC++20模块支持的方法和现状,希望可鼓励更多人参与到ClangdC++20模块的支持中,也希望对大家使用clangdC++20模块有帮助

相关推荐
han_hanker1 小时前
泛型的基本语法
java·开发语言
Jurio.2 小时前
Python Ray 分布式计算应用
linux·开发语言·python·深度学习·机器学习
廋到被风吹走2 小时前
【Java】Exception 异常体系解析 从原理到实践
java·开发语言
Pyeako2 小时前
python网络爬虫
开发语言·爬虫·python·requsets库
diegoXie2 小时前
【Python】 中的 * 与 **:Packing 与 Unpacking
开发语言·windows·python
老王熬夜敲代码3 小时前
C++中的thread
c++·笔记·面试
qq_479875433 小时前
C++ 鸭子类型” (Duck Typing)
开发语言·c++
崇山峻岭之间3 小时前
C++ Prime Plus 学习笔记033
c++·笔记·学习
暗然而日章4 小时前
C++基础:Stanford CS106L学习笔记 7 类
c++·笔记·学习