dlsym 0.0.1

Memory-safe bindings around the platform's dynamic library loading primitives
Documentation
//! Rust code generation from the [`ir`](super::ir).

use std::fmt::Write;

use super::ir::{Function, Header, Item, Type};

/// Options controlling code generation.
#[derive(Debug, Clone)]
pub struct Options {
    /// Name of the generated bindings struct.
    pub struct_name: String,
}

/// Rust keywords that cannot be used as plain identifiers.
const KEYWORDS: &[&str] = &[
    "as", "async", "await", "box", "break", "const", "continue", "crate", "dyn", "else", "enum",
    "extern", "false", "fn", "for", "gen", "if", "impl", "in", "let", "loop", "match", "mod",
    "move", "mut", "priv", "pub", "ref", "return", "self", "Self", "static", "struct", "super",
    "trait", "true", "try", "type", "unsafe", "use", "where", "while", "yield",
];

/// Generates a module body declaring a [`library!`](crate::library!) struct for every
/// function in `header`.
///
/// Variadic functions are skipped with a comment, as they cannot be wrapped
/// by `library!` yet.
pub fn emit(header: &Header, opts: &Options) -> String {
    let mut out = String::new();
    out.push_str("// @generated by dlsym-gen. Do not edit by hand.\n\n");
    let _ = writeln!(out, "::dlsym::library! {{");
    let _ = writeln!(out, "    pub struct {} {{", opts.struct_name);
    for item in &header.items {
        match item {
            Item::Function(f) if f.variadic => {
                let _ = writeln!(out, "        // skipped variadic function `{}`", f.name);
            }
            Item::Function(f) => emit_function(&mut out, f),
        }
    }
    out.push_str("    }\n}\n");
    out
}

fn emit_function(out: &mut String, f: &Function) {
    if let Some(doc) = &f.doc {
        for line in doc.lines() {
            let _ = writeln!(out, "        /// {}", line.trim_end());
        }
    }
    let params: Vec<String> = f
        .params
        .iter()
        .enumerate()
        .map(|(i, p)| {
            let name = match &p.name {
                Some(n) => ident(n),
                None => format!("arg{i}"),
            };
            format!("{name}: {}", rust_type(&p.ty))
        })
        .collect();
    let ret = match f.ret {
        Type::Void => String::new(),
        ref ty => format!(" -> {}", rust_type(ty)),
    };
    let _ = writeln!(
        out,
        "        pub fn {}({}){ret};",
        f.name,
        params.join(", ")
    );
}

/// Escapes `name` if it collides with a Rust keyword.
fn ident(name: &str) -> String {
    match name {
        // These cannot be raw identifiers.
        "self" | "Self" | "super" | "crate" | "_" => format!("{name}_"),
        n if KEYWORDS.contains(&n) => format!("r#{n}"),
        n => n.to_owned(),
    }
}

/// Maps a C type to its Rust spelling.
fn rust_type(ty: &Type) -> String {
    let ffi = |name: &str| format!("::core::ffi::{name}");
    match ty {
        Type::Void => "()".into(),
        Type::Bool => "bool".into(),
        Type::Char => ffi("c_char"),
        Type::SChar => ffi("c_schar"),
        Type::UChar => ffi("c_uchar"),
        Type::Short => ffi("c_short"),
        Type::UShort => ffi("c_ushort"),
        Type::Int => ffi("c_int"),
        Type::UInt => ffi("c_uint"),
        Type::Long => ffi("c_long"),
        Type::ULong => ffi("c_ulong"),
        Type::LongLong => ffi("c_longlong"),
        Type::ULongLong => ffi("c_ulonglong"),
        Type::SizeT => "usize".into(),
        Type::SSizeT => "isize".into(),
        Type::Float => "f32".into(),
        Type::Double => "f64".into(),
        Type::Pointer { pointee, is_const } => {
            let qual = if *is_const { "const" } else { "mut" };
            let inner = match **pointee {
                Type::Void => ffi("c_void"),
                ref t => rust_type(t),
            };
            format!("*{qual} {inner}")
        }
        Type::Named(name) => ident(name),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::codegen::ir::Param;

    fn param(name: &str, ty: Type) -> Param {
        Param {
            name: Some(name.into()),
            ty,
        }
    }

    #[test]
    fn emits_library_macro() {
        let header = Header {
            items: vec![
                Item::Function(Function {
                    name: "cos".into(),
                    params: vec![param("x", Type::Double)],
                    ret: Type::Double,
                    variadic: false,
                    doc: Some("Cosine of `x`.".into()),
                }),
                Item::Function(Function {
                    name: "memcpy".into(),
                    params: vec![
                        param("dest", Type::ptr(Type::Void)),
                        param("src", Type::const_ptr(Type::Void)),
                        Param {
                            name: None,
                            ty: Type::SizeT,
                        },
                    ],
                    ret: Type::ptr(Type::Void),
                    variadic: false,
                    doc: None,
                }),
                Item::Function(Function {
                    name: "set_type".into(),
                    params: vec![param("type", Type::Int)],
                    ret: Type::Void,
                    variadic: false,
                    doc: None,
                }),
                Item::Function(Function {
                    name: "printf".into(),
                    params: vec![param("fmt", Type::const_ptr(Type::Char))],
                    ret: Type::Int,
                    variadic: true,
                    doc: None,
                }),
            ],
        };
        let out = emit(
            &header,
            &Options {
                struct_name: "Demo".into(),
            },
        );
        assert_eq!(
            out,
            "\
// @generated by dlsym-gen. Do not edit by hand.

::dlsym::library! {
    pub struct Demo {
        /// Cosine of `x`.
        pub fn cos(x: f64) -> f64;
        pub fn memcpy(dest: *mut ::core::ffi::c_void, src: *const ::core::ffi::c_void, arg2: usize) -> *mut ::core::ffi::c_void;
        pub fn set_type(r#type: ::core::ffi::c_int);
        // skipped variadic function `printf`
    }
}
"
        );
    }
}