1use std::path::{Path, PathBuf};
4
5use crate::bindings::{generate_bindings_from_content, BindingOptions, GeneratedBinding};
6use crate::c_header::parse_c_header;
7use crate::compiler::{
8 compile_to_c_with_extra, extra_link_arg_allowed, validate_extra_args, CompileResult,
9};
10use crate::detector::{detect_language, find_compiler, Language};
11use crate::exports::{discover_exports_with_options, ExportOptions, ExportSource};
12use crate::imports::{generate_imports, GeneratedImport, ImportOptions};
13use crate::limits::read_header_content;
14
15#[derive(Clone, Debug)]
17pub struct LoadOptions {
18 pub generate_bindings: bool,
20 pub compile: bool,
22 pub output_dir: Option<PathBuf>,
24 pub binding_options: Option<BindingOptions>,
26 pub link: bool,
28 pub compile_args: Vec<String>,
30 pub link_args: Vec<String>,
32 pub exports: Vec<String>,
33 pub config_path: Option<PathBuf>,
34 pub consumer_languages: Vec<Language>,
35}
36
37impl Default for LoadOptions {
38 fn default() -> Self {
39 Self {
40 generate_bindings: true,
41 compile: true,
42 output_dir: None,
43 binding_options: None,
44 link: true,
45 link_args: Vec::new(),
46 compile_args: Vec::new(),
47 exports: Vec::new(),
48 config_path: None,
49 consumer_languages: Vec::new(),
50 }
51 }
52}
53
54impl LoadOptions {
55 pub fn exports<I, S>(mut self, exports: I) -> Self
56 where
57 I: IntoIterator<Item = S>,
58 S: Into<String>,
59 {
60 self.exports = exports.into_iter().map(Into::into).collect();
61 self
62 }
63
64 pub fn output_dir<P: AsRef<Path>>(mut self, output_dir: P) -> Self {
65 self.output_dir = Some(output_dir.as_ref().to_path_buf());
66 self
67 }
68
69 pub fn generate_bindings(mut self, generate_bindings: bool) -> Self {
70 self.generate_bindings = generate_bindings;
71 self
72 }
73
74 pub fn compile(mut self, compile: bool) -> Self {
75 self.compile = compile;
76 self
77 }
78
79 pub fn link(mut self, link: bool) -> Self {
80 self.link = link;
81 self
82 }
83
84 pub fn compile_args<I, S>(mut self, args: I) -> Self
85 where
86 I: IntoIterator<Item = S>,
87 S: Into<String>,
88 {
89 self.compile_args = args.into_iter().map(Into::into).collect();
90 self
91 }
92
93 pub fn config_path<P: AsRef<Path>>(mut self, path: P) -> Self {
94 self.config_path = Some(path.as_ref().to_path_buf());
95 self
96 }
97
98 pub fn consumer_languages<I>(mut self, languages: I) -> Self
99 where
100 I: IntoIterator<Item = Language>,
101 {
102 self.consumer_languages = languages.into_iter().collect();
103 self
104 }
105}
106
107#[derive(Clone, Debug)]
109pub struct LoadedModule {
110 pub output_path: PathBuf,
112 pub header_path: Option<PathBuf>,
114 pub bindings: Option<GeneratedBinding>,
116 pub language: Language,
118 pub source_path: PathBuf,
120 pub exports: Vec<String>,
121 pub export_source: ExportSource,
122 pub warnings: Vec<String>,
123 pub imports: Vec<GeneratedImport>,
124}
125
126impl LoadedModule {
127 pub fn bindings_code(&self) -> Option<&str> {
129 self.bindings.as_ref().map(|b| b.code.as_str())
130 }
131}
132
133pub fn load<S: AsRef<Path>>(source: S) -> Result<LoadedModule, LoadError> {
153 load_with_options(source, LoadOptions::default())
154}
155
156pub fn load_with_options<S: AsRef<Path>>(
158 source: S,
159 options: LoadOptions,
160) -> Result<LoadedModule, LoadError> {
161 let source = source.as_ref();
162 let source = source
163 .canonicalize()
164 .unwrap_or_else(|_| source.to_path_buf());
165
166 let Some(lang) = detect_language(&source) else {
167 return Err(LoadError::UnknownLanguage(source.clone()));
168 };
169
170 let export_options = ExportOptions {
171 exports: options.exports.clone(),
172 config_path: options.config_path.clone(),
173 };
174 let export_discovery = discover_exports_with_options(&source, lang, &export_options)
175 .map_err(|e| LoadError::ExportFailed(e.to_string()))?;
176
177 let output_dir = options.output_dir.clone().unwrap_or_else(|| {
178 std::env::var("CARGO_MANIFEST_DIR")
180 .map(PathBuf::from)
181 .unwrap_or_else(|_| PathBuf::from("target"))
182 .join("native")
183 .join(format!("{:?}", lang).to_lowercase())
184 });
185
186 std::fs::create_dir_all(&output_dir).map_err(|e| LoadError::Io {
187 path: output_dir.clone(),
188 error: e,
189 })?;
190
191 if options.compile {
192 find_compiler(lang).ok_or(LoadError::CompilerNotFound(lang))?;
193 validate_extra_args(&options.compile_args)
194 .map_err(|e| LoadError::CompilationFailed(lang, e.to_string()))?;
195 }
196
197 let result = if options.compile {
198 compile_to_c_with_extra(
199 &source,
200 &output_dir,
201 &options.compile_args,
202 &options.link_args,
203 )
204 .map_err(|e| LoadError::CompilationFailed(lang, e.to_string()))?
205 } else {
206 CompileResult {
207 output_path: source.clone(),
208 header_path: sibling_header(&source),
209 language: lang,
210 stdout: String::new(),
211 stderr: String::new(),
212 }
213 };
214
215 let import_source = result.header_path.clone().unwrap_or_else(|| source.clone());
216 let header_content = if import_source
217 .extension()
218 .is_some_and(|ext| matches!(ext.to_str(), Some("h" | "hh" | "hpp" | "hxx")))
219 || options.generate_bindings
220 || !options.consumer_languages.is_empty()
221 {
222 read_header_content(&import_source).ok()
223 } else {
224 None
225 };
226
227 let parsed_header = header_content.as_deref().map(parse_c_header);
228
229 let default_binding_opts = BindingOptions::default();
230 let binding_opts = options
231 .binding_options
232 .as_ref()
233 .unwrap_or(&default_binding_opts);
234
235 let bindings = if options.generate_bindings {
236 if let (Some(content), Some(header_path)) = (&header_content, result.header_path.as_ref()) {
237 Some(
238 generate_bindings_from_content(header_path, content, binding_opts)
239 .map_err(LoadError::BindingFailed)?,
240 )
241 } else {
242 None
243 }
244 } else {
245 None
246 };
247
248 let import_options =
249 ImportOptions::default().allowlist_functions(export_discovery.exports.clone());
250 let mut imports = Vec::new();
251 let mut warnings = export_discovery.warnings;
252 if options.generate_bindings && bindings.is_none() {
253 warnings.push("no header found; bindings not generated".to_string());
254 }
255 if options.link {
256 emit_cargo_link(lang, &result.output_path, &options.link_args, &mut warnings)?;
257 }
258 for language in &options.consumer_languages {
259 let generated = if let Some(parsed) = &parsed_header {
260 crate::imports::generate_imports_from_parsed(
261 &import_source,
262 *language,
263 parsed,
264 &import_options,
265 )
266 .map_err(LoadError::ImportFailed)?
267 } else {
268 generate_imports(&import_source, *language, &import_options)
269 .map_err(LoadError::ImportFailed)?
270 };
271 warnings.extend(generated.warnings.clone());
272 imports.push(generated);
273 }
274
275 Ok(LoadedModule {
276 output_path: result.output_path,
277 header_path: result.header_path,
278 bindings,
279 language: lang,
280 source_path: source,
281 exports: export_discovery.exports,
282 export_source: export_discovery.source,
283 warnings,
284 imports,
285 })
286}
287
288#[derive(Debug)]
290pub enum LoadError {
291 UnknownLanguage(PathBuf),
292 CompilerNotFound(Language),
293 CompilationFailed(Language, String),
294 Io {
295 path: PathBuf,
296 error: std::io::Error,
297 },
298 BindingFailed(String),
299 ExportFailed(String),
300 ImportFailed(String),
301}
302
303impl std::fmt::Display for LoadError {
304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305 match self {
306 LoadError::UnknownLanguage(path) => {
307 write!(f, "Unknown language for file: {}", path.display())
308 }
309 LoadError::CompilerNotFound(lang) => {
310 write!(f, "Compiler for {:?} not found", lang)
311 }
312 LoadError::CompilationFailed(lang, msg) => {
313 write!(f, "Compilation of {:?} failed: {}", lang, msg)
314 }
315 LoadError::Io { path, error } => {
316 write!(f, "IO error for {}: {}", path.display(), error)
317 }
318 LoadError::BindingFailed(msg) => {
319 write!(f, "Binding generation failed: {}", msg)
320 }
321 LoadError::ExportFailed(msg) => {
322 write!(f, "Export discovery failed: {}", msg)
323 }
324 LoadError::ImportFailed(msg) => {
325 write!(f, "Import generation failed: {}", msg)
326 }
327 }
328 }
329}
330
331impl std::error::Error for LoadError {}
332
333fn sibling_header(source: &Path) -> Option<PathBuf> {
334 let header = source.with_extension("h");
335 header.is_file().then_some(header)
336}
337
338fn emit_cargo_link(
339 language: Language,
340 output_path: &Path,
341 link_args: &[String],
342 warnings: &mut Vec<String>,
343) -> Result<(), LoadError> {
344 if std::env::var_os("CARGO").is_none() || std::env::var_os("OUT_DIR").is_none() || cfg!(test) {
345 return Ok(());
346 }
347 if !output_path.is_file() {
348 warnings.push(format!(
349 "link requested but output is missing: {}",
350 output_path.display()
351 ));
352 return Ok(());
353 }
354 for arg in link_args {
355 if !extra_link_arg_allowed(arg) {
356 return Err(LoadError::CompilationFailed(
357 language,
358 format!("rejected extra link argument: {arg}"),
359 ));
360 }
361 }
362 let ext = output_path
363 .extension()
364 .and_then(|e| e.to_str())
365 .unwrap_or("");
366 match ext {
367 "o" | "obj" => {
368 println!("cargo:rustc-link-arg={}", output_path.display());
369 }
370 "a" | "lib" => {
371 if let (Some(dir), Some(stem)) = (output_path.parent(), output_path.file_stem()) {
372 let stem = stem.to_string_lossy();
373 let libname = stem.strip_prefix("lib").unwrap_or(&stem);
374 println!("cargo:rustc-link-search=native={}", dir.display());
375 println!("cargo:rustc-link-lib=static={libname}");
376 }
377 }
378 "so" | "dylib" | "dll" => {
379 if let (Some(dir), Some(stem)) = (output_path.parent(), output_path.file_stem()) {
380 let stem = stem.to_string_lossy();
381 let libname = stem.strip_prefix("lib").unwrap_or(&stem);
382 println!("cargo:rustc-link-search=native={}", dir.display());
383 println!("cargo:rustc-link-lib=dylib={libname}");
384 }
385 }
386 _ => {
387 println!("cargo:rustc-link-arg={}", output_path.display());
388 }
389 }
390 for arg in link_args {
391 println!("cargo:rustc-link-arg={arg}");
392 }
393 Ok(())
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn test_load_c() {
402 if find_compiler(Language::C).is_none() {
403 return;
404 }
405 let dir = tempfile::tempdir().unwrap();
406 let path = dir.path().join("add.c");
407 let header = dir.path().join("add.h");
408 std::fs::write(&header, "int add(int a, int b);\n").unwrap();
409 std::fs::write(&path, "int add(int a, int b) { return a + b; }\n").unwrap();
410 let out = dir.path().join("out");
411
412 let result = load_with_options(&path, LoadOptions::default().output_dir(&out).link(false))
413 .expect("load C source");
414 assert!(result.output_path.exists());
415 assert_eq!(result.output_path.extension().unwrap(), "o");
416 let code = result.bindings_code().expect("bindings");
417 assert!(code.contains("pub fn add("));
418 }
419
420 #[test]
421 fn test_load_honors_compile_false() {
422 let dir = tempfile::tempdir().unwrap();
423 let path = dir.path().join("add.c");
424 let header = dir.path().join("add.h");
425 std::fs::write(&header, "int add(int a, int b);\n").unwrap();
426 std::fs::write(&path, "int add(int a, int b) { return a + b; }\n").unwrap();
427 let out = dir.path().join("out");
428
429 let result = load_with_options(
430 &path,
431 LoadOptions::default()
432 .output_dir(&out)
433 .compile(false)
434 .link(false),
435 )
436 .expect("load without compile");
437 assert_eq!(
438 result.output_path.canonicalize().unwrap(),
439 path.canonicalize().unwrap()
440 );
441 assert!(result.bindings_code().unwrap().contains("pub fn add("));
442 assert!(!out.join("add.o").exists());
443 }
444
445 #[test]
446 fn test_load_options() {
447 let opts = LoadOptions::default();
448 assert!(opts.generate_bindings);
449 assert!(opts.compile);
450 assert!(opts.link);
451 }
452}