integrations/rust/haskell.rs

170 lines
5.0 KiB
Rust

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<String>,
}
pub fn run_haskell_demo(args: &DemoArgs) -> Result<String, String> {
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<Self, String> {
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<String, String> {
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<PathBuf, String> {
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<PathBuf>) -> 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")
}