A recent PR to add 64-bit RISC-V support for the swiftcall calling convention attribute raised a bunch of related questions about what implementors should be doing here. Since this attribute is specifically tied up with promises of compatibility with the separate Swift project, I’m writing this RFC to communicate the Swift project’s expectations around it and the closely-related swiftasynccall attribute.
ABI stability
First and foremost, the Swift project considers everything about the Swift calling convention to be ABI-unstable on platforms where we have not declared ABI stability. That of course affects users of these calling convention attributes. This doesn’t mean nobody should use these attributes on other platforms; it just means that you need to take responsibility for ensuring that both sides of every call using the convention are compiled by compatible compilers. Toolchains released by the Swift project (including in Xcode) will of course contain compatible swiftc and clang compilers; otherwise I think this is somewhat caveat emptor. I have opened a PR to add this to the Clang attribute docs.
Note that this ABI instability is platform-specific, not architecture-specific. Different OS targets on the architecture can have different ABIs and therefore different values of ABI stability. In particular, Swift is ABI-stable on Apple’s x86-64 and aarch64 targets, but not on Linux, BSD, Windows, etc. x86-64 and aarch64 targets.
I understand that an ABI-unstable attribute is a bit of an unusual situation. I don’t think it’d be unreasonable for us to document ABI breaks of this at the granularity of LLVM releases, if that’s what the community wants. So something like
| Target | ABI-breaking releases |
|---|---|
| All 32-bit RISC-V platforms | 19, 20, 22 |
| All 64-bit RISC-V platforms | 20, 22 |
| etc. |
I don’t think that would be too much work to maintain or too large for the attribute docs. If the request is to be specific about the ABI breaks (e.g. “LLVM 20 (added support for swifterror parameters)”), that might be a bit too much for the attribute docs, but maybe we can find somewhere else in the manual that we can link from there.
Implementation expectations and roadmap
As far as actually implementing the calling convention goes, I think most efforts can probably be broken down into three stages based on complexity and difficulty of implementation.
Initial implementations of the calling convention should make an effort to get the basic rules of the convention semi-optimal. This will generally involve evaluating the register use of the target’s standard C calling convention and using better rules for swiftcc if necessary. In particular:
- Does the convention use a reasonable number of integer registers for arguments?
- This doesn’t need to be a ridiculously high number, and I understand that some legacy ISAs are very limited in GPR count. Ideally, though, the convention should use at least 4 GPRs as argument registers, and on a modern ISA with 32 or more GPRs, that number should really be at least 8.
- Does the convention make reasonable use of FP and vector registers?
- Same as above. In addition, you should think carefully about what the minimum hardware target for
swiftcallshould be. I strongly recommend that you default to assuming the existence of features like hard FP and only do things like passing FP values in integer registers when specifically requested by a command-line flag.
- Same as above. In addition, you should think carefully about what the minimum hardware target for
- Does the convention use a reasonable number of integer/FP registers for return values?
- Again, this doesn’t need to be a high number, but 2 at a minimum, preferably 4. A lower cap for this is generally good, because the fallback is just returning indirectly, which is not a big problem and often quite nice for the caller, since it can control exactly where the result goes. This decision does need to be made consistent with Clang’s
shouldPassIndirectlyfunction for the target.
- Again, this doesn’t need to be a high number, but 2 at a minimum, preferably 4. A lower cap for this is generally good, because the fallback is just returning indirectly, which is not a big problem and often quite nice for the caller, since it can control exactly where the result goes. This decision does need to be made consistent with Clang’s
- Does the convention do extra things with the stack argument area that aren’t really necessary for non-C calling conventions?
- Some C calling conventions do things like reserve space in the stack argument area for arguments that are passed in registers, in order to simplify the implementation of C varargs.[1] You should not do this for
swiftcc.
- Some C calling conventions do things like reserve space in the stack argument area for arguments that are passed in registers, in order to simplify the implementation of C varargs.[1] You should not do this for
- Is there a reasonable register choice for
swiftcontext?swiftcontextis by far the easiest of the extensions to the basic Swift CC to implement, and I recommend taking care of it in the initial commit. You just need to find a GPR that is normally a callee-save register and assign it toswiftcontextarguments. It should continue to be treated as callee-save despite being used as an argument register, so be sure to test that it gets restored if the register gets clobbered within a function body.
Again, it’s not strictly necessary to get the register use rules perfect in the initial commit, but it’s a good time to do so, because it minimizes ABI breaks later. And everything up to this point is pretty easy in LLVM.
Once that’s done, I recommend implementing swifterror as a refinement. swifterror has a pretty hacky representation in LLVM IR, but at an ABI level, it’s pretty simple: a specific GPR that’s not normally an argument/return register is used for both an argument and a return value. You don’t have to pin the register in the function or anything, you just need to make sure it’s got the right value on entry/exit and at calls. Using a register that’s normally a CSR will generally be better for register allocation. Unlike swiftcontext, since the swifterror value can change, the register stops being a CSR in functions with swifterror parameters. If you want to help make the LLVM representation less terrible, that would be great.
The last step is to implementing swiftasynccall. This is also conceptually straightforward but in practice somewhat tricky.
- The key feature of
swiftasynccallis thatmusttailcalls betweenswiftasynccallfunctions must result in a tail call, even for functions with very different signatures. The big challenge here is that functions therefore need to pop their own stack argument areas, which is very different from standard C calling conventions. - Like
swiftcall, you should make sure thatswiftasynccallhas good argument register use; it’s actually more important forswiftasynccallsince using more GPRs means avoiding using the SAA means fewer SAA size mismatches around tail calls. - You don’t need to worry about return value register use, since the return type is always
void. - You do need to support calling
swiftasynccallfunctions from non-swiftasynccallfunctions and vice-versa. The former happens in the Swift runtime; the latter is ubiquitous. You don’t need to support tail calls for either of these combinations, though.
It is totally fine to land an initial implementation without support for either swifterror or swiftasynccall. If a release ends up passing before you can land support for them, then okay, there’ll be an ABI break on that target in some future release. That’s not the end of the world as long as Swift hasn’t declared ABI stability there yet.
This is necessary even for non-varargs functions to make varargs calls compatible with non-varargs definitions and vice-versa. This is not required by C, but some ABIs are conservative about it because historically all functions were unprototyped and some of those were in fact variadic. ↩︎