Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FFI Conventions

Perry’s FFI (Foreign Function Interface) bridges TypeScript code and Rust native libraries. This is how the editor, terminal, and other packages call into platform-specific Rust code for rendering, input handling, and OS integration.

How It Works

TypeScript code calls functions declared as external. Perry compiles these calls into native function invocations against symbols in a linked Rust static library (.a file). The Rust side exports C-ABI functions that Perry can call directly.

NaN-Boxed String Parameters

Perry represents all values as 64-bit NaN-boxed floats internally. Strings are no exception — a string parameter is a NaN-boxed pointer to a StringHeader struct in memory.

On the Rust side, string parameters arrive as raw pointers:

#![allow(unused)]
fn main() {
use perry_runtime::str_from_header;

#[no_mangle]
pub extern "C" fn __wrapper_setTitle(title_ptr: i64) {
    let title = unsafe { str_from_header(title_ptr as *const u8) };
    // title is now a &str
}
}

The str_from_header() utility (from perry-runtime) extracts the string data from the NaN-boxed pointer.

Function Naming Convention

Perry generates wrapper symbols with a double-underscore prefix. A TypeScript declaration like:

declare function renderLine(text: string, lineNum: number, x: number, y: number): void;

becomes a call to the native symbol __wrapper_renderLine. The Rust implementation must use this exact name:

#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn __wrapper_renderLine(
    text: i64,    // NaN-boxed string pointer
    line_num: f64,
    x: f64,
    y: f64,
) {
    let text_str = unsafe { str_from_header(text as *const u8) };
    // render using platform APIs
}
}

package.json FFI Declarations

Every FFI function must be registered in the package’s package.json under perry.nativeLibrary.functions. This tells the Perry compiler which external symbols to expect during linking.

{
  "name": "@honeide/editor",
  "perry": {
    "nativeLibrary": {
      "name": "hone_editor_macos",
      "functions": [
        {
          "name": "renderLine",
          "params": ["i64", "i64", "f64", "f64"],
          "returns": "void"
        },
        {
          "name": "setViewportSize",
          "params": ["f64", "f64"],
          "returns": "void"
        },
        {
          "name": "getClipboardText",
          "params": [],
          "returns": "i64"
        },
        {
          "name": "setClipboardText",
          "params": ["i64"],
          "returns": "void"
        }
      ]
    }
  }
}

The name field is the Rust crate name (and the resulting .a library file). The functions array lists every function the TypeScript side calls.

Parameter Type Rules

FFI typeUse forRust type
f64Numbers (integers, floats, booleans)f64
i64Strings, pointersi64 (cast to *const u8 for strings)
voidNo return value()

Do not use i32. It causes verifier errors in Perry’s code generator. Always use f64 for numeric values and i64 for string/pointer values, even if the logical type is a 32-bit integer.

#![allow(unused)]
fn main() {
// WRONG — i32 causes verifier errors
pub extern "C" fn __wrapper_setLine(line: i32) { ... }

// CORRECT
pub extern "C" fn __wrapper_setLine(line: f64) {
    let line_num = line as i32; // cast inside the function
}
}

Return Values

Functions can return f64 (numbers), i64 (strings/pointers), or void. To return a string from Rust to TypeScript, allocate a StringHeader and return its pointer as i64:

#![allow(unused)]
fn main() {
use perry_runtime::alloc_string;

#[no_mangle]
pub extern "C" fn __wrapper_getClipboardText() -> i64 {
    let text = get_clipboard_contents();
    alloc_string(&text) as i64
}
}

Per-Platform Crates

Each platform has its own FFI crate that implements the same set of declared functions using native APIs:

PlatformCrateLocation
macOShone_editor_macoshone-editor/native/macos/
iOShone_editor_ioshone-editor/native/ios/
Windowshone_editor_windowshone-editor/native/windows/
Linuxhone_editor_linuxhone-editor/native/linux/
Androidhone_editor_androidhone-editor/native/android/

All crates export the same __wrapper_* symbols, so the TypeScript code is platform-agnostic. Perry links the correct crate based on the --target flag.