New type pattern: convert Arc<W1<T>> to Arc<W2<T>>

Hi,

Assuming we have 2 simple wrappers on some type (new type pattern):

Full example here: demo

struct W1<T>(T);
struct W2<T>(T);

I assume converting from W1<_> to W2<_> is zero cost?

pub fn w1_to_w2<T>(a: W1<T>) -> W2<T> {
    W2(a.0)
}

But my problem is converting Arc<W1<T>> to Arc<W2<T>> because I also want it to be safe and zero-cost, meaning - no extra allocation, no extra copy/clone.

From the documentation of Arc I found the solution (in unsafe RUST) with using of from_raw / into_raw:

pub fn arc_w1_to_arc_w2<T>(a: Arc<W1<T>>) -> Arc<W2<T>> 
{
    let a_ptr = Arc::into_raw(a);
    unsafe {
        let a = Arc::from_raw(a_ptr as *const W2<T>);
        return a;
    }
}

Q1: Is this code above safe?

Do I need to mark these wrappers W1/W2 with some #[repr(C)] to make sure size/allignment is identical for the same T?

[Question about Drop]

I notice that implementing Drop for W1<T> does not block this moving, probably this is some kind of UB, but "MIRI" does not complain?

Modified example demo with Drop

I mean

#[repr(transparent)]
pub struct W1<T>(T);
#[repr(transparent)]
pub struct W2<T>(T);

impl<T> Drop for W1<T> {
    fn drop(&mut self) {
        println!("Dropping W1");
    }
}

Then this code does not show "Dropping W1" when uncommenting lines with b:

fn main() {

    let a = Arc::new(W1(123_u16));
    let b = clone_arc_w1_to_arc_w2(&a);
    assert_eq!(b.0, 123);
    assert_eq!(a.0, 123);
    drop(b);
    println!("ok");
}

pub fn clone_arc_w1_to_arc_w2<T>(a: &Arc<W1<T>>) -> Arc<W2<T>> 
{
    let a_ptr = Arc::as_ptr(a);
    unsafe {
        let a = Arc::from_raw(a_ptr as *const W2<T>);
        return a;
    }
}

Of course this is pure theoretical problem - I do not plan to implement Drop for my wrapper types.

I would add [repr(transparent)] to the structs. This can only be applied to structs with only one field and then guarantees that the layout of the struct is identical to the layout of that field.

If applied to both structs it guarantees identical layout. Miri doesn't even complain about the current code, but this i believe is a "accident" of the layout algorithm and not a guarantee.

2 Likes

Rust doesn't have type-based aliasing analysis like C, but it still seems weird to me that you can have Arc<A> and Arc<B> exist at the same time and own the same object that has two different types.

1 Like

You can actually have two types for the same value in completely safe code even without any transmutes hidden in the standard library, through coercion:

use std::sync::Arc;

trait Tr1<T> {}
trait Tr2<T> {}
type W1<T> = dyn Tr1<T>;
type W2<T> = dyn Tr2<T>;

struct W<T>(T);
impl<T> Tr1<T> for W<T> {}
impl<T> Tr2<T> for W<T> {}

#[test]
fn example() {
    let original: Arc<W<&str>> = Arc::new(W("hello"));
    
    let w1: Arc<W1<&str>> = original.clone();
    let w2: Arc<W2<&str>> = original.clone();
}

It's much more common to convert Arc<T> to Arc<dyn Trait> once after the Arc is created, so all clones have the same type, but it's entirely possible to keep both and have two pointers giving two views of the same data. Simpler case with array-to-slice coercion instead of dyn:

fn example2() {
    let original: Arc<[i32; 3]> = Arc::new([1, 2, 3]);    
    let w1: Arc<[i32]> = original.clone();
}

(This doesn't help OP's specific problem at all, unless they can express the behaviors wanted for W1 and W2 as implementations upon dyn types; I mainly think this is useful for intuition-building about Rust types.)

4 Likes

Actually I am moving from Arc<A> to Arc<B> assuming A and B have same size/alignment. So they technically do not exist at the same time. But with using as_ptr in place of into_raw I could have both at the same time.

That approach is not safe against Drop - I mean when e.g. W1 implements Drop then this "move" or "clone" (with as_ptr) still works, "miri" does not complain and we end up with Drop from W2

Arc is a shared ownership. Arc::into_raw() release an ownership, not the ownership (like Box::into_raw()).

due to the memory layout of extra bookkeeping information, there's no operation to convert a Arc into an exclusive owner (i.e. Box).

note, you can't just check Arc::strong_count() is equal to 1, since there's always a chance a weak pointer is upgraded into a strong Arc in other threads between the check and the Arc::into_raw().

the only way I know of to make sure an Arc is the sole owner of the value is to check if a call to Arc::get_mut() succeeds or not. if so, then you can be sure there's no other pointers, strong or weak, to the same allocation.

2 Likes

How is this unsound? Either Drop W1 or Drop W2 runs, never both. The Drop function by itself won't lead to UB, because W1 and W2 have the same memory layout, so you can't do anthing dangerous in Drop.

You can't rely on Drop running in fully safe rust. So yeah you can't rely on Drop for W1 running, but that has nothing to do with this function.

But I can do some necessary things in one of these wrappers - that needs to be done when going out of scope (RAII), then assumption that Arc<W1<T>> will do some necessary clean-up when last Arc clone goes out of scope will be wrong - because it is enough to just convert one of them to Arc<W2<T>> with "my" method.

And in safe RUST - it is impossible to move out of the valuie of the type that implements Drop probably for that reason - this function will not compile if W1<T> implements Drop:

pub fn w1_to_w2<T>(a: W1<T>) -> W2<T> {
    W2(a.0)
}
// error[E0509]: cannot move out of type `W1<T>`, which implements the `Drop` trait
// ... move occurs because `a.0` has type `T`, which does not implement the `Copy` trait
// ... help: if `T` implemented `Clone`, you could clone the value

You can't rely on Drop actually being called. You can always call std::mem::forget in safe code and then the cleanup code isn't called. Drop being called can't be necessary for your code being sound. So this function doesn't make this any worse.

2 Likes

if I understand correctly - there are several methods to break "Drop contract" already in safe-RUST and this function will be just another one. Probably worth to mention about that in the function comments

1 Like

Yes exactly.

Sounds like a good idea.