Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -982,13 +982,81 @@ pub(super) fn compile_module_entry(
let pending = std::mem::take(&mut ctx.pending_declares);
let buffer_alias_used = ctx.buffer_data_slots.len() as u32;
let native_rep_records = std::mem::take(&mut ctx.native_rep_records);
let has_plugin_activate = hir
.exported_functions
.iter()
.any(|(name, _)| name == "activate");
let has_plugin_deactivate = hir
.exported_functions
.iter()
.any(|(name, _)| name == "deactivate");
drop(ctx);
llmod.ic_counter = ic_end;
llmod.buffer_alias_counter += buffer_alias_used;
llmod.native_rep_records.extend(native_rep_records);
for (name, ret, params) in pending {
llmod.declare_function(&name, ret, &params);
}

// Plugin ABI shim — only emitted when the entry module is being
// built as a dylib (perry compile --output-type dylib). The host's
// `loadPlugin` calls `GetProcAddress(handle, "plugin_activate")`
// (Windows) / `dlsym(handle, "plugin_activate")` (macOS/Linux) to
// find the entry, so every dylib must export that name (and
// `plugin_deactivate` if the user supplied one). The shim unwraps
// the NaN-boxed `api` handle, calls the user's `activate(api)`
// with the raw pointer, and returns 1 on success / 0 if the
// module doesn't export `activate` (host treats that as load
// failure). `perry_plugin_abi_version` is the version the runtime
// checks against the host's expected ABI before calling activate
// — bump when the shim contract changes.
if is_dylib {
use crate::codegen::helpers::scoped_fn_name;
use crate::nanbox::{POINTER_MASK_I64, POINTER_TAG_I64};

{
let abi_fn = llmod.define_function("perry_plugin_abi_version", I64, vec![]);
let _ = abi_fn.create_block("entry");
let blk = abi_fn.block_mut(0).unwrap();
blk.ret(I64, "2");
}

if has_plugin_activate {
let user_activate = scoped_fn_name(module_prefix, "activate");
llmod.declare_function(&user_activate, DOUBLE, &[DOUBLE]);
let fn_def = llmod.define_function(
"plugin_activate",
I64,
vec![(I64, "api_handle".to_string())],
);
let _ = fn_def.create_block("entry");
let blk = fn_def.block_mut(0).unwrap();
let lower48 = blk.and(I64, "api_handle", POINTER_MASK_I64);
let tagged = blk.or(I64, &lower48, POINTER_TAG_I64);
let boxed = blk.bitcast_i64_to_double(&tagged);
let _ = blk.call(DOUBLE, &user_activate, &[(DOUBLE, &boxed)]);
blk.ret(I64, "1");
} else {
let fn_def = llmod.define_function(
"plugin_activate",
I64,
vec![(I64, "_api_handle".to_string())],
);
let _ = fn_def.create_block("entry");
let blk = fn_def.block_mut(0).unwrap();
blk.ret(I64, "0");
}

if has_plugin_deactivate {
let user_deactivate = scoped_fn_name(module_prefix, "deactivate");
llmod.declare_function(&user_deactivate, DOUBLE, &[]);
let fn_def = llmod.define_function("plugin_deactivate", VOID, vec![]);
let _ = fn_def.create_block("entry");
let blk = fn_def.block_mut(0).unwrap();
blk.call_void(&user_deactivate, &[]);
blk.ret_void();
}
}
for ic_name in &ic_globals {
llmod.add_raw_global(format!(
"@{} = private global [2 x i64] zeroinitializer",
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,14 @@ mimalloc = { version = "0.1", default-features = false }

# Windows raw-mode for perry/tui input (#406). The Unix path uses
# libc tcsetattr; Windows uses SetConsoleMode via windows-sys.
# `Win32_System_LibraryLoader` powers `perry_plugin_load` / `perry_plugin_unload`
# (`LoadLibraryW` / `GetProcAddress` / `FreeLibrary` — the Win32 equivalents of
# `libc::dlopen` / `dlsym` / `dlclose` that the Unix branch uses).
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = [
"Win32_System_Console",
"Win32_Foundation",
"Win32_System_LibraryLoader",
] }

# #855: libc::mach_host_self is deprecated; the mach2 crate is the
Expand Down
180 changes: 142 additions & 38 deletions crates/perry-runtime/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,111 @@ const DEFAULT_PRIORITY: i32 = 10;
struct LibHandle(*mut libc::c_void);
unsafe impl Send for LibHandle {}

// ============================================================================
// Platform-specific raw library load/sym/close — implemented per OS.
//
// The plugin ABI is the same on every platform (perry_plugin_abi_version,
// plugin_activate, plugin_deactivate + the per-handler FFI surface in
// `register_hook` / `register_tool` / etc. above), so the only divergence
// between Unix and Windows is the dynamic-loader API:
// - Unix: `libc::dlopen` / `libc::dlsym` / `libc::dlclose` (POSIX)
// - Windows: `LoadLibraryW` / `GetProcAddress` / `FreeLibrary` (Win32)
//
// The helpers below collapse those to a small uniform shape so the
// `perry_plugin_load` / `perry_plugin_unload` bodies don't have to fork.
// ============================================================================

#[cfg(unix)]
type RawLibHandle = *mut libc::c_void;

#[cfg(windows)]
type RawLibHandle = windows_sys::Win32::Foundation::HMODULE;

#[cfg(unix)]
unsafe fn open_library(path: &str) -> Option<RawLibHandle> {
let c_path = CString::new(path).ok()?;
let h = libc::dlopen(c_path.as_ptr(), libc::RTLD_NOW | libc::RTLD_LOCAL);
if h.is_null() {
None
} else {
Some(h)
}
}

#[cfg(windows)]
unsafe fn open_library(path: &str) -> Option<RawLibHandle> {
use windows_sys::Win32::System::LibraryLoader::LoadLibraryW;
// LoadLibraryW takes a NUL-terminated UTF-16 path. Encode once, free on return.
let wide: Vec<u16> = path.encode_utf16().chain(std::iter::once(0)).collect();
let h = LoadLibraryW(wide.as_ptr());
if h.is_null() {
None
} else {
Some(h)
}
}

#[cfg(unix)]
unsafe fn lookup_symbol(handle: RawLibHandle, name: &str) -> Option<*mut libc::c_void> {
let c_name = CString::new(name).ok()?;
let p = libc::dlsym(handle, c_name.as_ptr());
if p.is_null() {
None
} else {
Some(p)
}
}

#[cfg(windows)]
unsafe fn lookup_symbol(handle: RawLibHandle, name: &str) -> Option<*mut libc::c_void> {
use windows_sys::Win32::System::LibraryLoader::GetProcAddress;
// GetProcAddress's `lpProcName` is a NUL-terminated *ANSI* string. The
// plugin ABI surface (perry_plugin_*, plugin_activate, plugin_deactivate)
// is all ASCII, so a CString is the right shape. GetProcAddress returns
// `Option<unsafe extern "system" fn() -> isize>` — None means "symbol
// not found", which is what we want to forward up the call chain.
let c_name = CString::new(name).ok()?;
let p = GetProcAddress(handle, c_name.as_ptr() as *const u8);
match p {
Some(f) => Some(f as *mut libc::c_void),
None => None,
}
}

#[cfg(unix)]
unsafe fn close_library(_handle: RawLibHandle) {
libc::dlclose(_handle);
}

#[cfg(windows)]
unsafe fn close_library(handle: RawLibHandle) {
use windows_sys::Win32::Foundation::FreeLibrary;
let _ = FreeLibrary(handle);
}

#[cfg(unix)]
fn last_load_error(path: &str) -> String {
unsafe {
let err = libc::dlerror();
if err.is_null() {
format!("dlopen failed for {}", path)
} else {
format!(
"dlopen failed for {}: {}",
path,
CStr::from_ptr(err).to_string_lossy()
)
}
}
}

#[cfg(windows)]
fn last_load_error(path: &str) -> String {
use windows_sys::Win32::Foundation::GetLastError;
let err = unsafe { GetLastError() };
format!("LoadLibraryW failed for {}: Win32 error {}", path, err)
}

struct PluginMetadata {
name: String,
version: String,
Expand All @@ -51,7 +156,10 @@ struct PluginMetadata {
struct PluginEntry {
id: u64,
path_name: String,
#[cfg(unix)]
// `*mut c_void` because the concrete handle type differs per OS
// (`*mut c_void` on Unix = `libc::dlopen` return; `HMODULE` on Windows
// = `*mut c_void` per windows-sys). Stored as a raw void pointer so the
// field's shape is platform-agnostic.
lib_handle: LibHandle,
activate_called: bool,
metadata: Option<PluginMetadata>,
Expand Down Expand Up @@ -587,56 +695,52 @@ pub extern "C" fn perry_plugin_off(api_handle: i64, event: f64, handler: f64) ->
// Host-side functions — called by the host application
// ============================================================================

/// Load a plugin from a shared library path
/// Returns the plugin ID (> 0) on success, 0 on failure
#[cfg(unix)]
/// Load a plugin from a shared library path.
/// Returns the plugin ID (> 0) on success, 0 on failure.
///
/// Cross-platform: Unix uses `libc::dlopen` (POSIX), Windows uses
/// `LoadLibraryW` (Win32). Both paths converge through the
/// `open_library` / `lookup_symbol` / `close_library` / `last_load_error`
/// helpers above, so the activate/deactivate orchestration is shared.
#[no_mangle]
pub extern "C" fn perry_plugin_load(path_val: f64) -> i64 {
let path_str = unsafe { extract_string(path_val) };

let c_path = match CString::new(path_str.clone()) {
Ok(p) => p,
Err(_) => {
eprintln!("[plugin] Invalid path: {}", path_str);
let handle = match unsafe { open_library(&path_str) } {
Some(h) => h,
None => {
eprintln!("[plugin] {}", last_load_error(&path_str));
return 0;
}
};

unsafe {
let handle = libc::dlopen(c_path.as_ptr(), libc::RTLD_NOW | libc::RTLD_LOCAL);
if handle.is_null() {
let err = libc::dlerror();
if !err.is_null() {
let err_str = CStr::from_ptr(err).to_string_lossy();
eprintln!("[plugin] dlopen failed for {}: {}", path_str, err_str);
}
return 0;
}

// Check ABI version if available
let abi_sym = CString::new("perry_plugin_abi_version").unwrap();
let abi_fn_ptr = libc::dlsym(handle, abi_sym.as_ptr());
if !abi_fn_ptr.is_null() {
// Check ABI version if the plugin exports it. A plugin that omits
// this symbol still loads — version checks are an opt-in safety net
// for plugin authors, not a hard requirement (matches the Unix
// behavior pre-Windows-support).
if let Some(abi_fn_ptr) = lookup_symbol(handle, "perry_plugin_abi_version") {
let abi_fn: extern "C" fn() -> u64 = std::mem::transmute(abi_fn_ptr);
let version = abi_fn();
if version != PLUGIN_ABI_VERSION {
eprintln!(
"[plugin] ABI version mismatch for {}: plugin={}, host={}",
path_str, version, PLUGIN_ABI_VERSION
);
libc::dlclose(handle);
close_library(handle);
return 0;
}
}

// Look up plugin_activate
let activate_sym = CString::new("plugin_activate").unwrap();
let activate_ptr = libc::dlsym(handle, activate_sym.as_ptr());
if activate_ptr.is_null() {
eprintln!("[plugin] No plugin_activate symbol in {}", path_str);
libc::dlclose(handle);
return 0;
}
// plugin_activate is mandatory — every plugin must export it.
let activate_ptr = match lookup_symbol(handle, "plugin_activate") {
Some(p) => p,
None => {
eprintln!("[plugin] No plugin_activate symbol in {}", path_str);
close_library(handle);
return 0;
}
};

let mut reg = REGISTRY.lock().unwrap();
let plugin_id = reg.alloc_plugin_id();
Expand All @@ -651,7 +755,7 @@ pub extern "C" fn perry_plugin_load(path_val: f64) -> i64 {
reg.plugins.push(PluginEntry {
id: plugin_id,
path_name: name.clone(),
lib_handle: LibHandle(handle),
lib_handle: LibHandle(handle as *mut libc::c_void),
activate_called: false,
metadata: None,
});
Expand Down Expand Up @@ -682,8 +786,7 @@ pub extern "C" fn perry_plugin_load(path_val: f64) -> i64 {
}
}

/// Unload a plugin by its ID
#[cfg(unix)]
/// Unload a plugin by its ID. Cross-platform: see `perry_plugin_load`.
#[no_mangle]
pub extern "C" fn perry_plugin_unload(plugin_id_val: i64) {
let plugin_id = plugin_id_val as u64;
Expand Down Expand Up @@ -711,13 +814,14 @@ pub extern "C" fn perry_plugin_unload(plugin_id_val: i64) {
drop(reg);

unsafe {
let deactivate_sym = CString::new("plugin_deactivate").unwrap();
let deactivate_ptr = libc::dlsym(handle, deactivate_sym.as_ptr());
if !deactivate_ptr.is_null() {
// plugin_deactivate is optional — a plugin that doesn't export it
// still unloads cleanly (the registry teardown already cleared
// its hooks/tools/services/routes/events).
if let Some(deactivate_ptr) = lookup_symbol(handle as RawLibHandle, "plugin_deactivate") {
let deactivate_fn: extern "C" fn() = std::mem::transmute(deactivate_ptr);
deactivate_fn();
}
libc::dlclose(handle);
close_library(handle as RawLibHandle);
}

eprintln!("[plugin] Unloaded: {} (id={})", name, plugin_id);
Expand Down
Loading
Loading