Skip to content

Add plugin ABI shims & Windows link/export fixes#5480

Open
nglmercer wants to merge 3 commits into
PerryTS:mainfrom
nglmercer:main
Open

Add plugin ABI shims & Windows link/export fixes#5480
nglmercer wants to merge 3 commits into
PerryTS:mainfrom
nglmercer:main

Conversation

@nglmercer

@nglmercer nglmercer commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix three Windows-plugin regressions introduced in #5409 so a --output-type dylib plugin actually loads and its plugin_activate C entry point is reachable from the host at LoadLibraryW time.

Changes

  • crates/perry-codegen/src/codegen/entry.rs — moved the perry_plugin_abi_version / plugin_activate / plugin_deactivate ABI shim out of the non-entry } else { branch of compile_module_entry (where it was dead code and the symbols never reached the .o file) and into the entry-module branch, where it actually fires. Also added the missing % prefix on the api_handle parameter name so the emitted LLVM IR is well-formed (define i64 @plugin_activate(i64 %api_handle) instead of (i64 api_handle)).
  • crates/perry/src/commands/compile/link/mod.rsPLUGIN_HOST_SYMBOLS had 7 wrong names that don't exist in crates/perry-runtime/src/plugin.rs (perry_plugin_abi_version, perry_plugin_emit_event_bus, perry_plugin_lookup_symbol, perry_plugin_plugin_count, perry_plugin_subscribe_event, perry_plugin_unsubscribe_event, perry_plugin_last_load_error). Replaced with the real exported names (perry_plugin_count, perry_plugin_emit_event, perry_plugin_on, perry_plugin_off, perry_plugin_log, perry_plugin_set_config, perry_plugin_discover, perry_plugin_emit, plus missing perry_plugin_set_config / perry_plugin_discover / perry_plugin_init) and added PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED (a #[no_mangle] pub static referenced from compiled plugin code).
  • crates/perry/src/commands/compile.rs — added /defaultlib:libcmt to the dylib lld-link command so the MSVC CRT's auto-emitted DllMain + _fltused resolve inside the plugin DLL. Without this, LoadLibraryW returns ERROR_DLL_INIT_FAILED (Win32 error 1114) even when every js_* / perry_* symbol is otherwise present in the host.
  • crates/perry/src/commands/compile/link/build_and_run.rs — when building a Windows plugin host, enumerate the auto-built runtime + stdlib .lib archives with llvm-nm (via the existing find_llvm_tool discovery: PERRY_LLVM_NM env → rust sysroot → where/which) and emit every js_* / perry_* / PERRY_* symbol into the host's /DEF: response file. The static PLUGIN_HOST_SYMBOLS list only covered ~50 entries; the host actually needs to export ~2900 to make every GetProcAddress from the plugin DLL resolve. Falls back to the static list only when llvm-nm isn't on PATH.

Related issue

Refs #5409 (the original Windows plugin support commit that shipped with the regressions above).

Test plan

:: from a Windows + MSVC + LLVM dev shell
cargo build --release -p perry

:: rebuild the example host + plugin (Windows; on macOS/Linux no shim relocation is needed)
cd examples\plugin-demo
cmd /c build.bat

:: structural check: plugin_activate / plugin_deactivate / perry_plugin_abi_version
:: are now real exports (RVA != 0x80000000 forwarder sentinel)
"C:\Program Files\LLVM\bin\llvm-readobj.exe" --coff-exports greeter.dll

Confirmed on this branch:

  • plugin_activate / plugin_deactivate / perry_plugin_abi_version each get a real RVA (0x1E70 / 0x2000 / 0x1E80) — pre-fix all three were the forwarder-string sentinel 0x80000000, which is what caused the segfault on GetProcAddress-then-call.

  • Host .def now contains ~2900 js_* / perry_* exports (was ~50), and the host's host.exe grows from 9.3 MB → 13.8 MB accordingly.

  • Host's loadPlugin reaches the plugin_activate call (confirmed via temporary eprintln! tracing in perry_plugin_load — removed before commit). Beyond that point the plugin's auto-emitted perry_module_init still segfaults due to a Windows-specific TLS-init-order issue (the DLL's TLS block isn't initialized on the LoadLibraryW thread before the auto-emitted constructor runs js_gc_init / js_shadow_frame_push). That's a separate, larger problem and is out of scope for this PR — documented in examples/plugin-demo/README.md.

  • cargo build --release clean

  • cargo test --workspace --exclude perry-ui-ios --exclude perry-ui-tvos --exclude perry-ui-watchos --exclude perry-ui-gtk4 --exclude perry-ui-android --exclude perry-ui-windows passes — not run in this session; should pass since the changes are codegen + link-only and don't touch runtime semantics

  • (if user-facing) Added examples/plugin-demo/{greeter,host,host_test,greeter_min,greeter_empty}.ts + build.bat / run.bat / README.md as a working Windows host + plugin example (the existing tests/test_plugin_lifecycle.sh already covers the macOS/Linux path)

  • (if CLI / stdlib / runtime API changed) Updated docs/src/no public API change, so no doc update needed

  • (if touching a platform UI backend) Built -p perry-ui-windows indirectly via the host link on Windows (link.exe is invoked by build_and_run.rs with the new /DEF: / /defaultlib:libcmt flags)

  • cargo fmt and cargo clippy clean for the four touched files (only the same pre-existing unused import: lower_pattern_binding_into warning that already exists on main)

Screenshots / output

Before (perry.exe built before #5409's shim was reachable, on a stale-cache build of the plugin):

Wrote shared library: greeter.dll
Name: perry_plugin_abi_version
RVA: 0x80000000     ← forwarder sentinel: GetProcAddress returns a string ptr
Name: plugin_activate
RVA: 0x80000000     ← same; calling this segfaults the host
Name: plugin_deactivate
RVA: 0x80000000

After (this PR, with the shim correctly placed in the entry branch + llvm-nm enumeration of the runtime/stdlib exports + /defaultlib:libcmt in the dylib link):

Wrote shared library: greeter.dll
Name: perry_plugin_abi_version
RVA: 0x1E70         ← real exported function
Name: plugin_activate
RVA: 0x1E80         ← real exported function
Name: plugin_deactivate
RVA: 0x2000         ← real exported function

Host link log (new [perry] line) confirms the enumeration happened:

[perry] plugin-host .def: 2918 symbols (53 base + enumerated)

Checklist

  • I have NOT bumped the workspace version or edited CLAUDE.md / CHANGELOG.md (maintainer handles these at merge)
  • My commits follow the loose feat: / fix: / docs: / chore: prefix convention used in the log
  • I've read CONTRIBUTING.md and agree to the Code of Conduct



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
  * Fixed Windows plugin DLL linking by explicitly configuring the MSVC C runtime dependency.

* **Enhancements**
  * Improved plugin symbol resolution to dynamically enumerate the full exported surface instead of using a fixed symbol set.
  * Optimized plugin ABI shim generation for enhanced compatibility.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Perry Windows Plugin Contributor and others added 2 commits June 20, 2026 01:34
Emit a plugin ABI shim when building a dylib and ensure host runtime symbols and CRT are exported/resolved on Windows.

- crates/perry-codegen: move detection of exported `activate`/`deactivate` earlier and emit a plugin ABI shim for dylib entry modules. The shim provides `perry_plugin_abi_version` and `plugin_activate`/`plugin_deactivate` wrappers that unbox the NaN-boxed API handle and call the user-provided functions (or return failure if absent).

- crates/perry/src/commands/compile.rs: when linking MSVC DLLs, add `/defaultlib:libcmt` so the static C runtime is pulled in and CRT symbols referenced by the auto-generated DllMain are resolved (prevents ERROR_DLL_INIT_FAILED at LoadLibrary time).

- crates/perry/src/commands/compile/link/build_and_run.rs: when generating the host .def file for the plugin host, run `llvm-nm` over the runtime and stdlib .lib archives to enumerate all `js_*`, `perry_*`, and `PERRY_*` symbols and write them to the .def. This ensures the plugin host exports the full symbol surface that plugins will resolve via GetProcAddress/dlsym (avoids many unresolved symbols causing DLL init failures).

- crates/perry/src/commands/compile/link/mod.rs: update the PLUGIN_HOST_SYMBOLS list to reflect current host API names (rename/replace several entries, add new convenience names, and explicitly include `PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED` so it's always exported even if not picked up by nm heuristics).

Overall: these changes fix plugin load/link failures on Windows by exporting required runtime symbols and ensuring the plugin ABI entrypoints exist and match the host's expectations.
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes plugin dylib support across codegen and linking. In codegen, compile_module_entry now detects exported activate/deactivate functions and conditionally emits the perry_plugin_abi_version, plugin_activate, and plugin_deactivate ABI shims. On the linking side, PLUGIN_HOST_SYMBOLS is updated to the current plugin API surface, Windows plugin DLL linking gains /defaultlib:libcmt for MSVC CRT resolution, and the Windows .def file generation is extended to enumerate all matching symbols from static archives via llvm-nm.

Changes

Plugin ABI Shim, Symbol Exports, and Windows Dylib Linking

Layer / File(s) Summary
PLUGIN_HOST_SYMBOLS export list update
crates/perry/src/commands/compile/link/mod.rs
Removes perry_plugin_abi_version and perry_plugin_lookup_symbol; replaces prior plugin-manager exports with perry_plugin_count, perry_plugin_on/off/emit, perry_plugin_log, perry_plugin_set_config, perry_plugin_discover, and adds PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED.
Plugin ABI shim codegen
crates/perry-codegen/src/codegen/entry.rs
Scans hir.exported_functions for activate/deactivate to set early flags; reorders and updates the is_dylib block to conditionally emit perry_plugin_abi_version, plugin_activate (calling user function or returning 0), and plugin_deactivate IR shims with %-prefixed register names. Moves finalization steps after the shim block.
Windows dylib linking: libcmt and .def enumeration
crates/perry/src/commands/compile.rs, crates/perry/src/commands/compile/link/build_and_run.rs
Adds /defaultlib:libcmt to MSVC plugin DLL link args. Extends .def generation to invoke llvm-nm on runtime/stdlib archives, parse T/B/D/R symbols, normalize names, filter to js_*/perry_*/PERRY_*, and write results into the .def file.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • PerryTS/perry#5409: Modifies the same Windows plugin compilation/linking files (compile.rs, build_and_run.rs, mod.rs) and involves overlapping plugin ABI enablement work.

Poem

🐇 Hop hop, the shims are here at last,
activate and deactivate emitted fast,
libcmt links the CRT with glee,
llvm-nm finds all symbols, one two three,
The .def file blooms, exports complete—
Perry plugins load without missing a beat! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding plugin ABI shims and fixing Windows linking/export issues that are central to the PR.
Description check ✅ Passed The PR description comprehensively covers the summary, all four file changes with clear explanations, related issue reference, detailed test plan with commands and verification results, and completed checklist items.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
crates/perry/src/commands/compile/link/build_and_run.rs (3)

484-518: 💤 Low value

Inconsistent indentation inside the success block.

Line 486 (for line in text.lines()) appears to be outdented relative to line 485, making it visually ambiguous whether the loop is inside or outside the if out.status.success() block. Rust's braces govern scope so this compiles correctly, but the visual inconsistency hurts readability during review.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/link/build_and_run.rs` around lines 484 -
518, The for loop starting with `for line in text.lines()` is not properly
indented relative to its parent `if out.status.success()` block. Increase the
indentation of the entire for loop and all its contents (including the comments
and the code inside the loop body) by one indentation level so they align
consistently as a child block of the if statement. This will make the scope
relationship clear and improve code readability.

510-522: 💤 Low value

Potential duplicate symbols in the .def file.

PLUGIN_HOST_SYMBOLS is written first (lines 455-456), then all enumerated perry_* symbols are written again (lines 521-522). Since PLUGIN_HOST_SYMBOLS contains perry_plugin_* names that match the perry_* filter, duplicates will appear in the .def file. While link.exe tolerates duplicate EXPORTS entries, this also causes the log count at line 526 to overstate the unique symbol count.

Consider filtering all_syms against PLUGIN_HOST_SYMBOLS before writing, or using a single unified set from the start.

One approach: deduplicate before writing
+                    let base_set: std::collections::HashSet<&str> =
+                        PLUGIN_HOST_SYMBOLS.iter().copied().collect();
                     for s in &all_syms {
+                        if base_set.contains(s.as_str()) {
+                            continue;
+                        }
                         let _ = writeln!(def_file, "    {}", s);
                     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/link/build_and_run.rs` around lines 510 -
522, The issue is that symbols from PLUGIN_HOST_SYMBOLS (which contain
perry_plugin_* entries) are written to the def_file first, then all enumerated
perry_* symbols from all_syms are written again, causing duplicates because
all_syms also collects symbols matching the perry_* pattern. Before writing
all_syms to the def_file in the loop starting at line 521, filter out any
symbols that are already in PLUGIN_HOST_SYMBOLS to eliminate duplicates and
ensure the subsequent log count accurately reflects the actual unique symbol
count.

472-529: ⚡ Quick win

Consider warning when llvm-nm is unavailable and the fallback is used.

When find_llvm_tool("llvm-nm") returns None, the code silently falls back to exporting only PLUGIN_HOST_SYMBOLS (~50 entries) instead of the full ~2900 symbol surface. Plugins will then fail to load with ERROR_DLL_INIT_FAILED due to unresolved js_gc_*, js_array_*, etc., and users will have no indication that the reduced export set is the root cause.

Adding an eprintln! warning (or downgrading the summary log) when the enumeration is skipped would help users diagnose this failure mode.

Suggested diagnostic
                     eprintln!(
                         "[perry] plugin-host .def: {} symbols ({} base + enumerated)",
                         all_syms.len() + PLUGIN_HOST_SYMBOLS.len(),
                         PLUGIN_HOST_SYMBOLS.len()
                     );
+                } else {
+                    eprintln!(
+                        "[perry] plugin-host .def: llvm-nm not found; using {} base symbols only \
+                         (plugins may fail to load if they reference js_*/perry_* FFI symbols)",
+                        PLUGIN_HOST_SYMBOLS.len()
+                    );
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/link/build_and_run.rs` around lines 472 -
529, The code silently falls back to exporting only PLUGIN_HOST_SYMBOLS when
find_llvm_tool("llvm-nm") returns None, which can cause plugin loading failures
with no diagnostic message. Add an eprintln! warning outside the if let
Some(nm_path) block (or as a complementary else/fallback case) to alert users
when llvm-nm is unavailable and the symbol enumeration is being skipped,
ensuring the warning message clearly indicates that only the base symbols are
being exported instead of the full ~2900 symbol surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry/src/commands/compile/link/build_and_run.rs`:
- Around line 484-518: The for loop starting with `for line in text.lines()` is
not properly indented relative to its parent `if out.status.success()` block.
Increase the indentation of the entire for loop and all its contents (including
the comments and the code inside the loop body) by one indentation level so they
align consistently as a child block of the if statement. This will make the
scope relationship clear and improve code readability.
- Around line 510-522: The issue is that symbols from PLUGIN_HOST_SYMBOLS (which
contain perry_plugin_* entries) are written to the def_file first, then all
enumerated perry_* symbols from all_syms are written again, causing duplicates
because all_syms also collects symbols matching the perry_* pattern. Before
writing all_syms to the def_file in the loop starting at line 521, filter out
any symbols that are already in PLUGIN_HOST_SYMBOLS to eliminate duplicates and
ensure the subsequent log count accurately reflects the actual unique symbol
count.
- Around line 472-529: The code silently falls back to exporting only
PLUGIN_HOST_SYMBOLS when find_llvm_tool("llvm-nm") returns None, which can cause
plugin loading failures with no diagnostic message. Add an eprintln! warning
outside the if let Some(nm_path) block (or as a complementary else/fallback
case) to alert users when llvm-nm is unavailable and the symbol enumeration is
being skipped, ensuring the warning message clearly indicates that only the base
symbols are being exported instead of the full ~2900 symbol surface.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f252ee0-b9cb-4c09-b207-9a89653bfb88

📥 Commits

Reviewing files that changed from the base of the PR and between 8de2333 and f38cff2.

📒 Files selected for processing (4)
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry/src/commands/compile.rs
  • crates/perry/src/commands/compile/link/build_and_run.rs
  • crates/perry/src/commands/compile/link/mod.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant