How can I feel safe in doing terribly unsafe things, like relying on slice layout?


#1

In nix, we provide Rusty interfaces to unix system functions. This includes functions that deal with iovecs. The order of pointer / length fields is the same for iovec as it is for &[u8], so I’ve been considering wrapping readv, writev, and other functions to accept &[&[u8]] instead of a struct. There’s an issue with some more detail.

Is this a terrible idea? core::raw::Slice is #[repr(C)] with those field types in that order. How likely is this representation to change? Can I do anything to feel safer about this, either in C or at build time for dependent projects?


#2

(I wasn’t sure if users or internals was the right forum, let me know if the other makes more sense.)


#3

If you take a look at the docs, std::raw is currently marked as unstable. But the stability marker links to the tracking issue discussing stabilizing it.

So far, the only opinion expressed there is against stabilizing std::raw::Slice, and thus avoiding committing to that particular layout being stable. But that might be a good point to point out your use case.

I don’t think I would rely on that detail without that part being stabilized, or some other commitment from the core team about the exactly layout.


#4

I mean, the safest thing to do is not depend on explicitly unstable implementation details. I can’t think of any reason it’s likely to change in the near future, but that doesn’t mean it won’t. I remember talks back in my D days about changing slices from (ptr, len) to (begin_ptr, end_ptr) for performance reasons.

… but if you’re going to do it anyway

#[test]
fn test_slice_repr_hasnt_changed() {
    let ptr = 0xDEADBEEF as *const u8;
    let len = 0xCAFEBABE;
    
    unsafe {
        let slice = ::std::slice::from_raw_parts(ptr, len);
        assert_eq!(slice.as_ptr(), ptr);
        assert_eq!(slice.len(), len);
        
        #[repr(C)]
        struct Slice { ptr: *const u8, len: usize }
        let slice: Slice = ::std::mem::transmute(slice);
        assert_eq!(slice.ptr, ptr);
        assert_eq!(slice.len, len);
    }
}

#5

Ah yes, that’s a good idea.

I was thinking of having a test like that. Or alternatively, a build.rs that contains those assertions.


#6

Even if slices are stable, the layout of iovec might not be. The relevant POSIX spec doesn’t even guarantee that iovec will have exactly two members, and says nothing about their order. While common platforms may be compatible with a Rust slice, I’d be uncomfortable baking that into the API. Would it be an option to define IoVec<'a> and provide functions to convert between IoVec<'a> and &'a [u8], which would be no-ops on Linux but maintain the type separation?


#7

You shouldn’t do this. Tests in the build script are only valid so long as you aren’t cross-compiling. If you are testing something in relation to the runtime environment, test it in the runtime environment, not the host compiler environment.


#8

(This should probably have a #[repr(C)].)


#9

Probably, yes.


#10

It would be possible, and after reading this thread, I’m thinking it is the best approach to take. Thanks all!


#11

Greate point. Thanks!