soa-derive

A Rust derive macro that turns Vec<T> into a cache-friendly struct-of-arrays layout with matching Vec, Ref, Slice, and Ptr helper types.

Library
Cargo
v0.14.0
488stars
MIT OR Apache-2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
34/100Needs Attention
Development Activity0
Maintenance0
Community56
Maturity60
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
74/100Good
Architecture80
Code Quality78
Innovation72
Learning Curve65

soa_derive is a Rust procedural macro crate that automatically converts a plain struct into a struct-of-arrays (SoA) representation. Annotating a struct with #[derive(StructOfArray)] generates a companion <Name>Vec type where each field becomes its own Vec, along with matching Ref, RefMut, Slice, SliceMut, Ptr, and PtrMut helper types that mirror the ergonomics of Vec<T>, &T, &[T], and their mutable/raw-pointer counterparts.

The generated types support push and iteration, field-selective traversal via the soa_zip! macro, nested struct-of-arrays via #[nested_soa], and pass-through of derives like Debug, PartialEq, or serde::Serialize via the #[soa_derive(...)] and #[soa_attr(...)] attributes. It targets performance-sensitive Rust code — simulation, game engines, numerical workloads — where a struct-of-arrays memory layout improves cache locality and enables field-selective iteration without hand-writing the boilerplate.

What You Get

  • A #[derive(StructOfArray)] macro that generates a <Name>Vec struct-of-arrays type from any plain struct.
  • Matching Ref, RefMut, Slice, SliceMut, Ptr, and PtrMut helper types mirroring &T, &mut T, &[T], &mut [T], and raw pointer equivalents.
  • The soa_zip! macro for iterating over an arbitrary subset of fields together, with mutable and immutable access mixed in one call.
  • Attribute passthrough (#[soa_derive(...)], #[soa_attr(...)]) to add trait derives like Debug, PartialEq, or serde::Serialize to specific generated types.
  • #[nested_soa] support for composing struct-of-arrays types inside other struct-of-arrays types.

Common Use Cases

  • Physics and particle simulations - store per-particle fields (position, velocity, mass) as separate arrays for cache-friendly iteration over millions of entities.
  • Game engine ECS-style data - lay out component data in struct-of-arrays form without manually maintaining parallel vectors.
  • Numerical/scientific computing - enable field-selective iteration over large datasets that would otherwise require a hand-rolled SoA struct.
  • Serialization of columnar data - derive serde::Serialize/Deserialize on the generated SoA type to persist data in a column-oriented format.

Under The Hood

Architecture The public soa_derive crate (src/lib.rs) is a thin facade: it re-exports the proc-macro from the internal crate, defines the StructOfArray trait, and carries the crate-level documentation. The actual codegen lives in soa-derive-internal, whose entry point registers #[proc_macro_derive(StructOfArray, attributes(soa_derive, soa_attr, nested_soa))], parses the annotated struct via syn into a shared Input (input.rs), and then fans out to independent generator modules — vec.rs, refs.rs, ptr.rs, slice.rs (derive/derive_mut), index.rs, iter.rs, and generic.rs — each emitting a TokenStream for one generated type family (the Vec container, Ref/RefMut, Ptr/PtrMut, Slice/SliceMut, Index/IndexMut impls, iterators, and generic slice/vec trait impls); names.rs centralizes the naming convention so every generator stays consistent. It’s a clean parse-once-then-fan-out pipeline, but every generator module depends directly on the shape of Input, so a breaking change there ripples through all of them.

Tech Stack The internal codegen crate depends on syn 2 (derive, extra-traits), quote 1, and proc-macro2 1; the public crate additionally depends on permutation 0.4 for sort-based reordering helpers. Dev-dependencies include bencher for the benchmark suite, serde/serde_json for round-trip serialization tests, and itertools; a build.rs uses rustc_version to detect the compiler at build time. A workspace example crate builds rustdoc for GitHub Pages, and CI (tests.yml) runs the suite across Rust 1.74, stable, beta, and nightly, with a separate docs.yml publishing example documentation on tag/push.

Code Quality The tests/ directory holds thirteen integration test files that exercise essentially every generated feature end-to-end — vec, slice, slice_mut, ptr, index, iter, zip, generic, nested_soa, replace, serde, soa_attr, and an extreme-case file — backed by a shared particles fixture, which is a genuinely thorough suite for a proc-macro crate. soa-derive-internal enables #![warn(clippy::all, clippy::pedantic)] with a short, deliberate allow-list rather than blanket suppression. There are no unit tests inside individual generator modules, relying entirely on integration tests against the generated code — standard practice for proc-macro crates, but worth noting explicitly.

API Design The entire integration surface is one attribute, #[derive(StructOfArray)] — no config files, no separate build step. The generated Vec/Ref/Slice family deliberately mirrors Vec<T>/&T/&[T], so existing Rust muscle memory transfers directly, and the README is upfront about where indexing and Deref can’t be supported because Rust requires those traits to return references. soa_zip! replaces what would otherwise be verbose manual zipping of parallel vectors. The trade-off is API surface: a struct derives six or more companion types, which a consumer has to learn, and macro-expansion errors can be harder to read than hand-written equivalents.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search