Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expressing the Swift struct and enum layouts to Rust #6

Open
Dante-Broggi opened this issue Jul 20, 2020 · 4 comments
Open

Expressing the Swift struct and enum layouts to Rust #6

Dante-Broggi opened this issue Jul 20, 2020 · 4 comments

Comments

@Dante-Broggi
Copy link
Contributor

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
Copy link
Owner

nvzqz commented Jul 20, 2020

  • To be clear, it would be unsound because:

    • A should have size == 3, stride == 4
    • B should have size == 4, stride == 4

    However Rust can only lay them out with a size+stride of 4 (A) and 6 (B). So supporting custom, non-opaque Swift data will require that consideration when implementing the layout algorithm for #[repr(Swift_5)].

    For your example, I think the following is sound-ish:

    #[repr(C, packed)]
    struct A(u16, u8);
    
    struct B(A, NonZeroU8);

    The new issue is that array/pointer layout for A will use offsets of 3 in Rust and 4 in Swift. So supporting a size/stride mismatch in Rust for #[repr(Swift_5)] will mean having custom layout code for Rust slices/arrays/pointers.

  • What's special about NonZeroU8 in your example?

  • Regarding ZSTs, would the following be Swift-safe?

    #[repr(C)]
    struct Value;

    I know that example isn't FFI-safe to pass into C and would instead require an FFI-safe ZST field:

    #[repr(C)]
    struct ValueFfi {
        _data: [u8; 0],
    }

    This pattern is explained in the nomicon. See playground for catching improper_ctypes.

    Because of the stride situation, this means that arrays/slices of () (unit) can't be passed safely between Rust and Swift.

@Dante-Broggi
Copy link
Contributor Author

Dante-Broggi commented Jul 20, 2020

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
Copy link
Owner

nvzqz commented Jul 22, 2020

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
Copy link
Owner

nvzqz commented Jul 22, 2020

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);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants