ezffi-macros 0.1.1

Proc-macros backing the ezffi crate
Documentation
use std::{
    collections::BTreeMap,
    env, fs,
    path::PathBuf,
    sync::LazyLock,
    time::{Duration, Instant},
};

use serde::Deserialize;

const FILE_NAME: &str = "ezffi-typemap.toml";

/// The one fixed location of the shared type map:
/// `<workspace_root>/target/ezffi-typemap.toml`. Living at the workspace
/// root to avoid compilation issues with cbindgen, it compiles to expand
/// and parse macros, we need our own solution for the header generation
static MAP_PATH: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
    let manifest = env::var("CARGO_MANIFEST_DIR").ok()?;
    let mut dir = PathBuf::from(manifest);

    // Walk up until we find `Cargo.lock`
    let workspace_root = loop {
        if dir.join("Cargo.lock").exists() {
            break dir;
        }

        if !dir.pop() {
            return None;
        }
    };

    Some(workspace_root.join("target").join(FILE_NAME))
});

#[derive(Deserialize)]
pub struct TypeEntry {
    pub c_type: String,
    pub c_compatible: bool,
}

#[derive(Deserialize, Default)]
pub struct Section {
    pub codegen_version: u32,
    #[serde(flatten)]
    pub types: BTreeMap<String, TypeEntry>,
}

/// One section per crate: `crate name -> Section`.
pub type Sections = BTreeMap<String, Section>;

pub struct TypeMap {
    sections: Sections,
    _lock: Lock,
}

impl TypeMap {
    pub fn load() -> Option<TypeMap> {
        MAP_PATH.as_ref()?;

        Some(TypeMap {
            sections: read(),
            _lock: Lock::acquire(),
        })
    }

    pub fn sections(&self) -> &Sections {
        &self.sections
    }

    pub fn store(mut self, krate: String, section: Section) {
        self.sections.insert(krate, section);
        write(&self.sections);
    }
}

fn read() -> Sections {
    let Some(path) = &*MAP_PATH else {
        return Sections::new();
    };

    let Ok(content) = fs::read_to_string(path) else {
        return Sections::new();
    };

    toml::from_str(&content).unwrap_or_default()
}

fn write(sections: &Sections) {
    let Some(path) = &*MAP_PATH else {
        return;
    };

    let mut out =
        String::from("# Generated by ezffi. Maps each crate's Rust types to their FFI types.\n\n");

    for (krate, section) in sections {
        out.push_str(&format!("[{krate}]\n"));

        out.push_str(&format!("codegen_version = {}\n", section.codegen_version));

        for (rust_ty, entry) in &section.types {
            out.push_str(&format!(
                "{rust_ty} = {{ c_type = {:?}, c_compatible = {} }}\n",
                entry.c_type, entry.c_compatible,
            ));
        }

        out.push('\n');
    }

    let _ = fs::write(path, out);
}

struct Lock(Option<PathBuf>);

impl Lock {
    fn acquire() -> Lock {
        let Some(map_path) = &*MAP_PATH else {
            return Lock(None);
        };
        let lock_path = map_path.with_extension("lock");
        if let Some(parent) = lock_path.parent() {
            let _ = fs::create_dir_all(parent);
        }

        let start = Instant::now();
        loop {
            match fs::OpenOptions::new()
                .write(true)
                .create_new(true)
                .open(&lock_path)
            {
                Ok(_) => return Lock(Some(lock_path)),
                Err(_) if start.elapsed() > Duration::from_secs(10) => {
                    let _ = fs::remove_file(&lock_path);
                }
                Err(_) => std::thread::sleep(Duration::from_millis(15)),
            }
        }
    }
}

impl Drop for Lock {
    fn drop(&mut self) {
        if let Some(path) = &self.0 {
            let _ = fs::remove_file(path);
        }
    }
}