LLVM's C++ API doesn't offer a stability guarantee. This means function signatures can change or be removed between versions, forcing projects to adapt.
On the other hand, LLVM has an extensive API surface. When a library
like llvm/lib/Y
relies functionality from another library,
the API is often exported in header files under
llvm/include/llvm/X/
, even if it is not intended to be
user-facing.
To be compatible with multiple LLVM versions, many projects rely on
#if
directives based on the LLVM_VERSION_MAJOR
macro. This post explores the specific techniques used by ccls to ensure
compatibility with LLVM versions 7 to 19. For the latest release (ccls
0.20241108), support for LLVM versions 7 to 9 has been
discontinued.
Given the tight coupling between LLVM and Clang, the
LLVM_VERSION_MAJOR
macro can be used for both version
detection. There's no need to check
CLANG_VERSION_MAJOR
.
Changed namespaces
In Oct 2018, https://reviews.llvm.org/D52783 moved the namespace
clang::vfs
to llvm::vfs
. To remain
compatibility, I renamed clang::vfs
uses and added a
conditional namespace alias:
1 |
|
Removed functions
In March 2019, https://reviews.llvm.org/D59377 removed the member
variable VirtualFileSystem
and removed
setVirtualFileSystem
. To adapt to this change, ccls employs
an #if
. 1
2
3
4
5
6
Clang->createFileManager(FS);
Clang->setVirtualFileSystem(FS);
Clang->createFileManager();
Changed function parameters
In April 2020, the LLVM monorepo integrated a new subproject: flang.
flang developers made many changes to clangDriver to reuse it for flang.
https://reviews.llvm.org/D86089 changed the constructor
clang::driver::Driver
. I added 1
2
3
4
5
driver::Driver d(args[0], llvm::sys::getDefaultTargetTriple(), *diags, vfs);
driver::Driver d(args[0], llvm::sys::getDefaultTargetTriple(), *diags, "ccls", vfs);
In November 2020, https://reviews.llvm.org/D90890 changed an argument of
ComputePreambleBounds
from
const llvm::MemoryBuffer *Buffer
to
const llvm::MemoryBufferRef &Buffer
. 1
2
3
4
5
6
7std::unique_ptr<llvm::MemoryBuffer> buf =
llvm::MemoryBuffer::getMemBuffer(content);
auto bounds = ComputePreambleBounds(*ci.getLangOpts(), *buf, 0);
auto bounds = ComputePreambleBounds(*ci.getLangOpts(), buf.get(), 0);
https://reviews.llvm.org/D91297 made a similar change and I adapted it similarly.
In Jan 2022, https://reviews.llvm.org/D116317 added a new parameter
bool Braced
to
CodeCompleteConsumer::ProcessOverloadCandidates
.
1 | void ProcessOverloadCandidates(Sema &s, unsigned currentArg, |
In late 2022 and early 2023, there were many changes to migrate from
llvm::Optional
to std::optional
.
1 |
|
In Sep 2023, https://github.com/llvm/llvm-project/pull/65647 changed
CompilerInvocationRefBase
to
CompilerInvocationBase
. I duplicated the code with
.
. 1
2
3
4
5
6
7
8
9
10
11
ci->getLangOpts().SpellChecking = false;
ci->getLangOpts().RecoveryAST = true;
ci->getLangOpts().RecoveryASTType = true;
ci->getLangOpts()->SpellChecking = false;
ci->getLangOpts()->RecoveryAST = true;
ci->getLangOpts()->RecoveryASTType = true;
In April 2024, https://github.com/llvm/llvm-project/pull/89548/ removed
llvm::StringRef::startswith
in favor of
starts_with
. starts_with
has been available since Oct 2022 and
startswith
had been deprecated. I added the following
snippet:
1 |
It's important to note that the converse approach 1
2
could break code that calls
std::string_view::starts_with
.
Changed enumerators
In November 2023, https://github.com/llvm/llvm-project/pull/71160 changed an unnamed enumeration to a scoped enumeration. To keep the following snippet compiling,
1 | switch (tag_d->getTagKind()) { |
I introduced macros.
1 |
In April 2024, https://github.com/llvm/llvm-project/pull/89639 renamed an enumerator. I have made the following adaptation:
1 |
|
Build system changes
In Dec 2022, https://reviews.llvm.org/D137838 added a new LLVM
library LLVMTargetParser. I adjusted ccls's CMakeLists.txt
:
1
2
3
4target_link_libraries(ccls PRIVATE LLVMOption LLVMSupport)
if(LLVM_VERSION_MAJOR GREATER_EQUAL 16) # llvmorg-16-init-15123-gf09cf34d0062
target_link_libraries(ccls PRIVATE LLVMTargetParser)
endif()
Summary
The above examples illustrate how to adapt to changes in the LLVM and Clang APIs. It's important to remember that API changes are a natural part of software development, and testing with different releases is crucial for maintaining compatibility with a wide range of LLVM versions.
When introducing new interfaces, we should pay a lot of attention to reduce the chance that the interface will be changed in a way that causes disruption to the downstream. That said, changes are normal. When an API change is justified, do it.
Downstream projects should be mindful of the stability guarantees of different LLVM APIs. Some API may be more prone to change than others. It's essential to write code in a way that can easily adapt to changes in the LLVM API.
LLVM C API
While LLVM offers a C API with an effort made towards compatibility, its capabilities often fall short.
Clang provides a C API called libclang. While highly stable, libclang's limited functionality makes it unsuitable for many tasks.
In 2018, when creating ccls (a fork of cquery), I encountered multiple limitations in libclang's ability to handle code completion and indexing. This led to rewriting the relevant code to leverage the Clang C++ API for a more comprehensive solution. The following commits offer insights into how the C API and the mostly equivalent but better C++ API works: