使用 Clang API 编译 C++
在 C++ 开发的过程中,我们通常使用命令行工具如 g++ 或 clang 来编译源代码。然而,在某些特定的场景下,比如构建自定义的编译工具链、集成编译功能到某个应用程序中,或者需要对编译过程进行深度定制时,直接调用编译器的 API 就显得尤为必要。今天,我们就来深入探讨如何利用 Clang API 来编译 C++ 源文件,将其转化为目标文件。
一、为何使用 Clang API
Clang 作为一款广泛使用的 C++ 编译器前端,不仅性能卓越,而且提供了丰富的 API 接口,允许开发者在其基础上进行扩展和定制。通过直接使用 Clang API,我们可以更加精细地控制编译过程的各个环节,从词法分析、语法分析到代码生成等。这为那些需要深度定制编译流程的项目提供了极大的灵活性。例如,你可以在这个过程中加入自定义的代码检查、优化步骤,甚至是特定的代码生成逻辑,以满足项目的独特需求。
二、代码实现
以下是一个使用 Clang API 实现的简化版编译器代码,它能够处理 -c
(编译为对象文件)和 -S
(编译为汇编文件)选项:
#include <clang/CodeGen/CodeGenAction.h>
#include <clang/Driver/Compilation.h>
#include <clang/Driver/Driver.h>
#include <clang/Frontend/CompilerInstance.h>
#include <clang/Frontend/FrontendOptions.h>
#include <llvm/Config/llvm-config.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Support/VirtualFileSystem.h>
using namespace clang;
constexpr llvm::StringRef kTargetTriple = "x86_64-unknown-linux-gnu";
namespace {
struct DiagsSaver : DiagnosticConsumer {
std::string message;
llvm::raw_string_ostream os{message};
void HandleDiagnostic(DiagnosticsEngine::Level diagLevel, const Diagnostic &info) override {
DiagnosticConsumer::HandleDiagnostic(diagLevel, info);
const char *level;
switch (diagLevel) {
default:
return;
case DiagnosticsEngine::Note:
level = "note";
break;
case DiagnosticsEngine::Warning:
level = "warning";
break;
case DiagnosticsEngine::Error:
case DiagnosticsEngine::Fatal:
level = "error";
break;
}
llvm::SmallString<256> msg;
info.FormatDiagnostic(msg);
auto &sm = info.getSourceManager();
auto loc = info.getLocation();
auto fileLoc = sm.getFileLoc(loc);
os << sm.getFilename(fileLoc) << ':' << sm.getSpellingLineNumber(fileLoc)
<< ':' << sm.getSpellingColumnNumber(fileLoc) << ": " << level << ": "
<< msg << '\n';
if (loc.isMacroID()) {
loc = sm.getSpellingLoc(loc);
os << sm.getFilename(loc) << ':' << sm.getSpellingLineNumber(loc) << ':'
<< sm.getSpellingColumnNumber(loc) << ": note: expanded from macro\n";
}
}
};
} // namespace
static std::pair<bool, std::string> compile(int argc, char *argv[]) {
auto fs = llvm::vfs::getRealFileSystem();
DiagsSaver dc;
std::vector<const char *> args{"clang"};
args.insert(args.end(), argv + 1, argv + argc);
auto diags = CompilerInstance::createDiagnostics(
#if LLVM_VERSION_MAJOR >= 20
*fs,
#endif
new DiagnosticOptions, &dc, false);
driver::Driver d(args[0], kTargetTriple, *diags, "cc", fs);
d.setCheckInputsExist(false);
std::unique_ptr<driver::Compilation> comp(d.BuildCompilation(args));
const auto &jobs = comp->getJobs();
if (jobs.size() != 1)
return {false, "only support one job"};
const llvm::opt::ArgStringList &ccArgs = jobs.begin()->getArguments();
auto invoc = std::make_unique<CompilerInvocation>();
CompilerInvocation::CreateFromArgs(*invoc, ccArgs, *diags);
auto ci = std::make_unique<CompilerInstance>();
ci->setInvocation(std::move(invoc));
ci->createDiagnostics(*fs, &dc, false);
ci->getDiagnostics().getDiagnosticOptions().ShowCarets = false;
ci->createFileManager(fs);
ci->createSourceManager(ci->getFileManager());
LLVMInitializeX86AsmParser();
LLVMInitializeX86AsmPrinter();
LLVMInitializeX86Target();
LLVMInitializeX86TargetInfo();
LLVMInitializeX86TargetMC();
switch (ci->getFrontendOpts().ProgramAction) {
case frontend::ActionKind::EmitObj: {
EmitObjAction action;
ci->ExecuteAction(action);
break;
}
case frontend::ActionKind::EmitAssembly: {
EmitAssemblyAction action;
ci->ExecuteAction(action);
break;
}
default:
return {false, "unhandled action"};
}
return {true, std::move(dc.message)};
}
int main(int argc, char *argv[]) {
auto [ok, err] = compile(argc, argv);
llvm::errs() << err;
}
这段代码的核心功能是创建一个编译实例,设置诊断信息处理方式,初始化目标架构相关组件,然后根据用户指定的选项(生成对象文件或汇编文件)来执行相应的编译动作。它还自定义了一个诊断信息处理类 DiagsSaver
,用于捕获编译过程中的各种诊断信息(如警告、错误等),并将这些信息格式化为便于阅读的字符串输出。
三、使用 CMake 构建代码
为了能够顺利地编译上述代码,我们需要一个合适的 CMake 配置文件来链接所需的 Clang 和 LLVM 库:
project(cc)
cmake_minimum_required(VERSION 3.16)
find_package(LLVM REQUIRED CONFIG)
find_package(Clang REQUIRED CONFIG)
include_directories(${LLVM_INCLUDE_DIRS} ${CLANG_INCLUDE_DIRS})
add_executable(cc main.cc)
if(NOT LLVM_ENABLE_RTTI)
target_compile_options(cc PRIVATE -fno-rtti)
endif()
if(CLANG_LINK_CLANG_DYLIB)
target_link_libraries(cc PRIVATE clang-cpp)
else()
target_link_libraries(cc PRIVATE
clangAST
clangBasic
clangCodeGen
clangDriver
clangFrontend
clangLex
clangParse
clangSema
)
endif()
if(LLVM_LINK_LLVM_DYLIB)
target_link_libraries(cc PRIVATE LLVM)
else()
target_link_libraries(cc PRIVATE
LLVMOption
LLVMSupport
LLVMTarget
LLVMX86AsmParser
LLVMX86CodeGen
LLVMX86Desc
LLVMX86Info
)
endif()
这个 CMake 配置文件的作用是定位系统中安装的 LLVM 和 Clang 库,设置项目的基本属性,并指定需要链接的库列表。通过这种方式,我们可以确保在编译过程中能够正确地找到并链接所有依赖的库文件,从而使项目能够成功构建。
四、构建与运行
首先,确保已经正确安装了 LLVM 和 Clang 开发库。这通常可以通过系统包管理器来完成,或者也可以自行从源码构建 LLVM 和 Clang。对于自行构建的情况,可以使用以下命令:
cmake ... -DLLVM_ENABLE_PROJECTS='clang'
ninja -C out/stable clang-cmake-exports clang
接下来,创建一个构建目录,并在其中运行 CMake 配置命令:
cmake -S. -Bout/debug -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=$HOME/Stable/bin/clang++ -DCMAKE_PREFIX_PATH="$HOME/llvm/out/stable"
ninja -C out/debug
这里,我们将预构建的 Clang 设置为 C++ 编译器,并指定了 LLVM 的安装路径。通过这种方式,可以确保构建过程使用正确的工具链和库文件。
完成构建后,就可以使用生成的可执行文件来编译 C++ 源文件了。例如:
echo 'void f() {}' > a.cc
out/debug/cc -S a.cc && head -n 5 a.s
out/debug/cc -c a.cc && ls a.o
第一条命令将生成汇编文件 a.s
,并显示其前五行内容;第二条命令将生成对象文件 a.o
,并列出该文件的相关信息。通过这些简单的示例,我们可以验证编译器是否能够正确地工作。
五、匿名文件处理
在实际的编译过程中,输入源文件和输出的 ELF 文件通常会存储在文件系统中。为了更好地管理这些临时文件,可以使用 llvm::FileRemover
类来创建临时文件,并在合适的时候自动删除它们:
std::error_code ec = llvm::sys::fs::createTemporaryFile("clang", "cc", fdIn, tempPath);
llvm::raw_fd_stream osIn(fdIn, true);
llvm::FileRemover remover(tempPath);
在 Linux 系统上,还可以利用 memfd_create
函数在内存中创建具有易失性存储支持的文件,从而避免频繁的磁盘 I/O 操作,提高性能:
int fdIn = memfd_create("input", MFD_CLOEXEC);
// 错误处理
int fdOut = memfd_create("output", MFD_CLOEXEC);
// 错误处理
std::string pathIn = "/proc/self/fd/" + std::to_string(fdIn);
std::string pathOut = "/proc/self/fd/" + std::to_string(fdOut);
这种方式特别适用于需要频繁创建和销毁临时文件的场景,能够有效提升程序的运行效率。
六、LLVM 目标架构初始化
为了能够生成 x86 架构的代码,需要初始化几个关键的 LLVM X86 库组件:
LLVMInitializeX86AsmPrinter();
LLVMInitializeX86Target();
LLVMInitializeX86TargetInfo();
LLVMInitializeX86TargetMC();
如果代码中使用了内联汇编功能,还需要额外初始化汇编解析器库:
LLVMInitializeX86AsmParser();
这些初始化函数的作用是注册相应的目标架构组件,使得 LLVM 能够正确地处理代码生成过程中的各种架构特定细节。通过调用这些函数,我们可以确保编译器能够为目标架构生成正确的机器代码。
七、支持的前端动作
目前,该代码支持两种前端动作:EmitAssembly
(生成汇编文件,对应 -S
选项)和 EmitObj
(生成对象文件,对应 -c
选项)。在代码中,根据用户指定的动作类型,选择执行相应的编译操作。这种设计使得编译器能够灵活地适应不同的编译需求,无论是生成中间的汇编代码以便进一步分析和调试,还是直接生成目标文件用于链接和执行,都能轻松实现。
八、诊断信息处理
Clang 的诊断系统相对复杂,涉及多个组件的协同工作。其中包括 DiagnosticConsumer
(用于消费和处理诊断信息)、DiagnosticsEngine
(诊断引擎,负责生成和管理诊断信息)以及 DiagnosticOptions
(用于配置诊断信息的输出选项)等。在我们的代码中,自定义了一个简单的 DiagnosticConsumer
类 DiagsSaver
,用于处理编译过程中产生的各种诊断信息,包括提示信息、警告、错误以及致命错误等。
当涉及到宏展开时,诊断信息还需要报告两个关键位置:一个是物理位置(fileLoc
),即触发问题的展开令牌所在的位置,这与 Clang 的错误行信息相匹配;另一个是宏替换列表中的拼写位置(通过 sm.getSpellingLoc(loc)
获取)。虽然 Clang 还会为链式展开的中间位置提供高亮显示,但我们的简单方法已经能够提供一个相当不错的近似结果。
void HandleDiagnostic(DiagnosticsEngine::Level diagLevel, const Diagnostic &info) override {
DiagnosticConsumer::HandleDiagnostic(diagLevel, info);
const char *level;
switch (diagLevel) {
default:
return;
case DiagnosticsEngine::Note:
level = "note";
break;
case DiagnosticsEngine::Warning:
level = "warning";
break;
case DiagnosticsEngine::Error:
case DiagnosticsEngine::Fatal:
level = "error";
break;
}
llvm::SmallString<256> msg;
info.FormatDiagnostic(msg);
auto &sm = info.getSourceManager();
auto loc = info.getLocation();
auto fileLoc = sm.getFileLoc(loc);
os << sm.getFilename(fileLoc) << ':' << sm.getSpellingLineNumber(fileLoc)
<< ':' << sm.getSpellingColumnNumber(fileLoc) << ": " << level << ": "
<< msg << '\n';
if (loc.isMacroID()) {
loc = sm.getSpellingLoc(loc);
os << sm.getFilename(loc) << ':' << sm.getSpellingLineNumber(loc) << ':'
<< sm.getSpellingColumnNumber(loc) << ": note: expanded from macro\n";
}
}
这段代码展示了如何捕获和格式化诊断信息,以便在控制台中输出易于理解和定位的错误和警告信息。这对于开发者在调试和优化代码时具有重要意义,能够帮助他们快速找到问题所在并进行修正。
九、总结与展望
通过使用 Clang API,我们成功地实现了一个能够编译 C++ 源文件并生成目标文件或汇编文件的简化编译器。这一过程不仅让我们深入理解了 Clang 编译器的内部工作机制,还展示了如何利用其强大的 API 来构建自定义的编译工具。这对于那些需要对编译流程进行深度定制的项目具有重要的实践意义。
在未来的工作中,可以进一步扩展和完善这一编译器,例如增加对更多编译选项的支持、优化诊断信息的处理和输出、提升对不同架构和平台的兼容性等。此外,还可以探索如何将这一技术应用到实际的开发工具链中,为开发者提供更加高效、灵活和强大的编译解决方案。随着 C++ 语言的不断发展和演进,对编译技术的要求也会越来越高,这将促使我们不断探索和创新,以适应新的需求和挑战。
科技脉搏,每日跳动。
与敖行客 Allthinker一起,创造属于开发者的多彩世界。
- 智慧链接 思想协作 -