use std::ffi::{CStr, CString, c_char, c_void};
use std::path::Path;
use kui_core::{Extension, Slot, Ui, UiEvent, Value};
use crate::convert::kstr;
use crate::{KUI_ABI_VERSION, KuiCtx, KuiEvent, KuiStr, KuiValue};
type ViewFn = extern "C" fn(*mut c_void, *mut KuiCtx);
type EventFn = extern "C" fn(*mut c_void, *const KuiEvent);
pub struct CExtension {
name: String,
handle: *mut c_void,
user: *mut c_void,
view: ViewFn,
on_event: Option<EventFn>,
free: Option<extern "C" fn(*mut c_void)>,
slots: Vec<String>,
}
impl CExtension {
pub unsafe fn open(path: impl AsRef<Path>) -> Result<Self, String> {
let path = path.as_ref();
let cpath = sys::path_arg(path)
.ok_or_else(|| format!("{}: path contains a NUL", path.display()))?;
let handle = unsafe { sys::load(&cpath) };
if handle.is_null() {
return Err(format!("{}: {}", path.display(), sys::last_error()));
}
let mut ext = Self {
name: String::new(),
handle,
user: std::ptr::null_mut(),
view: placeholder_view,
on_event: None,
free: None,
slots: Vec::new(),
};
let Some(abi) = (unsafe { ext.sym::<extern "C" fn() -> u32>("kui_ext_abi") }) else {
return Err(format!(
"{}: plugin declares no ABI; this build is {KUI_ABI_VERSION}",
path.display()
));
};
let claimed = abi();
if claimed != KUI_ABI_VERSION {
return Err(format!(
"{}: plugin is ABI {claimed}, this build is {KUI_ABI_VERSION}",
path.display()
));
}
let Some(view) = (unsafe { ext.sym::<ViewFn>("kui_ext_view") }) else {
return Err(format!("{}: no kui_ext_view", path.display()));
};
ext.view = view;
ext.on_event = unsafe { ext.sym::<EventFn>("kui_ext_on_event") };
ext.free = unsafe { ext.sym::<extern "C" fn(*mut c_void)>("kui_ext_free") };
if let Some(slots) =
unsafe { ext.sym::<extern "C" fn(*mut usize) -> *const KuiStr>("kui_ext_slots") }
{
let mut count = 0usize;
let p = slots(&mut count);
if !p.is_null() {
let names = unsafe { std::slice::from_raw_parts(p, count) };
ext.slots = names.iter().map(|s| kstr(*s).into_owned()).collect();
}
}
ext.name = match unsafe { ext.sym::<extern "C" fn() -> *const c_char>("kui_ext_name") } {
Some(f) => {
let p = f();
if p.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
}
None => String::new(),
};
if ext.name.is_empty() {
ext.name = path.file_stem().map_or_else(
|| path.display().to_string(),
|s| s.to_string_lossy().into_owned(),
);
}
if let Some(init) = unsafe { ext.sym::<extern "C" fn() -> *mut c_void>("kui_ext_init") } {
ext.user = init();
}
Ok(ext)
}
unsafe fn sym<T>(&self, name: &str) -> Option<T> {
debug_assert_eq!(size_of::<T>(), size_of::<*mut c_void>());
let cname = CString::new(name).ok()?;
let p = unsafe { sys::symbol(self.handle, &cname) };
(!p.is_null()).then(|| unsafe { std::mem::transmute_copy::<*mut c_void, T>(&p) })
}
}
extern "C" fn placeholder_view(_user: *mut c_void, _ctx: *mut KuiCtx) {}
#[cfg(unix)]
mod sys {
use super::{CStr, CString, c_char, c_void};
unsafe extern "C" {
fn dlopen(path: *const c_char, flags: i32) -> *mut c_void;
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
fn dlclose(handle: *mut c_void) -> i32;
fn dlerror() -> *const c_char;
}
const RTLD_NOW: i32 = 2;
#[cfg(target_vendor = "apple")]
const RTLD_LOCAL: i32 = 4;
#[cfg(not(target_vendor = "apple"))]
const RTLD_LOCAL: i32 = 0;
pub type PathArg = CString;
pub unsafe fn load(path: &PathArg) -> *mut c_void {
unsafe { dlerror() };
unsafe { dlopen(path.as_ptr(), RTLD_NOW | RTLD_LOCAL) }
}
pub unsafe fn symbol(handle: *mut c_void, name: &CStr) -> *mut c_void {
unsafe { dlsym(handle, name.as_ptr()) }
}
pub unsafe fn unload(handle: *mut c_void) {
unsafe { dlclose(handle) };
}
pub fn last_error() -> String {
let e = unsafe { dlerror() };
if e.is_null() {
"unknown error".into()
} else {
unsafe { CStr::from_ptr(e) }.to_string_lossy().into_owned()
}
}
pub fn path_arg(path: &std::path::Path) -> Option<PathArg> {
CString::new(path.as_os_str().as_encoded_bytes()).ok()
}
}
#[cfg(windows)]
mod sys {
use super::{CStr, c_char, c_void};
#[link(name = "kernel32")]
unsafe extern "system" {
fn LoadLibraryExW(path: *const u16, file: *mut c_void, flags: u32) -> *mut c_void;
fn GetProcAddress(handle: *mut c_void, name: *const c_char) -> *mut c_void;
fn FreeLibrary(handle: *mut c_void) -> i32;
fn GetLastError() -> u32;
}
pub type PathArg = Vec<u16>;
const LOAD_WITH_ALTERED_SEARCH_PATH: u32 = 0x8;
pub unsafe fn load(path: &PathArg) -> *mut c_void {
unsafe {
LoadLibraryExW(
path.as_ptr(),
std::ptr::null_mut(),
LOAD_WITH_ALTERED_SEARCH_PATH,
)
}
}
pub unsafe fn symbol(handle: *mut c_void, name: &CStr) -> *mut c_void {
unsafe { GetProcAddress(handle, name.as_ptr()) }
}
pub unsafe fn unload(handle: *mut c_void) {
unsafe { FreeLibrary(handle) };
}
pub fn last_error() -> String {
format!("LoadLibraryW failed (GetLastError {})", unsafe {
GetLastError()
})
}
pub fn path_arg(path: &std::path::Path) -> Option<PathArg> {
use std::os::windows::ffi::OsStrExt;
let abs = std::fs::canonicalize(path);
let path = abs.as_deref().unwrap_or(path);
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
if wide.contains(&0) {
return None;
}
wide.push(0);
Some(wide)
}
}
impl Extension for CExtension {
fn name(&self) -> &str {
&self.name
}
fn slots(&self) -> &[String] {
&self.slots
}
fn view(&mut self, slot: &Slot<'_>, ui: &mut Ui<'_>) -> Result<(), String> {
let mut ctx = KuiCtx::borrowing_in(ui);
ctx.slot_name = Some(slot.name.to_owned());
ctx.slot_namespace = Some(slot.namespace.to_owned());
ctx.slot_params =
(!matches!(slot.params, Value::Null)).then(|| KuiValue(slot.params.clone()));
(self.view)(self.user, &mut ctx);
Ok(())
}
fn on_event(&mut self, ev: &UiEvent) -> Vec<Value> {
let Some(cb) = self.on_event else {
return Vec::new();
};
let payload = KuiValue(ev.payload.clone());
let mut out = KuiEvent {
origin: ev.origin.0,
key: ev.key.0,
payload: &payload,
window: ev.window.0,
slot: ev.slot.map_or(0, |k| k.0),
..Default::default()
};
crate::slots::collect_replies(&mut out, |ev| cb(self.user, ev))
}
}
impl Drop for CExtension {
fn drop(&mut self) {
if let Some(free) = self.free {
free(self.user);
}
unsafe { sys::unload(self.handle) };
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub(crate) fn a_library_with_no_kui_symbols() -> &'static str {
if cfg!(target_vendor = "apple") {
"/usr/lib/libSystem.B.dylib"
} else if cfg!(windows) {
"kernel32.dll"
} else if cfg!(target_env = "musl") {
"libc.so"
} else {
"libc.so.6"
}
}
#[test]
fn a_plugin_without_kui_ext_abi_is_refused_before_anything_else() {
let path = a_library_with_no_kui_symbols();
let err = match unsafe { CExtension::open(path) } {
Ok(ext) => panic!("{path} loaded as a plugin named {:?}", ext.name()),
Err(err) => err,
};
assert_eq!(
err,
format!("{path}: plugin declares no ABI; this build is {KUI_ABI_VERSION}")
);
}
}