rkyv / rkyv

Zero-copy deserialization framework for Rust
https://rkyv.org
MIT License
2.81k stars 166 forks source link

Easier way to use archived and unarchived types together #167

Open djkoloski opened 3 years ago

djkoloski commented 3 years ago

Not entirely sure how best to approach this. There's a combinatorial type problem trying to shim everything together with traits so it might take some thinking. Maybe the best approach will end up being to make it easier to write code for both types at the same time.

safasofuoglu commented 2 years ago

I'm interested in this. As a workaround today to simplify my APIs, can I always use ArchivedMyStruct, treating MyStruct effectively as a schema definition? What are the performance/ergonomics implications of that approach? I believe one is being unable to derive Debug, PartialEq etc for ArchivedMyStruct.

djkoloski commented 2 years ago

You actually can use derives for your archived types! This does help alleviate most of the annoying and repetitive work; check out the #[archive_attr(...)] attribute for more details. Example:

#[derive(Archive, Serialize, Deserialize)]
#[derive(Debug, PartialEq, PartialOrd)]
// This will add #[derive(Debug, PartialEq, PartialOrd)] to the generated type
#[archive_attr(derive(Debug, PartialEq, PartialOrd)]
pub struct MyStruct {
    x: i32,
    y: i32,
    width: i32,
    height: i32,
}

For custom implementations, there should be no performance implications but the ergonomics are not great. Right now, you have to essentially duplicate most of the impls you write: one for the unarchived version and one for the archived version. In most cases, the code will actually even be the same! So one option being explored is providing a simple macro that just duplicates some impl and rewrites it for the archived type. It might look something like this:

#[impl_archived]
impl MyType {
    fn area(&self) -> i32 {
        self.width * self.height
    }
}

// This would generate the following in addition to the original impl:

impl Archived<MyType> {
    fn area(&self) -> i32 {
        self.width * self.height
    }
}
safasofuoglu commented 2 years ago

Right now, you have to essentially duplicate most of the impls you write: one for the unarchived version and one for the archived version

I understand, that's why I mentioned the idea of always dealing with ArchivedMyStruct (constructing, processing, passing around) everywhere. Now I realize it won't work, because I can't serialize ArchivedMyStruct.

safasofuoglu commented 2 years ago

Thinking about this, what would prevent me from being able to serialize (or: store) ArchivedMyStruct? Isn't it always pin-safe so that it can be stored by copying the underlying memory?

djkoloski commented 2 years ago

You could reimplement serialization for archived types. However it's not just a simple memcpy because the relative positions of objects may change. There might be more interesting ideas in there though!

djkoloski commented 1 month ago

From #512:

I don't have a good answer for this one unfortunately. A while ago I tried to unify archived and un-archived types with rel, but that ended up being too much work and complexity to continue to put effort into. The main options are:

  1. Unify through a common trait. This has the downside of making all of your code generic.
  2. Unify through an enum. This has the downside of making everything more complicated.
  3. Duplicate all of your inherent impls via macro. This has the downside of duplicating code and making everything more complicated.
  4. Unify your archived and unarchived types. This has the downside that you have to change your type to support specific endianness, and that it can't support types with relative pointers (which is kind of the whole point of rkyv).

Whichever route you think is best for your specific situation is up to you. I don't have a good way to solve it right now.