Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
136 changes: 68 additions & 68 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,14 @@ 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;
Expand All @@ -698,6 +706,66 @@ pub(super) fn compile_module_entry(
for raw in &typed_parse_rodata {
llmod.add_raw_global(raw.clone());
}

// 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();
}
}
} else {
// Issue #753: idempotent init guard. Every non-entry module gets
// a one-byte `@__perry_init_done_<prefix>` flag and a thin
Expand Down Expand Up @@ -1017,81 +1085,13 @@ 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
10 changes: 10 additions & 0 deletions crates/perry/src/commands/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5467,6 +5467,16 @@ pub fn run_with_parse_cache(
if is_dylib_windows {
// MSVC link.exe takes the output path as `/OUT:<path>`, not `-o`.
cmd.arg(format!("/OUT:{}", exe_path.display()));
// Pull in the MSVC static C runtime (libcmt) so the CRT
// auto-generated DllMain + `_fltused` etc. resolve. Without
// this, `LoadLibraryW` of the plugin DLL returns
// `ERROR_DLL_INIT_FAILED` (Win32 error 1114) because the
// plugin's auto-emitted `DllMain` references unresolved
// CRT symbols. `/FORCE:UNRESOLVED` lets the link succeed
// with those still-unresolved entries, but the loader
// fails DLL_PROCESS_ATTACH. Linking libcmt resolves
// everything in the plugin itself.
cmd.arg("/defaultlib:libcmt");
} else {
cmd.arg("-o").arg(&exe_path);
}
Expand Down
72 changes: 72 additions & 0 deletions crates/perry/src/commands/compile/link/build_and_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,78 @@ pub(crate) fn build_and_run_link(
for sym in PLUGIN_HOST_SYMBOLS {
let _ = writeln!(def_file, " {}", sym);
}
// Enumerate every `js_*` / `perry_*` symbol from the auto-built
// runtime + stdlib .lib archives. Plugins reference these via
// `GetProcAddress` after `LoadLibraryW`, so the host must
// export them — listing only the static PLUGIN_HOST_SYMBOLS
// subset leaves hundreds of `js_gc_*`, `js_array_*`,
// `js_string_*` etc. unresolved at LoadLibrary time and the
// DLL fails to initialize (Win32 error 1114 =
// ERROR_DLL_INIT_FAILED). The `llvm-nm` enumeration
// guarantees the full surface; it costs ~5000 .def lines and
// a few hundred ms of `nm` time per link.
let mut libs: Vec<std::path::PathBuf> = vec![runtime_lib.to_path_buf()];
if let Some(ref s) = stdlib_lib {
libs.push(s.clone());
}
if let Some(nm_path) = find_llvm_tool("llvm-nm") {
let mut all_syms: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
for lib in &libs {
if !std::path::Path::new(lib).exists() {
continue;
}
if let Ok(out) = std::process::Command::new(&nm_path)
.args(["--extern-only", "--defined-only"])
.arg(lib)
.output()
{
if out.status.success() {
let text = String::from_utf8_lossy(&out.stdout);
for line in text.lines() {
// llvm-nm format: "<addr> <type> <name>" e.g.
// "00000000 T _js_array_alloc"
// "00000000 B PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED"
// "00000000 D __imp_PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED"
// We capture T (text), B (BSS), D (data), R (rodata) and
// skip the __imp_ PE-import stubs (the leading-underscore
// non-prefixed name is the actual export).
let mut parts = line.split_whitespace();
let _addr = parts.next();
let ty = parts.next();
let name_field = parts.next();
if let (Some(ty), Some(rest)) = (ty, name_field) {
if !matches!(ty, "T" | "B" | "D" | "R") {
continue;
}
let bare = rest.strip_prefix('_').unwrap_or(rest);
let export_name = if let Some(stripped) =
bare.strip_prefix("__imp_")
{
stripped
} else {
bare
};
if export_name.starts_with("js_")
|| export_name.starts_with("perry_")
|| export_name.starts_with("PERRY_")
{
all_syms.insert(export_name.to_string());
}
}
}
}
}
}
for s in &all_syms {
let _ = writeln!(def_file, " {}", s);
}
eprintln!(
"[perry] plugin-host .def: {} symbols ({} base + enumerated)",
all_syms.len() + PLUGIN_HOST_SYMBOLS.len(),
PLUGIN_HOST_SYMBOLS.len()
);
}
}
cmd.arg(format!("/DEF:{}", def_path.display()));
} else {
Expand Down
19 changes: 12 additions & 7 deletions crates/perry/src/commands/compile/link/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,8 @@ pub(super) const PLUGIN_HOST_SYMBOLS: &[&str] = &[
"perry_debug_trace_init_done",
"perry_init_guard_check_and_set",
// Plugin manager (perry-plugin)
"perry_plugin_abi_version",
"perry_plugin_load",
"perry_plugin_unload",
"perry_plugin_lookup_symbol",
"perry_plugin_emit_hook",
"perry_plugin_emit_event",
"perry_plugin_invoke_tool",
Expand All @@ -112,14 +110,21 @@ pub(super) const PLUGIN_HOST_SYMBOLS: &[&str] = &[
"perry_plugin_list_plugins",
"perry_plugin_list_hooks",
"perry_plugin_list_tools",
"perry_plugin_plugin_count",
"perry_plugin_count",
"perry_plugin_set_metadata",
"perry_plugin_get_config",
"perry_plugin_subscribe_event",
"perry_plugin_unsubscribe_event",
"perry_plugin_emit_event_bus",
"perry_plugin_on",
"perry_plugin_off",
"perry_plugin_emit",
"perry_plugin_log",
"perry_plugin_set_config",
"perry_plugin_discover",
"perry_plugin_init",
"perry_plugin_last_load_error",
// Process-wide class-field inline guard (perry-runtime/src/object/mod.rs).
// A `#[no_mangle] pub static`; not picked up by `llvm-nm` filter heuristics
// for every codebase, so list it explicitly so the plugin DLL can find it
// at LoadLibrary time.
"PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED",
];

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
Loading