From 50838e2e72327797bac07e92be091cc909e7795b Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Wed, 25 Feb 2026 12:39:07 +0100 Subject: [PATCH 1/3] pod!: bytemuk behind feature --- list-view/Cargo.toml | 2 +- pod/Cargo.toml | 10 ++++++---- pod/src/lib.rs | 1 + pod/src/option.rs | 16 +++++++++++----- pod/src/optional_keys.rs | 17 ++++++++-------- pod/src/primitives.rs | 32 +++++++++++++++++++++++-------- tlv-account-resolution/Cargo.toml | 2 +- type-length-value/Cargo.toml | 2 +- 8 files changed, 54 insertions(+), 28 deletions(-) diff --git a/list-view/Cargo.toml b/list-view/Cargo.toml index 48773f42..33e55d5e 100644 --- a/list-view/Cargo.toml +++ b/list-view/Cargo.toml @@ -13,7 +13,7 @@ num-derive = "0.4.2" num_enum = "0.7.5" num-traits = "0.2.19" solana-program-error = "3.0.0" -spl-pod = { version = "0.7.2", path = "../pod" } +spl-pod = { version = "0.7.2", path = "../pod", features = ["bytemuck"] } thiserror = "2.0.18" [dev-dependencies] diff --git a/pod/Cargo.toml b/pod/Cargo.toml index 15ef8517..fc33ee4c 100644 --- a/pod/Cargo.toml +++ b/pod/Cargo.toml @@ -8,16 +8,18 @@ license = "Apache-2.0" edition = "2021" [features] +default = [] +bytemuck = ["dep:bytemuck", "dep:bytemuck_derive", "solana-address/bytemuck"] serde = ["dep:serde", "solana-address/decode"] borsh = ["dep:borsh", "solana-address/borsh"] wincode = ["dep:wincode"] [dependencies] borsh = { version = "1.5.7", features = ["derive", "unstable__schema"], optional = true } -bytemuck = { version = "1.23.2" } -bytemuck_derive = { version = "1.10.1" } +bytemuck = { version = "1.23.2", optional = true } +bytemuck_derive = { version = "1.10.1", optional = true } serde = { version = "1.0.228", optional = true, features = ["derive"] } -solana-address = { version = "2.2.0", features = ["bytemuck"] } +solana-address = { version = "2.2.0", features = ["copy"] } solana-program-error = "3.0.0" solana-program-option = "3.0.0" wincode = { version = "0.4.4", features = ["derive"], optional = true } @@ -26,7 +28,7 @@ wincode = { version = "0.4.4", features = ["derive"], optional = true } base64 = { version = "0.22.1" } serde_json = "1.0.145" solana-address = { version = "2.2.0", features = ["decode"] } -spl-pod = { path = ".", features = ["wincode"] } +spl-pod = { path = ".", features = ["bytemuck", "wincode"] } test-case = "3.3.1" [lib] diff --git a/pod/src/lib.rs b/pod/src/lib.rs index 67b8f30e..a76f64e3 100644 --- a/pod/src/lib.rs +++ b/pod/src/lib.rs @@ -1,5 +1,6 @@ //! Crate containing `Pod` types and `bytemuck` utilities used in SPL +#[cfg(feature = "bytemuck")] pub mod bytemuck; pub mod option; pub mod optional_keys; diff --git a/pod/src/option.rs b/pod/src/option.rs index c3138d3d..53a9e42f 100644 --- a/pod/src/option.rs +++ b/pod/src/option.rs @@ -6,8 +6,9 @@ //! [`Option`](https://doc.rust-lang.org/std/num/type.NonZeroU64.html) //! and provide the same memory layout optimization. +#[cfg(feature = "bytemuck")] +use bytemuck::{Pod, Zeroable}; use { - bytemuck::{Pod, Zeroable}, solana_address::{Address, ADDRESS_BYTES}, solana_program_error::ProgramError, solana_program_option::COption, @@ -17,7 +18,7 @@ use { /// /// This trait is used to indicate that a type can be `None` according to a /// specific value. -pub trait Nullable: PartialEq + Pod + Sized { +pub trait Nullable: PartialEq + Copy + Sized { /// Value that represents `None` for the type. const NONE: Self; @@ -83,13 +84,15 @@ impl PodOption { /// /// `PodOption` is a transparent wrapper around a `Pod` type `T` with identical /// data representation. -unsafe impl Pod for PodOption {} +#[cfg(feature = "bytemuck")] +unsafe impl Pod for PodOption {} /// ## Safety /// /// `PodOption` is a transparent wrapper around a `Pod` type `T` with identical /// data representation. -unsafe impl Zeroable for PodOption {} +#[cfg(feature = "bytemuck")] +unsafe impl Zeroable for PodOption {} impl From for PodOption { fn from(value: T) -> Self { @@ -128,9 +131,12 @@ impl Nullable for Address { #[cfg(test)] mod tests { - use {super::*, crate::bytemuck::pod_slice_from_bytes}; + use super::*; + #[cfg(feature = "bytemuck")] + use crate::bytemuck::pod_slice_from_bytes; const ID: Address = Address::from_str_const("TestSysvar111111111111111111111111111111111"); + #[cfg(feature = "bytemuck")] #[test] fn test_pod_option_address() { let some_address = PodOption::from(ID); diff --git a/pod/src/optional_keys.rs b/pod/src/optional_keys.rs index d2869443..49c26c0d 100644 --- a/pod/src/optional_keys.rs +++ b/pod/src/optional_keys.rs @@ -1,18 +1,15 @@ //! Optional addresses that can be used a `Pod`s #[cfg(feature = "borsh")] use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use { - bytemuck_derive::{Pod, Zeroable}, - solana_address::Address, - solana_program_error::ProgramError, - solana_program_option::COption, -}; +#[cfg(feature = "bytemuck")] +use bytemuck_derive::{Pod, Zeroable}; #[cfg(feature = "serde")] use { core::{convert::TryFrom, fmt, str::FromStr}, serde::de::{Error, Unexpected, Visitor}, serde::{Deserialize, Deserializer, Serialize, Serializer}, }; +use {solana_address::Address, solana_program_error::ProgramError, solana_program_option::COption}; /// A Pubkey that encodes `None` as all `0`, meant to be usable as a `Pod` type, /// similar to all `NonZero*` number types from the `bytemuck` library. @@ -20,7 +17,8 @@ use { feature = "borsh", derive(BorshDeserialize, BorshSerialize, BorshSchema) )] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))] +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[repr(transparent)] pub struct OptionalNonZeroPubkey(pub Address); impl TryFrom> for OptionalNonZeroPubkey { @@ -129,8 +127,11 @@ impl<'de> Deserialize<'de> for OptionalNonZeroPubkey { #[cfg(test)] mod tests { - use {super::*, crate::bytemuck::pod_from_bytes, solana_address::ADDRESS_BYTES}; + #[cfg(feature = "bytemuck")] + use crate::bytemuck::pod_from_bytes; + use {super::*, solana_address::ADDRESS_BYTES}; + #[cfg(feature = "bytemuck")] #[test] fn test_pod_non_zero_option() { assert_eq!( diff --git a/pod/src/primitives.rs b/pod/src/primitives.rs index c7d62d74..5a088fe6 100644 --- a/pod/src/primitives.rs +++ b/pod/src/primitives.rs @@ -1,6 +1,7 @@ //! primitive types that can be used in `Pod`s #[cfg(feature = "borsh")] use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +#[cfg(feature = "bytemuck")] use bytemuck_derive::{Pod, Zeroable}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -12,7 +13,8 @@ use wincode::{SchemaRead, SchemaWrite}; #[cfg_attr(feature = "wincode", wincode(assert_zero_copy))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(from = "bool", into = "bool"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))] +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[repr(transparent)] pub struct PodBool(pub u8); impl PodBool { @@ -76,7 +78,8 @@ macro_rules! impl_int_conversion { #[cfg_attr(feature = "wincode", wincode(assert_zero_copy))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(from = "u16", into = "u16"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))] +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[repr(transparent)] pub struct PodU16(pub [u8; 2]); impl_int_conversion!(PodU16, u16); @@ -86,7 +89,8 @@ impl_int_conversion!(PodU16, u16); #[cfg_attr(feature = "wincode", wincode(assert_zero_copy))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(from = "i16", into = "i16"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))] +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[repr(transparent)] pub struct PodI16(pub [u8; 2]); impl_int_conversion!(PodI16, i16); @@ -100,7 +104,8 @@ impl_int_conversion!(PodI16, i16); )] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(from = "u32", into = "u32"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))] +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[repr(transparent)] pub struct PodU32(pub [u8; 4]); impl_int_conversion!(PodU32, u32); @@ -114,7 +119,8 @@ impl_int_conversion!(PodU32, u32); )] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(from = "u64", into = "u64"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))] +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[repr(transparent)] pub struct PodU64(pub [u8; 8]); impl_int_conversion!(PodU64, u64); @@ -124,7 +130,8 @@ impl_int_conversion!(PodU64, u64); #[cfg_attr(feature = "wincode", wincode(assert_zero_copy))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(from = "i64", into = "i64"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))] +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[repr(transparent)] pub struct PodI64([u8; 8]); impl_int_conversion!(PodI64, i64); @@ -138,7 +145,8 @@ impl_int_conversion!(PodI64, i64); )] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(from = "u128", into = "u128"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))] +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[repr(transparent)] pub struct PodU128(pub [u8; 16]); impl_int_conversion!(PodU128, u128); @@ -172,8 +180,11 @@ impl_usize_conversion!(PodU128, u128); #[cfg(test)] mod tests { - use {super::*, crate::bytemuck::pod_from_bytes}; + use super::*; + #[cfg(feature = "bytemuck")] + use crate::bytemuck::pod_from_bytes; + #[cfg(feature = "bytemuck")] #[test] fn test_pod_bool() { assert!(pod_from_bytes::(&[]).is_err()); @@ -201,6 +212,7 @@ mod tests { assert_eq!(pod_true, deserialized_true); } + #[cfg(feature = "bytemuck")] #[test] fn test_pod_u16() { assert!(pod_from_bytes::(&[]).is_err()); @@ -219,6 +231,7 @@ mod tests { assert_eq!(pod_u16, deserialized); } + #[cfg(feature = "bytemuck")] #[test] fn test_pod_i16() { assert!(pod_from_bytes::(&[]).is_err()); @@ -242,6 +255,7 @@ mod tests { assert_eq!(pod_i16, deserialized); } + #[cfg(feature = "bytemuck")] #[test] fn test_pod_u64() { assert!(pod_from_bytes::(&[]).is_err()); @@ -263,6 +277,7 @@ mod tests { assert_eq!(pod_u64, deserialized); } + #[cfg(feature = "bytemuck")] #[test] fn test_pod_i64() { assert!(pod_from_bytes::(&[]).is_err()); @@ -286,6 +301,7 @@ mod tests { assert_eq!(pod_i64, deserialized); } + #[cfg(feature = "bytemuck")] #[test] fn test_pod_u128() { assert!(pod_from_bytes::(&[]).is_err()); diff --git a/tlv-account-resolution/Cargo.toml b/tlv-account-resolution/Cargo.toml index 9dd256f8..c8388dbc 100644 --- a/tlv-account-resolution/Cargo.toml +++ b/tlv-account-resolution/Cargo.toml @@ -23,7 +23,7 @@ solana-pubkey = { version = "3.0.0", features = ["curve25519"] } spl-discriminator = { version = "0.5.1", path = "../discriminator" } spl-list-view = { version = "0.1.0", path = "../list-view" } spl-program-error = { version = "0.8.0", path = "../program-error" } -spl-pod = { version = "0.7.1", path = "../pod" } +spl-pod = { version = "0.7.1", path = "../pod", features = ["bytemuck"] } spl-type-length-value = { version = "0.9.0", path = "../type-length-value" } thiserror = "2.0" diff --git a/type-length-value/Cargo.toml b/type-length-value/Cargo.toml index 7be69d4b..5e000486 100644 --- a/type-length-value/Cargo.toml +++ b/type-length-value/Cargo.toml @@ -21,7 +21,7 @@ solana-msg = "3.0.0" solana-program-error = "3.0.0" spl-discriminator = { version = "0.5.1", path = "../discriminator" } spl-type-length-value-derive = { version = "0.2", path = "../type-length-value-derive", optional = true } -spl-pod = { version = "0.7.1", path = "../pod" } +spl-pod = { version = "0.7.1", path = "../pod", features = ["bytemuck"] } thiserror = "2.0" [lib] From 6099fa28ac7221e92769e8a9f2cca9ae7ce931e3 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Thu, 26 Feb 2026 17:05:23 +0100 Subject: [PATCH 2/3] copied/clone methods for Nullable --- pod/src/option.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/pod/src/option.rs b/pod/src/option.rs index 53a9e42f..9b222e69 100644 --- a/pod/src/option.rs +++ b/pod/src/option.rs @@ -18,7 +18,7 @@ use { /// /// This trait is used to indicate that a type can be `None` according to a /// specific value. -pub trait Nullable: PartialEq + Copy + Sized { +pub trait Nullable: PartialEq + Sized { /// Value that represents `None` for the type. const NONE: Self; @@ -78,6 +78,24 @@ impl PodOption { Some(&mut self.0) } } + + /// Maps a `PodOption` to an `Option` by copying the contents of the option. + #[inline] + pub fn copied(&self) -> Option + where + T: Copy, + { + self.as_ref().copied() + } + + /// Maps a `PodOption` to an `Option` by cloning the contents of the option. + #[inline] + pub fn cloned(&self) -> Option + where + T: Clone, + { + self.as_ref().cloned() + } } /// ## Safety @@ -191,4 +209,29 @@ mod tests { let def = PodOption::
::default(); assert_eq!(def, None.try_into().unwrap()); } + + #[test] + fn test_copied() { + let some_address = PodOption::from(ID); + assert_eq!(some_address.copied(), Some(ID)); + + let none_address = PodOption::from(Address::NONE); + assert_eq!(none_address.copied(), None); + } + + #[derive(Clone, Debug, PartialEq)] + struct TestNonCopyNullable([u8; 4]); + + impl Nullable for TestNonCopyNullable { + const NONE: Self = Self([0u8; 4]); + } + + #[test] + fn test_cloned_with_non_copy_nullable() { + let some = PodOption::from(TestNonCopyNullable([1, 2, 3, 4])); + assert_eq!(some.cloned(), Some(TestNonCopyNullable([1, 2, 3, 4]))); + + let none = PodOption::from(TestNonCopyNullable::NONE); + assert_eq!(none.cloned(), None); + } } From d06cb71eb45167de3d2bf12f0b768f118cadf8c9 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Thu, 26 Feb 2026 17:06:09 +0100 Subject: [PATCH 3/3] remove empty default --- pod/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pod/Cargo.toml b/pod/Cargo.toml index fc33ee4c..722a9247 100644 --- a/pod/Cargo.toml +++ b/pod/Cargo.toml @@ -8,7 +8,6 @@ license = "Apache-2.0" edition = "2021" [features] -default = [] bytemuck = ["dep:bytemuck", "dep:bytemuck_derive", "solana-address/bytemuck"] serde = ["dep:serde", "solana-address/decode"] borsh = ["dep:borsh", "solana-address/borsh"]