How to wrap a borrowed value in a newtype that is also a borrowed value?
Asked Answered
G

3

7

I am trying to use the newtype pattern to wrap a pre-existing type. That inner type has a modify method which lets us work with a borrowed mutable value in a callback:

struct Val;

struct Inner(Val);

impl Inner {
    fn modify<F>(&self, f: F)
    where F: FnOnce(&mut Val) -> &mut Val { … }
}

Now I want to provide a very similar method on my newtype Outer, which however should not work on Vals but again a newtype wrapper WrappedVal:

struct Outer(Inner);
struct WrappedVal(Val);

impl Outer {
    fn modify<F>(&self, f: F)
    where
        F: FnOnce(&mut WrappedVal) -> &mut WrappedVal,
    {
        self.0.modify(|v| f(/* ??? */));
    }
}

This code is a reduced example from the original API. I don't know why the reference is returned from the closure, maybe to facilitate chaining, but it shouldn't be necessary. It takes &self because it uses internal mutability - it's a type representing a peripheral register on an embedded system

How do I get a &mut WrappedVal from a &mut Val?

I have tried various things, but all were busted by the borrow-checker. I cannot move the Val out of the mutable reference to construct a proper WrappedVal, and I couldn't get lifetimes to compile either when experimenting around with struct WrappedVal(&'? mut Val) (which I don't really want actually, since they are complicating a trait implementation).

I eventually got it to compile (see Rust playground demo) using the absolute horror of

self.0.modify(|v| unsafe {
    (f((v as *mut Val as *mut WrappedVal).as_mut().unwrap()) as *mut WrappedVal as *mut Val)
        .as_mut()
        .unwrap()
});

but surely there must be a better way?

Genaro answered 1/1, 2019 at 22:59 Comment(0)
Z
4

There is no safe way with your current definition, and your unsafe code is not guaranteed to be safe. There's no contract that the layout of a WrappedVal matches that of a Val, even though that's all it holds.

Solution not using unsafe

Don't do it. Instead, wrap the reference:

struct WrappedVal<'a>(&'a mut Val);

impl Outer {
    fn modify<F>(&self, f: F)
    where
        F: FnOnce(WrappedVal) -> WrappedVal,
    {
        self.0.modify(|v| f(WrappedVal(v)).0)
    }
}

Solution using unsafe

You can state that your type has the same representation as the type it wraps, making the pointers compatible via repr(transparent):

#[repr(transparent)]
struct WrappedVal(given::Val);

impl Outer {
    fn modify<F>(&self, f: F)
    where
        F: FnOnce(&mut WrappedVal) -> &mut WrappedVal,
    {
        self.0.modify(|v| {
            // Insert documentation why **you** think this is safe
            // instead of copy-pasting from Stack Overflow
            let wv = unsafe { &mut *(v as *mut given::Val as *mut WrappedVal) };
            let wv = f(wv);
            unsafe { &mut *(wv as *mut WrappedVal as *mut given::Val) }
        })
    }
}

With repr(transparent) in place, the two pointers are interchangable. I ran a quick test with Miri and your full example and didn't receive any errors, but that's not a silver bullet that I didn't mess something else up.

Zymogenesis answered 1/1, 2019 at 23:52 Comment(4)
Thanks for mentioning repr(transparent), I didn't know about that. How should I argue that the cast is safe - surely if the values have the same memory representation, I should be able to call methods from both types on the same memory location? Do I need to assess any other aspect?Genaro
I had attempted the safe solution where a &mut Val is wrapped, but as hinted at in the question I couldn't get the lifetimes right. I don't want to litter my code (especially Outer) with lifetime parameters that will only be needed locally in one method for WrappedVal, am I approaching this wrong? I'm already surprised that you could omit the lifetime argument in FnOnce(WrappedVal) -> WrappedVal.Genaro
@Genaro yes, using lifetimes like that in a trait is beyond what Rust can currently express; generic associated types (GATs) need to be implemented first. Traits are more complicated than plain functions due to their flexibility.Zymogenesis
@Genaro My comment inside the code is meant to dissuade future people from just copy-pasting the code without reading the rest of the answer. My belief that the code is safe comes from the usage of repr(transparent), so that's what I'd use in my comment, yeah.Zymogenesis
F
5

Using the ref_cast library you can write:

#[derive(RefCast)]
#[repr(transparent)]
struct WrappedVal(Val);

Then you can convert using WrappedVal::ref_cast_mut(v).

Fecit answered 11/3, 2020 at 14:3 Comment(0)
Z
4

There is no safe way with your current definition, and your unsafe code is not guaranteed to be safe. There's no contract that the layout of a WrappedVal matches that of a Val, even though that's all it holds.

Solution not using unsafe

Don't do it. Instead, wrap the reference:

struct WrappedVal<'a>(&'a mut Val);

impl Outer {
    fn modify<F>(&self, f: F)
    where
        F: FnOnce(WrappedVal) -> WrappedVal,
    {
        self.0.modify(|v| f(WrappedVal(v)).0)
    }
}

Solution using unsafe

You can state that your type has the same representation as the type it wraps, making the pointers compatible via repr(transparent):

#[repr(transparent)]
struct WrappedVal(given::Val);

impl Outer {
    fn modify<F>(&self, f: F)
    where
        F: FnOnce(&mut WrappedVal) -> &mut WrappedVal,
    {
        self.0.modify(|v| {
            // Insert documentation why **you** think this is safe
            // instead of copy-pasting from Stack Overflow
            let wv = unsafe { &mut *(v as *mut given::Val as *mut WrappedVal) };
            let wv = f(wv);
            unsafe { &mut *(wv as *mut WrappedVal as *mut given::Val) }
        })
    }
}

With repr(transparent) in place, the two pointers are interchangable. I ran a quick test with Miri and your full example and didn't receive any errors, but that's not a silver bullet that I didn't mess something else up.

Zymogenesis answered 1/1, 2019 at 23:52 Comment(4)
Thanks for mentioning repr(transparent), I didn't know about that. How should I argue that the cast is safe - surely if the values have the same memory representation, I should be able to call methods from both types on the same memory location? Do I need to assess any other aspect?Genaro
I had attempted the safe solution where a &mut Val is wrapped, but as hinted at in the question I couldn't get the lifetimes right. I don't want to litter my code (especially Outer) with lifetime parameters that will only be needed locally in one method for WrappedVal, am I approaching this wrong? I'm already surprised that you could omit the lifetime argument in FnOnce(WrappedVal) -> WrappedVal.Genaro
@Genaro yes, using lifetimes like that in a trait is beyond what Rust can currently express; generic associated types (GATs) need to be implemented first. Traits are more complicated than plain functions due to their flexibility.Zymogenesis
@Genaro My comment inside the code is meant to dissuade future people from just copy-pasting the code without reading the rest of the answer. My belief that the code is safe comes from the usage of repr(transparent), so that's what I'd use in my comment, yeah.Zymogenesis
A
1

Another safe solution, with bytemuck:

#[derive(bytemuck::TransparentWrapper)]
#[repr(transparent)]
struct WrappedVal(Val);

Conversion is done as bytemuck::TransparentWrapper::wrap_ref(reference). There are other options, such as removing the newtype, conversion of slices, Box, Rc, Arc, Vec (with bytemuck::allocation::TransparentWrapperAlloc).

Avron answered 12/6, 2024 at 18:12 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.