//! The project-cdylib descriptor contract (C13/C16, cycle 0102). //! //! A research project compiles to a cdylib exporting ONE symbol, //! `AURA_PROJECT`, a [`ProjectDescriptor`]. The descriptor has two ABI tiers: //! a **C tier** (`magic`, `descriptor_version`, the version stamps, the //! namespace — all C-compatible field types) the host validates BEFORE //! trusting anything, and a **Rust tier** (the vocabulary resolver and the //! enumerable type-id list) the host touches only after both stamps match. //! The stamp cannot certify the channel it rides on, so it must not itself //! depend on the Rust ABI it certifies. //! //! The stamps are baked at *aura-core compile time* — per consuming build — //! so host and dylib each carry their own side's truth. use crate::node::PrimitiveBuilder; /// First 8 bytes of the C tier; ASCII "AURAPROJ". pub const AURA_DESCRIPTOR_MAGIC: u64 = 0x4155_5241_5052_4F4A; /// Bumped on any layout change of [`ProjectDescriptor`]. pub const AURA_DESCRIPTOR_VERSION: u32 = 1; /// The one exported symbol name (NUL-terminated for the symbol lookup). pub const AURA_PROJECT_SYMBOL: &[u8] = b"AURA_PROJECT\0"; /// The compiling rustc's `--version` line (via build.rs). pub const RUSTC_VERSION: &str = env!("AURA_RUSTC_VERSION"); /// This aura-core's crate version. pub const CORE_VERSION: &str = env!("CARGO_PKG_VERSION"); /// C-ABI string slice: pointer + length over `'static` UTF-8 bytes. /// No NUL convention needed; readable without trusting the Rust ABI. #[repr(C)] #[derive(Clone, Copy)] pub struct StrSlice { pub ptr: *const u8, pub len: usize, } impl StrSlice { pub const fn new(s: &'static str) -> Self { Self { ptr: s.as_ptr(), len: s.len() } } /// Read the slice back as UTF-8. /// /// # Safety /// `ptr`/`len` must describe live UTF-8 bytes — true for any descriptor /// built by [`aura_project!`](crate::aura_project), whose strings are /// `'static` and whose library is loaded-and-held (never unloaded). pub unsafe fn as_str(&self) -> Option<&str> { if self.ptr.is_null() { return None; } let bytes = unsafe { core::slice::from_raw_parts(self.ptr, self.len) }; core::str::from_utf8(bytes).ok() } } /// The exported project descriptor. Field ORDER is ABI: C tier first, /// Rust tier last; validated front-to-back. #[repr(C)] pub struct ProjectDescriptor { // ---- C tier: validated BEFORE any Rust-ABI field is touched ---- pub magic: u64, pub descriptor_version: u32, pub rustc_version: StrSlice, pub aura_core_version: StrSlice, pub namespace: StrSlice, // ---- Rust tier: only after both stamps match ---- pub vocabulary: fn(&str) -> Option, pub type_ids: fn() -> &'static [&'static str], } // The raw pointers are to 'static data; the fn pointers are Sync by nature. unsafe impl Sync for ProjectDescriptor {} /// Declare a project crate's export in one place. Emits the `AURA_PROJECT` /// static with this build's stamps baked in. #[macro_export] macro_rules! aura_project { (namespace: $ns:literal, vocabulary: $vocab:path, type_ids: $ids:path $(,)?) => { #[unsafe(no_mangle)] pub static AURA_PROJECT: $crate::project::ProjectDescriptor = $crate::project::ProjectDescriptor { magic: $crate::project::AURA_DESCRIPTOR_MAGIC, descriptor_version: $crate::project::AURA_DESCRIPTOR_VERSION, rustc_version: $crate::project::StrSlice::new( $crate::project::RUSTC_VERSION, ), aura_core_version: $crate::project::StrSlice::new( $crate::project::CORE_VERSION, ), namespace: $crate::project::StrSlice::new($ns), vocabulary: $vocab, type_ids: $ids, }; }; } #[cfg(test)] mod tests { use super::*; fn fake_vocab(type_id: &str) -> Option { let _ = type_id; None } fn fake_ids() -> &'static [&'static str] { &["t::A"] } aura_project! { namespace: "t", vocabulary: fake_vocab, type_ids: fake_ids, } #[test] fn stamps_are_baked_and_nonempty() { assert!(RUSTC_VERSION.starts_with("rustc ")); assert!(!CORE_VERSION.is_empty()); } #[test] fn str_slice_round_trips() { let s = StrSlice::new("hello"); assert_eq!(unsafe { s.as_str() }, Some("hello")); } /// `as_str` refuses a null pointer instead of dereferencing it — the /// loader's boundary-safety primitive for a descriptor field it hasn't /// otherwise validated yet. #[test] fn str_slice_null_ptr_is_none() { let s = StrSlice { ptr: std::ptr::null(), len: 0 }; assert_eq!(unsafe { s.as_str() }, None); } /// `as_str` refuses non-UTF-8 bytes rather than producing a mangled or /// panicking read, so a corrupt/foreign descriptor field degrades to /// `None` instead of undefined behaviour downstream. #[test] fn str_slice_invalid_utf8_is_none() { static BAD: [u8; 2] = [0xFF, 0xFE]; let s = StrSlice { ptr: BAD.as_ptr(), len: BAD.len() }; assert_eq!(unsafe { s.as_str() }, None); } #[test] fn macro_emits_a_valid_descriptor() { let d = &AURA_PROJECT; assert_eq!(d.magic, AURA_DESCRIPTOR_MAGIC); assert_eq!(d.descriptor_version, AURA_DESCRIPTOR_VERSION); assert_eq!(unsafe { d.namespace.as_str() }, Some("t")); assert_eq!(unsafe { d.rustc_version.as_str() }, Some(RUSTC_VERSION)); assert_eq!(unsafe { d.aura_core_version.as_str() }, Some(CORE_VERSION)); assert!((d.vocabulary)("nope").is_none()); assert_eq!((d.type_ids)(), &["t::A"]); } }