#[repr(Interoperable_2024)]

While I agree in terms of naming within the language, I do think the standard itself should use a semver with both major and minor numbers, so that it's easy to say things like "Python supports safe 1.4, Go supports safe 1.3", which would imply support for the new additions.

3 Likes

Yeah, or even just within rust to say "rust 1.x supports safe 1.y".

I selected «Something else», and my answer is: I don't know and I don't care enough to learn enough to build an informed opinion. A stabilized representation sounds really nice but I don't know enough to point out downsides.

In other words, go ahead and do what you find the best. :+1:

1 Like

With large number differences it isn't as big a problem, but with small number differences it can be a bigger problem. For example, what month and day does the date 1/2/2022 refer to? In the USA, that will be 2 January 2022, but in parts of Europe that will be 1 February 2022. I'm just trying to avoid a really ridiculous issue[1] by solving the naming right now.

I would like to think so, but some of the things I've seen on the news have convinced me that the quote is correct as-is...:sob:


  1. And let's face it, getting the numbers backwards is going to be a really ridiculous issue, but it's still going to require time to answer on URLO, Stackoverflow, etc., etc., etc. I don't have enough time or brain space to deal with it in my life, I'd rather just prevent it from happening in the first place. ↩︎

What if some language can't/won't support only some of the features? What if v1.1 adds support for fancy-backflip-generics, and v1.2 adds support for u256, and there's a language that supports any sized integers, but can't express generics?

Then that language will be stuck at safe 1.0 support, and also then we likely made a mistake in our approach to enabling fancy-backflip-generics if it isn't straightforwardly supportable by anyone with C FFI support.

2 Likes

An alternative (not one that I support, but one that I think should be mentioned) is to have supported extensions, similar to how OpenGL operates. I don't know how that would work in practice, but it's a possibility.

Once again though, I'm with @josh on this, and I strongly prefer that 'safe' grows monotonically. I dislike the idea of extensions because it moves away from interoperability.

1 Like

Things might be difficult to say Python supports safe 1.4, Go supports safe 1.3

Suppose we have old and useful crate which is created under safe 1.0, but rust update the semver to safe 1.63

Even if we have no change made in the old crate, could we say, the ffi could using safe 1.63 with no error generates?

or we have to lock the symver to safe 1.0, or the oldest crate we have?


maybe derive a static #[repr(C)] entry for those type automatically is a better choice. Otherwise we may be locked to a rather old abi version.

I would be shocked if safe made it up to 1.63. That would imply that we made a LOT of mistakes in defining the layout (or that our great-great-great-grandchildren are still using our code). Regardless, if a crate is that useful I would expect people to continue to upgrade it. So if it's using safe 1.0, I'd expect someone to release a new version of the crate that is upgraded to use safe 1.63.

The other possibility is that there is a crate that is still in use where the original source was lost long ago, and all that is left is the binary. In that case, it's time to rewrite the crate from scratch so that you have some source code, and migrate all users to the new crate. Not having the source is a recipe for disaster[1].


  1. Just to be clear, I'm aware that in certain sectors people have written emulators for old machines because the business owners would rather pay to write an emulator than rewrite the code in a modern language. This just kicks the problem down the road as knowledge of how to write the emulator becomes more and more esoteric. Eventually, you have a situation where no-one knows how it works, and everyone is afraid of fixing anything. Perfect recipe for disaster. ↩︎

1 Like

I'd imagine minor version increases wouldn't likely be caused by mistakes, but moreso new things that get added support.

1 Like

True, true. And I was forgetting about my initial suggestion that safe and rust-dynamic have lock-step version numbers. So, even if we don't add much to safe, its version number could still increase quite rapidly as we experiment with rust-dynamic.

IMHO, Open source community would be glad to update everything update. But for companies, they might not update their code/binary since an update may produce bugs.

Open source community may met the bugs, too.

It might not always good to update everything, even if you are in the open source community.

I had update my Manjaro kernel to 5.18, and failed to boot due to a newly added feature, "intel-IBT", nvidia kernel modules does not support IBT, thus with IBT on, nvidia kernel module failed to load, which prevent my laptop from boot.

I agree that companies don't update often, precisely because once it works well, they don't want to risk downtime and bugs. However, the upgrade we're talking about here would be to move from something like #[repr(Safe_1.0)] to #[repr(Safe_1.63)] (or extern safe_1.0 to extern safe_1.63). This can be done mechanically, and tested to make sure no conflicts occur. If we've done our jobs correctly, and the compiler is implemented correctly, then it will all just work. If there is a breaking change, then semantic versioning dictates that we'd go from safe_1.x to safe_2.x. However, I think that would be as big a change as the python 2->3 change was. The point releases shouldn't break the older ABI at all.

1 Like

This is not reliable. Even update linux kernel would make conflicts. Why you believe Rust could maintance those code well?

If Rust do not provide dll with rust-abi, we could compile the abi to something like #[repr(C)], but once Rust provide a abi, more abi might occurs, handling those might be difficult.

what's more, #[repr(Safe_1.0)] may not equivlent to #[repr(Safe_1.0)], since we could modify the defination of Vec in std::alloc to a strange but useful abi:

#[repr(transparent)]
struct Vec<T>(NonNull<T>,PhantomData<T>);
// NonNull<T> points to:
#[repr(C)]
struct _Vec{
    cap:usize,// alloc a large enough memory
    len:usize,
    // NonNull<T> points here.
}
// under such defination, `Vec<T> as mut* T` is a `no-op`
// which may acquire data faster.

We are not modifying the #[repr(Safe_1.0)] flag, but the defination of Vec changes.

This is why I use #[repr(Safe_1.63)] to explain the problem: with this repr, we could not even transfer a Vec among libraries.

(compile from source may not have such issue, since nobody could access the private field of a vec, we are safe to change all of the implementation of vec, and compile it.)

1 Like

This example is the difference between the language providing a stable ABI and a library providing a stable ABI. The latter requires the former, but the former does not guarantee the latter.

Swift pulled off the impossible of keeping library ABI compatible while still allowing library evolution by transparently introducing (and importantly, removing when possible) dynamic polymorphization over possible library evolution in otherwise monomorphic code.

extern "rust-dynamic" may do such a thing, but extern "safe" is just concerned with the much easier C kind of ABI stability.

(And it's maybe worth noting that even C ABIs are only stable because the object files aren't finished compiling yet, and still have to be linked! An object file just says "I want the _strlen@ symbol" and the linker (potentially dynamically) patches references to the symbol to the actual address later.)

2 Likes

Agreed, which is why I said:

:wink:

There are always ways of breaking things; to write a language that genuinely prevents all possible errors, you'd have to enumerate all and only the set of correct programs, which I strongly suspect is impossible.

The limit of the mechanical change is simply string substitution in the original code, replacing safe_1.0 with safe_1.63 everywhere[1]. The compiler still needs to accept it, testing still needs to be done, etc., etc., etc. The 'if we've done our jobs right' part means that we make sure that we always increase the semantic version numbers of safe appropriately. As much as I wish it were different, the compiler is no better than all of us humans can make it...


  1. "Everywhere" being those places where it is syntactically and semantically correct to do so. This means using the power of the compiler to determine if the given string means what we mean in this discussion, and not just some random string floating in the code somewhere. ↩︎

Linux explicitly doesn't have a stable abi or api. #[repr(safe)] would have a stable abi by definition.

Any type that doesn't opt into this abi would not be allowed as field of a type that does opt in. If Vec were to opt in, it wouldn't be allowed to ever change the definition anymore.

we COULD have a stable abi, but the problem is that, SHOULD we have a fixed abi now?

If no fixed abi is provided, we could write C-like abi manually. But if a fixed abi is provided. we may face different Rust library with different abi.

Transfer a type from a lib to another might be unsafe since those library may use a different abi.

That might not acceptable.

1 Like

If you really don't want to stabilize Vec, that's fine. Someone can write a version of Vec (SafeVec?) that is stabilized. Or if we want to be really crazy, maybe the compiler will be able to do some kind of magic with #[repr(safe)] on Vec, forcing it to a stable ABI only when #[repr(safe)] is in use. Maybe we invent a new const trait that only has constant functions that the compiler can use to layout the ABI at compile time if it encounters #[repr(safe)] which can then be implemented on Vec. Maybe something else happens.

This isn't a death knell, it's just an annoyance that we have to track and solve.

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[1]) 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.


  1. There's always edge cases. ↩︎

4 Likes