inauguration 0.2.0

.in language and general compiler CLI (Core IR, hybrid SIL, staging, plugins)
Documentation
use crate::boundary_ir::IN_ABI_VERSION;

pub const IN_MODULE_ENTRY_SYMBOL: &str = "in_module_vtable";

#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InCallStatus {
    pub code: u32,
    pub reserved: u32,
    pub error_len: u64,
    pub error_ptr: *const u8,
}

impl InCallStatus {
    pub const OK: u32 = 0;

    #[must_use]
    pub fn ok() -> Self {
        Self {
            code: Self::OK,
            reserved: 0,
            error_len: 0,
            error_ptr: std::ptr::null(),
        }
    }

    #[must_use]
    pub fn is_ok(&self) -> bool {
        self.code == Self::OK
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModuleDescriptor {
    pub abi_version: u32,
    pub pointer_width: u32,
    pub endian: u32,
    pub layout_hash: u32,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
pub struct DynamicLoaderFixture {
    pub entry_symbol: String,
    pub abi_version: u32,
    pub pointer_width: u32,
    pub endian: u32,
    pub layout_hash: u32,
}

pub trait DynamicModule {
    fn descriptor(&self) -> ModuleDescriptor;
    fn init(&self, host: *const ()) -> InCallStatus;
    fn shutdown(&self) -> InCallStatus;
    fn symbol(&self, name: &str) -> Option<*const ()>;
}

#[derive(Debug, thiserror::Error)]
pub enum DynamicModuleError {
    #[error("io: {0}")]
    Io(#[from] std::io::Error),
    #[error("load failed for {path}: {reason}")]
    LoadFailed { path: String, reason: String },
    #[error("entry symbol `{symbol}` missing from {path}")]
    EntryMissing { path: String, symbol: String },
    #[error("abi_version mismatch: expected {expected}, got {found}")]
    AbiVersionMismatch { expected: u32, found: u32 },
    #[error("dynamic module loading requires Unix or Windows")]
    UnsupportedPlatform,
}

pub fn validate_descriptor(descriptor: &ModuleDescriptor) -> Result<(), DynamicModuleError> {
    if descriptor.abi_version != IN_ABI_VERSION {
        return Err(DynamicModuleError::AbiVersionMismatch {
            expected: IN_ABI_VERSION,
            found: descriptor.abi_version,
        });
    }
    Ok(())
}

#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;

#[cfg(unix)]
pub use unix::load_dynamic_module;
#[cfg(windows)]
pub use windows::load_dynamic_module;

#[cfg(not(any(unix, windows)))]
pub fn load_dynamic_module(
    _path: &std::path::Path,
) -> Result<Box<dyn DynamicModule>, DynamicModuleError> {
    Err(DynamicModuleError::UnsupportedPlatform)
}

#[must_use]
pub fn workspace_root() -> std::path::PathBuf {
    std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("in-cli parent")
        .to_path_buf()
}

pub fn fixture_manifest() -> Result<DynamicLoaderFixture, String> {
    let path = workspace_root().join("fixtures/dynamic-loader/manifest.json");
    let source =
        std::fs::read_to_string(&path).map_err(|err| format!("read {}: {err}", path.display()))?;
    serde_json::from_str(&source).map_err(|err| format!("parse {}: {err}", path.display()))
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::process::Command;

    #[test]
    fn fixture_manifest_matches_contract() {
        let fixture = fixture_manifest().expect("fixture manifest");
        assert_eq!(fixture.entry_symbol, IN_MODULE_ENTRY_SYMBOL);
        assert_eq!(fixture.abi_version, IN_ABI_VERSION);
        assert_eq!(fixture.pointer_width, 64);
        assert_eq!(fixture.endian, 0);
        assert_eq!(fixture.layout_hash, 0);
    }

    #[test]
    fn validate_descriptor_rejects_abi_mismatch() {
        let descriptor = ModuleDescriptor {
            abi_version: IN_ABI_VERSION + 1,
            pointer_width: 64,
            endian: 0,
            layout_hash: 0,
        };
        let err = validate_descriptor(&descriptor).expect_err("abi mismatch");
        assert!(matches!(
            err,
            DynamicModuleError::AbiVersionMismatch { expected, found }
            if expected == IN_ABI_VERSION && found == IN_ABI_VERSION + 1
        ));
    }

    #[test]
    fn validate_descriptor_accepts_current_abi() {
        let descriptor = ModuleDescriptor {
            abi_version: IN_ABI_VERSION,
            pointer_width: 64,
            endian: 0,
            layout_hash: 0,
        };
        validate_descriptor(&descriptor).expect("current abi");
    }

    #[cfg(windows)]
    #[test]
    fn windows_loader_is_stub() {
        let err = load_dynamic_module(Path::new("fixture.dll")).expect_err("windows stub");
        assert!(matches!(err, DynamicModuleError::UnsupportedPlatform));
    }

    #[cfg(unix)]
    fn compile_echo_fixture() -> std::path::PathBuf {
        let root = workspace_root();
        let src = root.join("fixtures/dynamic-loader/echo_module.c");
        let out_dir = std::env::temp_dir().join(format!(
            "in-dynamic-loader-{}-{}",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .expect("clock")
                .as_nanos()
        ));
        std::fs::create_dir_all(&out_dir).expect("temp dir");
        let lib_name = if cfg!(target_os = "macos") {
            "libecho_module.dylib"
        } else {
            "libecho_module.so"
        };
        let lib = out_dir.join(lib_name);
        let status = Command::new("cc")
            .args([
                "-shared",
                "-fPIC",
                "-O2",
                "-I",
                root.join("shared/abi").to_str().expect("abi include"),
                "-o",
                lib.to_str().expect("lib path"),
                src.to_str().expect("source path"),
            ])
            .status()
            .expect("cc");
        assert!(status.success(), "cc failed to build echo fixture");
        lib
    }

    #[cfg(unix)]
    #[test]
    fn unix_loads_echo_fixture_module() {
        let lib = compile_echo_fixture();
        let module = load_dynamic_module(&lib).expect("load echo fixture");
        let descriptor = module.descriptor();
        assert_eq!(descriptor.abi_version, IN_ABI_VERSION);
        assert_eq!(descriptor.pointer_width, 64);
        assert_eq!(descriptor.endian, 0);
        assert_eq!(descriptor.layout_hash, 0);
        let init_status = module.init(std::ptr::null());
        assert!(init_status.is_ok());
        let shutdown_status = module.shutdown();
        assert!(shutdown_status.is_ok());
        let symbol = module.symbol("echo_add").expect("echo_add symbol");
        assert!(!symbol.is_null());
        type EchoAdd = unsafe extern "C" fn(i32, i32) -> i32;
        let echo_add: EchoAdd = unsafe { std::mem::transmute(symbol) };
        assert_eq!(unsafe { echo_add(2, 3) }, 5);
    }
}