nvzqz / swift-bindgen

Bridging the gap between Swift and Rust
Apache License 2.0
150 stars 9 forks source link

Expressing the Swift struct and enum layouts to Rust #6

Open Dante-Broggi opened 4 years ago

Dante-Broggi commented 4 years ago

The Swift struct and enum layouts, distinguish between the size (store size) and stride (array size) of types. In particular, the stable Swift 5 layout algorithm (mostly) documented here, which would be largely reasonable to define as a Rust #[repr(Swift_5)], implemented naively is unsound.

The notable example is in this playground

A possible way to fix this is to first add a (Sized-like) implicit auto trait Strided which would encode the necessary guarantee (that the size equals the stride) and which would not be implemented by #[repr(Swift_5)] types.

Another point of remark is that Swift requires all types to have nonzero stride, even if they have zero size. This means that Swift assumes all ZSTs have unique addresses in memory, if they have an address, though I do not know if Swift acts upon this assumption.

nvzqz commented 4 years ago
Dante-Broggi commented 4 years ago

The NonZeroU8 is to demonstrate that, if Rust uses a 'size' of 4 for both structs, when compiling foo, it would believe that the 4th byte is padding, and thus safe for arbitrary or even uninitialized data, but that 'padding' would be sharing storage with the NonZeroU8, which cannot have arbitrary data (namely not 0).

Regarding ZSTs, the primary problem I see is things like this which would certainly be a bug if it occurred in Swift.

Edit: I think that in addition, Swift assumes that all allocations are done by stride, not size, and ZSTs are the only Rust types which are not allocated based upon what Swift believes is a valid stride.

nvzqz commented 4 years ago

I just discovered that #[repr(C, packed)] actually makes alignment be 1. I guess that makes sense. Not sure how you'd represent it in Swift and I don't know if Swift respects it for imported C types (I assume it does). It seems to me that it's a rare enough case to not worry about now.

nvzqz commented 4 years ago

For wrapping and indexing into Swift.Array<T> from Rust, I came up with a wrapper type that will handle Rust zero-sized types while keeping the original type zero-sized (playground):

// Required for ManuallyDrop in union.
#![feature(untagged_unions)]

use std::mem;

#[repr(C)]
union WrapperInner<T> {
    value: mem::ManuallyDrop<T>,
    _size: u8,
}

struct Wrapper<T>(WrapperInner<T>);

struct Zst;

fn main() {
    // `Wrapper<u32>` has the same memory layout as `u32`.
    assert_eq!(mem::size_of::<u32>(), 4);
    assert_eq!(mem::align_of::<u32>(), 4);

    assert_eq!(mem::size_of::<Wrapper<u32>>(), 4);
    assert_eq!(mem::align_of::<Wrapper<u32>>(), 4);

    // `Wrapper<Zst>` has the same alignment as `Zst` but always a size of 1.
    assert_eq!(mem::size_of::<Zst>(), 0);
    assert_eq!(mem::align_of::<Zst>(), 1);

    assert_eq!(mem::size_of::<Wrapper<Zst>>(), 1);
    assert_eq!(mem::align_of::<Wrapper<Zst>>(), 1);
}