4928e289f7
A research project is now a loadable external cdylib crate. Inside a directory whose ancestry holds an Aura.toml, aura discovers the project root cargo-style, locates the compiled dylib via cargo metadata (debug default, --release opt-in), loads it load-and-hold, and refuses mismatches before trusting anything: the AURA_PROJECT descriptor (aura-core::project, #[repr(C)]) carries a C-ABI stamp prefix (rustc + aura-core version, baked per consuming build by the new aura-core build.rs) validated before any Rust-ABI field is read. The vocabulary charter gates the merged resolution: project type ids are ::-namespaced (std stays bare), duplicates refuse, and the enumerable type-id list must agree with the resolver, so introspection can never silently omit a project type. All blueprint verbs resolve through the merged project + std vocabulary via a per-invocation Env threaded through the dispatch chains; registry, trace-store, and data paths anchor at the project runs root (Aura.toml [paths], paths-only by design — instrument geometry stays the recorded sidecar, C15). RunManifest gains the Tier-1 project provenance field (namespace + dylib sha256 + best-effort commit), stamped beside topology_hash on the blueprint-run paths; pre-0102 registry lines load unchanged. Default node names strip the namespace, so :: never reaches the param-path address space. Proven by the demo-project fixture (built by the e2e via cargo, path-dep on this workspace): run twice bit-identical, provenance recorded, introspection lists demo::* beside std, registry anchors at the discovered root from a subdirectory; the badcharter fixture proves the charter refusal through the real libloading path; a never-built project refuses with a cargo-build hint. Outside a project every path collapses to the previous literals — goldens and manifest pins byte-identical. Verification: cargo build --workspace clean; cargo test --workspace 862 passed / 0 failed (incl. 7 project_load e2e); clippy -D warnings clean (one precedent-matching allow(too_many_arguments) on run_oos_blueprint, whose arity the Env threading raised to 8); doc build unchanged. Docs/ledger aligned: Aura.toml field lists are paths-only in project-layout.md, glossary, C16/C17; new C13 realization note records the per-invocation-reload reading and the load-and-hold one-shot scope boundary. New deps, per-case review (aura-cli leaf binary only, never the frozen artifact): libloading, toml. refs #180
160 lines
5.7 KiB
Rust
160 lines
5.7 KiB
Rust
//! 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<PrimitiveBuilder>,
|
|
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<PrimitiveBuilder> {
|
|
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"]);
|
|
}
|
|
}
|