use std::any::Any;
use std::ffi::{c_void, CString};
use std::sync::Arc;
use crate::status::{Error, Result};
use crate::timestamp::Timestamp;
pub mod type_id {
pub const NONE: u64 = 0;
pub const BYTES: u64 = 1;
pub const I64: u64 = 2;
pub const F64: u64 = 3;
pub const BOOL: u64 = 4;
pub const STR: u64 = 5;
pub const BUFFER: u64 = 6;
pub const HOST_OBJECT: u64 = 7; }
pub mod dtype {
pub const U8: i32 = 0;
pub const I8: i32 = 1;
pub const U16: i32 = 2;
pub const I16: i32 = 3;
pub const I32: i32 = 4;
pub const I64: i32 = 5;
pub const F16: i32 = 6;
pub const F32: i32 = 7;
pub const F64: i32 = 8;
}
pub const MAX_DIMS: usize = 8;
pub fn dtype_size(dt: i32) -> usize {
match dt {
dtype::U8 | dtype::I8 => 1,
dtype::U16 | dtype::I16 | dtype::F16 => 2,
dtype::I32 | dtype::F32 => 4,
dtype::I64 | dtype::F64 => 8,
_ => 0,
}
}
pub fn fnv1a_type_id(mangled_name: &str) -> u64 {
let mut h: u64 = 14695981039346656037;
for b in mangled_name.bytes() {
h ^= b as u64;
h = h.wrapping_mul(1099511628211);
}
if h < 16 {
h + 16
} else {
h
}
}
static TYPE_NAMES: std::sync::LazyLock<std::sync::Mutex<std::collections::BTreeMap<u64, String>>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::BTreeMap::new()));
pub fn register_type_name(id: u64, name: &str) {
TYPE_NAMES
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(id, name.to_string());
}
pub fn type_name(id: u64) -> String {
match id {
type_id::NONE => "any/undeclared".to_string(),
type_id::BYTES => "Bytes".to_string(),
type_id::I64 => "I64".to_string(),
type_id::F64 => "F64".to_string(),
type_id::BOOL => "Bool".to_string(),
type_id::STR => "Str".to_string(),
type_id::BUFFER => "Buffer".to_string(),
type_id::HOST_OBJECT => "HostObject".to_string(),
other => TYPE_NAMES
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&other)
.cloned()
.unwrap_or_else(|| format!("type#{other}")),
}
}
#[derive(Clone, Debug)]
pub struct BufferData {
pub bytes: Vec<u8>,
pub shape: [i64; MAX_DIMS],
pub strides: [i64; MAX_DIMS],
pub ndim: i32,
pub dtype: i32,
}
impl BufferData {
pub fn new(shape: &[i64], dt: i32) -> Result<Self> {
if shape.is_empty() || shape.len() > MAX_DIMS {
return Err(Error::InvalidArg(format!(
"ndim must be in 1..={MAX_DIMS}, got {}",
shape.len()
)));
}
let esz = dtype_size(dt);
if esz == 0 {
return Err(Error::InvalidArg(format!("unknown dtype {dt}")));
}
let mut count: i64 = 1;
for &d in shape {
if d < 0 {
return Err(Error::InvalidArg("shape must not be negative".into()));
}
count = count
.checked_mul(d)
.ok_or_else(|| Error::InvalidArg("shape product overflow".into()))?;
}
let total = (count as usize)
.checked_mul(esz)
.ok_or_else(|| Error::InvalidArg("buffer byte count overflow".into()))?;
let mut s = [0i64; MAX_DIMS];
let mut st = [0i64; MAX_DIMS];
s[..shape.len()].copy_from_slice(shape);
let mut acc = esz as i64;
for i in (0..shape.len()).rev() {
st[i] = acc;
acc *= shape[i];
}
Ok(Self {
bytes: vec![0u8; total],
shape: s,
strides: st,
ndim: shape.len() as i32,
dtype: dt,
})
}
pub fn shape_slice(&self) -> &[i64] {
&self.shape[..self.ndim as usize]
}
}
#[derive(Clone, Debug)]
pub enum Builtin {
Bytes(Vec<u8>),
I64(i64),
F64(f64),
Bool(bool),
Str(CString),
Buffer(BufferData),
}
impl Builtin {
pub fn type_id(&self) -> u64 {
match self {
Builtin::Bytes(_) => type_id::BYTES,
Builtin::I64(_) => type_id::I64,
Builtin::F64(_) => type_id::F64,
Builtin::Bool(_) => type_id::BOOL,
Builtin::Str(_) => type_id::STR,
Builtin::Buffer(_) => type_id::BUFFER,
}
}
pub fn byte_size(&self) -> u64 {
match self {
Builtin::Bytes(v) => v.len() as u64,
Builtin::I64(_) | Builtin::F64(_) => 8,
Builtin::Bool(_) => 1,
Builtin::Str(s) => s.as_bytes().len() as u64,
Builtin::Buffer(b) => b.bytes.len() as u64,
}
}
fn data_ptr(&self) -> *mut c_void {
match self {
Builtin::Bytes(v) => v.as_ptr() as *mut c_void,
Builtin::I64(v) => v as *const i64 as *mut c_void,
Builtin::F64(v) => v as *const f64 as *mut c_void,
Builtin::Bool(v) => v as *const bool as *mut c_void,
Builtin::Str(s) => s.as_ptr() as *mut c_void,
Builtin::Buffer(b) => b.bytes.as_ptr() as *mut c_void,
}
}
}
pub struct Foreign {
pub ptr: *mut c_void,
pub drop_fn: Option<unsafe extern "C" fn(*mut c_void)>,
pub type_id: u64,
}
impl Drop for Foreign {
fn drop(&mut self) {
if let Some(f) = self.drop_fn {
if !self.ptr.is_null() {
unsafe { f(self.ptr) };
}
}
self.ptr = std::ptr::null_mut();
}
}
impl std::fmt::Debug for Foreign {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Foreign{{ptr:{:?}, type_id:{}}}", self.ptr, self.type_id)
}
}
unsafe impl Send for Foreign {}
unsafe impl Sync for Foreign {}
pub enum Payload {
Native(Box<dyn Any + Send + Sync>),
Builtin(Builtin),
Foreign(Foreign),
}
impl Payload {
pub fn type_id(&self) -> u64 {
match self {
Payload::Native(_) => type_id::NONE,
Payload::Builtin(b) => b.type_id(),
Payload::Foreign(f) => f.type_id,
}
}
pub fn byte_size(&self) -> u64 {
match self {
Payload::Native(_) | Payload::Foreign(_) => 0,
Payload::Builtin(b) => b.byte_size(),
}
}
pub fn data_ptr(&self) -> *mut c_void {
match self {
Payload::Native(b) => {
let p: *const dyn Any = &**b;
p as *const () as *mut c_void
}
Payload::Builtin(b) => b.data_ptr(),
Payload::Foreign(f) => f.ptr,
}
}
}
impl std::fmt::Debug for Payload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Payload::Native(_) => f.write_str("Native"),
Payload::Builtin(b) => write!(f, "{b:?}"),
Payload::Foreign(x) => write!(f, "{x:?}"),
}
}
}
#[derive(Clone, Default)]
pub struct Packet {
data: Option<Arc<Payload>>,
ts: Timestamp,
}
impl Packet {
pub fn empty() -> Self {
Self::default()
}
pub fn new<T: Any + Send + Sync>(value: T) -> Self {
Self {
data: Some(Arc::new(Payload::Native(Box::new(value)))),
ts: Timestamp::unset(),
}
}
pub fn new_interop<T: Any + Send + Sync>(value: T, type_id: u64) -> Self {
let _ = type_id; let boxed = Box::new(value);
let ptr = Box::into_raw(boxed) as *mut c_void;
unsafe extern "C" fn drop_boxed<T>(p: *mut c_void) {
drop(unsafe { Box::from_raw(p as *mut T) });
}
Self {
data: Some(Arc::new(Payload::Foreign(Foreign {
ptr,
drop_fn: Some(drop_boxed::<T>),
type_id,
}))),
ts: Timestamp::unset(),
}
}
pub fn from_i64(v: i64) -> Self {
Self::from_builtin(Builtin::I64(v))
}
pub fn from_f64(v: f64) -> Self {
Self::from_builtin(Builtin::F64(v))
}
pub fn from_bool(v: bool) -> Self {
Self::from_builtin(Builtin::Bool(v))
}
pub fn from_bytes(v: Vec<u8>) -> Self {
Self::from_builtin(Builtin::Bytes(v))
}
pub fn from_string(v: &str) -> Self {
Self::from_builtin(Builtin::Str(std::ffi::CString::new(v).unwrap_or_default()))
}
pub fn as_i64(&self) -> Option<i64> {
match self.as_builtin()? {
Builtin::I64(v) => Some(*v),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self.as_builtin()? {
Builtin::F64(v) => Some(*v),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self.as_builtin()? {
Builtin::Str(s) => s.to_str().ok(),
_ => None,
}
}
pub fn from_builtin(b: Builtin) -> Self {
Self {
data: Some(Arc::new(Payload::Builtin(b))),
ts: Timestamp::unset(),
}
}
pub unsafe fn from_foreign(
ptr: *mut c_void,
type_id: u64,
drop_fn: Option<unsafe extern "C" fn(*mut c_void)>,
) -> Self {
Self {
data: Some(Arc::new(Payload::Foreign(Foreign {
ptr,
drop_fn,
type_id,
}))),
ts: Timestamp::unset(),
}
}
pub fn at(mut self, ts: Timestamp) -> Self {
self.ts = ts;
self
}
pub fn set_timestamp(&mut self, ts: Timestamp) {
self.ts = ts;
}
pub fn timestamp(&self) -> Timestamp {
self.ts
}
pub fn is_empty(&self) -> bool {
self.data.is_none()
}
pub fn type_id(&self) -> u64 {
self.data.as_deref().map_or(type_id::NONE, Payload::type_id)
}
pub fn byte_size(&self) -> u64 {
self.data.as_deref().map_or(0, Payload::byte_size)
}
pub fn payload(&self) -> Option<&Payload> {
self.data.as_deref()
}
pub fn ref_count(&self) -> usize {
self.data.as_ref().map_or(0, Arc::strong_count)
}
pub fn arc_ref(&self) -> Option<&Arc<Payload>> {
self.data.as_ref()
}
pub fn into_arc(self) -> Option<Arc<Payload>> {
self.data
}
pub fn from_arc(arc: Arc<Payload>, ts: Timestamp) -> Self {
Self {
data: Some(arc),
ts,
}
}
pub fn get<T: Any>(&self) -> Option<&T> {
match self.data.as_deref()? {
Payload::Native(b) => b.downcast_ref::<T>(),
_ => None,
}
}
pub fn foreign_ptr(&self) -> Option<*const c_void> {
match self.data.as_deref()? {
Payload::Foreign(f) => Some(f.ptr as *const c_void),
_ => None,
}
}
pub fn as_builtin(&self) -> Option<&Builtin> {
match self.data.as_deref()? {
Payload::Builtin(b) => Some(b),
_ => None,
}
}
pub fn make_mutable_builtin(&mut self) -> Result<&mut Builtin> {
let arc = self.data.as_mut().ok_or_else(|| {
Error::InvalidArg("cannot get a writable view of an empty packet".into())
})?;
if Arc::get_mut(arc).is_none() {
let copy = match &**arc {
Payload::Builtin(b) => Payload::Builtin(b.clone()),
_ => {
return Err(Error::InvalidArg(
"custom payload cannot be copied by the engine (only builtin types support CoW)".into(),
))
}
};
*arc = Arc::new(copy);
}
match Arc::get_mut(arc).expect("just guaranteed exclusive") {
Payload::Builtin(b) => Ok(b),
_ => Err(Error::InvalidArg(
"this packet is not a builtin payload; CoW not supported".into(),
)),
}
}
pub fn debug_string(&self) -> String {
let ty = match self.data.as_deref() {
None => "Empty".to_string(),
Some(Payload::Native(_)) => "Native".to_string(),
Some(Payload::Foreign(f)) => format!("Foreign(type#{})", f.type_id),
Some(Payload::Builtin(b)) => match b {
Builtin::Bytes(v) => format!("Bytes[{}]", v.len()),
Builtin::I64(v) => format!("I64({v})"),
Builtin::F64(v) => format!("F64({v})"),
Builtin::Bool(v) => format!("Bool({v})"),
Builtin::Str(s) => format!("Str({:?})", s.to_string_lossy()),
Builtin::Buffer(b) => format!(
"Buffer[{}] dtype={}",
b.shape_slice()
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join("x"),
b.dtype
),
},
};
format!("Packet{{type={ty}, ts={}}}", self.ts)
}
}
impl std::fmt::Debug for Packet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.debug_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn native_roundtrip() {
let p = Packet::new(42i32).at(Timestamp(7));
assert_eq!(p.get::<i32>(), Some(&42));
assert_eq!(
p.get::<i64>(),
None,
"type mismatch must return None, not UB"
);
assert_eq!(p.timestamp(), Timestamp(7));
assert!(!p.is_empty());
}
#[test]
fn empty_packet() {
let p = Packet::empty();
assert!(p.is_empty());
assert_eq!(p.type_id(), type_id::NONE);
assert_eq!(p.timestamp(), Timestamp::unset());
}
#[test]
fn clone_shares_and_does_not_copy() {
let a = Packet::from_builtin(Builtin::I64(5));
let b = a.clone();
assert_eq!(a.ref_count(), 2);
assert_eq!(b.ref_count(), 2);
assert!(std::ptr::eq(
a.payload().unwrap() as *const Payload,
b.payload().unwrap() as *const Payload
));
}
#[test]
fn cow_is_zero_copy_when_exclusive() {
let mut p = Packet::from_builtin(Builtin::Buffer(
BufferData::new(&[2, 3], dtype::U8).unwrap(),
));
let before = p.payload().unwrap() as *const Payload;
p.make_mutable_builtin().unwrap();
let after = p.payload().unwrap() as *const Payload;
assert!(
std::ptr::eq(before, after),
"no copy should happen when exclusive"
);
}
#[test]
fn cow_copies_when_shared_and_does_not_disturb_other() {
let a = Packet::from_builtin(Builtin::Buffer(BufferData::new(&[4], dtype::U8).unwrap()));
let mut b = a.clone();
assert_eq!(a.ref_count(), 2);
match b.make_mutable_builtin().unwrap() {
Builtin::Buffer(buf) => buf.bytes[0] = 0xAB,
_ => unreachable!(),
}
assert_eq!(a.ref_count(), 1, "a should be exclusive again after copy");
match a.as_builtin().unwrap() {
Builtin::Buffer(buf) => assert_eq!(buf.bytes[0], 0),
_ => unreachable!(),
}
match b.as_builtin().unwrap() {
Builtin::Buffer(buf) => assert_eq!(buf.bytes[0], 0xAB),
_ => unreachable!(),
}
}
#[test]
fn cow_rejects_non_builtin() {
let mut p = Packet::new(1u8);
assert!(
p.make_mutable_builtin().is_err(),
"custom payload cannot be copied; must error rather than silently fail"
);
}
#[test]
fn buffer_strides_are_row_major_contiguous() {
let b = BufferData::new(&[2, 3, 4], dtype::F32).unwrap();
assert_eq!(b.ndim, 3);
assert_eq!(b.shape_slice(), &[2, 3, 4]);
assert_eq!(&b.strides[..3], &[3 * 4 * 4, 4 * 4, 4]);
assert_eq!(b.bytes.len(), 2 * 3 * 4 * 4);
}
#[test]
fn buffer_rejects_bad_args() {
assert!(
BufferData::new(&[], dtype::U8).is_err(),
"ndim 0 is invalid"
);
assert!(
BufferData::new(&[1, 2, 3, 4, 5, 6, 7, 8, 9], dtype::U8).is_err(),
"must reject exceeding MAX_DIMS"
);
assert!(
BufferData::new(&[2, 2], 999).is_err(),
"must reject unknown dtype"
);
assert!(
BufferData::new(&[-1], dtype::U8).is_err(),
"must reject negative shape"
);
}
#[test]
fn dtype_sizes() {
assert_eq!(dtype_size(dtype::U8), 1);
assert_eq!(dtype_size(dtype::F16), 2);
assert_eq!(dtype_size(dtype::F32), 4);
assert_eq!(dtype_size(dtype::F64), 8);
assert_eq!(dtype_size(12345), 0, "unknown dtype returns 0");
}
#[test]
fn foreign_drop_fn_called_exactly_once() {
use std::sync::atomic::{AtomicUsize, Ordering};
static CALLS: AtomicUsize = AtomicUsize::new(0);
unsafe extern "C" fn drop_it(p: *mut c_void) {
CALLS.fetch_add(1, Ordering::SeqCst);
drop(unsafe { Box::from_raw(p as *mut u32) });
}
let raw = Box::into_raw(Box::new(7u32)) as *mut c_void;
{
let p = unsafe { Packet::from_foreign(raw, 99, Some(drop_it)) };
let q = p.clone();
assert_eq!(q.type_id(), 99);
assert_eq!(
CALLS.load(Ordering::SeqCst),
0,
"must not free while references remain"
);
}
assert_eq!(
CALLS.load(Ordering::SeqCst),
1,
"must free exactly once when refcount hits zero"
);
}
#[test]
fn fnv1a_matches_cpp_sugar_layer() {
assert_eq!(fnv1a_type_id("i"), 12638195996648667684);
assert_eq!(fnv1a_type_id("d"), 12638183902020757363);
assert!(
fnv1a_type_id("") >= 16,
"empty name must also avoid the builtin range"
);
}
#[test]
fn interop_packet_carries_explicit_type_id() {
let tid = fnv1a_type_id("i");
let p = Packet::new_interop(7i32, tid);
assert_eq!(p.type_id(), tid);
let ptr = p.foreign_ptr().expect("should get a data pointer");
assert_eq!(unsafe { *(ptr as *const i32) }, 7);
}
#[test]
fn byte_size_accounting() {
assert_eq!(Packet::from_builtin(Builtin::I64(0)).byte_size(), 8);
assert_eq!(
Packet::from_builtin(Builtin::Bytes(vec![0; 100])).byte_size(),
100
);
assert_eq!(Packet::new(0u64).byte_size(), 0);
}
#[test]
fn debug_string_is_informative() {
let p = Packet::from_builtin(Builtin::Buffer(
BufferData::new(&[3, 224, 224], dtype::F32).unwrap(),
))
.at(Timestamp(42));
let s = p.debug_string();
assert!(s.contains("3x224x224"), "should contain shape: {s}");
assert!(s.contains("42"), "should contain timestamp: {s}");
}
}