I had a proposal in the discussion on IRLO that (I think) was interesting. It started with this comment. I didn't see it in your sum-up.
Here is the gist of my idea: the only place where this ABI are the entry points of your code (main
for a binary, any publicly exported function for a library), and the exit point (when calling external library). Those boundary must use the same ABI that whatever is used on the other side. This include struct layout, calling convention, … However, anything in between can use any add-hoc ABI.
Example: we have the following call-graph (a -> b
means that a
is calling b
):
f1 -> f2 -> f3 -> g1 -> g2 -> g3 -> h1 -> h2
Where f1
, g1
and h1
are pub
function respectively exported by the library F
, G
, and H
. The other functions are not publicly visible (they are internal to their respective libraries).
First, we need to compile H
. During compilation the ABI of the public functions is going to be fixed. Lets name it ABI_H. Internally the ABI doesn't need to be ABI_H
, as long as it's a compatible ABI with a mechanic transformation (for example changing the endianness of a number). Let's name it ABI_h
.
Then the library G
will be compiled. The calling convention, and the layout of all the objects passed to h1()
by g3()
must match the ABI_H ABI. Internally the compiler could be allowed to do the appropriate transformation to the types if they don't match , or report a build error if the transformation is not possible. Once again, the ABI of the public funtions of G
will have to be fixed. Lets name it ABI_G
, and ABI_g
for the internal ABI.
Finally F
is compiled and linked against G
, and the same reasoning is applied. Let's name the internal ABI ABI_f
.
To sum-up, the functions must communicate with the following schema:
f1 -> ABI_f -> f2 -> ABI_G -> g1 -> ABI_g -> g2 -> ABI_g -> g3 -> ABI_H -> h1 -> ABI_h -> h2
In order to have all of this working, the only think needed is that ABI_f
mast be compatible with ABI_G
, itself with ABI_g
, itself with ABI_H
, and finally itself with ABI_h
. Technically a single crate could use multiple internal ABI. And as you can see, there is no assumption on which languages were used or compiled to write F
, G
and H
, the only important thing is how to be able to consume those library. If all ABI are the same (like the C ABI, Swift ABI, or the Rust ABI) this obviously works, and doesn't need any transformation.
To conclude all of this, the only place where ABI need to be enforced is:
- entry points: publicly visible items (function, object, …)
- exit points: when calling external functions (or syscalls, interruptions, …)
This also means that a crate could be compiled multiple times with different public ABI (in order to export itself with the C ABI for a C consumer or the Swift ABI for a Switft consumer) without any change in the source code. If the public ABI is incompatible with the types used (like Result<T>
for the C ABI) it would result in a compile time error.