slightknack / rust-abi-wiki

A wiki about Rust's ABI and plans to stabilize it. Open to editing, all PRs will be merged.
https://internals.rust-lang.org/t/a-stable-modular-abi-for-rust/12347
MIT License
31 stars 4 forks source link

Enforcing ABI only at the boundary (both entry and exit points) #4

Open robinmoussu opened 3 years ago

robinmoussu commented 3 years ago

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:

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.

slightknack commented 3 years ago

I remember reading through this conversational thread and thought it was a great idea. One thing to think about, in the case of dynamic linking, is how each ABI will be declared. For example, let's say that there exists the following call-chain:

f1 -> g1

Obviously, this may cross an ABI boundary, so let's make that clear:

f1 -> ABI_G -> g1

now assume g is a precompiled dynamically-linked library. If the compiler were asked to compile against ABI_G's interface, how would this interface be made known to the compiler? There are a few solutions, I was just wondering what your take on this was.

Also, if you'd like to open an PR and summarize this point, that'd be much appreciated!

robinmoussu commented 3 years ago

now assume g is a precompiled dynamically-linked library. If the compiler were asked to compile against ABI_G's interface, how would this interface be made known to the compiler? There are a few solutions, I was just wondering what your take on this was.

When compiling, you have no choice but to specify the ABI of your public interface (even if this means using the default ABI of your toolchain, for example the C ABI, with gnu extentions for a C library compiled with GCC). As stated above, you can compile your dynlib multiple time to expose multiple ABI (just like you can compile for different target like x86 or x86_64).

Ideally the default ABI for Rust would be something that don't have any restrictions on the types you are using (like Result), nor having runtime penalties (like a translation of the layout of your objects when being called). If this is not possible, then I think the best is to not have any default, and cargo would require the user explicitly set the public ABI.

Also, if you'd like to open an PR and summarize this point, that'd be much appreciated!

I will try to do it this week.

slightknack commented 3 years ago

Ideally the default ABI for Rust would be something that don't have any restrictions on the types you are using (like Result), nor having runtime penalties (like a translation of the layout of your objects when being called). If this is not possible, then I think the best is to not have any default, and cargo would require the user explicitly set the public ABI.

So I think this works into the 'stable' part of 'A Stable Modularizable ABI for Rust' Ideally, Rust would have it's own ABI (as @ckaran suggested, potentially called OneRing) that is similar to the one currently in use by the compiler today - other ABIs could be fallen back upon as necessary. The best would most likely to default to OneRing, but defaulting to explicitly set ABIs when an ambiguity arises.

I will try to do it this week.

Great! No rush, though. 😄

robinmoussu commented 3 years ago

I took a look at the rustbook you started to write. Where do you think that this chapter would fit? In ABI Selection?

jan-hudec commented 3 years ago

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).

I think this is fundamentally true. The binary interface of private functions and objects that don't cross crate boundary is not observable, so optimizations can arbitrarily modify it under the as-if rule.

Ideally the default ABI for Rust would be something that don't have any restrictions on the types you are using (like Result), nor having runtime penalties (like a translation of the layout of your objects when being called).

For minimum viable product, I think neither needs to hold. Without complex generics, and with a bunch of shims and adapters to glue things together, it is still useful.

Even more, I think it's actually better to add some runtime overhead for sake of stability of the library API itself. Generics (except those generic over lifetimes only) are monomorphised, which emits them into the dependent create and therefore the providing crate can't touch them at all without breaking its ABI. Making them into “think templates” a'la momo and boxing things as trait objects has a runtime cost, but it keeps the code in the dynamic library, so now it can be evolved without breaking the ABI.

For large functions it might even be offset by the benefit of making the code smaller and therefore more of it fitting in the CPU cache. And you don't want to try to dynamically load small generic-heavy utilities anyway, it's not useful for them.

slightknack commented 3 years ago

I took a look at the rustbook you started to write. Where do you think that this chapter would fit? In ABI Selection?

Yes, that'd be the right spot. Sorry for not responding sooner, I apologize for that.

slightknack commented 3 years ago

For minimum viable product, I think neither needs to hold. Without complex generics, and with a bunch of shims and adapters to glue things together, it is still useful.

I agree. I think a MVP, simply put, should not support complex generics: stuff like Vec, Option, Result - sure - but probably not traits nor types with trait-bound generics.