Defining Custom `&[T]`-like Types

Some backstory: I'm working on a safe, Rust-y wrapper for CUDA. I'm trying to define a pair of types - DeviceBuffer<T> and DeviceSlice<T> - which work sort of like Vec<T> and &[T] - they're composed of a raw pointer and a length, where DeviceBuffer owns the backing store and DeviceSlice does not. The difference is that the backing store only exists as memory allocated on the GPU. Importantly, the raw pointer is not valid (or at least, is not valid for the CPU) - dereferencing it will result in a segfault or other undefined behavior.

In a perfect world, I'd be able to implement Deref for DeviceBuffer<T> and it would all just work, returning a DeviceSlice<T> like how Vec's Deref returns a &[T]. Unfortunately, in reality Deref::deref would have to return &DeviceSlice<T>, and the borrow checker won't let me return &DeviceSlice::from_raw(self.ptr, self.len) - the DeviceSlice lives on the stack and thus the reference would be invalid.

Vec can do this, because there's some compiler magic to treat &[T] (which is really a two-word non-owning fat pointer, just like DeviceSlice) as a reference, but as far as I can tell, DeviceBuffer can't. As I understand it, doing this would require custom dynamically-sized-types, which we don't currently have.

As far as I can see, there are two potential workarounds for this problem.

  1. I could require the user to call DeviceBuffer::as_slice to get a DeviceSlice. I would then have to also create DeviceSliceMut to represent &mut [T], and probably implement most of their functionality on all three, which is cumbersome and unergonomic.
  2. I could abuse the fact that &[T] is exactly what I already need - a pointer and a length. I could create a fake &[T] using from_raw_parts and then transmute or pointer-cast it into a &DeviceSlice<T> (which would be redefined as struct DeviceSlice<T>([T]), so that it too would become a DST), and then just be very careful to encapsulate the [T] such that it could never be dereferenced.

I'm moderately confident the second one is unsound - creating an invalid reference is UB even if it isn't dereferenced, right? On the other hand, I'm only moderately confident about that, because I think embedded software often converts constant raw pointers (ie. not referring to the stack or any heap allocation, but instead to some memory-mapped IO) to references, and that must be sound. Perhaps I'm mistaken about that, though.

So, my questions:

  • Is it possible to implement option 2 without invoking undefined behavior?
  • Is there any other approach to creating custom slice-like types? What have other crates done in this situation?

You can implement #2 as:

struct DeviceBuffer<T>(*mut T, usize, PhantomData<T>);
struct DeviceSlice<T: ?Sized>(T);

impl<T> Deref for DeviceBuffer<T> {
    type Target = DeviceSlice<[T]>;

    fn deref(&self) -> &Self::Target {
        unsafe {
            let s = std::slice::from_raw_parts(self.0, self.1);
            std::mem::transmute(s)
        }
    }
}

I’m not sure what you gain though since you’re not exposing a normal Rust slice, and thus don’t get any of the existing APIs from it. What’s the user going to do with a &DeviceSlice<[T]>? It seems like you should just implement dedicated APIs to get a DeviceSlice, which is your #1.

But maybe I missed something ...

DeviceSlice is not gaurenteed to be the same memory layout as a slice, so you're code is unsound. You could add a #[repr(transparent)] to make it work.

Edit: this only matters for ffi, thanks @vitalyd for spotting this.

I’m not sure what you gain though since you’re not exposing a normal Rust slice, and thus don’t get any of the existing APIs from it. What’s the user going to do with a &DeviceSlice<[T]> ? It seems like you should just implement dedicated APIs to get a DeviceSlice , which is your #1.

Well, if I use a slice then I can implement a bunch of methods on DeviceSlice and know that (because of Deref coercion) they're also available on DeviceBuffer. Additionally, I don't need a third struct to represent &mut [T] because &mut DeviceSlice<T> would work. I would definitely need to implement my own versions of all of the appropriate functions from slice, but I'd rather not have to implement them separately for DeviceBuffer, DeviceSlice and DeviceSliceMut. I could use a macro or supertrait to do that, but those have their own drawbacks.

DeviceSlice is not gaurenteed to be the same memory layout as a slice, so you’re code is unsound. You could add a #[repr(transparent)] to make it work.

Right, I would definitely need #[repr(transparent)]. My question is about whether it's undefined behavior to create an &[T] slice with an invalid pointer, even if the pointer is never dereferenced.

1 Like

Why is repr(transparent) needed? That repr is for ABI purposes so that, eg, a calling convention (say passing the wrapper in/out of functions) will be the same for the wrapper as the underlying.

This is a single field DST - I don’t think it needs any repr, at least not for transmuting from a fat ptr to a slice to a fat ptr to it.

1 Like

Yeah, you're right! Its been a while since I read repr(transparent) rfc, so i forgot.

Technically, there's no guarantee of that for repr(Rust) -- the compiler would be allowed to add padding to the front, or something. That said, I haven't heard a good reason the compiler would ever need to do so, and there's discussion about possibly guaranteeing that single-field structs (with no align, etc) have the same layout as their one field:

Yeah but this would mean code in std is currently unsound (if this weren’t already the case). For example, rust/os_str.rs at b7c6e8f1805cd8a4b0a1c1f22f17a89e9e2cea23 · rust-lang/rust · GitHub - Slice is repr(Rust).

Crates that ship with the compiler are allowed to depend on implementation details not part of the stability promise -- that includes things like the current implementation of field reordering.

As usual, the insidious part of UB is that it might work -- for now.

1 Like

So put me in the camp of folks that think/thought a DST struct with a single field is implicitly "transparent" in terms of layout/forming a reference to it. There are no fields to (re)order here, and although theoretically it's possible the compiler will decide to put padding (or some such) in front of the single field, it seems far fetched (and surprising!). This is also particularly bothersome because custom DSTs are already a pain to create (either you rely on unsized coercion or you need transmute()), but I guess we can add another wrinkle to it :slight_smile:.

That said, I'd be happy if the discussion you linked to earlier actually materializes in a guarantee. I suspect there's substantial code in the wild that makes the same assumptions.

Also, as a meta comment, it'd be nice if std could be looked at as "reference" code - i.e. someone can look at how it does things, and modulo some scary comment about making implementation assumptions, it shows how to do things properly. I think it mostly does that, but then perhaps the Slice example I gave above could be changed to put a repr(transparent) on it.

I would too, but I'm also in favour of more guarantees than will probably happen :wink:

I think such a PR would be accepted happily.