Skip to content

Support for multi-versioned public use of zerocopy #619

@jswrenn

Description

@jswrenn

There are a number of high-profile crates in the Rust ecosystem that define types whose memory representation is part of their documented contract. By implementing zerocopy's traits for these types, their users could benefit from zerocopy's compiler-time checks that operations depending on these contracts are sound. But: Where should these trait implementations go?

It is risky for zerocopy to provide trait implementations on foreign types, since zerocopy cannot check the soundness of its implementations on foreign types. If zerocopy's trait implementations implementations on foreign types fell out of sync with the definitions of those types, unsoundness would ensue.

It is strictly safer for third-party types to provide their own implementations of zerocopy's traits, using zerocopy's derive feature. However, zerocopy is still a crate in active development, with occasional breaking changes. For greater interoperability in the ecosystem, we would like our public customers to track zerocopy's latest version. To do so, customers today must either:

  1. document their zerocopy support as unstable
  2. be willing to release new breaking changes of their crate, in step with zerocopy

Both of these options adds friction to the adoption of zerocopy in the Rust ecosystem.

Proposed Solution

There exists a third not-yet-quite-possible-today option: Provided multi-versioned support for zerocopy.

A customer taking this approach would specify a new optional dependency for each version of zerocopy they support; e.g., they would start with:

[dependencies]
zerocopy_0_7 = {  package = "zerocopy", version = "0.7", optional = true, features = ["derive"] }

...and provide feature-gated implementations for that version:

#[cfg_attr(
    feature = "zerocopy_0_7",
    derive(
        zerocopy_0_7::FromZeroes,
        zerocopy_0_7::FromBytes,
        zerocopy_0_7::AsBytes
    ),
    zerocopy_0_7(root = ::zerocopy_0_7),
)]
#[repr(C)]
pub struct sembuf {
    pub sem_num: c_ushort,
    pub sem_op: c_short,
    pub sem_flg: c_short,
}

Upon a new major release of zerocopy, the customer could append a new version to their Cargo.toml:

  [dependencies]
  zerocopy_0_7 = {  package = "zerocopy", version = "0.7", optional = true, features = ["derive"] }
+ zerocopy_0_8 = {  package = "zerocopy", version = "0.8", optional = true, features = ["derive"] }

...and add new, corresponding feature-gated derives:

  #[cfg_attr(
      feature = "zerocopy_0_7",
      derive(
          zerocopy_0_7::FromZeroes,
          zerocopy_0_7::FromBytes,
          zerocopy_0_7::AsBytes
      ),
  )]
+ #[cfg_attr(
+     feature = "zerocopy_0_8",
+     derive(
+         zerocopy_0_8::KnownLayout,
+         zerocopy_0_8::TryFromBytes,
+         zerocopy_0_8::FromZeroes,
+         zerocopy_0_8::FromBytes,
+         zerocopy_0_8::AsBytes
+     ),
+ )]
  #[repr(C)]
  pub struct sembuf {
      pub sem_num: c_ushort,
      pub sem_op: c_short,
      pub sem_flg: c_short,
  }

Technical Details

Presently, zerocopy's derives unconditionally expand to code referencing the path ::zerocopy. This makes depending on multiple versions of zerocopy presently impossible, since they cannot all share that path. To support this approach, zerocopy can make one of two technical changes:

1. Root-setting helper attribute.

Zerocopy could provide a helper attribute for setting its crate root; e.g.:

#[cfg_attr(
    feature = "zerocopy_0_7",
    derive(
        zerocopy_0_7::FromZeroes,
        zerocopy_0_7::FromBytes,
        zerocopy_0_7::AsBytes
    ),
    zerocopy_0_7(crate = ::zerocopy_0_7), // <--- this
)]

The attribute would instruct zerocopy's derives to expand to paths beginning with ::zerocopy_0_7, instead of ::zerocopy.

The drawback of this approach is that this attribute adds line noise to critical derives.

2. Re-export old versions

Alternatively, zerocopy could optionally depend on older versions of itself:

[dependencies]
# ...
v_0_6 = {  package = "zerocopy", version = "0.6", optional = true }
# ...

...and provide versioned re-exports of itself at its root:

#[doc(hidden)]
mod version {
    pub use super:: as v0_7;

    #[cfg(feature = "v_0_6")]
    pub use ::v_0_6;
}

We would then modify zerocopy's derives to always expand to paths beginning with ::zerocopy::version::v_XXX.

Unresolved question: In this approach, how would we populate the zerocopy path on the consumer side?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions