self_cell icon indicating copy to clipboard operation
self_cell copied to clipboard

Safe API for mapping between types whose owner is Arc<T>

Open alex opened this issue 2 years ago • 15 comments

A frequent use case I have is multiple self_cell types which all have an owner of Arc<T>, and I'd like to be able to "map" between them. Here's an example of this:

use std::sync::Arc;

struct X(());
struct T1<'a>(&'a ());
#[derive(Debug)]
struct T2<'a>(&'a ());

self_cell::self_cell! {
    struct O1 {
        owner: Arc<X>,
        #[covariant]
        dependent: T1,
    }
}

self_cell::self_cell! {
    struct O2 {
        owner: Arc<X>,
        #[covariant]
        dependent: T2,
    }
}

unsafe fn relifetime<'a, 'b>(t: &'a T1<'a>) -> &'b T1<'b> {
    std::mem::transmute(t)
}

fn map(o: &O1, f: impl for<'this> FnOnce(&'this X, &T1<'this>) -> T2<'this>) -> O2 {
    O2::new(Arc::clone(&o.borrow_owner()), |x| {
        f(&x, unsafe { relifetime(o.borrow_dependent()) })
    })
}

fn main() {
    let o1 = O1::new(Arc::new(X(())), |x| T1(&x.0));
    let o2 = map(&o1, |_, t1| T2(t1.0));
    drop(o1);

    o2.with_dependent(|_, v| println!("{:?}", v));
}

Given an O1, I'd like to be able to safely create an O2 which points at the same data.

The implementation of map here works, but it'd be great if there was some way this functionality could be generalized as a part of self-cell, so consumers didn't need to carry unsafe code.

alex avatar Apr 06 '24 02:04 alex

I see the use-case, however there are at least two major blockers that I don't have solutions for of the top of my head:

  • The API allows borrowing the container, not only the inner part, i.e. take a reference to the Arc itself and not the inner thing, we would need some kind of generic mechanism to disallow that.
  • We would need to somehow identify types with stable pointers, there is stable_deref_trait but it is unsafe to implement. Back when I designed self_cell I considered using that trait, but I want an API that is completely safe-to-use, no exceptions. So that trait a non-starter :/

Maybe there is some other way to model the problem. Could you please provide some real examples of the kind of mapping that is done from one dependent type to the next. Also do you need a completely new value, or is it enough to transform the existing one maybe something conceptually like fn map(self, new_dependent_ctor) -> SelfCell<Owner, NewDependentType>.

Wait does this work for you?

use std::sync::Arc;

struct X(());
struct T1<'a>(&'a ());
#[derive(Debug)]
struct T2<'a>(&'a ());

self_cell::self_cell! {
    struct O1 {
        owner: Arc<X>,
        #[covariant]
        dependent: T1,
    }
}

self_cell::self_cell! {
    struct O2 {
        owner: O1,
        #[covariant]
        dependent: T2,
    }
}

fn map(o: O1, f: impl for<'this> std::ops::FnOnce(&'this X, &T1<'this>) -> T2<'this>) -> O2 {
    O2::new(o, |x| f(&x.borrow_owner(), x.borrow_dependent()))
}

fn main() {
    let o1 = O1::new(Arc::new(X(())), |x| T1(&x.0));
    let o2 = map(o1, |_, t1| T2(t1.0));

    o2.with_dependent(|_, v| println!("{:?}", v));
}

Voultapher avatar Apr 06 '24 07:04 Voultapher

Sure, here's three real examples (search in the files for their callers) that follow this pattern:

  • https://github.com/pyca/cryptography/blob/5f19fad7be68f75a4522ec88624114306f35294d/src/rust/src/x509/ocsp_resp.rs#L444-L467
  • https://github.com/pyca/cryptography/blob/5f19fad7be68f75a4522ec88624114306f35294d/src/rust/src/x509/ocsp_resp.rs#L468-L489
  • https://github.com/pyca/cryptography/blob/5f19fad7be68f75a4522ec88624114306f35294d/src/rust/src/x509/crl.rs#L466-L487

alex avatar Apr 06 '24 11:04 alex

What about the nesting I showed, would that solve your use-cases? You could even drop the Arc and just directly use the interior type.

Voultapher avatar Apr 06 '24 12:04 Voultapher

Hmm, so the challenge is that it doesn't work for O2 to have an owned O1, as it means cloning more than just the Arc.

alex avatar Apr 06 '24 12:04 alex

I'm not sure I follow. The example I provided is fully functional.

Voultapher avatar Apr 06 '24 15:04 Voultapher

Sorry, I meant "it doesn't address my use case", not that it doesn't function.

alex avatar Apr 06 '24 15:04 alex

Can you please create a minimal example that shows the behavior you need but can't be done by nesting.

Voultapher avatar Apr 06 '24 17:04 Voultapher

This issue is not that nesting doesn't work, it's that there'd be a performance cost.

On Sat, Apr 6, 2024 at 1:35 PM Lukas Bergdoll @.***> wrote:

Can you please create a minimal example that shows the behavior you need but can't be done by nesting.

— Reply to this email directly, view it on GitHub https://github.com/Voultapher/self_cell/issues/55#issuecomment-2041146585, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAAGBACURHQLOVAYXCU5SDY4AW6FAVCNFSM6AAAAABF2BRP7WVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANBRGE2DMNJYGU . You are receiving this because you authored the thread.Message ID: @.***>

-- All that is necessary for evil to succeed is for good people to do nothing.

alex avatar Apr 06 '24 20:04 alex

Please elaborate.

Voultapher avatar Apr 07 '24 08:04 Voultapher

Sure, take the following use case:

type V<'a> = Vec<&'a str>;
type S<'a> = &'a str;

self_cell::self_cell! {
    struct O1 {
        owner: Arc<str>,
        #[covariant]
        dependent: V,
    }
}

self_cell::self_cell! {
    struct O2 {
        owner: Arc<str>,
        #[covariant]
        dependent: S,
    }
}

fn main() {
    // ...
    map(o1, |owner, dependent| O2::new(owner, dependent[0]));
    // ...
}

If the owner in O2 were O1, instead of Arc<str>, then creating one would requiring cloning the Vec<&str>, instead of simply being a reference count increment.

alex avatar Apr 09 '24 23:04 alex

The suggested map function:

fn map(o: O1, f: impl for<'this> std::ops::FnOnce(&'this X, &T1<'this>) -> T2<'this>) -> O2 {
    O2::new(o, |x| f(&x.borrow_owner(), x.borrow_dependent()))
}

moves o1, so there is no copy at all, not even a ref-increment. Or do you need o1 to be around afterwards? I assumed no because you drop it immediately afterwards. Also by nesting it's not lost, you can still access it via the owner.

Voultapher avatar Apr 10 '24 07:04 Voultapher

Yes, unfortunately I need to keep the original alive, so for my use case it doesn't work for map to take O1 by value.

alex avatar Apr 10 '24 11:04 alex

The value of O1 would still be around though, it's the owner of O2. Calling .borrow_owner() would yield a &O1. How is O1 used afterwards?

Voultapher avatar Apr 10 '24 12:04 Voultapher

In my use cases, O1 is somewhere else on the heap, with other references to it, so it's not possible to move it.

alex avatar Apr 10 '24 12:04 alex

Ok, back to square one. Unless you have an idea how to circumvent the two major roadblocks I explained in the initial response, I'm out of ideas short of re-architect your code so you can move O1.

Voultapher avatar Apr 10 '24 12:04 Voultapher

Hmm, I'm not sure I understand the first problem, can you explain a bit more?

For the second (unsafe trait required), yes I think this is a challenge. Either you have to hard-code self_cell to only support a few types, or I think any trait has to be unsafe.

alex avatar Apr 13 '24 17:04 alex

Take this example:

type Dependent<'a> = &'a Arc<String>;

self_cell!(
    struct SelfCell {
        owner: Arc<String>,

        #[covarint]
        dependent: Dependent,
    }
);

The reference to the inner part of Arc stay stable, however the dependent type references the Arc directly, and that doesn't stay stable. I'm not aware of a mechanism to generically expose that relationship. Does that help?

Voultapher avatar Apr 14 '24 17:04 Voultapher

Ah yes, I think you need to only pass the map() callback the result of deref(), you can't give it a ref to the owner itself.

On Sun, Apr 14, 2024 at 1:12 PM Lukas Bergdoll @.***> wrote:

Take this example:

type Dependent<'a> = &'a Arc<String>; self_cell!( struct SelfCell { owner: Arc<String>,

    #[covarint]
    dependent: Dependent,
});

The reference to the inner part of Arc stay stable, however the dependent type references the Arc directly, and that doesn't stay stable. I'm not aware of a mechanism to generically expose that relationship. Does that help?

— Reply to this email directly, view it on GitHub https://github.com/Voultapher/self_cell/issues/55#issuecomment-2054124634, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAAGBCIBTHXY4GH667JT4LY5K2ILAVCNFSM6AAAAABF2BRP7WVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANJUGEZDINRTGQ . You are receiving this because you authored the thread.Message ID: @.***>

-- All that is necessary for evil to succeed is for good people to do nothing.

alex avatar Apr 14 '24 19:04 alex

Given the constraints that I want to keep, namely not giving certain types special treatment, and zero unsafe-to-use parts of the API, I'm not seeing a way forward with this issue.

I'm proposing to close it.

Voultapher avatar Apr 15 '24 06:04 Voultapher