baryl 0.0.2

Public SDK for Baryl, a full-system emulation and introspection engine
//! Finding the installed runtime and proving it before the first call crosses.
//!
//! `Baryl::open` goes through here on its way in. The chain is: locate the
//! library, read one descriptor out of it, refuse a version range that does not
//! overlap this build's, then prove every slot is present — so nothing further
//! up has an absent call to answer for.

use std::{
    ffi::{CStr, CString, c_char, c_int, c_void},
    path::{Path, PathBuf},
    sync::OnceLock,
};

use anyhow::Context;
use log::debug;

/// The filename to look for. The runtime's own name for itself is on its
/// descriptor.
const CORE_SO: &str = "libbaryl-core.so";

/// The four calls, spelled out. Bindgen wraps each in an `Option`, so assigning
/// through these aliases is what proves this file and the header agree.
type OpenFn = unsafe extern "C" fn(
    *const c_char,
    *const crate::sys::BarylOptions,
    *mut c_char,
    usize,
) -> *mut c_void;
type RunFn = unsafe extern "C" fn(crate::sys::BarylRef, *mut crate::sys::BarylOutcome) -> c_int;
type ControlFn = unsafe extern "C" fn(crate::sys::BarylRef) -> *const crate::sys::Control;
type CloseFn = unsafe extern "C" fn(crate::sys::BarylRef);

/// The loaded runtime. Never unloaded, so everything reached through it is
/// `'static`, and one of these is built per process.
pub struct BarylCore {
    /// The descriptor's address, which every call is made against.
    pub(crate) descriptor: &'static crate::sys::BarylDescriptor,
    pub(crate) open: OpenFn,
    pub(crate) run: RunFn,
    pub(crate) control: ControlFn,
    pub(crate) close: CloseFn,
    /// Which file answered, for the load line and for a bug report.
    path: PathBuf,
}

// SAFETY: every field points into a `.so` mapped for the process lifetime and
// never written -- `name` into that same `.so`'s rodata, the slots into its text.
unsafe impl Send for BarylCore {}
unsafe impl Sync for BarylCore {}

static CORE: OnceLock<BarylCore> = OnceLock::new();

impl BarylCore {
    /// The loaded runtime, or why there is none.
    ///
    /// Idempotent and safe to race: two threads calling this at once load one
    /// library and agree on one result.
    pub fn load() -> anyhow::Result<&'static BarylCore> {
        if let Some(core) = CORE.get() {
            return Ok(core);
        }
        let core = BarylCore::open_so(&locate()?)?;
        debug!("loaded {} from {}", core.name(), core.path.display());
        Ok(CORE.get_or_init(|| core))
    }

    /// The runtime's own name for itself; the filename if it is not UTF-8.
    pub fn name(&self) -> &'static str {
        // SAFETY: a static C string in the loaded `.so`, per the header.
        unsafe { CStr::from_ptr(self.descriptor.name) }
            .to_str()
            .unwrap_or(CORE_SO)
    }

    /// dlopen, dlsym the descriptor, check the version pair, then the slots.
    fn open_so(path: &Path) -> anyhow::Result<BarylCore> {
        let c_path = CString::new(path.as_os_str().as_encoded_bytes())
            .with_context(|| format!("core path has an interior NUL: {}", path.display()))?;
        // RTLD_GLOBAL, permanently: every library the runtime goes on to dlopen
        // resolves the allocator calls out of the global scope, and none of
        // them carries a `DT_NEEDED` naming the runtime.
        let handle = unsafe { libc::dlopen(c_path.as_ptr(), libc::RTLD_NOW | libc::RTLD_GLOBAL) };
        anyhow::ensure!(!handle.is_null(), "dlopen {}: {}", path.display(), dlerror());
        let sym = unsafe { libc::dlsym(handle, c"baryl_descriptor".as_ptr()) };
        anyhow::ensure!(!sym.is_null(), "{} exports no baryl_descriptor", path.display());
        // SAFETY: the symbol is the runtime's own descriptor static, which
        // lives for as long as the library stays mapped.
        let d: &'static crate::sys::BarylDescriptor = unsafe { &*sym.cast() };

        let (min, cur) = (d.min_version, d.version);
        anyhow::ensure!(
            cur >= crate::sys::BARYL_MIN_ABI_VERSION && min <= crate::sys::BARYL_ABI_VERSION,
            "{} serves ABI {min}..={cur}, outside this build's {}..={}; \
             run `baryl core install`",
            path.display(),
            crate::sys::BARYL_MIN_ABI_VERSION,
            crate::sys::BARYL_ABI_VERSION,
        );
        // Each annotation checks the alias above against what bindgen wrote,
        // and each `?` is the last time an absent slot is anyone's question.
        let open: OpenFn = d.open.context("the core's descriptor has no `open`")?;
        let run: RunFn = d.run.context("the core's descriptor has no `run`")?;
        let control: ControlFn = d
            .control
            .context("the core's descriptor has no `control`")?;
        let close: CloseFn = d.close.context("the core's descriptor has no `close`")?;

        Ok(BarylCore {
            descriptor: d,
            open: open,
            run: run,
            control: control,
            close: close,
            path: path.to_path_buf(),
        })
    }
}

/// The dynamic loader's own reason, or a stand-in when it offers none.
fn dlerror() -> String {
    let e = unsafe { libc::dlerror() };
    if e.is_null() {
        return "no reason from the loader".to_string();
    }
    unsafe { CStr::from_ptr(e) }.to_string_lossy().into_owned()
}

/// The first runtime in the search order, or an error naming every directory
/// that was looked in.
fn locate() -> anyhow::Result<PathBuf> {
    let dirs = search_dirs();
    if let Some(found) = dirs.iter().map(|d| d.join(CORE_SO)).find(|p| p.is_file()) {
        return Ok(found);
    }
    let looked: Vec<String> = dirs.iter().map(|d| d.display().to_string()).collect();
    anyhow::bail!(
        "no runtime installed: run `baryl setup`, or set $BARYL_INSTALL_DIR to a \
         prefix holding lib/{CORE_SO}. Looked in {}",
        looked.join(", ")
    )
}

/// Where a runtime is looked for, in order: `$BARYL_INSTALL_DIR/lib` first so
/// an explicit prefix wins, then beside the running binary, then
/// `/opt/baryl/lib`.
//
// TODO: a checkout builds `build/dist/seeker` and `build/dist/hunter` as
// separate trees, and nothing here resolves a subsystem across both.
fn search_dirs() -> Vec<PathBuf> {
    let mut dirs: Vec<PathBuf> = Vec::new();
    if let Some(prefix) = std::env::var_os("BARYL_INSTALL_DIR") {
        dirs.push(PathBuf::from(prefix).join("lib"));
    }
    if let Some(exe) = std::env::current_exe()
        .ok()
        .and_then(|e| e.parent().map(Path::to_path_buf))
    {
        dirs.push(exe.join("../lib"));
        dirs.push(exe);
    }
    dirs.push(PathBuf::from("/opt/baryl/lib"));
    dirs
}