If you want to be able to use Vec
in an ABI portable across compilers but still change the fields/layout of Vec
(not have a stable ABI), there actually is an existing solution available in Rust today:
dyn Trait
. This works for any type: define the API you want on the trait and pass around &dyn Trait
, &mut dyn Trait
, or Box<dyn Trait>
. Every function call is dispatched via dynamic call.
Of course, dyn Trait
isn't ABI stable. Providing an ABI stable dyn Trait
is fairly simple in theory, however:
- Give all of the methods a stable ABI, and
- Mark it
#[abi_frozen]
to give the vtable a stable layout.
The job of #[abi_frozen]
is to indicate two things:
- fnptrs in the vtable have a stable order (probably lexographic, to avoid source order being meaningful).
- Adding methods to the trait is an ABI breaking change.
You want to add more functionality to an existing #[abi_frozen]
trait? Make a new subtrait.
Generic functionality? Make it dyn
compatible.
const
? Mark it #[frozen]
, never change it, put it in the .rdylib
.
Annoyed by #[frozen]
? Non-const
things can gain be ABI stable and still grow new functionality via append-only structures.
Annoyed by dynamic costs? Lack of inlining making your zero-cost abstractions not so zero-cost? #[inline(frozen)]
will save you by making the function implementation part of your ABI so it can be inlined. Hope you don't have any bugs.
Or we can start being more adventurous. There's still one really annoying problem I've been flirting around.
Object Safety.
If you do something too interesting with a trait like pass by-value, you're out of luck, you can't use dyn Trait
. You fundamentally can't pass something by-value without knowing its size at compile-time, and being able to change struct size for library evolution was a constraint at the beginning of this.
Okay, you could just box to pass by-value. You could even optimize this by using &move
abi-by-ref to delay the allocation until it's necessary. Maybe even pass extra flags so you can pass foreign types as owned Box
already to avoid redundantly moving it to a new box if it's already been boxed.
Well alright. I had a reasonable transition and then ruined it with practical compromise. ... Doesn't that mean using Vec<MyT>
dynamically results in a lot of excess boxing, as it ends up as Vec<Box<dyn MyT>>
? In fact, then you have to keep track of if [MyT]
is [MyT]
or actually [Box<dyn MyT>]
from the other side of the bridge! Actually, at least this specific case is just a library—
Enter: The Swift ABI.
Rust's type system is (I think? It's been forever since I followed Swift language developments) more expressive than Swift's, and many “interesting” types just being a reference counted heap pointer already certainly doesn't hurt. I wouldn't be surprised to learn there's still some ObjC-style string-based function lookup in there somewhere. But a true rust-dynamic
ABI that supports (almost) all the expressive power of Rust APIs will necessarily look quite similar to the Swift ABI.
Or you could just recompile when updating your dependencies.