diff --git a/examples/demo-app/build.rs b/examples/demo-app/build.rs index a4ac3d0..2b4c873 100644 --- a/examples/demo-app/build.rs +++ b/examples/demo-app/build.rs @@ -3,9 +3,24 @@ use std::path::PathBuf; fn main() { let foreign = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("foreign-code"); let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); - let lib = equilibrium_ffi::load(foreign.join("math.c")).expect("load math.c"); - if let Some(code) = lib.bindings_code() { - std::fs::write(out_dir.join("math_bindings.rs"), code).expect("write bindings"); + + if cfg!(target_os = "macos") && std::path::Path::new("/usr/bin/ar").exists() { + // SAFETY: build scripts are single-threaded at the point this runs. + unsafe { + std::env::set_var("AR", "/usr/bin/ar"); + } } - println!("cargo:rerun-if-changed=foreign-code/*"); + cc::Build::new() + .file(foreign.join("math.c")) + .compile("math"); + + let binding = equilibrium_ffi::generate_bindings( + &foreign.join("math.h"), + &equilibrium_ffi::BindingOptions::default(), + ) + .expect("generate bindings"); + std::fs::write(out_dir.join("math_bindings.rs"), binding.code).expect("write bindings"); + + println!("cargo:rerun-if-changed=foreign-code/math.c"); + println!("cargo:rerun-if-changed=foreign-code/math.h"); } diff --git a/examples/full-demo/build.rs b/examples/full-demo/build.rs index 6365fd4..e40110c 100644 --- a/examples/full-demo/build.rs +++ b/examples/full-demo/build.rs @@ -3,9 +3,25 @@ use std::path::PathBuf; fn main() { let foreign = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("foreign-code"); let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); - let lib = equilibrium_ffi::load(foreign.join("calculator.c")).expect("load calculator.c"); - if let Some(code) = lib.bindings_code() { - std::fs::write(out_dir.join("calculator_bindings.rs"), code).expect("write bindings"); + + if cfg!(target_os = "macos") && std::path::Path::new("/usr/bin/ar").exists() { + // SAFETY: build scripts are single-threaded at the point this runs. + unsafe { + std::env::set_var("AR", "/usr/bin/ar"); + } } - println!("cargo:rerun-if-changed=foreign-code/*"); + cc::Build::new() + .file(foreign.join("calculator.c")) + .compile("calculator"); + println!("cargo:rustc-link-lib=m"); + + let binding = equilibrium_ffi::generate_bindings( + &foreign.join("calculator.h"), + &equilibrium_ffi::BindingOptions::default(), + ) + .expect("generate bindings"); + std::fs::write(out_dir.join("calculator_bindings.rs"), binding.code).expect("write bindings"); + + println!("cargo:rerun-if-changed=foreign-code/calculator.c"); + println!("cargo:rerun-if-changed=foreign-code/calculator.h"); } diff --git a/examples/full-demo/src/main.rs b/examples/full-demo/src/main.rs index acc45ac..0d6be83 100644 --- a/examples/full-demo/src/main.rs +++ b/examples/full-demo/src/main.rs @@ -1,37 +1,31 @@ //! Full Demo - Actually calling C functions from Rust via FFI -extern "C" { - fn calc_add(a: i32, b: i32) -> i32; - fn calc_subtract(a: i32, b: i32) -> i32; - fn calc_multiply(a: i32, b: i32) -> i32; - fn calc_divide(a: f64, b: f64) -> f64; - fn calc_power(base: i32, exp: i32) -> i32; - fn calc_sqrt(n: f64) -> f64; +mod ffi { + include!(concat!(env!("OUT_DIR"), "/calculator_bindings.rs")); } fn main() { println!("=== Full Equilibrium Demo ==="); println!("Calling C functions from Rust!\n"); - + unsafe { // Arithmetic operations println!("Arithmetic:"); - println!(" 10 + 5 = {}", calc_add(10, 5)); - println!(" 10 - 5 = {}", calc_subtract(10, 5)); - println!(" 10 * 5 = {}", calc_multiply(10, 5)); - println!(" 10.0 / 5.0 = {:.2}", calc_divide(10.0, 5.0)); - + println!(" 10 + 5 = {}", ffi::calc_add(10, 5)); + println!(" 10 - 5 = {}", ffi::calc_subtract(10, 5)); + println!(" 10 * 5 = {}", ffi::calc_multiply(10, 5)); + println!(" 10.0 / 5.0 = {:.2}", ffi::calc_divide(10.0, 5.0)); + // Advanced operations println!("\nAdvanced:"); - println!(" 2^8 = {}", calc_power(2, 8)); - println!(" sqrt(144) = {:.2}", calc_sqrt(144.0)); - println!(" sqrt(2) = {:.10}", calc_sqrt(2.0)); + println!(" 2^8 = {}", ffi::calc_power(2, 8)); + println!(" sqrt(144) = {:.2}", ffi::calc_sqrt(144.0)); + println!(" sqrt(2) = {:.10}", ffi::calc_sqrt(2.0)); } - + println!("\n✓ All C functions called successfully!"); println!("\nThis demonstrates:"); println!(" - C code compiled via cc crate"); - println!(" - Rust FFI declarations"); + println!(" - Rust FFI declarations generated by equilibrium-ffi"); println!(" - Calling C functions from Rust"); - println!(" - Equilibrium would automate the FFI declaration part"); } diff --git a/examples/polyglot-gui/src/polyglot.rs b/examples/polyglot-gui/src/polyglot.rs index db728a7..92cee1d 100644 --- a/examples/polyglot-gui/src/polyglot.rs +++ b/examples/polyglot-gui/src/polyglot.rs @@ -322,200 +322,6 @@ fn language_accent(lang: &str) -> &'static str { } } -pub fn constellation_lines(frame: &ConstellationFrame) -> Vec { - frame - .rows - .iter() - .map(|row| { - debug_assert!(row.spans.iter().all(|span| span.start <= span.end - && span.end <= row.text.len() - && span.color <= 0x00ff_ffff)); - row.text.clone() - }) - .collect() -} - -fn constellation_frame( - width: usize, - height: usize, - tick: f32, - stars: &[Star], - shooting_star: &ShootingStar, - bursts: &[Burst], -) -> ConstellationFrame { - let rows = height.clamp(6, 360); - let cols = width.clamp(20, 920); - let tick_bucket = (tick * 6.0) as usize; - let mut grid: Vec> = (0..rows) - .map(|y| constellation_base_row(y, rows, cols, tick, tick_bucket)) - .collect(); - - for star in stars { - let x = (star.x * cols as f32).clamp(0.0, (cols - 1) as f32) as usize; - let y = (star.y * rows as f32).clamp(0.0, (rows - 1) as f32) as usize; - let pulse = ((star.phase + tick * (0.35 + star.z)).sin() + 1.0) * 0.5; - let ch = match ((pulse * 5.0 + star.z * 3.0) as usize).min(7) { - 0 => '.', - 1 => '·', - 2 => ':', - 3 => '*', - 4 => '+', - 5 => 'o', - 6 => '✦', - _ => '✧', - }; - grid[y][x] = ConstellationCell { - ch, - color: language_color(7), - }; - } - - for i in 0..24 { - let fade = i as f32 / 24.0; - let tx = shooting_star.x - shooting_star.vx * i as f32 * 9.0; - let ty = shooting_star.y - shooting_star.vy * i as f32 * 9.0; - if (0.0..=1.0).contains(&tx) && (0.0..=1.0).contains(&ty) { - let x = (tx * cols as f32).clamp(0.0, (cols - 1) as f32) as usize; - let y = (ty * rows as f32).clamp(0.0, (rows - 1) as f32) as usize; - grid[y][x] = ConstellationCell { - ch: if fade < 0.2 { - '✦' - } else if fade < 0.45 { - '/' - } else if fade < 0.7 { - '·' - } else { - '.' - }, - color: 0xf59e0b, - }; - } - } - - for burst in bursts { - let bx = (burst.x * cols as f32).clamp(0.0, (cols - 1) as f32) as i32; - let by = (burst.y * rows as f32).clamp(0.0, (rows - 1) as f32) as i32; - let radius = 1 + (burst.age * 4.0) as i32; - for dy in -radius..=radius { - for dx in -radius..=radius { - let x = bx + dx; - let y = by + dy; - if x >= 0 - && y >= 0 - && (x as usize) < cols - && (y as usize) < rows - && dx.abs() + dy.abs() <= radius - { - grid[y as usize][x as usize] = ConstellationCell { - ch: match burst.seed % 4 { - 0 => '@', - 1 => '#', - 2 => '%', - _ => '&', - }, - color: if burst.seed % 2 == 0 { - language_color(5) - } else { - language_color(6) - }, - }; - } - } - } - } - - ConstellationFrame { - rows: grid.into_iter().map(constellation_row_data).collect(), - } -} - -fn constellation_base_row( - y: usize, - rows: usize, - cols: usize, - tick: f32, - tick_bucket: usize, -) -> Vec { - let fy = y as f32 / rows as f32; - let y_wave = (fy * 10.0 - tick * 0.25).cos(); - (0..cols) - .map(|x| { - let fx = x as f32 / cols as f32; - let ribbon = - ((fx * 12.0 + tick * 0.35).sin() + y_wave + ((fx + fy) * 18.0).sin() * 0.45) / 2.45; - let shimmer = (x * 17 + y * 29 + tick_bucket).is_multiple_of(113); - let ch = if shimmer { - ':' - } else if ribbon > 0.78 { - '.' - } else if ribbon > 0.62 { - '·' - } else if ribbon < -0.86 { - ',' - } else { - ' ' - }; - let color = match ch { - ':' => { - if (x + y + tick_bucket).is_multiple_of(2) { - language_color(3) - } else { - language_color(4) - } - } - '.' | '·' => language_color(0), - ',' => language_color(1), - _ => 0x334155, - }; - ConstellationCell { ch, color } - }) - .collect() -} - -fn language_color(index: usize) -> u32 { - match index % 8 { - 0 => 0x10b981, - 1 => 0x38bdf8, - 2 => 0xf59e0b, - 3 => 0x22d3ee, - 4 => 0x4ade80, - 5 => 0x60a5fa, - 6 => 0xfb7185, - _ => 0xe879f9, - } -} - -fn constellation_row_data(row: Vec) -> ConstellationRow { - let mut text = String::with_capacity(row.len() * 2); - let mut spans = Vec::new(); - let mut run_start = 0usize; - let mut run_color = row.first().map_or(0x334155, |cell| cell.color); - - for cell in row { - let start = text.len(); - if cell.color != run_color && start > run_start { - spans.push(ConstellationSpan { - start: run_start, - end: start, - color: run_color, - }); - run_start = start; - run_color = cell.color; - } - text.push(cell.ch); - } - - if text.len() > run_start { - spans.push(ConstellationSpan { - start: run_start, - end: text.len(), - color: run_color, - }); - } - - ConstellationRow { text, spans } -} - pub fn rust_is_prime(n: u64) -> bool { if n < 2 { return false; diff --git a/src/bin/eq/main.rs b/src/bin/eq/main.rs index b44e606..0aeb265 100644 --- a/src/bin/eq/main.rs +++ b/src/bin/eq/main.rs @@ -9,8 +9,9 @@ use clap::{Parser, Subcommand}; use console::{style, Style, Term}; -use dialoguer::{theme::ColorfulTheme, MultiSelect}; +use dialoguer::{theme::ColorfulTheme, Confirm, MultiSelect}; use equilibrium_ffi::{compiler_version_at, find_binary}; +use std::io::IsTerminal; use std::path::{Path, PathBuf}; use std::process::{Command, ExitCode}; @@ -164,7 +165,15 @@ impl PkgMgr { PkgMgr::Dnf => vec!["install".into(), "-y".into(), pkg.into()], PkgMgr::Pacman => vec!["-S".into(), "--noconfirm".into(), pkg.into()], // -e = exact match; --id avoids interactive prompts - PkgMgr::Winget => vec!["install".into(), "-e".into(), "--id".into(), pkg.into()], + PkgMgr::Winget => vec![ + "install".into(), + "-e".into(), + "--id".into(), + pkg.into(), + "--accept-package-agreements".into(), + "--accept-source-agreements".into(), + "--disable-interactivity".into(), + ], PkgMgr::Scoop => vec!["install".into(), pkg.into()], } } @@ -231,9 +240,7 @@ fn available_managers() -> Vec { v } -fn install_compiler(c: &Compiler) -> bool { - // On Windows, if the process cwd is a UNC path (e.g. \\wsl$\...) then - // cmd.exe subprocesses spawned by winget/scoop will fail. Move to %TEMP%. +fn install_cwd() -> Option { #[cfg(target_os = "windows")] { let cwd = std::env::current_dir() @@ -244,10 +251,20 @@ fn install_compiler(c: &Compiler) -> bool { let tmp = std::env::var("TEMP") .or_else(|_| std::env::var("TMP")) .unwrap_or_else(|_| r"C:\Windows\Temp".to_string()); - let _ = std::env::set_current_dir(&tmp); + return Some(PathBuf::from(tmp)); } } + None +} + +fn spawn_status(mut cmd: Command) -> std::io::Result { + if let Some(dir) = install_cwd() { + cmd.current_dir(dir); + } + cmd.status() +} +fn install_compiler(c: &Compiler) -> bool { let managers = available_managers(); if managers.is_empty() { println!( @@ -273,9 +290,9 @@ fn install_compiler(c: &Compiler) -> bool { let bucket_cmd = format!("scoop bucket add {bucket}"); println!(" {} {}", style("$").dim(), style(&bucket_cmd).cyan()); let scoop_bin = find_binary("scoop", &[]).unwrap_or_else(|| PathBuf::from("scoop")); - let _ = Command::new(&scoop_bin) - .args(["bucket", "add", bucket]) - .status(); + let mut bucket_add = Command::new(&scoop_bin); + bucket_add.args(["bucket", "add", bucket]); + let _ = spawn_status(bucket_add); } let args = mgr.install_args(pkg); @@ -288,7 +305,9 @@ fn install_compiler(c: &Compiler) -> bool { let status = if mgr.needs_sudo() { // Use argv directly — never invoke a shell around sudo/package managers. - Command::new("sudo").arg(mgr.cmd()).args(&args).status() + let mut cmd = Command::new("sudo"); + cmd.arg(mgr.cmd()).args(&args); + spawn_status(cmd) } else { // Resolve full path for wax/brew/winget in case they aren't on PATH let home = std::env::var("HOME").unwrap_or_default(); @@ -301,7 +320,9 @@ fn install_compiler(c: &Compiler) -> bool { ], ) .unwrap_or_else(|| PathBuf::from(mgr.cmd())); - Command::new(bin).args(&args).status() + let mut cmd = Command::new(bin); + cmd.args(&args); + spawn_status(cmd) }; if status.map(|s| s.success()).unwrap_or(false) { @@ -325,12 +346,7 @@ fn cmd_install(names: Vec) -> ExitCode { let mut had_error = false; for name in &names { - let name_lower = name.to_lowercase(); - if let Some(s) = statuses.iter().find(|s| { - s.compiler.id == name_lower - || s.compiler.bin == name_lower - || s.compiler.lang.to_lowercase().contains(&name_lower) - }) { + if let Some(s) = statuses.iter().find(|s| compiler_matches(s.compiler, name)) { if !s.compiler.supported { println!( "{} {} is not supported on this platform.", @@ -349,6 +365,9 @@ fn cmd_install(names: Vec) -> ExitCode { } } else { println!("{} Unknown compiler: {name}", style("✗").red()); + if name.eq_ignore_ascii_case("c") { + println!(" C (clang/gcc) is assumed present. For C#, use: eq install dotnet"); + } had_error = true; } } @@ -360,7 +379,11 @@ fn cmd_install(names: Vec) -> ExitCode { ExitCode::SUCCESS }; } - let install_ok = run_installs_parallel(&to_install); + if !confirm_privileged_install() { + println!("Aborted."); + return ExitCode::FAILURE; + } + let install_ok = run_installs(&to_install); return if had_error { ExitCode::FAILURE } else { @@ -397,7 +420,11 @@ fn cmd_install(names: Vec) -> ExitCode { Ok(Some(chosen)) if !chosen.is_empty() => { let selected: Vec<&'static Compiler> = chosen.iter().map(|&i| missing[i].compiler).collect(); - run_installs_parallel(&selected) + if !confirm_privileged_install() { + println!("Aborted."); + return ExitCode::FAILURE; + } + run_installs(&selected) } _ => { println!("Nothing selected."); @@ -406,44 +433,55 @@ fn cmd_install(names: Vec) -> ExitCode { } } -/// Install multiple compilers in parallel, one thread each. -fn run_installs_parallel(compilers: &[&'static Compiler]) -> ExitCode { - use std::sync::{Arc, Mutex}; - use std::thread; +fn compiler_matches(c: &Compiler, name: &str) -> bool { + let n = name.to_lowercase(); + c.id.eq_ignore_ascii_case(&n) + || c.bin.eq_ignore_ascii_case(&n) + || c.lang.eq_ignore_ascii_case(name) + || (c.id == "dotnet" && matches!(n.as_str(), "csharp" | "c#" | "cs")) +} + +fn sudo_may_be_used() -> bool { + available_managers().iter().any(|mgr| mgr.needs_sudo()) +} + +fn confirm_privileged_install() -> bool { + if !sudo_may_be_used() { + return true; + } + if std::env::var_os("EQ_INSTALL_YES").is_some() { + return true; + } + if !std::io::stdin().is_terminal() { + eprintln!( + "{} refusing to run sudo without a TTY. Re-run interactively, or set EQ_INSTALL_YES=1 (or EQ_INSTALL_NO_SUDO=1 to skip system package managers).", + style("!").yellow() + ); + return false; + } + Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Installing system packages requires sudo. Continue?") + .default(false) + .interact() + .unwrap_or(false) +} +fn run_installs(compilers: &[&'static Compiler]) -> ExitCode { println!( - "\n{} Installing {} compiler(s) in parallel…\n", + "\n{} Installing {} compiler(s)…\n", style("→").cyan(), compilers.len() ); - // Shared output buffer so lines from different threads don't interleave. - let log: Arc>> = Arc::new(Mutex::new(vec![])); - - let handles: Vec<_> = compilers - .iter() - .map(|c| { - let log = Arc::clone(&log); - let name = c.lang.as_str(); - let compiler: &'static Compiler = c; - thread::spawn(move || { - let ok = install_compiler(compiler); - let msg = if ok { - format!("{} {} installed", style("✓").green(), name) - } else { - format!("{} {} failed", style("✗").red(), name) - }; - log.lock().unwrap().push((msg, ok)); - ok - }) - }) - .collect(); - - let all_ok = handles.into_iter().all(|h| h.join().unwrap_or(false)); - - println!(); - for (msg, _) in log.lock().unwrap().iter() { - println!("{msg}"); + let mut all_ok = true; + for compiler in compilers { + let ok = install_compiler(compiler); + if ok { + println!("{} {} installed", style("✓").green(), compiler.lang); + } else { + println!("{} {} failed", style("✗").red(), compiler.lang); + all_ok = false; + } } if all_ok { diff --git a/src/bindings.rs b/src/bindings.rs index 096646c..011c5dc 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -3,7 +3,8 @@ use std::path::{Path, PathBuf}; use crate::c_header::{ - c_type_to_rust, parse_c_header, EnumDef, FunctionDef, ParsedHeader, StructDef, TypedefDef, + c_type_to_rust_checked, parse_c_header, parse_enum_discriminant, rust_ident, EnumDef, + FunctionDef, ParsedHeader, StructDef, TypedefDef, }; use crate::limits::read_header_content; @@ -99,15 +100,19 @@ fn emit_bindings_from_parsed( ) { for enum_def in &parsed.enums { if should_include(&enum_def.name, &options.allowlist_types) { - code.push_str(&generate_enum(enum_def, options)); - code.push('\n'); + if let Some(generated) = generate_enum(enum_def, warnings) { + code.push_str(&generated); + code.push('\n'); + } } } for struct_def in &parsed.structs { if should_include(&struct_def.name, &options.allowlist_types) { - code.push_str(&generate_struct(struct_def, options)); - code.push('\n'); + if let Some(generated) = generate_struct(struct_def, options, warnings) { + code.push_str(&generated); + code.push('\n'); + } } } @@ -124,8 +129,10 @@ fn emit_bindings_from_parsed( .iter() .any(|e| format!("enum {}", e.name) == typedef.target); if !is_struct_alias && !is_enum_alias { - code.push_str(&generate_typedef(typedef, options)); - code.push('\n'); + if let Some(generated) = generate_typedef(typedef, warnings) { + code.push_str(&generated); + code.push('\n'); + } } } } @@ -134,7 +141,10 @@ fn emit_bindings_from_parsed( code.push_str("extern \"C\" {\n"); for func in &parsed.functions { if should_include(&func.name, &options.allowlist_functions) { - code.push_str(&generate_function(func)); + match generate_function(func) { + Ok(generated) => code.push_str(&generated), + Err(reason) => warnings.push(reason), + } } else { warnings.push(format!("Skipped function: {}", func.name)); } @@ -142,28 +152,73 @@ fn emit_bindings_from_parsed( code.push_str("}\n"); } -fn generate_typedef(typedef: &TypedefDef, _options: &BindingOptions) -> String { - let rust_type = c_type_to_rust(&typedef.target); - format!("pub type {} = {};\n", typedef.name, rust_type) +fn generate_typedef(typedef: &TypedefDef, warnings: &mut Vec) -> Option { + let Some(name) = rust_ident(&typedef.name) else { + warnings.push(format!( + "Skipped typedef with invalid name: {}", + typedef.name + )); + return None; + }; + match c_type_to_rust_checked(&typedef.target) { + Ok(rust_type) => Some(format!("pub type {name} = {rust_type};\n")), + Err(reason) => { + warnings.push(format!("Skipped typedef {name}: {reason}")); + None + } + } } -fn generate_enum(enum_def: &EnumDef, _options: &BindingOptions) -> String { +fn generate_enum(enum_def: &EnumDef, warnings: &mut Vec) -> Option { + let Some(name) = rust_ident(&enum_def.name) else { + warnings.push(format!("Skipped enum with invalid name: {}", enum_def.name)); + return None; + }; let mut code = String::new(); code.push_str("#[repr(C)]\n"); code.push_str("#[derive(Debug, Copy, Clone, PartialEq, Eq)]\n"); - code.push_str(&format!("pub enum {} {{\n", enum_def.name)); + code.push_str(&format!("pub enum {name} {{\n")); + let mut any = false; for (variant_name, variant_value) in &enum_def.variants { + let Some(variant) = rust_ident(variant_name) else { + warnings.push(format!( + "Skipped enum variant with invalid name: {variant_name}" + )); + continue; + }; if let Some(value) = variant_value { - code.push_str(&format!(" {} = {},\n", variant_name, value)); + let Some(n) = parse_enum_discriminant(value) else { + warnings.push(format!( + "Skipped enum discriminant for {name}::{variant}: `{value}`" + )); + continue; + }; + code.push_str(&format!(" {variant} = {n},\n")); } else { - code.push_str(&format!(" {},\n", variant_name)); + code.push_str(&format!(" {variant},\n")); } + any = true; + } + if !any { + warnings.push(format!("Skipped empty enum: {name}")); + return None; } code.push_str("}\n"); - code + Some(code) } -fn generate_struct(struct_def: &StructDef, options: &BindingOptions) -> String { +fn generate_struct( + struct_def: &StructDef, + options: &BindingOptions, + warnings: &mut Vec, +) -> Option { + let Some(name) = rust_ident(&struct_def.name) else { + warnings.push(format!( + "Skipped struct with invalid name: {}", + struct_def.name + )); + return None; + }; let mut code = String::new(); let mut derives = vec!["Copy", "Clone"]; if options.derive_debug { @@ -174,33 +229,49 @@ fn generate_struct(struct_def: &StructDef, options: &BindingOptions) -> String { } code.push_str(&format!("#[derive({})]\n", derives.join(", "))); code.push_str("#[repr(C)]\n"); - code.push_str(&format!("pub struct {} {{\n", struct_def.name)); + code.push_str(&format!("pub struct {name} {{\n")); for (field_type, field_name) in &struct_def.fields { - let rust_type = c_type_to_rust(field_type); - code.push_str(&format!(" pub {}: {},\n", field_name, rust_type)); + let Some(field) = rust_ident(field_name) else { + warnings.push(format!( + "Skipped field with invalid name on {name}: {field_name}" + )); + continue; + }; + match c_type_to_rust_checked(field_type) { + Ok(rust_type) => { + code.push_str(&format!(" pub {field}: {rust_type},\n")); + } + Err(reason) => { + warnings.push(format!("Skipped field {name}.{field}: {reason}")); + } + } } code.push_str("}\n"); - code + Some(code) } -fn generate_function(func: &FunctionDef) -> String { - let rust_return = c_type_to_rust(&func.return_type); - let params: Vec = func - .params - .iter() - .map(|(typ, name)| format!("{}: {}", name, c_type_to_rust(typ))) - .collect(); +fn generate_function(func: &FunctionDef) -> Result { + let name = rust_ident(&func.name) + .ok_or_else(|| format!("Skipped function with invalid name: {}", func.name))?; + let rust_return = c_type_to_rust_checked(&func.return_type) + .map_err(|reason| format!("Skipped function {name}: {reason}"))?; + let mut params = Vec::new(); + for (typ, param_name) in &func.params { + let pname = rust_ident(param_name) + .ok_or_else(|| format!("Skipped function {name}: invalid parameter `{param_name}`"))?; + let rust_type = c_type_to_rust_checked(typ) + .map_err(|reason| format!("Skipped function {name}: {reason}"))?; + params.push(format!("{pname}: {rust_type}")); + } let return_clause = if rust_return == "()" { String::new() } else { - format!(" -> {}", rust_return) + format!(" -> {rust_return}") }; - format!( - " pub fn {}({}){};\n", - func.name, + Ok(format!( + " pub fn {name}({}){return_clause};\n", params.join(", "), - return_clause - ) + )) } #[cfg(test)] @@ -383,4 +454,70 @@ mod tests { assert!(binding.code.contains("pub type handle_t = c_int;")); assert!(binding.code.contains("pub fn open()")); } + + #[test] + fn test_generate_bindings_rejects_malicious_enum_discriminant() { + let dir = tempdir().unwrap(); + let header = dir.path().join("evil.h"); + std::fs::write( + &header, + "typedef enum { OK = 1, BAD = 1; include!(\"/tmp/pwn.rs\"); 0 } Evil;\nint foo(void);\n", + ) + .unwrap(); + + let opts = BindingOptions::default(); + let binding = generate_bindings(&header, &opts).unwrap(); + assert!(!binding.code.contains("include!")); + assert!(!binding.code.contains("/tmp/pwn")); + assert!(binding.code.contains("pub fn foo()")); + assert!(binding + .warnings + .iter() + .any(|w| w.contains("discriminant") || w.contains("Skipped"))); + } + + #[test] + fn test_generate_bindings_char_double_pointer() { + let dir = tempdir().unwrap(); + let header = dir.path().join("argv.h"); + std::fs::write(&header, "int count_args(char **argv);\n").unwrap(); + + let opts = BindingOptions::default(); + let binding = generate_bindings(&header, &opts).unwrap(); + assert!( + binding.code.contains("*mut *mut c_char"), + "got: {}", + binding.code + ); + assert!(binding.warnings.is_empty(), "{:?}", binding.warnings); + } + + #[test] + fn test_generate_bindings_skips_multiline_prototype() { + let dir = tempdir().unwrap(); + let header = dir.path().join("multi.h"); + std::fs::write( + &header, + "int sneaky(\n int a,\n int b);\nint ok(void);\n", + ) + .unwrap(); + + let opts = BindingOptions::default(); + let binding = generate_bindings(&header, &opts).unwrap(); + assert!(!binding.code.contains("sneaky")); + assert!(binding.code.contains("pub fn ok()")); + } + + #[test] + fn test_generate_bindings_skips_unknown_type_passthrough() { + let dir = tempdir().unwrap(); + let header = dir.path().join("weird.h"); + std::fs::write(&header, "void evil(not a type x);\nint ok(void);\n").unwrap(); + + let opts = BindingOptions::default(); + let binding = generate_bindings(&header, &opts).unwrap(); + assert!(!binding.code.contains("not a type")); + assert!(!binding.code.contains("pub fn evil(")); + assert!(binding.code.contains("pub fn ok()")); + } } diff --git a/src/c_header.rs b/src/c_header.rs index 687cf8b..97132ee 100644 --- a/src/c_header.rs +++ b/src/c_header.rs @@ -278,10 +278,134 @@ pub(crate) fn c_type_to_rust(c_type: &str) -> String { } } s if s.starts_with("const ") => c_type_to_rust(s.strip_prefix("const ").unwrap()), - other => other.to_string(), + other if is_c_identifier(other) => other.to_string(), + _ => "*mut c_void".to_string(), } } +pub(crate) fn c_type_to_rust_checked(c_type: &str) -> Result { + let mapped = c_type_to_rust(c_type); + let trimmed = c_type.trim(); + if mapped == "*mut c_void" + && trimmed != "void *" + && trimmed != "void*" + && !trimmed.ends_with("void *") + && !trimmed.ends_with("void*") + && !is_known_or_ident_type(trimmed) + { + return Err(format!("unsupported C type `{trimmed}`")); + } + Ok(mapped) +} + +fn is_known_or_ident_type(c_type: &str) -> bool { + let c_type = c_type.trim(); + if c_type.ends_with('*') { + return is_known_or_ident_type(c_type.strip_suffix('*').unwrap().trim()); + } + if let Some(inner) = c_type.strip_prefix("const ") { + return is_known_or_ident_type(inner); + } + is_c_abi_safe_scalar(c_type) || is_c_identifier(c_type) +} + +pub(crate) fn is_c_identifier(name: &str) -> bool { + let mut chars = name.chars(); + match chars.next() { + Some(c) if c.is_ascii_alphabetic() || c == '_' => {} + _ => return false, + } + chars.all(|c| c.is_ascii_alphanumeric() || c == '_') +} + +pub(crate) fn rust_ident(name: &str) -> Option { + if !is_c_identifier(name) { + return None; + } + if is_rust_keyword(name) { + Some(format!("r#{name}")) + } else { + Some(name.to_string()) + } +} + +fn is_rust_keyword(name: &str) -> bool { + matches!( + name, + "as" | "async" + | "await" + | "break" + | "const" + | "continue" + | "crate" + | "dyn" + | "else" + | "enum" + | "extern" + | "false" + | "fn" + | "for" + | "if" + | "impl" + | "in" + | "let" + | "loop" + | "match" + | "mod" + | "move" + | "mut" + | "pub" + | "ref" + | "return" + | "self" + | "Self" + | "static" + | "struct" + | "super" + | "trait" + | "true" + | "type" + | "unsafe" + | "use" + | "where" + | "while" + | "abstract" + | "become" + | "box" + | "do" + | "final" + | "macro" + | "override" + | "priv" + | "typeof" + | "unsized" + | "virtual" + | "yield" + | "try" + | "gen" + ) +} + +pub(crate) fn parse_enum_discriminant(value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + return None; + } + if let Some(hex) = value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) + { + return i64::from_str_radix(hex, 16).ok(); + } + if value.starts_with('+') || value.starts_with('-') { + return value.parse().ok(); + } + if value.bytes().all(|b| b.is_ascii_digit()) { + return value.parse().ok(); + } + None +} + pub(crate) fn is_c_abi_safe_type(c_type: &str) -> bool { let c_type = c_type.trim(); if c_type.is_empty() { @@ -290,9 +414,18 @@ pub(crate) fn is_c_abi_safe_type(c_type: &str) -> bool { if c_type.starts_with("struct ") || c_type.starts_with("enum ") { return false; } - if c_type.ends_with('*') { - return true; + if let Some(inner) = c_type.strip_suffix('*') { + let inner = inner.trim(); + let inner = inner.strip_prefix("const ").unwrap_or(inner).trim(); + if inner == "void" { + return true; + } + return is_c_abi_safe_type(inner); } + is_c_abi_safe_scalar(c_type) +} + +fn is_c_abi_safe_scalar(c_type: &str) -> bool { matches!( c_type, "void" diff --git a/src/compiler.rs b/src/compiler.rs index ae2b730..1b9f6b3 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -22,6 +22,8 @@ pub enum CompileError { Io(std::io::Error), /// Language doesn't support C output. UnsupportedCOutput { language: Language }, + /// Extra compiler/link argument was rejected. + InvalidExtraArg { arg: String }, } impl std::fmt::Display for CompileError { @@ -37,6 +39,9 @@ impl std::fmt::Display for CompileError { CompileError::UnsupportedCOutput { language } => { write!(f, "{:?} doesn't support direct C output", language) } + CompileError::InvalidExtraArg { arg } => { + write!(f, "rejected extra compiler argument: {arg}") + } } } } @@ -119,10 +124,17 @@ pub fn compile_to_c_with_lang_and_extra( ))); } + std::fs::create_dir_all(output_dir)?; + validate_extra_args(compile_args)?; + // Find compiler let info = find_compiler(language).ok_or(CompileError::CompilerNotFound { language })?; - let compiler = info.compiler.as_ref().unwrap(); + let compiler = info + .compiler_path + .clone() + .or_else(|| info.compiler.as_ref().map(PathBuf::from)) + .ok_or(CompileError::CompilerNotFound { language })?; // Determine output file name let stem = input @@ -130,7 +142,7 @@ pub fn compile_to_c_with_lang_and_extra( .and_then(|s| s.to_str()) .unwrap_or("output"); - let c_output = output_dir.join(format!("{stem}.c")); + let c_output = output_dir.join(artifact_filename(language, stem)); let header_output = output_dir.join(format!("{stem}.h")); // Build command @@ -140,7 +152,7 @@ pub fn compile_to_c_with_lang_and_extra( let mut args = language.to_c_args(&input_str, &output_str); args.extend(compile_args.iter().cloned()); - let output = Command::new(compiler) + let output = Command::new(&compiler) .args(&args) .current_dir(input.parent().unwrap_or(Path::new("."))) .output()?; @@ -155,16 +167,35 @@ pub fn compile_to_c_with_lang_and_extra( }); } + let output_path = if c_output.exists() { + c_output + } else { + let alt = output_dir.join(format!("lib{stem}.a")); + if alt.exists() { + alt + } else { + return Err(CompileError::CompilationFailed { + stderr: format!( + "compiler succeeded but output is missing: {} (stderr: {stderr})", + c_output.display() + ), + exit_code: output.status.code(), + }); + } + }; + // Check if header was generated (language-specific) let header_path = if header_output.exists() { Some(header_output) + } else if let Some(copied) = copy_sibling_header(input, output_dir) { + Some(copied) } else { // Try to generate header for some languages generate_header(input, output_dir, language).ok() }; Ok(CompileResult { - output_path: c_output, + output_path, header_path, language, stdout, @@ -172,6 +203,133 @@ pub fn compile_to_c_with_lang_and_extra( }) } +fn artifact_filename(language: Language, stem: &str) -> String { + match language { + Language::C + | Language::Cpp + | Language::Zig + | Language::D + | Language::Odin + | Language::Hare => format!("{stem}.o"), + Language::V => format!("{stem}.c"), + Language::Nim => format!("{stem}.a"), + Language::CSharp => format!("{stem}.dll"), + Language::Rust => { + if cfg!(target_os = "windows") { + format!("{stem}.dll") + } else if cfg!(target_os = "macos") { + format!("{stem}.dylib") + } else { + format!("{stem}.so") + } + } + } +} + +fn copy_sibling_header(input: &Path, output_dir: &Path) -> Option { + let header = input.with_extension("h"); + if !header.is_file() { + return None; + } + let dest = output_dir.join(header.file_name()?); + std::fs::copy(&header, &dest).ok()?; + Some(dest) +} + +pub(crate) fn validate_extra_args(args: &[String]) -> Result<(), CompileError> { + for arg in args { + if !extra_compile_arg_allowed(arg) { + return Err(CompileError::InvalidExtraArg { arg: arg.clone() }); + } + } + Ok(()) +} + +pub(crate) fn extra_compile_arg_allowed(arg: &str) -> bool { + if arg.is_empty() || !arg.is_ascii() { + return false; + } + if arg.chars().any(|c| c.is_control() || c.is_whitespace()) { + return false; + } + if arg.starts_with('@') || !arg.starts_with('-') { + return false; + } + if let Some(rest) = arg.strip_prefix("-O") { + return rest.chars().all(|c| c.is_ascii_alphanumeric()); + } + let lower = arg.to_ascii_lowercase(); + const DENY: &[&str] = &[ + "-fplugin", + "-load", + "-plugin", + "-wl,", + "-xlinker", + "-wrapper", + "-specs", + "-b", + "-femit-bin", + "--output", + "-o", + "-of", + ]; + for prefix in DENY { + if lower == *prefix + || lower.starts_with(&format!("{prefix}=")) + || lower.starts_with(&format!("{prefix}:")) + { + return false; + } + if *prefix == "-o" && lower.starts_with("-o") && !lower.starts_with("-objc") { + return false; + } + if *prefix == "-of" && lower.starts_with("-of") { + return false; + } + if *prefix == "-b" + && (lower == "-b" || lower.starts_with("-b/") || lower.starts_with("-b=")) + { + return false; + } + } + if arg.starts_with("-I") && arg.len() > 2 { + let path = &arg[2..]; + return !path.contains("..") && path.chars().all(|c| c.is_ascii_graphic()); + } + extra_flag_syntax(arg) +} + +fn extra_flag_syntax(arg: &str) -> bool { + let bytes = arg.as_bytes(); + if bytes.len() < 2 || bytes[0] != b'-' { + return false; + } + let rest = if bytes[1] == b'-' { + &arg[2..] + } else { + &arg[1..] + }; + if rest.is_empty() { + return false; + } + let mut chars = rest.chars(); + match chars.next() { + Some(c) if c.is_ascii_alphabetic() => {} + _ => return false, + } + let remaining: String = chars.collect(); + if remaining.is_empty() { + return true; + } + remaining + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '+' | '-' | '=' | ':' | '/')) +} + +pub(crate) fn extra_link_arg_allowed(arg: &str) -> bool { + extra_compile_arg_allowed(arg) && (arg.starts_with("-l") || arg.starts_with("-L")) +} + /// Generate a C header file for the compiled code. fn generate_header( input: &Path, @@ -350,6 +508,40 @@ mod tests { } } + #[test] + fn test_compile_rejects_plugin_arg() { + if find_compiler(Language::C).is_none() { + return; + } + let dir = tempdir().unwrap(); + let c_file = dir.path().join("test.c"); + std::fs::write(&c_file, "int add(int a, int b) { return a + b; }\n").unwrap(); + let output_dir = dir.path().join("out"); + let err = compile_to_c_with_extra( + &c_file, + &output_dir, + &["-fplugin=/tmp/x.so".to_string()], + &[], + ) + .unwrap_err(); + assert!(err.to_string().contains("rejected extra compiler argument")); + } + + #[test] + fn test_rejects_dangerous_compile_args() { + assert!(!extra_compile_arg_allowed("-fplugin=/tmp/x.so")); + assert!(!extra_compile_arg_allowed("@response")); + assert!(!extra_compile_arg_allowed("-o")); + assert!(!extra_compile_arg_allowed("-ofoo.o")); + assert!(!extra_compile_arg_allowed("-Wl,-rpath,/tmp")); + assert!(!extra_compile_arg_allowed("-fPIC\n-o/tmp/x")); + assert!(extra_compile_arg_allowed("-fPIC")); + assert!(extra_compile_arg_allowed("-O2")); + assert!(extra_compile_arg_allowed("-std=c11")); + assert!(extra_compile_arg_allowed("-I/usr/include")); + assert!(!extra_compile_arg_allowed("-I../secret")); + } + #[test] fn test_compile_batch_c_files() { // Skip if no C compiler is available diff --git a/src/detector.rs b/src/detector.rs index 45cb3ce..80ac357 100644 --- a/src/detector.rs +++ b/src/detector.rs @@ -33,6 +33,7 @@ pub enum Language { pub struct LanguageInfo { pub language: Language, pub compiler: Option, + pub compiler_path: Option, pub version: Option, } @@ -106,10 +107,51 @@ impl Language { Language::D => &["dmd", "gdc"], Language::C => &["gcc", "cc"], Language::Cpp => &["g++", "c++"], + Language::CSharp => &["dotnet"], _ => &[], } } + pub fn version_args(&self) -> &'static [&'static str] { + match self { + Language::Zig | Language::Odin | Language::V | Language::Hare => &["version"], + _ => &["--version"], + } + } + + /// PIC flags for ELF/Mach-O. MSVC clang rejects `-fPIC`. + fn pic_c_flag() -> Option<&'static str> { + if cfg!(windows) { + None + } else { + Some("-fPIC") + } + } + + fn pic_nim_pass_c() -> Option<&'static str> { + if cfg!(windows) { + None + } else { + Some("--passC:-fPIC") + } + } + + fn pic_d_reloc() -> Option<&'static str> { + if cfg!(windows) { + None + } else { + Some("--relocation-model=pic") + } + } + + fn pic_odin_reloc() -> Option<&'static str> { + if cfg!(windows) { + None + } else { + Some("-reloc-mode:pic") + } + } + /// Get the command to compile to C intermediate. pub fn to_c_args(&self, input: &str, output: &str) -> Vec { match self { @@ -123,29 +165,31 @@ impl Language { Language::Zig => { // Zig doesn't have direct C output, but we can use translate-c for headers // For actual code, we emit object files - vec![ - "build-obj".to_string(), - format!("-femit-bin={output}"), - input.to_string(), - ] + let mut args = vec!["build-obj".to_string()]; + if let Some(pic) = Self::pic_c_flag() { + args.push(pic.to_string()); + } + args.push("-OReleaseFast".to_string()); + args.push(format!("-femit-bin={output}")); + args.push(input.to_string()); + args } Language::C => { - // C is already C, just preprocess - vec![ - "-E".to_string(), - "-o".to_string(), - output.to_string(), - input.to_string(), - ] + let mut args = vec!["-c".to_string()]; + if let Some(pic) = Self::pic_c_flag() { + args.push(pic.to_string()); + } + args.extend(["-o".to_string(), output.to_string(), input.to_string()]); + args } Language::Cpp => { // Compile to object, we'll need headers separately - vec![ - "-c".to_string(), - "-o".to_string(), - output.to_string(), - input.to_string(), - ] + let mut args = vec!["-c".to_string()]; + if let Some(pic) = Self::pic_c_flag() { + args.push(pic.to_string()); + } + args.extend(["-o".to_string(), output.to_string(), input.to_string()]); + args } Language::CSharp => { // C# to native requires AOT compilation @@ -166,31 +210,49 @@ impl Language { } Language::D => { // D can emit C headers with -HC flag (LDC2) - vec![ - "-c".to_string(), - "-of".to_string(), - output.to_string(), + let mut args = vec!["-c".to_string()]; + if let Some(pic) = Self::pic_d_reloc() { + args.push(pic.to_string()); + } + args.extend([ + format!("-of={output}"), "-HC".to_string(), // Generate C header input.to_string(), - ] + ]); + args } Language::Nim => { - // Nim compiles to C by default - vec![ + let cache = Path::new(output) + .parent() + .unwrap_or_else(|| Path::new(".")) + .join("nimcache"); + let mut args = vec![ "c".to_string(), - "--nimcache:.".to_string(), - format!("-o:{output}"), - input.to_string(), - ] + format!("--nimcache:{}", cache.display()), + "--noMain".to_string(), + "--app:staticlib".to_string(), + "--mm:none".to_string(), + ]; + if let Some(pic) = Self::pic_nim_pass_c() { + args.push(pic.to_string()); + } + args.push(format!("-o:{output}")); + args.push(input.to_string()); + args } Language::Odin => { // Odin compiles to object files - vec![ + let mut args = vec![ "build".to_string(), input.to_string(), - "-out:".to_string() + output, + "-file".to_string(), + format!("-out:{output}"), "-build-mode:obj".to_string(), - ] + ]; + if let Some(pic) = Self::pic_odin_reloc() { + args.push(pic.to_string()); + } + args } Language::Hare => { // Hare compiles to object files via QBE @@ -234,6 +296,14 @@ pub fn detect_language(path: &Path) -> Option { None } +const WELL_KNOWN_BIN_DIRS: &[&str] = &[ + "/home/linuxbrew/.linuxbrew/bin", + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/local/sbin", + "/usr/local/odin", +]; + /// Resolve a compiler binary on `PATH` or under `extra_paths`. pub fn find_binary(bin: &str, extra_paths: &[&str]) -> Option { which::which(bin).ok().or_else(|| { @@ -271,25 +341,27 @@ pub fn compiler_version_at(path: &Path, version_args: &[&str]) -> Option } } +fn language_info(language: Language, name: &str, path: PathBuf) -> LanguageInfo { + let version = compiler_version_at(&path, language.version_args()); + LanguageInfo { + language, + compiler: Some(name.to_string()), + compiler_path: Some(path), + version, + } +} + /// Check if a compiler is available on the system. pub fn find_compiler(language: Language) -> Option { let compiler_name = language.default_compiler(); - if let Some(path) = find_binary(compiler_name, &[]) { - return Some(LanguageInfo { - language, - compiler: Some(compiler_name.to_string()), - version: compiler_version_at(&path, &["--version"]), - }); + if let Some(path) = find_binary(compiler_name, WELL_KNOWN_BIN_DIRS) { + return Some(language_info(language, compiler_name, path)); } for alt in language.alternative_compilers() { - if let Some(path) = find_binary(alt, &[]) { - return Some(LanguageInfo { - language, - compiler: Some((*alt).to_string()), - version: compiler_version_at(&path, &["--version"]), - }); + if let Some(path) = find_binary(alt, WELL_KNOWN_BIN_DIRS) { + return Some(language_info(language, alt, path)); } } @@ -431,6 +503,7 @@ mod tests { ); let info = info.unwrap(); assert!(info.compiler.is_some()); + assert!(info.compiler_path.is_some()); } #[test] @@ -442,21 +515,69 @@ mod tests { } #[test] - fn test_to_c_args_c_preprocess() { - let args = Language::C.to_c_args("foo.c", "foo.i"); - assert!(args.contains(&"-E".to_string())); + fn test_to_c_args_c_object() { + let args = Language::C.to_c_args("foo.c", "foo.o"); + assert!(args.contains(&"-c".to_string())); + if cfg!(windows) { + assert!(!args.contains(&"-fPIC".to_string())); + } else { + assert!(args.contains(&"-fPIC".to_string())); + } + assert!(!args.contains(&"-E".to_string())); assert!(args.contains(&"foo.c".to_string())); - assert!(args.contains(&"foo.i".to_string())); + assert!(args.contains(&"foo.o".to_string())); } #[test] - fn test_to_c_args_zig_no_duplicate_flag() { + fn test_to_c_args_zig_pic_releasefast() { let args = Language::Zig.to_c_args("foo.zig", "foo.o"); assert!(args.contains(&"build-obj".to_string())); + if cfg!(windows) { + assert!(!args.contains(&"-fPIC".to_string())); + } else { + assert!(args.contains(&"-fPIC".to_string())); + } + assert!(args.contains(&"-OReleaseFast".to_string())); let femit_count = args.iter().filter(|a| a.starts_with("-femit-bin")).count(); assert_eq!(femit_count, 1, "should have exactly one -femit-bin flag"); } + #[test] + fn test_to_c_args_nim_cache_under_output() { + let args = Language::Nim.to_c_args("foo.nim", "/tmp/out/foo.a"); + assert!(args + .iter() + .any(|a| a.starts_with("--nimcache:") && a.contains("out"))); + assert!(!args.iter().any(|a| a == "--nimcache:.")); + assert!(args.contains(&"--app:staticlib".to_string())); + if cfg!(windows) { + assert!(!args.contains(&"--passC:-fPIC".to_string())); + } else { + assert!(args.contains(&"--passC:-fPIC".to_string())); + } + } + + #[test] + fn test_to_c_args_odin_pic() { + let args = Language::Odin.to_c_args("foo.odin", "foo.o"); + assert!(args.contains(&"-file".to_string())); + if cfg!(windows) { + assert!(!args.contains(&"-reloc-mode:pic".to_string())); + } else { + assert!(args.contains(&"-reloc-mode:pic".to_string())); + } + } + + #[test] + fn test_version_args_match_catalogue() { + assert_eq!(Language::Zig.version_args(), &["version"]); + assert_eq!(Language::Odin.version_args(), &["version"]); + assert_eq!(Language::V.version_args(), &["version"]); + assert_eq!(Language::Hare.version_args(), &["version"]); + assert_eq!(Language::C.version_args(), &["--version"]); + assert_eq!(Language::CSharp.alternative_compilers(), &["dotnet"]); + } + #[test] fn test_scan_directory_empty() { let dir = tempdir().unwrap(); diff --git a/src/imports.rs b/src/imports.rs index 716a7b5..5dc5c2b 100644 --- a/src/imports.rs +++ b/src/imports.rs @@ -1,6 +1,8 @@ use std::path::{Path, PathBuf}; -use crate::c_header::{header_stem, is_c_abi_safe_type, parse_c_header, FunctionDef, ParsedHeader}; +use crate::c_header::{ + header_stem, is_c_abi_safe_type, is_c_identifier, parse_c_header, FunctionDef, ParsedHeader, +}; use crate::detector::Language; use crate::limits::read_header_content; @@ -76,11 +78,12 @@ pub fn generate_imports_from_parsed( } fn supports_import(function: &FunctionDef) -> bool { - is_c_abi_safe_type(&function.return_type) + is_c_identifier(&function.name) + && is_c_abi_safe_type(&function.return_type) && function .params .iter() - .all(|(param_type, _)| is_c_abi_safe_type(param_type)) + .all(|(param_type, name)| is_c_identifier(name) && is_c_abi_safe_type(param_type)) } fn render_imports( diff --git a/src/loader.rs b/src/loader.rs index d8a4257..1329ba0 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -4,7 +4,9 @@ use std::path::{Path, PathBuf}; use crate::bindings::{generate_bindings_from_content, BindingOptions, GeneratedBinding}; use crate::c_header::parse_c_header; -use crate::compiler::compile_to_c_with_extra; +use crate::compiler::{ + compile_to_c_with_extra, extra_link_arg_allowed, validate_extra_args, CompileResult, +}; use crate::detector::{detect_language, find_compiler, Language}; use crate::exports::{discover_exports_with_options, ExportOptions, ExportSource}; use crate::imports::{generate_imports, GeneratedImport, ImportOptions}; @@ -69,6 +71,25 @@ impl LoadOptions { self } + pub fn compile(mut self, compile: bool) -> Self { + self.compile = compile; + self + } + + pub fn link(mut self, link: bool) -> Self { + self.link = link; + self + } + + pub fn compile_args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.compile_args = args.into_iter().map(Into::into).collect(); + self + } + pub fn config_path>(mut self, path: P) -> Self { self.config_path = Some(path.as_ref().to_path_buf()); self @@ -167,15 +188,29 @@ pub fn load_with_options>( error: e, })?; - let _compiler = find_compiler(lang).ok_or(LoadError::CompilerNotFound(lang))?; + if options.compile { + find_compiler(lang).ok_or(LoadError::CompilerNotFound(lang))?; + validate_extra_args(&options.compile_args) + .map_err(|e| LoadError::CompilationFailed(lang, e.to_string()))?; + } - let result = compile_to_c_with_extra( - &source, - &output_dir, - &options.compile_args, - &options.link_args, - ) - .map_err(|e| LoadError::CompilationFailed(lang, e.to_string()))?; + let result = if options.compile { + compile_to_c_with_extra( + &source, + &output_dir, + &options.compile_args, + &options.link_args, + ) + .map_err(|e| LoadError::CompilationFailed(lang, e.to_string()))? + } else { + CompileResult { + output_path: source.clone(), + header_path: sibling_header(&source), + language: lang, + stdout: String::new(), + stderr: String::new(), + } + }; let import_source = result.header_path.clone().unwrap_or_else(|| source.clone()); let header_content = if import_source @@ -199,7 +234,10 @@ pub fn load_with_options>( let bindings = if options.generate_bindings { if let (Some(content), Some(header_path)) = (&header_content, result.header_path.as_ref()) { - generate_bindings_from_content(header_path, content, binding_opts).ok() + Some( + generate_bindings_from_content(header_path, content, binding_opts) + .map_err(LoadError::BindingFailed)?, + ) } else { None } @@ -211,6 +249,12 @@ pub fn load_with_options>( ImportOptions::default().allowlist_functions(export_discovery.exports.clone()); let mut imports = Vec::new(); let mut warnings = export_discovery.warnings; + if options.generate_bindings && bindings.is_none() { + warnings.push("no header found; bindings not generated".to_string()); + } + if options.link { + emit_cargo_link(lang, &result.output_path, &options.link_args, &mut warnings)?; + } for language in &options.consumer_languages { let generated = if let Some(parsed) = &parsed_header { crate::imports::generate_imports_from_parsed( @@ -286,20 +330,116 @@ impl std::fmt::Display for LoadError { impl std::error::Error for LoadError {} +fn sibling_header(source: &Path) -> Option { + let header = source.with_extension("h"); + header.is_file().then_some(header) +} + +fn emit_cargo_link( + language: Language, + output_path: &Path, + link_args: &[String], + warnings: &mut Vec, +) -> Result<(), LoadError> { + if std::env::var_os("CARGO").is_none() || std::env::var_os("OUT_DIR").is_none() || cfg!(test) { + return Ok(()); + } + if !output_path.is_file() { + warnings.push(format!( + "link requested but output is missing: {}", + output_path.display() + )); + return Ok(()); + } + for arg in link_args { + if !extra_link_arg_allowed(arg) { + return Err(LoadError::CompilationFailed( + language, + format!("rejected extra link argument: {arg}"), + )); + } + } + let ext = output_path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or(""); + match ext { + "o" | "obj" => { + println!("cargo:rustc-link-arg={}", output_path.display()); + } + "a" | "lib" => { + if let (Some(dir), Some(stem)) = (output_path.parent(), output_path.file_stem()) { + let stem = stem.to_string_lossy(); + let libname = stem.strip_prefix("lib").unwrap_or(&stem); + println!("cargo:rustc-link-search=native={}", dir.display()); + println!("cargo:rustc-link-lib=static={libname}"); + } + } + "so" | "dylib" | "dll" => { + if let (Some(dir), Some(stem)) = (output_path.parent(), output_path.file_stem()) { + let stem = stem.to_string_lossy(); + let libname = stem.strip_prefix("lib").unwrap_or(&stem); + println!("cargo:rustc-link-search=native={}", dir.display()); + println!("cargo:rustc-link-lib=dylib={libname}"); + } + } + _ => { + println!("cargo:rustc-link-arg={}", output_path.display()); + } + } + for arg in link_args { + println!("cargo:rustc-link-arg={arg}"); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; - use tempfile::Builder; #[test] fn test_load_c() { - let tmp = Builder::new().tempfile_in(std::env::temp_dir()).unwrap(); - let path = tmp.path(); - std::fs::write(path, "int add(int a, int b) { return a + b; }").unwrap(); + if find_compiler(Language::C).is_none() { + return; + } + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("add.c"); + let header = dir.path().join("add.h"); + std::fs::write(&header, "int add(int a, int b);\n").unwrap(); + std::fs::write(&path, "int add(int a, int b) { return a + b; }\n").unwrap(); + let out = dir.path().join("out"); + + let result = load_with_options(&path, LoadOptions::default().output_dir(&out).link(false)) + .expect("load C source"); + assert!(result.output_path.exists()); + assert_eq!(result.output_path.extension().unwrap(), "o"); + let code = result.bindings_code().expect("bindings"); + assert!(code.contains("pub fn add(")); + } - let result = load(path); - // May fail if no C compiler available, but shouldn't panic - println!("{:?}", result); + #[test] + fn test_load_honors_compile_false() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("add.c"); + let header = dir.path().join("add.h"); + std::fs::write(&header, "int add(int a, int b);\n").unwrap(); + std::fs::write(&path, "int add(int a, int b) { return a + b; }\n").unwrap(); + let out = dir.path().join("out"); + + let result = load_with_options( + &path, + LoadOptions::default() + .output_dir(&out) + .compile(false) + .link(false), + ) + .expect("load without compile"); + assert_eq!( + result.output_path.canonicalize().unwrap(), + path.canonicalize().unwrap() + ); + assert!(result.bindings_code().unwrap().contains("pub fn add(")); + assert!(!out.join("add.o").exists()); } #[test]