A key premise of LLVM is that it provides an abstraction over different targets. A frontend just needs to worry about generating LLVM IR, and then LLVM takes care of everything else, generating highly efficient code for a wide variety of targets.
The main area where this premise currently breaks down is target-specific ABI differences, especially concerning the call ABI. Part of the call ABI is handled by LLVM, but a large part of it is a frontend responsibility. There are many different ABIs, with many complex and subtle rules. Failing to implement them correctly will result in miscompilations.
Every LLVM-based frontend that wants to expose a C FFI interface currently has to manually implement these ABI rules. Clang implements these in the CodeGen/Targets.
We regularly get questions on how to support C FFI on Discord, and for some reason nobody is ever happy about “you have to re-implement these ten thousand lines of Clang code” being the answer.
Why doesn’t LLVM “just” handle this?
LLVM handles part of the call ABI lowering, such as the assignment of scalar arguments/returns to registers. Why can’t LLVM simply do the right thing for all argument types?
The primary reason for this is that the LLVM type system is not expressive enough to make all ABI decisions. Here are a few examples:
- LLVM does not have a representation for unions at all.
- LLVM does not have type-level alignment annotations. For example, two otherwise identical structs, but one with an explicit alignment attribute that matches its default alignment, do not have the same ABI. In fact, alignment has to be computed in at least three different ways to satisfy various ABI rules.
- Do you think
__int128
and_BitInt(128)
have the same ABI? Think again.
Why don’t we extend LLVM’s type system to handle these cases? LLVM generally tries to omit any type information that is not semantically relevant. LLVM will represent both int
and unsigned
as an i32
. This kind of inherent canonicalization is important for optimization purposes.
While directly extending the type system would be a bad idea, it would in principle be possible to convey additional information using attributes/metadata at call-sites and declarations only, similar to how we have the zeroext
and signext
attributes to distinguish unsigned/signed integers for ABI purposes.
Doing this in full generality, providing all the information necessary for struct and union passing, would be significantly harder though, and essentially introduce a second shadow type system into LLVM IR.
An additional consideration here is that it is beneficial for optimization purposes if cases where large structures need to be passed in memory, are explicitly represented as such in LLVM IR. For example, this allows optimizing away redundant copies of the memory, etc.
LLVM’s IR design is generally very hostile towards working with aggregate SSA values, and our historical trend is to reduce reliance on struct types in IR. Making these first-class citizens would be a substantial shift in optimization philosophy, which goes far beyond ABI questions.
Proposal
The proposal is to introduce an LLVM ABI lowering library (LLVMABI), which provides information to frontends on how to correctly produce LLVM IR for a specific target. The initial focus of the library would be on call ABI lowering, as this is the hardest part. It can be extended to handle other ABI aspects as well.
The high level sketch of how this would look like is:
- The library will have its own type system, which is independent of both the LLVM IR type system and the Clang type system. The types will support encode exactly as much information as is necessary for correct ABI lowering.
- The library will provide per-target implementations of
ABIInfo
, extracting what Clang currently does. - The main result of ABI classification will be something like
ABIArgInfo
, which specifies whether the argument is passed directly, indirectly, etc. - The frontend is then responsible for generating LLVM IR based on the ABIArgInfo.
Some notes:
- Clang will be switched to use the new ABI lowering library. I think it’s very important that Clang makes use of it, not just 3rd-party frontends, otherwise we’ll certainly get divergences.
- Yes, this does mean that Clang will have to lower to an additional type system. My hope is that this will not add a lot of additional overhead if it is cached, but that’s one of the things that remains to be seen.
- While the motivation here is purely about the C ABI, I think it will be unavoidable to also support the parts of the C++ ABI that relate to the calling convention. The C++ ABI is a minor modification of the C ABI, and I don’t think they can be usefully separated.
- Layering: I think this library could be implemented completely independently of IR, depending only on Support. But having it depend on IR would probably make it more useful, as we could also provide LLVM IR types where relevant.
Implementation
I’d like to offer creating a prototype for this as a GSoC project. At this point, I’m mainly looking for some feedback on the general direction, and any insights people familiar with ABI lowering may have. The details of the design will have to be ironed out later.
There is an old llvm-abi project, also discussed here, which started implementing this concept out-of-tree. It’s many years out of date now and only support x86, but may serve as inspiration. I think it’s important that the ABI lowering library is in-tree and used by Clang to guarantee continued maintenance.
An alternative approach to the call ABI problem has recently been explored in Ideas about C calling convention lowering to LLVM IR. The approach there is to have the frontend attach additional ABI classification metadata, that allows LLVM to perform the ABI lowering.