From f5170b3ea505d2c9be5a19a33047a549f3eac081 Mon Sep 17 00:00:00 2001 From: tatianajrogel Date: Tue, 25 Aug 2026 18:30:52 -0700 Subject: [PATCH 1/2] Add a borsh feature to implement Borsh traits Adds an optional 'borsh' feature implementing BorshSerialize and BorshDeserialize for SmallVec, matching the existing serde support in shape. Schema support (BorshSchema) is gated behind an additional 'borsh-unstable__schema' feature, mirroring borsh's own unstable__schema flag, since that flag pulls in borsh's derive machinery and is explicitly documented as unstable upstream. Closes #291 --- Cargo.toml | 3 +++ src/lib.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/tests.rs | 17 ++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 19391ff..a58f369 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,12 +18,15 @@ std = [] specialization = [] may_dangle = [] serde = ["dep:serde_core"] +borsh = ["dep:borsh"] +borsh-unstable__schema = ["borsh", "borsh/unstable__schema"] internals = [] [dependencies] bytes = { version = "1", optional = true, default-features = false } serde_core = { version = "1.0.221", optional = true, default-features = false } malloc_size_of = { version = "0.1.1", optional = true, default-features = false } +borsh = { version = "1", optional = true, default-features = false } [dev-dependencies] serde_test = "1.0" diff --git a/src/lib.rs b/src/lib.rs index f7b1bba..5d89290 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,6 +30,13 @@ //! When this optional dependency is enabled, `SmallVec` implements the `serde::Serialize` and //! `serde::Deserialize` traits. //! +//! ### `borsh` +//! +//! When this optional dependency is enabled, `SmallVec` implements the `borsh::BorshSerialize` +//! and `borsh::BorshDeserialize` traits. Enabling the additional `borsh-unstable__schema` +//! feature also implements `borsh::BorshSchema`, mirroring borsh's own `unstable__schema` +//! feature (unstable because it depends on borsh's derive machinery). +//! //! ### `specialization` //! //! **This feature is unstable and requires a nightly build of the Rust toolchain.** @@ -93,6 +100,13 @@ use serde_core::{ de::{Deserialize, Deserializer, SeqAccess, Visitor}, ser::{Serialize, SerializeSeq, Serializer}, }; +#[cfg(feature = "borsh")] +use borsh::{ + io::{Read, Result as BorshIoResult, Write}, + BorshDeserialize, BorshSerialize, +}; +#[cfg(feature = "borsh-unstable__schema")] +use borsh::BorshSchema; #[cfg(feature = "std")] use std::io; @@ -2915,6 +2929,48 @@ where } } +#[cfg(feature = "borsh")] +#[cfg_attr(docsrs, doc(cfg(feature = "borsh")))] +impl BorshSerialize for SmallVec +where + T: BorshSerialize, +{ + #[inline] + fn serialize(&self, writer: &mut W) -> BorshIoResult<()> { + self.as_slice().serialize(writer) + } +} + +#[cfg(feature = "borsh")] +#[cfg_attr(docsrs, doc(cfg(feature = "borsh")))] +impl BorshDeserialize for SmallVec +where + T: BorshDeserialize, +{ + #[inline] + fn deserialize_reader(reader: &mut R) -> BorshIoResult { + let items = as BorshDeserialize>::deserialize_reader(reader)?; + Ok(SmallVec::from_vec(items)) + } +} + +#[cfg(feature = "borsh-unstable__schema")] +#[cfg_attr(docsrs, doc(cfg(feature = "borsh-unstable__schema")))] +impl BorshSchema for SmallVec +where + T: BorshSchema, +{ + fn add_definitions_recursively( + definitions: &mut alloc::collections::BTreeMap, + ) { + as BorshSchema>::add_definitions_recursively(definitions); + } + + fn declaration() -> borsh::schema::Declaration { + as BorshSchema>::declaration() + } +} + #[cfg(feature = "malloc_size_of")] impl MallocShallowSizeOf for SmallVec { fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { diff --git a/src/tests.rs b/src/tests.rs index cfd801b..d1260ab 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -820,6 +820,23 @@ fn test_serde() { ); } +#[cfg(feature = "borsh")] +#[test] +fn test_borsh() { + use borsh::{from_slice, to_vec}; + + let mut small_vec: SmallVec = SmallVec::new(); + small_vec.extend([1, 2, 3, 4]); + + let bytes = to_vec(&small_vec).unwrap(); + let decoded: SmallVec = from_slice(&bytes).unwrap(); + assert_eq!(small_vec, decoded); + + // Round-trip should also match a plain Vec's encoding. + let vec_bytes = to_vec(&alloc::vec![1i32, 2, 3, 4]).unwrap(); + assert_eq!(bytes, vec_bytes); +} + #[test] fn grow_to_shrink() { let mut v: SmallVec = SmallVec::new(); From 0ca4ca084a2bc8452b4e033f32ff491a9762d289 Mon Sep 17 00:00:00 2001 From: tatianajrogel Date: Tue, 25 Aug 2026 21:53:09 -0700 Subject: [PATCH 2/2] Fix BorshDeserialize to keep small results inline, apply rustfmt The previous BorshDeserialize impl built a Vec and converted via from_vec(), which always keeps the Vec's heap allocation - so every borsh-deserialized SmallVec spilled to the heap regardless of length, defeating the type's purpose. Deserialize element-by-element instead (same wire format: u32 length prefix + elements), matching how the existing serde Deserialize impl above already does it via push(). Added a regression test asserting a result that fits inline actually stays inline (spilled() == false), plus verified with real cargo: - cargo test --all-features (nightly, matching CI's all-features job) - cargo build --target thumbv7m-none-eabi --no-default-features --features borsh (verifies the no_std claim in the doc comment) - cargo +nightly fmt --all --check (matching CI's style check) All 76 tests pass; ran cargo fmt to fix formatting CI would have flagged on the new use-statements and BorshSchema impl. --- Cargo.lock | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 35 ++++++++++----- src/tests.rs | 10 +++++ 3 files changed, 159 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6d327a..3c6d7ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,90 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "malloc_size_of" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d719de8b8f230028cf8192ae4c1b25267cd6b8a99d2747d345a70b8c81aa13" +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -58,7 +130,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -74,12 +146,24 @@ dependencies = [ name = "smallvec" version = "2.0.0-alpha.12" dependencies = [ + "borsh", "bytes", "malloc_size_of", "serde_core", "serde_test", ] +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "3.0.3" @@ -91,8 +175,47 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] diff --git a/src/lib.rs b/src/lib.rs index 5d89290..b49f2cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,6 +91,13 @@ use core::ptr::copy; use core::ptr::copy_nonoverlapping; use core::ptr::NonNull; +#[cfg(feature = "borsh-unstable__schema")] +use borsh::BorshSchema; +#[cfg(feature = "borsh")] +use borsh::{ + io::{Read, Result as BorshIoResult, Write}, + BorshDeserialize, BorshSerialize, +}; #[cfg(feature = "bytes")] use bytes::{buf::UninitSlice, BufMut}; #[cfg(feature = "malloc_size_of")] @@ -100,13 +107,6 @@ use serde_core::{ de::{Deserialize, Deserializer, SeqAccess, Visitor}, ser::{Serialize, SerializeSeq, Serializer}, }; -#[cfg(feature = "borsh")] -use borsh::{ - io::{Read, Result as BorshIoResult, Write}, - BorshDeserialize, BorshSerialize, -}; -#[cfg(feature = "borsh-unstable__schema")] -use borsh::BorshSchema; #[cfg(feature = "std")] use std::io; @@ -2949,8 +2949,20 @@ where { #[inline] fn deserialize_reader(reader: &mut R) -> BorshIoResult { - let items = as BorshDeserialize>::deserialize_reader(reader)?; - Ok(SmallVec::from_vec(items)) + // Deserialize element-by-element (same wire format as Vec: a u32 length + // prefix followed by that many elements) rather than building a Vec and + // converting with `from_vec`, which would always keep the heap allocation - + // even a result with `len <= N` would spill. Pushing keeps it inline as long + // as it fits, matching how the `serde` Deserialize impl above behaves. + let len = u32::deserialize_reader(reader)?; + let mut result = SmallVec::new(); + result.try_reserve(len as usize).map_err(|_| { + borsh::io::Error::new(borsh::io::ErrorKind::InvalidData, "allocation failure") + })?; + for _ in 0..len { + result.push(T::deserialize_reader(reader)?); + } + Ok(result) } } @@ -2961,7 +2973,10 @@ where T: BorshSchema, { fn add_definitions_recursively( - definitions: &mut alloc::collections::BTreeMap, + definitions: &mut alloc::collections::BTreeMap< + borsh::schema::Declaration, + borsh::schema::Definition, + >, ) { as BorshSchema>::add_definitions_recursively(definitions); } diff --git a/src/tests.rs b/src/tests.rs index d1260ab..a7b32c9 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -831,10 +831,20 @@ fn test_borsh() { let bytes = to_vec(&small_vec).unwrap(); let decoded: SmallVec = from_slice(&bytes).unwrap(); assert_eq!(small_vec, decoded); + assert!(decoded.spilled()); // 4 elements > inline capacity of 2 // Round-trip should also match a plain Vec's encoding. let vec_bytes = to_vec(&alloc::vec![1i32, 2, 3, 4]).unwrap(); assert_eq!(bytes, vec_bytes); + + // A result that fits inline should deserialize inline, not via a heap Vec + // conversion (regression test: an earlier version always spilled here). + let mut small: SmallVec = SmallVec::new(); + small.extend([1, 2]); + let small_bytes = to_vec(&small).unwrap(); + let small_decoded: SmallVec = from_slice(&small_bytes).unwrap(); + assert_eq!(small, small_decoded); + assert!(!small_decoded.spilled()); } #[test]