Expand description
wincode is a fast, bincode‑compatible serializer/deserializer focused on in‑place initialization and direct memory writes.
In short, wincode operates over traits that facilitate direct writes of memory
into final destinations (including heap-allocated buffers) without intermediate
staging buffers.
§Quickstart
wincode traits are implemented for many built-in types (like Vec, integers, etc.).
You’ll most likely want to start by using wincode on your own struct types, which can be
done easily with the derive macros.
#[derive(SchemaWrite, SchemaRead)]
struct MyStruct {
data: Vec<u8>,
win: bool,
}
let val = MyStruct { data: vec![1,2,3], win: true };
assert_eq!(wincode::serialize(&val).unwrap(), bincode::serialize(&val).unwrap());§Motivation
Typical Rust API design employs a construct-then-move style of programming.
Common APIs like Vec::push, iterator adaptors, Box::new (and its Rc/Arc
variants), and even returning a fully-initialized struct from a function all
follow this pattern. While this style feels intuitive and ergonomic, it
inherently entails copying unless the compiler can perform elision – which,
today, it generally cannot. To see why this is a consequence of the design,
consider the following code:
Box::new(MyStruct::new());MyStruct must be constructed before it can be moved into Box’s allocation.
This is a classic code ordering problem: to avoid the copy, Box::new needs
to execute code before MyStruct::new() runs. Vec::push, iterator collection,
and similar APIs have this same problem.
(See these design meeting notes or
or the
placement-by-return RFC
for a more in-depth discussion on this topic.) The result of this is that even
performance conscious developers routinely introduce avoidable copying without
realizing it. serde inherits these issues since it neither attempts to
initialize in‑place nor exposes APIs to do so.
These patterns are not inherent limitations of Rust, but are consequences of
conventions and APIs that do not consider in-place initialization as part of
their design. The tools for in-place construction do exist (see
MaybeUninit and raw pointer APIs), but they are
rarely surfaced in libraries and can be cumbersome to use (see addr_of_mut!),
so programmers are often not even aware of them or avoid them.
wincode makes in-place initialization a first class design goal, and fundamentally
operates on traits that facilitate direct writes of memory.
§Adapting foreign types
wincode can also be used to implement serialization/deserialization
on foreign types, where serialization/deserialization schemes on those types are unoptimized (and
out of your control as a foreign type). For example, consider the following struct,
defined outside of your crate:
use serde::{Serialize, Deserialize};
#[repr(transparent)]
#[derive(Clone, Copy, Serialize, Deserialize)]
struct Address([u8; 32]);
#[repr(transparent)]
#[derive(Clone, Copy, Serialize, Deserialize)]
struct Hash([u8; 32]);
#[derive(Serialize, Deserialize)]
pub struct A {
pub addresses: Vec<Address>,
pub hash: Hash,
}serde’s default, naive, implementation will perform per-element visitation of all bytes
in Vec<Address> and Hash. Because these fields are “plain old data”, ideally we would
avoid per-element visitation entirely and read / write these fields in a single pass.
The situation worsens if this struct needs to be written into a heap allocated data structure,
like a Vec<A> or Box<[A]>. As discussed in motivation, all
those bytes will be initialized on the stack before being copied into the heap allocation.
wincode can solve this with the following:
mod foreign_crate {
// Defined in some foreign crate...
use serde::{Serialize, Deserialize};
#[repr(transparent)]
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct Address(pub [u8; 32]);
#[repr(transparent)]
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct Hash(pub [u8; 32]);
#[derive(Serialize, Deserialize)]
pub struct A {
pub addresses: Vec<Address>,
pub hash: Hash,
}
}
#[derive(SchemaWrite, SchemaRead)]
#[wincode(from = "foreign_crate::A")]
pub struct MyA {
addresses: Vec<Pod<foreign_crate::Address>>,
hash: Pod<foreign_crate::Hash>,
}
let val = foreign_crate::A {
addresses: vec![foreign_crate::Address([0; 32]), foreign_crate::Address([1; 32])],
hash: foreign_crate::Hash([0; 32]),
};
let bincode_serialize = bincode::serialize(&val).unwrap();
let wincode_serialize = MyA::serialize(&val).unwrap();
assert_eq!(bincode_serialize, wincode_serialize);
let bincode_deserialize: foreign_crate::A = bincode::deserialize(&bincode_serialize).unwrap();
let wincode_deserialize = MyA::deserialize(&bincode_serialize).unwrap();
assert_eq!(val, bincode_deserialize);
assert_eq!(val, wincode_deserialize);Now, when deserializing A:
- All initialization is done in-place, including heap-allocated memory
(true of all supported contiguous heap-allocated structures in
wincode). - Byte fields are read and written in a single pass.
§Compatibility
- Produces the same bytes as
bincodefor the covered shapes when using bincode’s default configuration, provided yourSchemaWriteandSchemaReadschemas andcontainersmatch the layout implied by yourserdetypes. - Length encodings are pluggable via
SeqLen.
§Zero copy deserialization
wincode supports zero copy deserialization of contiguous byte slices
(serialized with Vec<u8>, Box<[u8]>, [u8; N], etc.).
use wincode::{SchemaWrite, SchemaRead};
#[derive(SchemaWrite, SchemaRead)]
struct ByteRef<'a> {
bytes: &'a [u8],
}
let bytes: Vec<u8> = vec![1, 2, 3, 4, 5];
let byte_ref = ByteRef { bytes: &bytes };
let serialized = wincode::serialize(&byte_ref).unwrap();
let deserialized = wincode::deserialize(&serialized).unwrap();
assert_eq!(byte_ref, deserialized);§Derive attributes
§Top level
| Attribute | Type | Default | Description |
|---|---|---|---|
from | Type | None | Indicates that type is a mapping from another type (example in previous section) |
no_suppress_unused | bool | false | Disable unused field lints suppression. Only usable on structs with from. |
struct_extensions | bool | false | Generates placement initialization helpers on SchemaRead struct implementations |
tag_encoding | Type | None | Specifies the encoding/decoding schema to use for the variant discriminant. Only usable on enums. |
§no_suppress_unused
When creating a mapping type with #[wincode(from = "AnotherType")], fields are typically
comprised of containers (of course not strictly always true). As a result, these structs
purely exist for the compiler to generate optimized implementations, and are never actually
constructed. As a result, unused field lints will be triggered, which can be annoying.
By default, when from is used, the derive macro will generate dummy function that references all
the struct fields, which suppresses those lints. This function will ultimately be compiled out of your
build, but you can disable this by setting no_suppress_unused to true. You can also avoid
these lint errors with visibility modifiers (e.g., pub).
Note that this only works on structs, as it is not possible to construct an arbitrary enum variant.
§tag_encoding
Allows specifying the encoding/decoding schema to use for the variant discriminant. Only usable on enums.
Deserialize and
Serialize impl for your enum on the serde / bincode side.
Example:
use wincode::{SchemaWrite, SchemaRead};
#[derive(SchemaWrite, SchemaRead)]
#[wincode(tag_encoding = "u8")]
enum Enum {
A,
B,
C,
}
assert_eq!(&wincode::serialize(&Enum::B).unwrap(), &1u8.to_le_bytes());§struct_extensions
You may have some exotic serialization logic that requires you to implement SchemaRead manually
for a type. In these scenarios, you’ll likely want to leverage some additional helper methods
to reduce the amount of boilerplate that is typically required when dealing with uninitialized
fields.
For example:
#[derive(SchemaRead, SchemaWrite)]
#[wincode(struct_extensions)]
struct Header {
num_required_signatures: u8,
num_signed_accounts: u8,
num_unsigned_accounts: u8,
}
#[derive(SchemaRead, SchemaWrite)]
#[wincode(struct_extensions)]
struct Payload {
header: Header,
data: Vec<u8>,
}
#[derive(SchemaWrite)]
struct Message {
payload: Payload,
}
// Assume for some reason we have to manually implement `SchemaRead` for `Message`.
impl<'de> SchemaRead<'de> for Message {
type Dst = Message;
fn read(reader: &mut impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
// We have to do a big ugly cast like this to get a mutable MaybeUninit<Payload>.
let mut payload = unsafe {
&mut *(&raw mut (*dst.as_mut_ptr()).payload).cast::<MaybeUninit<Payload>>()
};
// `Payload::uninit_header_mut` is generated by `#[wincode(struct_extensions)]`.
// This avoids having to do a big ugly cast like we had to do for payload above.
//
// Project a mutable `MaybeUninit<Header>` from the `MaybeUninit<Payload>`.
let header = Payload::uninit_header_mut(payload);
// Similarly, `Header::read_num_required_signatures` is generated
// by `#[wincode(struct_extensions)]`.
//
// Read directly into the projected MaybeUninit<Header> slot.
Header::read_num_required_signatures(reader, header)?;
// ...
Header::read_num_signed_accounts(reader, header)?;
Header::read_num_unsigned_accounts(reader, header)?;
// Alternatively, we could have done `Payload::read_header(reader, payload)?;`
// rather than reading all the fields individually.
Payload::read_data(reader, payload)?;
Ok(())
}
}
let msg = Message {
payload: Payload {
header: Header {
num_required_signatures: 1,
num_signed_accounts: 2,
num_unsigned_accounts: 3
},
data: vec![4, 5, 6, 7, 8, 9]
}
};
let serialized = wincode::serialize(&msg).unwrap();
let deserialized = wincode::deserialize(&serialized).unwrap();
assert_eq!(msg, deserialized);#[wincode(struct_extensions)] generates three methods per field:
uninit_<field_name>_mut- Gets a mutable
MaybeUninitprojection to the<field_name>slot.
- Gets a mutable
read_<field_name>- Reads into a
MaybeUninit’s<field_name>slot from the givenReader.
- Reads into a
write_uninit_<field_name>- Writes a
MaybeUninit’s<field_name>slot with the given value.
- Writes a
§Field level
| Attribute | Type | Default | Description |
|---|---|---|---|
with | Type | None | Overrides the default SchemaRead or SchemaWrite implementation for the field. |
§Variant level (enum variants)
| Attribute | Type | Default | Description |
|---|---|---|---|
tag | Expr | None | Specifies the discriminant expression for the variant. Only usable on enums. |
§tag
Specifies the discriminant expression for the variant. Only usable on enums.
Deserialize and
Serialize impl for your enum on the serde / bincode side.
Example:
use wincode::{SchemaWrite, SchemaRead};
#[derive(SchemaWrite, SchemaRead)]
enum Enum {
#[wincode(tag = 5)]
A,
#[wincode(tag = 8)]
B,
#[wincode(tag = 13)]
C,
}
assert_eq!(&wincode::serialize(&Enum::A).unwrap(), &5u32.to_le_bytes());Re-exports§
pub use error::Error;pub use error::ReadError;pub use error::ReadResult;pub use error::Result;pub use error::WriteError;pub use error::WriteResult;
Modules§
- containers
- This module provides specialized implementations of standard library collection types that
provide control over the length encoding (see
SeqLen), as well as special case opt-in raw-copy overrides (seePod). - error
- Error types and helpers.
- io
ReaderandWriterimplementations.- len
- Support for heterogenous sequence length encoding.
Enums§
- Type
Meta - Indicates what kind of assumptions can be made when encoding or decoding a type.
Traits§
- Deserialize
- Helper over
SchemaReadthat automatically constructs a reader and initializes a destination. - Deserialize
Owned - A variant of
Deserializefor types that can be deserialized without borrowing from the reader. - Schema
Read - Types that can be read (deserialized) from a
Reader. - Schema
Read Owned - A type that can be read (deserialized) from a
Readerwithout borrowing from it. - Schema
Write - Types that can be written (serialized) to a
Writer. - Serialize
- Helper over
SchemaWritethat automatically constructs a writer and serializes a source.
Functions§
- deserialize
- Deserialize a type from the given bytes.
- deserialize_
from - Deserialize a type from the given bytes into the given target.
- serialize
alloc - Serialize a type into a
Vecof bytes. - serialize_
into - Serialize a type into the given writer.
- serialized_
size - Get the size in bytes of the type when serialized.
Derive Macros§
- Schema
Read derive - Implement
SchemaReadfor a struct or enum. - Schema
Write derive - Implement
SchemaWritefor a struct or enum.