#![cfg_attr(not(test), no_std)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links)]
pub use ix_schema_derive::Ix;
pub trait Ix {
const MANIFEST: Manifest<'static>;
}
pub trait MigrateFrom<Prev: Ix>: Ix + Sized {
fn migrate_from(prev: Prev) -> Self;
}
pub trait Upgrade<Target> {
fn upgrade(self) -> Target;
}
#[macro_export]
macro_rules! migrate_chain {
($first:ty $(=> $rest:ty)+) => {
$crate::migrate_chain!(@grow [$first] $(=> $rest)+);
};
(@grow [$pred:ty $(, $older:ty)*] => $new:ty $(=> $rest:ty)*) => {
impl $crate::Upgrade<$new> for $pred {
fn upgrade(self) -> $new {
<$new as $crate::MigrateFrom<$pred>>::migrate_from(self)
}
}
$(
impl $crate::Upgrade<$new> for $older {
fn upgrade(self) -> $new {
<$new as $crate::MigrateFrom<$pred>>::migrate_from(
<$older as $crate::Upgrade<$pred>>::upgrade(self),
)
}
}
)*
$crate::migrate_chain!(@grow [$new, $pred $(, $older)*] $(=> $rest)*);
};
(@grow [$($seen:ty),+]) => {};
}
pub trait Driver<T: Ix> {
const SUPPORTED: bool;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Manifest<'a> {
pub type_name: &'a str,
pub doc: &'a str,
pub schema_version: u32,
pub layout: LayoutSpec,
pub fields: &'a [FieldSpec<'a>],
pub variants: &'a [VariantSpec<'a>],
pub evolution: EvolutionSpec<'a>,
}
impl Manifest<'_> {
#[must_use]
pub const fn packed_field_bytes(&self) -> usize {
let mut sum = 0;
let mut i = 0;
while i < self.fields.len() {
sum += self.fields[i].size;
i += 1;
}
sum
}
#[must_use]
pub const fn padding_bytes(&self) -> usize {
self.layout.size.saturating_sub(self.packed_field_bytes())
}
#[must_use]
pub const fn is_gap_free(&self) -> bool {
self.padding_bytes() == 0
}
#[must_use]
pub const fn extends(&self, prev: &Manifest<'_>) -> bool {
let mut i = 0;
while i < prev.fields.len() {
let want = prev.fields[i];
let mut matched = false;
let mut j = 0;
while j < self.fields.len() {
let have = self.fields[j];
if str_eq(have.name, want.name) {
matched = have.offset == want.offset
&& have.size == want.size
&& str_eq(have.type_name, want.type_name);
break;
}
j += 1;
}
if !matched {
return false;
}
i += 1;
}
true
}
}
const fn str_eq(a: &str, b: &str) -> bool {
let a = a.as_bytes();
let b = b.as_bytes();
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
#[macro_export]
macro_rules! assert_compatible {
($new:ty : $old:ty) => {
const _: () = ::core::assert!(
<$new as $crate::Ix>::MANIFEST.extends(&<$old as $crate::Ix>::MANIFEST),
::core::concat!(
stringify!($new),
" is not a layout-compatible extension of ",
stringify!($old),
),
);
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LayoutSpec {
pub size: usize,
pub align: usize,
pub repr: Repr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Repr {
Rust,
C,
Transparent,
Packed(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FieldSpec<'a> {
pub name: &'a str,
pub doc: &'a str,
pub type_name: &'a str,
pub offset: usize,
pub size: usize,
pub align: usize,
pub since: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VariantKind {
Unit,
Tuple,
Struct,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VariantFieldSpec<'a> {
pub name: Option<&'a str>,
pub type_name: &'a str,
pub size: usize,
pub align: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VariantSpec<'a> {
pub name: &'a str,
pub doc: &'a str,
pub discriminant: Option<i64>,
pub kind: VariantKind,
pub fields: &'a [VariantFieldSpec<'a>],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EvolutionSpec<'a> {
pub migrates_from: Option<u32>,
pub changes: &'a [FieldChange<'a>],
}
impl EvolutionSpec<'_> {
pub const GENESIS: EvolutionSpec<'static> = EvolutionSpec {
migrates_from: None,
changes: &[],
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldChange<'a> {
Added {
name: &'a str,
},
Removed {
name: &'a str,
},
Renamed {
from: &'a str,
to: &'a str,
},
Transformed {
name: &'a str,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[repr(C)]
struct Sample {
a: u32,
b: u16,
}
impl Ix for Sample {
const MANIFEST: Manifest<'static> = Manifest {
type_name: "ix_schema::tests::Sample",
doc: "",
schema_version: 1,
layout: LayoutSpec {
size: core::mem::size_of::<Sample>(),
align: core::mem::align_of::<Sample>(),
repr: Repr::C,
},
fields: &[
FieldSpec {
name: "a",
doc: "",
type_name: "u32",
offset: core::mem::offset_of!(Sample, a),
size: core::mem::size_of::<u32>(),
align: core::mem::align_of::<u32>(),
since: 1,
},
FieldSpec {
name: "b",
doc: "",
type_name: "u16",
offset: core::mem::offset_of!(Sample, b),
size: core::mem::size_of::<u16>(),
align: core::mem::align_of::<u16>(),
since: 1,
},
],
variants: &[],
evolution: EvolutionSpec::GENESIS,
};
}
#[test]
fn manifest_reports_declared_shape() {
let m = Sample::MANIFEST;
assert_eq!(m.schema_version, 1);
assert_eq!(m.layout.repr, Repr::C);
assert_eq!(m.fields.len(), 2);
assert_eq!(m.fields[0].name, "a");
assert_eq!(m.fields[0].offset, 0);
assert_eq!(m.evolution, EvolutionSpec::GENESIS);
}
#[test]
fn padding_matches_real_layout() {
let m = Sample::MANIFEST;
assert_eq!(m.packed_field_bytes(), 6);
assert_eq!(m.layout.size, 8);
assert_eq!(m.padding_bytes(), 2);
assert!(!m.is_gap_free());
}
struct ZerocopyDriver;
impl<T: Ix> Driver<T> for ZerocopyDriver {
const SUPPORTED: bool = matches!(T::MANIFEST.layout.repr, Repr::C | Repr::Transparent)
&& T::MANIFEST.is_gap_free();
}
#[test]
fn driver_support_folds_from_const_manifest() {
const { assert!(!<ZerocopyDriver as Driver<Sample>>::SUPPORTED) };
let supported = <ZerocopyDriver as Driver<Sample>>::SUPPORTED;
assert!(!supported);
}
#[test]
fn retyping_a_carried_field_breaks_extends() {
const OLD: Manifest<'static> = Manifest {
type_name: "X",
doc: "",
schema_version: 1,
layout: LayoutSpec {
size: 4,
align: 4,
repr: Repr::C,
},
fields: &[FieldSpec {
name: "v",
doc: "",
type_name: "u32",
offset: 0,
size: 4,
align: 4,
since: 1,
}],
variants: &[],
evolution: EvolutionSpec::GENESIS,
};
const NEW: Manifest<'static> = Manifest {
type_name: "X",
doc: "",
schema_version: 2,
layout: LayoutSpec {
size: 4,
align: 4,
repr: Repr::C,
},
fields: &[FieldSpec {
name: "v",
doc: "",
type_name: "f32",
offset: 0,
size: 4,
align: 4,
since: 1,
}],
variants: &[],
evolution: EvolutionSpec::GENESIS,
};
const { assert!(!NEW.extends(&OLD)) };
assert!(!NEW.extends(&OLD));
assert!(OLD.extends(&OLD));
}
#[test]
fn migration_trait_is_type_safe() {
#[repr(C)]
struct V1 {
id: u32,
}
#[repr(C)]
struct V2 {
id: u32,
flags: u8,
}
impl Ix for V1 {
const MANIFEST: Manifest<'static> = Manifest {
type_name: "V1",
doc: "",
schema_version: 1,
layout: LayoutSpec {
size: core::mem::size_of::<V1>(),
align: core::mem::align_of::<V1>(),
repr: Repr::C,
},
fields: &[],
variants: &[],
evolution: EvolutionSpec::GENESIS,
};
}
impl Ix for V2 {
const MANIFEST: Manifest<'static> = Manifest {
type_name: "V2",
doc: "",
schema_version: 2,
layout: LayoutSpec {
size: core::mem::size_of::<V2>(),
align: core::mem::align_of::<V2>(),
repr: Repr::C,
},
fields: &[],
variants: &[],
evolution: EvolutionSpec {
migrates_from: Some(1),
changes: &[FieldChange::Added { name: "flags" }],
},
};
}
impl MigrateFrom<V1> for V2 {
fn migrate_from(prev: V1) -> Self {
V2 {
id: prev.id,
flags: 0,
}
}
}
let v2 = V2::migrate_from(V1 { id: 7 });
assert_eq!(v2.id, 7);
assert_eq!(v2.flags, 0);
assert_eq!(V2::MANIFEST.evolution.migrates_from, Some(1));
}
}