摘要: 本文记录了在 Windows 11 环境下,将 UE5 引擎从 MSVC 编译切换到 Clang-cl 编译,并利用 LLVM Source-based Code Coverage 对引擎进行覆盖率插桩的完整过程。

背景与动机

Unreal Engine 官方提供了完善的自动化测试框架,但并没有开箱即用的代码覆盖率支持。如果你在做引擎级别的测试,你可能需要知道测试到底覆盖了引擎的哪些代码。

在 Windows 上,MSVC 的覆盖率工具生态不如 LLVM。而 UE5 从源码层面支持使用 Clang-cl(LLVM 的 MSVC 兼容前端)在 Windows 上编译。因此,可以切换到 Clang 编译引擎,再利用 LLVM 的 -fprofile-instr-generate -fcoverage-mapping 进行插桩

环境信息

组件 本项目使用的版本 说明
操作系统 Windows 11
Unreal Engine 5.5.4(源码构建) 从 Epic Games GitHub 拉取
Visual Studio 2022 Professional 17.10 LTSC
MSVC 工具链 v14.38.33130 UE5.5 推荐版本
Windows SDK 10.0.22621.0
LLVM/Clang 18.1.8 UE5.5 对应版本

⚠️ 关于版本匹配: 不同 UE 版本对应不同的 LLVM 版本。例如 UE5.6 对应 LLVM 18.1.8(Windows),UE5.3 对应 LLVM 14.0.1。请查阅你所使用的 UE 版本的 Release Notes 中 “Platform SDK Upgrades” 部分,或者参考 unreal-clangd 项目的版本对照表 来确认精确版本。

前提条件

  • 你已经有一份可以通过 MSVC 正常编译并运行的 UE5 源码构建
  • 你有 Visual Studio 2022 和对应的 MSVC 工具链
  • 你有足够的磁盘空间(Clang 重编会生成大量中间文件)

第一步:安装 LLVM

本文使用的UE版本是5.5.4,对应 LLVM 的 18.1.8 版本

前往 LLVM 的 GitHub Release 页面下载 Windows 安装包:

https://github.com/llvm/llvm-project/releases/tag/llvmorg-18.1.8

下载文件:LLVM-18.1.8-win64.exe

注意: Release 页面上有很多 .tar.xz 文件,那些是 Linux/Mac 的。往下翻找到 .exe 结尾的 Windows 安装包。

安装时:

  • 安装路径保持默认 C:\Program Files\LLVM
  • 勾选 “Add LLVM to the system PATH for all users”

安装完成后,打开新的终端验证:

clang-cl --version
# 预期输出:clang version 18.1.8

llvm-profdata --version
# 预期输出:llvm-profdata, part of LLVM 18.1.8

llvm-cov --version
# 预期输出:llvm-cov, part of LLVM 18.1.8

同时确认覆盖率运行时库存在:

Test-Path "C:\Program Files\LLVM\lib\clang\18\lib\windows\clang_rt.profile-x86_64.lib"
# 预期输出:True

第二步:配置 UBT 切换编译器为 Clang

编辑(或创建)以下路径的文件:

%APPDATA%\Unreal Engine\UnrealBuildTool\BuildConfiguration.xml

完整路径类似:C:\Users\<你的用户名>\AppData\Roaming\Unreal Engine\UnrealBuildTool\BuildConfiguration.xml

写入以下内容:

<?xml version="1.0" encoding="utf-8" ?>
<Configuration xmlns="https://www.unrealengine.com/BuildConfiguration">
    <WindowsPlatform>
        <Compiler>Clang</Compiler>
    </WindowsPlatform>
</Configuration>

这一行 <Compiler>Clang</Compiler> 告诉 UnrealBuildTool(UBT)使用 clang-cl 作为 C++ 编译前端,同时仍然使用 MSVC 的链接器和运行时库。

image-20260409215125686


第三步:修改 VCToolChain.cs — 补全覆盖率插桩逻辑

这是整个过程中唯一需要修改引擎源码的地方

UE5.5 的 UBT 内部已经有完整的 bCodeCoverage 标志传递链路:

TargetRules (-CodeCoverage CLI 参数)
  → CppCompileEnvironment.bCodeCoverage
  → LinkEnvironment.bCodeCoverage

也就是说,从命令行参数到编译/链接环境的配置对象,Epic 已经全部做好了。但是,VCToolChain.cs(真正生成 clang-cl 编译命令的地方)完全没有读取这个标志——它被传递了一路,最后静默地被忽略了。

我们只需要在 VCToolChain.cs 中补上"读取标志 → 注入 Clang 参数"这最后一步。

文件位置

Engine\Source\Programs\UnrealBuildTool\Platform\Windows\VCToolChain.cs

修改 1:注入编译参数

AppendCLArguments_Global 方法中,找到 IsClang() 分支内的 PGO 代码块(搜索 bPGOProfile),在该代码块之后添加:

// Code coverage instrumentation
if (CompileEnvironment.bCodeCoverage)
{
    Arguments.Add("-fprofile-instr-generate");
    Arguments.Add("-fcoverage-mapping");
}

这两个参数的作用:

  • -fprofile-instr-generate:让编译器插入运行时计数器,程序运行后会记录每行代码的执行次数
  • -fcoverage-mapping:在二进制中嵌入源码位置映射信息,让 llvm-cov 能把计数器数据对应回具体的源文件和行号

image-20260409215339727

修改 2:链接覆盖率运行时库

AppendLinkArguments 方法末尾(ASan 代码块之后、方法关闭大括号之前)添加:

// Code coverage - link profile runtime
if (LinkEnvironment.bCodeCoverage && Target.WindowsPlatform.Compiler.IsClang())
{
    Arguments.Add("\"C:\\Program Files\\LLVM\\lib\\clang\\18\\lib\\windows\\clang_rt.profile-x86_64.lib\"");
}

这个 clang_rt.profile-x86_64.lib 是 LLVM 的 compiler-rt 运行时库中的 profile 模块,包含了覆盖率计数器的写入逻辑。没有它,程序运行时虽然有插桩代码,但无法把数据写入 .profraw 文件。

注意: 这里硬编码了 LLVM 18 的路径。如果你的 LLVM 版本不同(比如 17 或 19),需要相应修改路径中的版本号。

image-20260409215422199

重编 UBT

修改保存后,需要重新编译 UBT 本身,使修改生效:

dotnet build "Engine\Source\Programs\UnrealBuildTool\UnrealBuildTool.csproj"

确认输出中显示 Build succeeded


第四步:用 Clang 编译引擎(先不插桩)

建议先验证 Clang 能正常编译和运行引擎,再加覆盖率。

清理 MSVC 中间文件

从 MSVC 切换到 Clang,中间文件格式不兼容,必须清理:

Remove-Item -Recurse -Force "Engine\Intermediate\Build\Win64"

编译引擎

Engine\Build\BatchFiles\Build.bat UnrealEditor Win64 Development

编译日志开头应该显示:

Using Clang 18.1.8 compiler (C:\Program Files\LLVM) with Visual Studio 2022 14.38.33145 runtime

看到这行就确认编译器已经切换为 Clang。全引擎首次编译约 6000+ 个动作,耗时 1-3 小时。

验证运行

启动编辑器确认正常。

可能遇到的问题: 如果启动时报 UnrealEditor-libpas.dll 找不到,执行:

Copy-Item "Engine\Binaries\Win64\NotForLicensees\UnrealEditor-libpas.dll" "Engine\Binaries\Win64\"

这是因为 libpas(UE5.5 新增的内存分配器)的 DLL 生成在 NotForLicensees 子目录下,运行时搜索路径找不到。


第五步:带覆盖率插桩重编

确认 Clang 编译的引擎能正常运行后,加上 -CodeCoverage 标志重编。

# 清理中间文件
Remove-Item -Recurse -Force "Engine\Intermediate\Build\Win64"

# 编译引擎(带覆盖率)
Engine\Build\BatchFiles\Build.bat UnrealEditor Win64 Development -CodeCoverage

# 编译项目(带覆盖率)
Engine\Build\BatchFiles\Build.bat <YourProject>Editor Win64 Development -project="<项目路径>\<YourProject>.uproject" -CodeCoverage

-CodeCoverage 标志会通过 UBT 的内置传递链路,最终触发我们在第三步中添加的代码,往每个编译命令中注入 -fprofile-instr-generate -fcoverage-mapping


第六步:运行并收集覆盖率数据

设置输出路径

New-Item -ItemType Directory -Force -Path "C:\CoverageData"
$env:LLVM_PROFILE_FILE = "C:\CoverageData\coverage-%p-%m.profraw"

环境变量 LLVM_PROFILE_FILE 指定 .profraw 文件的输出位置。%p 是进程 ID,%m 是二进制签名,避免多个模块的数据互相覆盖。

启动编辑器

Engine\Binaries\Win64\UnrealEditor.exe "<项目路径>\<YourProject>.uproject"

正常使用编辑器——加载场景、运行测试、触发你想测试的功能。

正常关闭编辑器

这一步至关重要。 LLVM 覆盖率运行时通过 atexit() 钩子在进程正常退出时写入 .profraw 文件。如果强制杀进程或引擎崩溃,数据会丢失。

验证数据生成

Get-ChildItem "C:\CoverageData\*.profraw"

你应该看到多个 .profraw 文件(引擎的每个 DLL 模块会生成独立的文件)。在我们的验证中,一次正常使用后生成了数百个 profraw 文件。


第七步:生成覆盖率报告(可选)

合并原始数据

llvm-profdata merge -sparse C:\CoverageData\*.profraw -o C:\CoverageData\merged.profdata

生成 HTML 报告

你需要指定包含覆盖率数据的二进制文件(DLL)。以渲染相关模块为例:

llvm-cov show ^
    -object="Engine\Binaries\Win64\UnrealEditor-Renderer.dll" ^
    -object="Engine\Binaries\Win64\UnrealEditor-RenderCore.dll" ^
    -object="Engine\Binaries\Win64\UnrealEditor-RHI.dll" ^
    -object="Engine\Binaries\Win64\UnrealEditor-D3D12RHI.dll" ^
    -instr-profile=C:\CoverageData\merged.profdata ^
    -format=html ^
    -output-dir=C:\CoverageReport ^
    -ignore-filename-regex=".*ThirdParty.*"

打开 C:\CoverageReport\index.html 即可查看详细的行级覆盖率报告。

文本摘要

如果只需要快速查看覆盖率百分比:

llvm-cov report ^
    -object="Engine\Binaries\Win64\UnrealEditor-Renderer.dll" ^
    -object="Engine\Binaries\Win64\UnrealEditor-RenderCore.dll" ^
    -object="Engine\Binaries\Win64\UnrealEditor-RHI.dll" ^
    -instr-profile=C:\CoverageData\merged.profdata

JSON 导出(CI 集成)

llvm-cov export ^
    -object="Engine\Binaries\Win64\UnrealEditor-Renderer.dll" ^
    -instr-profile=C:\CoverageData\merged.profdata ^
    > C:\CoverageData\coverage.json

已知问题

1. libpas.dll 路径问题

UE5.5 新增的 libpas 内存分配器的 DLL 生成在 Engine\Binaries\Win64\NotForLicensees\ 子目录下,编辑器启动时搜索路径找不到它。解决方法是手动复制到 Engine\Binaries\Win64\ 目录下。

2. Unity Build 与覆盖率精度

UE 默认开启 Unity Build(将多个 .cpp 合并为一个编译单元加速编译)。LLVM 的 source-based coverage 通过 -fcoverage-mapping 记录的是预处理前的源码位置,理论上能映射回原始文件,但 Unity Build 可能在某些边缘情况下导致覆盖率数据不精确。

如果需要最高精度的覆盖率报告,可以在 BuildConfiguration.xml 中关闭 Unity Build:

<BuildConfiguration>
    <bUseUnityBuild>false</bUseUnityBuild>
</BuildConfiguration>

注意:关闭后编译动作数会从约 6000 增加到 20000+,编译时间会显著增加。并且在我们的实际测试中,关闭 Unity Build 后出现了 0xc000007b(DLL 格式错误)的运行时问题,尚未完全解决。建议先使用默认的 Unity Build 模式验证覆盖率流程,再按需尝试关闭。

3. clang_rt.profile 路径硬编码

当前的 VCToolChain.cs 修改中,clang_rt.profile-x86_64.lib 的路径是硬编码的。如果你的 LLVM 安装位置不同或版本号不同,需要手动修改。如果是团队或 CI 环境使用,建议改为通过环境变量或 UBT 的 EnvVars.ToolChainDir 动态获取。


总结

整个过程的核心思路:

  1. 安装 LLVM — 提供 clang-cl 编译器和覆盖率工具链
  2. 配置 UBT 使用 Clang — XML 配置切换编译器
  3. 修改 VCToolChain.cs — 补上 Epic 漏掉的最后一步,让 -CodeCoverage 标志真正注入 Clang 参数
  4. 编译验证 — 先不带覆盖率验证 Clang 编译通过,再加 -CodeCoverage 重编
  5. 采集数据 — 设置 LLVM_PROFILE_FILE 环境变量,正常运行后自动生成 .profraw
  6. 生成报告llvm-profdata merge + llvm-cov show 得到行级覆盖率

本文基于 UE5.5.4 + LLVM 18.1.8 + Windows 11 环境验证,所有步骤均实际跑通。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐