perry.nativeLibrary manifest — spec v1
New here? Start with Native Bindings — Overview for the architectural picture and the Authoring Guide for a step-by-step that uses this manifest. This page is reference-grade detail.
This page is the authoritative spec for the perry.nativeLibrary
field a native-bindings package declares in its package.json. The
Perry compiler reads this manifest at resolve time and uses it to:
- Decide whether the import is “native” (calls into a Rust
staticlib) vs. plain TypeScript / JavaScript. - Map TypeScript-side function calls onto the right
extern "C"symbol with the right calling convention. - Pull the right
.aarchive into the link line, with the right frameworks / system libs / pkg-config dependencies for the user’s compile target.
A companion JSON schema lives at
docs/api/manifest.schema.json for
editor validation.
Versioning
The schema is versioned via the abiVersion field. Every wrapper
declares which perry-ffi ABI it was built against:
{
"perry": {
"nativeLibrary": {
"abiVersion": "0.5",
"...": "..."
}
}
}
The perry binary refuses to load a wrapper whose declared
abiVersion doesn’t satisfy the bundled perry-ffi’s semver range.
Transitional rule for the v0.5.x cycle: missing abiVersion
is allowed but emits a warning naming the package and pointing at
this spec. From v0.6.0 onwards it becomes a hard error.
See docs/src/native-libraries/abi.md for what the v0.5
ABI surface actually contains.
Top-level shape
{
"perry": {
"nativeLibrary": {
// Required from v0.6.0; warning-only in v0.5.x.
"abiVersion": "0.5",
// FFI function declarations — what TypeScript-side
// call sites bind to. See "Functions" below.
"functions": [
{ "name": "js_my_thing", "params": ["string"], "returns": "string" }
],
// Per-target build configuration. Optional; if omitted, no
// crate is built and the wrapper is purely a `.d.ts`-style
// declaration of pre-built symbols (rare).
"targets": {
"macos": { "...": "..." },
"ios": { "...": "..." },
"linux": { "...": "..." },
"windows": { "...": "..." },
"android": { "...": "..." },
"web": { "...": "..." },
"harmonyos": { "...": "..." },
"tvos": { "...": "..." },
"watchos": { "...": "..." },
"visionos": { "...": "..." }
}
}
}
}
abiVersion
Semver string (e.g. "0.5", "0.5.3", "^0.5").
The compiler interprets this as a range. The range must include the
bundled perry-ffi’s exact version. A wrapper declaring "0.5"
loads under any 0.5.x Perry; one declaring "0.5.3" loads only
when the runtime is exactly 0.5.3.
When the runtime fails the range check, compilation aborts with:
error: native library `<package>` declares perry-ffi ABI "0.5"
but this Perry build ships perry-ffi 0.6.1.
Update the package or use an older Perry release.
functions
Array of function declarations. Each entry binds a TypeScript-visible
name to an extern "C" symbol exported by the wrapper’s staticlib.
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Symbol name (Perry prepends an underscore on macOS). |
params | array of string | yes | Parameter ABI types — see “Param types” below. |
returns | string | yes | Return ABI type — see “Return types” below. |
Param types
| Manifest value | Maps to Rust signature | When TS callsite passes … |
|---|---|---|
"string" | *const StringHeader | a JS string |
"number" | f64 | any JS number |
"i32" | i32 | a number truncated to int |
"i64" | i64 | a BigInt or large number |
"bool" | bool | a boolean |
"ptr" | *const c_void | an opaque handle (advanced) |
Return types
| Manifest value | Rust signature | TypeScript view |
|---|---|---|
"string" | -> *const u8 (see note) | a string |
"ptr" | -> *const u8 (see note) | a string |
"i64_str" | -> i64 | a string (the i64 is a *StringHeader) |
"i64" | -> i64 | a number |
"number" | -> f64 | a number |
"void" | -> () | undefined |
| anything else | -> f64 | a number |
Note on
"string"vs."i64_str": both produce a string on the TypeScript side, but they differ in how Rust returns the pointer. Use"string"/"ptr"when yourextern "C" fnis declared-> *const u8(or*const StringHeader); use"i64_str"when it’s-> i64and the value happens to be aStringHeaderaddress (closes #222).
targets.<target>
Per-target build configuration. The <target> key is one of:
macos, ios, linux, windows, android, web, harmonyos,
tvos, watchos, visionos. Simulator variants use the same key
as their device counterpart (ios covers both ios-simulator and
ios).
| Field | Type | Required | Notes |
|---|---|---|---|
crate | path string | yes* | Path (relative to package.json) to the Cargo crate that produces the staticlib. Required when prebuilt is absent. |
lib | string | yes* | Library name (without the lib prefix or .a extension). Required when prebuilt is absent. |
frameworks | array of string | no | Apple-only — system frameworks to pass to clang -framework (resolved from the SDK’s System/Library/Frameworks). |
optionalFrameworks | array of string | no | Apple-only — vendored third-party frameworks linked only when frameworksEnv resolves to a directory containing them. -framework <name> per entry. Static frameworks only (see below). Snake_case optional_frameworks also accepted. |
frameworksEnv | string | no | Name of an env var that points at the directory holding optionalFrameworks. When set + the path is a directory, -F <dir> is added to the link line; when unset, the optional frameworks are skipped silently. Snake_case frameworks_env also accepted. |
libs | array of string | no | System libraries to pass to the linker (-lcurl, etc.). |
libDirs | array of paths | no | Extra linker search paths. Emitted before libs as -L<dir> (or /LIBPATH:<dir> on Windows MSVC). Relative entries resolve against package.json. |
pkgConfig | array of string | no | pkg-config package names. The compiler runs pkg-config --libs and forwards the output. |
swift_sources | array of paths | no | Swift sources to compile via swiftc and link in. Used by SwiftUI wrappers. |
metal_sources | array of paths | no | Metal shader sources to compile via xcrun metal into <app>.app/default.metallib. |
prebuilt | path string | no | Path (relative to package.json) to a pre-built .a archive. When present, Perry uses this instead of running cargo build. |
When both prebuilt and crate/lib are absent for the user’s
compile target, the wrapper is silently skipped on that target —
useful for platform-specific bindings that only exist on macOS, etc.
Vendored frameworks (optionalFrameworks + frameworksEnv)
Some Apple SDKs can’t be redistributed through npm (licensing) or
are too large to vendor — GoogleSignIn is the canonical example. For
these, the wrapper declares the SDK’s framework name(s) in
optionalFrameworks and the name of an environment variable in
frameworksEnv. The app developer builds/downloads the framework
locally, points the env var at the directory holding it, and Perry’s
linker adds -F <dir> plus -framework <name> for each entry.
"targets": {
"ios": {
"crate": "crate-ios",
"lib": "perry_google_auth",
"optionalFrameworks": ["GoogleSignIn"],
"frameworksEnv": "PERRY_GOOGLE_SIGN_IN_FRAMEWORK_DIR"
}
}
PERRY_GOOGLE_SIGN_IN_FRAMEWORK_DIR=/path/to/Frameworks \
perry compile app.ts --target ios
When the env var is unset (or points at a non-directory), the
optional frameworks are skipped silently. This pairs with a Swift
bridge guarded by #if canImport(GoogleSignIn): the no-SDK fallback
compiles and the binary still links, returning a runtime
“framework not linked” result instead of failing with undefined
symbols. The same build.rs opt-in (-F $DIR to swiftc) must
gate the bridge’s compile so both halves agree.
Contract — static frameworks only. -framework links the
archive directly; Perry does not embed the .framework into
<app>.app/Frameworks/ or add an @executable_path/Frameworks
rpath. A dynamic framework would link but fail to load at runtime.
Vendor a statically-linked .framework (or a .xcframework slice
containing a static Mach-O). Embedding dynamic frameworks +
resource bundles is tracked as future work (#1304).
Resolution
- The user writes
import { foo } from "@perry/iroh". - Perry resolves
@perry/irohagainstnode_modules/. If a matching directory has aperry.nativeLibrarymanifest in itspackage.json, this file’s spec applies and the wrapper is used. - If
node_modules/<name>/exists without a manifest, the import falls through to V8 (existing behavior — TypeScript / JavaScript package). - If no
node_modulesentry matches, Perry consults its built-in well-known bindings table (see #466 Phase 4) — the same spec applies to the bundled wrapper. - None of the above match → resolution error.
A wrapper installed in node_modules always beats the well-known
table — that’s how users override a bundled binding with a fork or
a beta version.
Reference example
Minimal — three FFI functions, two targets. Matches the
perry-ext-dotenv shape:
{
"name": "@perry/dotenv",
"version": "0.5.0",
"perry": {
"nativeLibrary": {
"abiVersion": "0.5",
"functions": [
{ "name": "js_dotenv_config", "params": [], "returns": "number" },
{ "name": "js_dotenv_config_path", "params": ["string"], "returns": "number" },
{ "name": "js_dotenv_parse", "params": ["string"], "returns": "string" }
],
"targets": {
"macos": { "crate": "native/macos", "lib": "perry_ext_dotenv" },
"linux": { "crate": "native/linux", "lib": "perry_ext_dotenv" }
}
}
}
}
A larger reference is Bloom Engine’s manifest (~230 functions,
6 targets, frameworks + metal_sources) in the bloom repo.
Compatibility & migration
The manifest schema is itself versioned by abiVersion. The major
version of perry-ffi is the major version of this manifest spec —
they move in lockstep:
- 0.5.x — current;
abiVersionis recommended but optional. - 0.6.0 —
abiVersionbecomes required; missing field is a hard resolution error. - 1.0.0 — first stable release; backwards-compat guarantees begin.
Anything not documented on this page (custom keys, undocumented
returns values) is unsupported and may break between releases.
File a request under #466 and we’ll consider adding it to v1.