#![allow(unused_imports)]
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use std::sync::{Mutex, OnceLock};
use znative::{BuiltinFn, HostApi, InitFn, PluginInfo, ABI_VERSION, INIT_SYMBOL};
struct LoadedPlugin {
name: String,
version: String,
path: String,
_lib: libloading::Library,
}
#[derive(Clone, Copy)]
struct BuiltinEntry {
func: BuiltinFn,
_pad: (),
}
fn plugins() -> &'static Mutex<Vec<LoadedPlugin>> {
static P: OnceLock<Mutex<Vec<LoadedPlugin>>> = OnceLock::new();
P.get_or_init(|| Mutex::new(Vec::new()))
}
fn registry() -> &'static Mutex<HashMap<String, BuiltinEntry>> {
static R: OnceLock<Mutex<HashMap<String, BuiltinEntry>>> = OnceLock::new();
R.get_or_init(|| Mutex::new(HashMap::new()))
}
fn staging() -> &'static Mutex<Vec<(String, BuiltinFn)>> {
static S: OnceLock<Mutex<Vec<(String, BuiltinFn)>>> = OnceLock::new();
S.get_or_init(|| Mutex::new(Vec::new()))
}
fn load_lock() -> &'static Mutex<()> {
static L: OnceLock<Mutex<()>> = OnceLock::new();
L.get_or_init(|| Mutex::new(()))
}
fn ownership() -> &'static Mutex<HashMap<String, String>> {
static O: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
O.get_or_init(|| Mutex::new(HashMap::new()))
}
fn pending_completions() -> &'static Mutex<Vec<(String, String, String)>> {
static PC: OnceLock<Mutex<Vec<(String, String, String)>>> = OnceLock::new();
PC.get_or_init(|| Mutex::new(Vec::new()))
}
fn installed_completions() -> &'static Mutex<HashMap<String, String>> {
static IC: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
IC.get_or_init(|| Mutex::new(HashMap::new()))
}
extern "C" fn host_register_builtin(
_host: *const HostApi,
name: *const c_char,
handler: BuiltinFn,
) -> c_int {
if name.is_null() {
return 1;
}
let name = unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned();
staging().lock().unwrap().push((name, handler));
0
}
extern "C" fn host_print(_host: *const HostApi, text: *const c_char) {
if text.is_null() {
return;
}
let s = unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned();
use std::io::Write as _;
let mut out = std::io::stdout();
let _ = out.write_all(s.as_bytes());
let _ = out.flush();
}
extern "C" fn host_eval(_host: *const HostApi, code: *const c_char) -> c_int {
if code.is_null() {
return 1;
}
let code = unsafe { CStr::from_ptr(code) }.to_string_lossy().into_owned();
crate::fusevm_bridge::try_with_executor(|exec| exec.execute_script(&code))
.map(|r| r.unwrap_or(1))
.unwrap_or(1)
}
extern "C" fn host_getvar(_host: *const HostApi, name: *const c_char) -> *mut c_char {
if name.is_null() {
return std::ptr::null_mut();
}
let name = unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned();
match crate::ported::params::getsparam(&name) {
Some(v) => match CString::new(v) {
Ok(c) => c.into_raw(),
Err(_) => std::ptr::null_mut(),
},
None => std::ptr::null_mut(),
}
}
extern "C" fn host_setvar(
_host: *const HostApi,
name: *const c_char,
value: *const c_char,
) -> c_int {
if name.is_null() || value.is_null() {
return 1;
}
let name = unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned();
let value = unsafe { CStr::from_ptr(value) }.to_string_lossy().into_owned();
crate::ported::params::setsparam(&name, &value);
0
}
extern "C" fn host_free_cstring(_host: *const HostApi, s: *mut c_char) {
if !s.is_null() {
unsafe { drop(CString::from_raw(s)) };
}
}
extern "C" fn host_register_completion(
_host: *const HostApi,
cmd: *const c_char,
generator: *const c_char,
) -> c_int {
if cmd.is_null() || generator.is_null() {
return 1;
}
let cmd = unsafe { CStr::from_ptr(cmd) }.to_string_lossy().into_owned();
let generator = unsafe { CStr::from_ptr(generator) }
.to_string_lossy()
.into_owned();
pending_completions()
.lock()
.unwrap()
.push((cmd, generator, String::new()));
0
}
extern "C" fn host_getfunction(_host: *const HostApi, name: *const c_char) -> *mut c_char {
if name.is_null() {
return std::ptr::null_mut();
}
let name = unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned();
match crate::ported::modules::parameter::getpmfunction(std::ptr::null_mut(), &name) {
Some(pm) if (pm.node.flags & crate::ported::zsh_h::PM_UNSET as i32) == 0 => {
match pm.u_str.and_then(|s| CString::new(s).ok()) {
Some(c) => c.into_raw(),
None => std::ptr::null_mut(),
}
}
_ => std::ptr::null_mut(),
}
}
extern "C" fn host_addfunction(
_host: *const HostApi,
name: *const c_char,
body: *const c_char,
) -> c_int {
if name.is_null() || body.is_null() {
return 1;
}
let name = unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned();
let body = unsafe { CStr::from_ptr(body) }.to_string_lossy().into_owned();
if name.is_empty() {
return 1;
}
crate::ported::modules::parameter::setfunction(&name, body, 0);
0
}
fn host_api() -> *const HostApi {
static API: OnceLock<usize> = OnceLock::new();
let addr = API.get_or_init(|| {
let boxed = Box::new(HostApi {
abi_version: ABI_VERSION,
ctx: std::ptr::null_mut(),
register_builtin: host_register_builtin,
print: host_print,
eval: host_eval,
getvar: host_getvar,
setvar: host_setvar,
free_cstring: host_free_cstring,
register_completion: host_register_completion,
getfunction: host_getfunction,
addfunction: host_addfunction,
});
Box::into_raw(boxed) as usize
});
*addr as *const HostApi
}
pub fn load(path: &str) -> Result<String, String> {
let _guard = load_lock().lock().unwrap();
let expanded = expand_tilde(path);
let lib = unsafe { libloading::Library::new(&expanded) }
.map_err(|e| format!("cannot load `{}`: {}", path, e))?;
let init: libloading::Symbol<InitFn> = unsafe {
lib.get(INIT_SYMBOL)
.map_err(|_| format!("`{}`: not a zshrs plugin (no {})", path,
String::from_utf8_lossy(&INIT_SYMBOL[..INIT_SYMBOL.len() - 1])))?
};
staging().lock().unwrap().clear();
let pc_start = pending_completions().lock().unwrap().len();
let info_ptr: *const PluginInfo = init(host_api());
if info_ptr.is_null() {
staging().lock().unwrap().clear();
pending_completions().lock().unwrap().truncate(pc_start);
return Err(format!("`{}`: plugin init failed (ABI mismatch or error)", path));
}
let info = unsafe { &*info_ptr };
if info.abi_version != ABI_VERSION {
staging().lock().unwrap().clear();
pending_completions().lock().unwrap().truncate(pc_start);
return Err(format!(
"`{}`: ABI version {} != host {}",
path, info.abi_version, ABI_VERSION
));
}
let name = cstr_or(info.name, "unknown");
let version = cstr_or(info.version, "?");
if plugins().lock().unwrap().iter().any(|p| p.name == name) {
staging().lock().unwrap().clear();
pending_completions().lock().unwrap().truncate(pc_start);
return Err(format!("plugin `{}` already loaded", name));
}
let staged: Vec<(String, BuiltinFn)> = std::mem::take(&mut *staging().lock().unwrap());
{
let mut reg = registry().lock().unwrap();
let mut own = ownership().lock().unwrap();
for (cmd, func) in staged {
reg.insert(cmd.clone(), BuiltinEntry { func, _pad: () });
own.insert(cmd, name.clone());
}
}
{
let mut pc = pending_completions().lock().unwrap();
for entry in pc.iter_mut().skip(pc_start) {
entry.2 = name.clone();
}
}
plugins().lock().unwrap().push(LoadedPlugin {
name: name.clone(),
version: version.clone(),
path: expanded,
_lib: lib,
});
tracing::info!(plugin = %name, version = %version, path, "loaded native plugin");
Ok(name)
}
pub fn flush_pending_completions() {
let pending: Vec<(String, String, String)> = {
let mut pc = pending_completions().lock().unwrap();
if pc.is_empty() {
return;
}
std::mem::take(&mut *pc)
};
for (cmd, generator, owner) in pending {
let glue = format!(
"_zshrs_plug_{cmd}() {{ \
local -a _zp_m; \
_zp_m=(\"${{(@f)$({gen} $CURRENT $words)}}\"); \
compadd -- $_zp_m; \
}}; \
(( ${{+functions[compdef]}} )) && compdef _zshrs_plug_{cmd} {cmd} 2>/dev/null; :",
cmd = cmd,
gen = generator,
);
let _ = crate::ported::exec::execute_script(&glue);
installed_completions()
.lock()
.unwrap()
.insert(cmd, owner);
}
}
pub fn unload(name: &str) -> Result<(), String> {
let _guard = load_lock().lock().unwrap();
let present = plugins().lock().unwrap().iter().any(|p| p.name == name);
if !present {
return Err(format!("plugin `{}` not loaded", name));
}
{
let mut own = ownership().lock().unwrap();
let mut reg = registry().lock().unwrap();
let owned: Vec<String> = own
.iter()
.filter(|(_, o)| o.as_str() == name)
.map(|(c, _)| c.clone())
.collect();
for cmd in owned {
reg.remove(&cmd);
own.remove(&cmd);
}
}
pending_completions()
.lock()
.unwrap()
.retain(|(_, _, o)| o != name);
installed_completions()
.lock()
.unwrap()
.retain(|_, o| o != name);
let mut ps = plugins().lock().unwrap();
if let Some(pos) = ps.iter().position(|p| p.name == name) {
let p = ps.remove(pos);
tracing::info!(plugin = %name, "unloaded native plugin");
drop(p); }
Ok(())
}
pub fn dispatch(cmd: &str, args: &[String]) -> Option<i32> {
let entry = { registry().lock().unwrap().get(cmd).copied() }?;
let mut owned: Vec<CString> = Vec::with_capacity(args.len() + 1);
owned.push(CString::new(cmd).ok()?);
for a in args {
owned.push(CString::new(a.as_str()).unwrap_or_else(|_| {
CString::new(a.replace('\0', "")).unwrap_or_default()
}));
}
let ptrs: Vec<*const c_char> = owned.iter().map(|c| c.as_ptr()).collect();
let rc = (entry.func)(host_api(), ptrs.len(), ptrs.as_ptr());
Some(rc as i32)
}
pub fn list() -> Vec<(String, String, String)> {
let mut v: Vec<(String, String, String)> = plugins()
.lock()
.unwrap()
.iter()
.map(|p| (p.name.clone(), p.version.clone(), p.path.clone()))
.collect();
v.sort_by(|a, b| a.0.cmp(&b.0));
v
}
pub fn is_plugin_command(name: &str) -> bool {
registry().lock().unwrap().contains_key(name)
}
pub fn zmodload_rust_cmd(
nam: &str,
args: &[String],
ops: &crate::ported::zsh_h::options,
) -> i32 {
use crate::ported::utils::zwarnnam;
use crate::ported::zsh_h::OPT_ISSET;
if OPT_ISSET(ops, b'u') {
if args.is_empty() {
zwarnnam(nam, "what do you want to unload?");
return 1;
}
let mut ret = 0;
for name in args {
if let Err(e) = unload(name) {
zwarnnam(nam, &e);
ret = 1;
}
}
return ret;
}
if args.is_empty() {
for (name, version, path) in list() {
println!("{} {} {}", name, version, path);
}
return 0;
}
let mut ret = 0;
for path in args {
if let Err(e) = load(path) {
zwarnnam(nam, &e);
ret = 1;
}
}
ret
}
fn cstr_or(p: *const c_char, dflt: &str) -> String {
if p.is_null() {
dflt.to_string()
} else {
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
}
fn expand_tilde(path: &str) -> String {
if let Some(rest) = path.strip_prefix("~/") {
if let Some(home) = dirs::home_dir() {
return home.join(rest).to_string_lossy().into_owned();
}
}
path.to_string()
}