use std::ffi::{c_char, c_int, c_void, CStr, CString};
use std::path::Path;
use super::host::HostInterp;
const CANDIDATES: &[&str] = &[
"/opt/homebrew/opt/tcl-tk/lib/libtcl9tk9.0.dylib",
"/usr/local/opt/tcl-tk/lib/libtcl9tk9.0.dylib",
];
pub struct Libtk {
handle: *mut c_void,
pub path: String,
}
impl Libtk {
pub fn open() -> Result<Libtk, String> {
let path = match std::env::var("TCLRS_LIBTK") {
Ok(p) => p,
Err(_) => CANDIDATES
.iter()
.find(|p| Path::new(p).exists())
.map(|p| (*p).to_string())
.ok_or_else(|| format!("no Tk dylib at any of {CANDIDATES:?}"))?,
};
let c = CString::new(path.clone()).map_err(|e| e.to_string())?;
let handle = unsafe { libc::dlopen(c.as_ptr(), libc::RTLD_NOW | libc::RTLD_LOCAL) };
if handle.is_null() {
let err = unsafe { libc::dlerror() };
let msg = if err.is_null() {
"unknown error".to_string()
} else {
unsafe { std::ffi::CStr::from_ptr(err) }
.to_string_lossy()
.into_owned()
};
return Err(format!("dlopen({path}): {msg}"));
}
Ok(Libtk { handle, path })
}
pub fn library_root(&self) -> Option<String> {
let addr = self.sym("Tk_Init").ok()?;
let mut info = std::mem::MaybeUninit::<libc::Dl_info>::uninit();
let name = unsafe {
if libc::dladdr(addr, info.as_mut_ptr()) == 0 {
return None;
}
let info = info.assume_init();
if info.dli_fname.is_null() {
return None;
}
CStr::from_ptr(info.dli_fname)
.to_string_lossy()
.into_owned()
};
Path::new(&name)
.parent()
.map(|p| p.to_string_lossy().into_owned())
.filter(|p| !p.is_empty())
}
pub fn sym(&self, name: &str) -> Result<*mut c_void, String> {
let c = CString::new(name).map_err(|e| e.to_string())?;
let p = unsafe { libc::dlsym(self.handle, c.as_ptr()) };
if p.is_null() {
Err(format!("dlsym({name}) in {}: not found", self.path))
} else {
Ok(p)
}
}
}
pub unsafe fn seed_library_path(interp_ptr: *mut c_void, root: &str) -> Vec<String> {
let host = super::interp::host_of(interp_ptr);
if host.is_null() {
return Vec::new();
}
let shared = super::interp::shared_for(host);
let beside = Path::new(root).join("tcl9.0");
let tcl_library = match std::env::var("TCL_LIBRARY") {
Ok(dir) if !dir.is_empty() => Some(dir),
_ => beside
.join("init.tcl")
.exists()
.then(|| beside.to_string_lossy().into_owned()),
};
let mut state = shared.lock().expect("interpreter lock");
let mut candidates: Vec<String> = Vec::new();
if let Some(dir) = tcl_library {
let held = state
.globals
.get("tcl_library")
.map(crate::runtime::to_tcl_string)
.unwrap_or_default();
if held.is_empty() {
state.globals.insert(
"tcl_library".to_string(),
fusevm::Value::Str(std::sync::Arc::new(dir.clone())),
);
}
candidates.push(dir);
}
candidates.push(root.to_string());
let held = state
.globals
.get("auto_path")
.map(crate::runtime::to_tcl_string)
.unwrap_or_default();
let mut entries = crate::list::split(&held).unwrap_or_default();
let mut added: Vec<String> = Vec::new();
for dir in candidates {
if !entries.contains(&dir) {
entries.push(dir.clone());
added.push(dir);
}
}
state.globals.insert(
"auto_path".to_string(),
fusevm::Value::Str(std::sync::Arc::new(crate::list::join(&entries))),
);
added
}
pub type TkInit = unsafe extern "C" fn(*mut c_void) -> c_int;
pub type TkPkgInitStubsCheck =
unsafe extern "C" fn(*mut c_void, *const c_char, c_int) -> *const c_char;
pub unsafe fn call_tk_init(lib: &Libtk, interp: *mut HostInterp) -> Result<c_int, String> {
let f: TkInit = std::mem::transmute(lib.sym("Tk_Init")?);
Ok(f(interp as *mut c_void))
}