Skip to main content

equilibrium_ffi/
loader.rs

1//! One-liner FFI loading — compile, generate bindings, link.
2
3use std::path::{Path, PathBuf};
4
5use crate::bindings::{generate_bindings, BindingOptions, GeneratedBinding};
6use crate::compiler::compile_to_c;
7use crate::detector::{detect_language, find_compiler, Language};
8use crate::exports::{discover_exports_with_options, ExportOptions, ExportSource};
9use crate::imports::{generate_imports, GeneratedImport, ImportOptions};
10
11/// Options for loading a foreign module.
12#[derive(Clone, Debug)]
13pub struct LoadOptions {
14    /// Generate Rust bindings from the header (default: true)
15    pub generate_bindings: bool,
16    /// Compile the source (default: true)
17    pub compile: bool,
18    /// Output directory (default: target/native)
19    pub output_dir: Option<PathBuf>,
20    /// Custom binding options
21    pub binding_options: Option<BindingOptions>,
22    /// Link the compiled library (default: true for object files)
23    pub link: bool,
24    /// Extra link args
25    pub link_args: Vec<String>,
26    /// Extra compile args
27    pub compile_args: Vec<String>,
28    pub exports: Vec<String>,
29    pub config_path: Option<PathBuf>,
30    pub consumer_languages: Vec<Language>,
31}
32
33impl Default for LoadOptions {
34    fn default() -> Self {
35        Self {
36            generate_bindings: true,
37            compile: true,
38            output_dir: None,
39            binding_options: None,
40            link: true,
41            link_args: Vec::new(),
42            compile_args: Vec::new(),
43            exports: Vec::new(),
44            config_path: None,
45            consumer_languages: Vec::new(),
46        }
47    }
48}
49
50impl LoadOptions {
51    pub fn exports<I, S>(mut self, exports: I) -> Self
52    where
53        I: IntoIterator<Item = S>,
54        S: Into<String>,
55    {
56        self.exports = exports.into_iter().map(Into::into).collect();
57        self
58    }
59
60    pub fn output_dir<P: AsRef<Path>>(mut self, output_dir: P) -> Self {
61        self.output_dir = Some(output_dir.as_ref().to_path_buf());
62        self
63    }
64
65    pub fn generate_bindings(mut self, generate_bindings: bool) -> Self {
66        self.generate_bindings = generate_bindings;
67        self
68    }
69
70    pub fn config_path<P: AsRef<Path>>(mut self, path: P) -> Self {
71        self.config_path = Some(path.as_ref().to_path_buf());
72        self
73    }
74
75    pub fn consumer_languages<I>(mut self, languages: I) -> Self
76    where
77        I: IntoIterator<Item = Language>,
78    {
79        self.consumer_languages = languages.into_iter().collect();
80        self
81    }
82}
83
84/// Result of loading a foreign module.
85#[derive(Clone, Debug)]
86pub struct LoadedModule {
87    /// Path to the compiled output (C file or object)
88    pub output_path: PathBuf,
89    /// Path to the generated header (if any)
90    pub header_path: Option<PathBuf>,
91    /// The generated Rust bindings
92    pub bindings: Option<GeneratedBinding>,
93    /// The language that was loaded
94    pub language: Language,
95    /// Path to the original source
96    pub source_path: PathBuf,
97    pub exports: Vec<String>,
98    pub export_source: ExportSource,
99    pub warnings: Vec<String>,
100    pub imports: Vec<GeneratedImport>,
101}
102
103impl LoadedModule {
104    /// Check if this module has bindings.
105    pub fn has_bindings(&self) -> bool {
106        self.bindings.is_some()
107    }
108
109    /// Get the binding code if available.
110    pub fn bindings_code(&self) -> Option<&str> {
111        self.bindings.as_ref().map(|b| b.code.as_str())
112    }
113}
114
115/// Load a foreign source file — compiles, generates bindings, returns ready-to-use module.
116///
117/// # Example
118///
119/// ```ignore
120/// use equilibrium::load;
121///
122/// // Simple one-liner
123/// let lib = load("native/math.c")?;
124///
125/// // Access compiled output and bindings
126/// println!("Compiled: {:?}", lib.output_path);
127/// if let Some(code) = lib.bindings_code() {
128///     println!("Bindings: {}", code);
129/// }
130/// ```
131///
132/// # Arguments
133/// * `source` - Path to the source file (e.g., "native/v/math.v")
134pub fn load<S: AsRef<Path>>(source: S) -> Result<LoadedModule, LoadError> {
135    load_with_options(source, LoadOptions::default())
136}
137
138/// Load with custom options.
139pub fn load_with_options<S: AsRef<Path>>(
140    source: S,
141    options: LoadOptions,
142) -> Result<LoadedModule, LoadError> {
143    let source = source.as_ref();
144    let source = source
145        .canonicalize()
146        .unwrap_or_else(|_| source.to_path_buf());
147
148    let Some(lang) = detect_language(&source) else {
149        return Err(LoadError::UnknownLanguage(source.clone()));
150    };
151
152    let export_options = ExportOptions {
153        exports: options.exports.clone(),
154        config_path: options.config_path.clone(),
155    };
156    let export_discovery = discover_exports_with_options(&source, lang, &export_options)
157        .map_err(|e| LoadError::ExportFailed(e.to_string()))?;
158
159    let output_dir = options.output_dir.clone().unwrap_or_else(|| {
160        // Use CARGO_MANIFEST_DIR if available, otherwise current dir
161        std::env::var("CARGO_MANIFEST_DIR")
162            .map(PathBuf::from)
163            .unwrap_or_else(|_| PathBuf::from("target"))
164            .join("native")
165            .join(format!("{:?}", lang).to_lowercase())
166    });
167
168    std::fs::create_dir_all(&output_dir).map_err(|e| LoadError::Io {
169        path: output_dir.clone(),
170        error: e,
171    })?;
172
173    let _compiler = find_compiler(lang).ok_or(LoadError::CompilerNotFound(lang))?;
174
175    let result = compile_to_c(&source, &output_dir)
176        .map_err(|e| LoadError::CompilationFailed(lang, e.to_string()))?;
177
178    let bindings = if options.generate_bindings {
179        if let Some(ref header_path) = result.header_path {
180            generate_bindings(
181                header_path,
182                options
183                    .binding_options
184                    .as_ref()
185                    .unwrap_or(&BindingOptions::default()),
186            )
187            .ok()
188        } else {
189            None
190        }
191    } else {
192        None
193    };
194
195    let import_source = result.header_path.clone().unwrap_or_else(|| source.clone());
196    let import_options =
197        ImportOptions::default().allowlist_functions(export_discovery.exports.clone());
198    let mut imports = Vec::new();
199    let mut warnings = export_discovery.warnings;
200    for language in &options.consumer_languages {
201        let generated = generate_imports(&import_source, *language, &import_options)
202            .map_err(LoadError::ImportFailed)?;
203        warnings.extend(generated.warnings.clone());
204        imports.push(generated);
205    }
206
207    Ok(LoadedModule {
208        output_path: result.output_path,
209        header_path: result.header_path,
210        bindings,
211        language: lang,
212        source_path: source,
213        exports: export_discovery.exports,
214        export_source: export_discovery.source,
215        warnings,
216        imports,
217    })
218}
219
220/// Errors that can occur when loading a module.
221#[derive(Debug)]
222pub enum LoadError {
223    UnknownLanguage(PathBuf),
224    CompilerNotFound(Language),
225    CompilationFailed(Language, String),
226    Io {
227        path: PathBuf,
228        error: std::io::Error,
229    },
230    BindingFailed(String),
231    ExportFailed(String),
232    ImportFailed(String),
233}
234
235impl std::fmt::Display for LoadError {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        match self {
238            LoadError::UnknownLanguage(path) => {
239                write!(f, "Unknown language for file: {}", path.display())
240            }
241            LoadError::CompilerNotFound(lang) => {
242                write!(f, "Compiler for {:?} not found", lang)
243            }
244            LoadError::CompilationFailed(lang, msg) => {
245                write!(f, "Compilation of {:?} failed: {}", lang, msg)
246            }
247            LoadError::Io { path, error } => {
248                write!(f, "IO error for {}: {}", path.display(), error)
249            }
250            LoadError::BindingFailed(msg) => {
251                write!(f, "Binding generation failed: {}", msg)
252            }
253            LoadError::ExportFailed(msg) => {
254                write!(f, "Export discovery failed: {}", msg)
255            }
256            LoadError::ImportFailed(msg) => {
257                write!(f, "Import generation failed: {}", msg)
258            }
259        }
260    }
261}
262
263impl std::error::Error for LoadError {}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use tempfile::Builder;
269
270    #[test]
271    fn test_load_c() {
272        let tmp = Builder::new().tempfile_in(std::env::temp_dir()).unwrap();
273        let path = tmp.path();
274        std::fs::write(path, "int add(int a, int b) { return a + b; }").unwrap();
275
276        let result = load(path);
277        // May fail if no C compiler available, but shouldn't panic
278        println!("{:?}", result);
279    }
280
281    #[test]
282    fn test_load_options() {
283        let opts = LoadOptions::default();
284        assert!(opts.generate_bindings);
285        assert!(opts.compile);
286        assert!(opts.link);
287    }
288}