Skip to content

RFC: Custom symbol export names for FFI, plugins, and embedding #4921

Description

@nglmercer

Problem

Perry currently generates all LLVM symbol names automatically using the pattern perry_fn_<module_prefix>__<function_name>. There is no mechanism for TypeScript code to control the exported symbol name.

This limitation affects multiple use cases beyond plugins:

Use Case What is Needed
Plugins (perry/plugin) Export plugin_activate, plugin_deactivate, perry_plugin_abi_version with exact names
C/C++ FFI Export functions that C can call by a known name
WASM Human-readable export names instead of __wasm_func_<idx>
Embedding Other languages need to find functions by name via dlsym or equivalent
Dynamic loading Any scenario requiring runtime symbol lookup

Concrete Example: Plugin System Gap

The documentation (docs/src/plugins/creating-plugins.md) states:

Perry automatically:

  • Generates perry_plugin_abi_version() returning the current ABI version
  • Generates plugin_activate(api_handle) calling your activate() function
  • Generates plugin_deactivate() calling your deactivate() function

However, the codegen does not emit these symbols. What actually gets exported:

# From nm -D theme.dylib
perry_module_init
perry_fn_theme_plugin_dylib_ts__activate      # wrong name
perry_fn_theme_plugin_dylib_ts__deactivate    # wrong name
__perry_wrap_perry_fn_theme_plugin_dylib_ts__activate

What the runtime expects (crates/perry-runtime/src/plugin.rs:507-528):

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);
    return 0;  // Plugin fails to load
}

Root Cause

The codegen in crates/perry-codegen/src/codegen/entry.rs only emits perry_module_init for dylib output (line 133). There is no mechanism to:

  1. Detect that the module exports activate/deactivate functions
  2. Generate wrapper functions with specific symbol names
  3. Allow users to control symbol names in general

The scoped_fn_name helper (crates/perry-codegen/src/codegen/helpers.rs:111-112) hardcodes the naming pattern:

pub(super) fn scoped_fn_name(module_prefix: &str, hir_name: &str) -> String {
    format!("perry_fn_{}__{}", module_prefix, sanitize(hir_name))
}

Questions

Before proposing a solution, I would like feedback on:

  1. Is this a recognized gap? The documentation promises auto-generated plugin symbols, but they are not implemented.

  2. What should the general capability be? This is not just a plugin problem -- it is a general FFI/embedding limitation. Should Perry provide a way to control exported symbol names?

  3. What approach fits Perry's design? Some options:

    • TypeScript decorator (e.g. @exportName("symbol_name"))
    • Manifest configuration (e.g. perry.toml export map)
    • Compiler directive or special comment
    • Something else entirely
  4. Should plugin-specific wrappers be auto-generated (as documented) or explicit (user-controlled via the general mechanism)?

Context

I am working on a plugin system for a text editor and encountered this gap. I implemented a working runtime plugin system using QuickJS (rquickjs) as a workaround, but compiled plugins would be preferable for type safety and performance.

Impact

Without this capability:

  • The documented plugin system does not work on any platform
  • Users cannot create FFI boundaries with custom symbol names
  • WASM exports remain index-based (__wasm_func_<idx>)
  • Embedding Perry-compiled code requires knowing the mangled symbol names

With this capability:

  • Plugin system works as documented
  • General FFI/embedding use cases are enabled
  • WASM can have readable export names
  • Users have full control over their binary interface

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions