使用 Clang 重编 UE5 引擎并实现源码级代码覆盖率插桩
摘要: 本文记录了在 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 的链接器和运行时库。

第三步:修改 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能把计数器数据对应回具体的源文件和行号

修改 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),需要相应修改路径中的版本号。

重编 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 动态获取。
总结
整个过程的核心思路:
- 安装 LLVM — 提供 clang-cl 编译器和覆盖率工具链
- 配置 UBT 使用 Clang — XML 配置切换编译器
- 修改 VCToolChain.cs — 补上 Epic 漏掉的最后一步,让
-CodeCoverage标志真正注入 Clang 参数 - 编译验证 — 先不带覆盖率验证 Clang 编译通过,再加
-CodeCoverage重编 - 采集数据 — 设置
LLVM_PROFILE_FILE环境变量,正常运行后自动生成.profraw - 生成报告 —
llvm-profdata merge+llvm-cov show得到行级覆盖率
本文基于 UE5.5.4 + LLVM 18.1.8 + Windows 11 环境验证,所有步骤均实际跑通。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)