use crate::interop::SharedStats; use libloading::Library; use std::env; use std::ffi::{CStr, CString}; use std::fs; use std::os::raw::{c_char, c_int}; use std::path::{Path, PathBuf}; unsafe extern "C" { fn hs_init(argc: *mut c_int, argv: *mut *mut *mut c_char); fn hs_exit(); } type HsComputeStats = unsafe extern "C" fn(c_int, c_int, *mut SharedStats) -> c_int; type HsMakeMessage = unsafe extern "C" fn(*const c_char, c_int, c_int) -> *mut c_char; type HsFreeString = unsafe extern "C" fn(*mut c_char); #[derive(Clone, Debug, PartialEq, Eq)] pub struct DemoArgs { pub name: String, pub left: i32, pub right: i32, pub library_path: Option, } pub fn run_haskell_demo(args: &DemoArgs) -> Result { let library_path = resolve_library_path(args.library_path.as_deref())?; let runtime = HaskellRuntime::start()?; let output = load_and_run(&library_path, args); drop(runtime); output } struct HaskellRuntime; impl HaskellRuntime { fn start() -> Result { let mut argc: c_int = 1; let program_name = CString::new("integrations-hs-runtime") .map_err(|_| "failed to create runtime program name".to_string())?; let mut argv = vec![program_name.as_ptr() as *mut c_char, std::ptr::null_mut()]; let mut argv_ptr = argv.as_mut_ptr(); unsafe { hs_init(&mut argc, &mut argv_ptr); } Ok(Self) } } impl Drop for HaskellRuntime { fn drop(&mut self) { unsafe { hs_exit(); } } } fn load_and_run(library_path: &Path, args: &DemoArgs) -> Result { let library = unsafe { Library::new(library_path) } .map_err(|error| format!("failed to load {}: {error}", library_path.display()))?; let compute_stats: HsComputeStats = unsafe { *library .get(b"hs_compute_stats\0") .map_err(|error| format!("failed to load hs_compute_stats: {error}"))? }; let make_message: HsMakeMessage = unsafe { *library .get(b"hs_make_message\0") .map_err(|error| format!("failed to load hs_make_message: {error}"))? }; let free_string: HsFreeString = unsafe { *library .get(b"hs_free_string\0") .map_err(|error| format!("failed to load hs_free_string: {error}"))? }; let mut stats = SharedStats::default(); let status = unsafe { compute_stats(args.left, args.right, &mut stats) }; if status != 0 { return Err(format!("hs_compute_stats returned status {status}")); } let name = CString::new(args.name.replace('\0', "?")) .map_err(|_| "failed to prepare demo name".to_string())?; let message_ptr = unsafe { make_message(name.as_ptr(), args.left, args.right) }; if message_ptr.is_null() { return Err("hs_make_message returned a null pointer".to_string()); } let message = unsafe { CStr::from_ptr(message_ptr) } .to_string_lossy() .into_owned(); unsafe { free_string(message_ptr); } Ok(format!( "Rust -> Haskell demo\nLibrary: {}\nInputs: name={}, left={}, right={}\nStats from Haskell: total={}, product={}, gap={}\nMessage from Haskell: {}", library_path.display(), args.name, args.left, args.right, stats.total, stats.product, stats.gap, message, )) } fn resolve_library_path(explicit_path: Option<&str>) -> Result { if let Some(path) = explicit_path { return Ok(PathBuf::from(path)); } if let Ok(path) = env::var("HASKELL_FOREIGN_LIB") { return Ok(PathBuf::from(path)); } let dist_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("haskell") .join("dist-newstyle"); let mut matches = Vec::new(); collect_matching_libraries(&dist_dir, &mut matches)?; matches.sort(); matches.into_iter().next().ok_or_else(|| { "could not find the Haskell foreign library under haskell/dist-newstyle; run `make haskell-build` first or pass an explicit path".to_string() }) } fn collect_matching_libraries(root: &Path, matches: &mut Vec) -> Result<(), String> { if !root.exists() { return Ok(()); } let entries = fs::read_dir(root) .map_err(|error| format!("failed to read {}: {error}", root.display()))?; for entry in entries { let entry = entry.map_err(|error| format!("failed to inspect {}: {error}", root.display()))?; let path = entry.path(); if path.is_dir() { collect_matching_libraries(&path, matches)?; continue; } if is_haskell_foreign_library(&path) { matches.push(path); } } Ok(()) } fn is_haskell_foreign_library(path: &Path) -> bool { let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else { return false; }; let is_library = file_name.ends_with(".so") || file_name.ends_with(".dylib") || file_name.ends_with(".dll"); is_library && file_name.contains("interop_hs") }