What is the native bridge?
Thenative API resolves the app’s React Native native modules and exposes the ones Unbound uses most often. Use it for capabilities that live in native code rather than in JavaScript.
The loader also installs two JSI globals:
NativePluginis the versioned generic native plugin bridge.NativePlatformcontains platform-wide helpers that are not plugin-specific.
Resolving native modules
Native module names change between Discord versions, sogetNativeModule accepts several candidate names in priority order. It checks them against both NativeModules and the TurboModule registry and returns the first match.
Built-in handles
Unbound resolvesBundleInfo, BundleManager, DeviceInfo, and the file manager for you. For file operations, use fs instead of the file manager directly, because fs handles paths and encodings.
Reloading
reload() saves pending settings, then asks the bundle manager to reload.
Platform helpers
NativePlatform exposes device information, app provenance, and Hermes bytecode evaluation. It is optional because it exists only inside a running Unbound loader environment.
NativePlatform.evaluateBytecode takes a Hermes ArrayBuffer and evaluates it in the current runtime. It throws if the argument is not valid Hermes bytecode or if evaluation fails.
Generic plugin bridge
NativePlugin is the public JavaScript interface to the native ABI. The loader publishes it as globalThis.NativePlugin. The client then scopes it to each plugin based on the capabilities in that plugin’s manifest.
string
The version of the JavaScript API that plugins use.
string
The version of the native ABI that the loader implements.
string[]
The native capabilities available in this loader build.
Objective-C operations
Theobjc namespace supports:
- Class lookup, allocation, selector calls, and superclass calls
- Ivar access and associated objects
- Registered structs, arrays, and data conversion
- Generic hooks
- Hook arguments are borrowed until the callback returns. Retain any value that needs to outlive the callback.
original()is synchronous only when the callback runs on the hook thread. Calls from another thread fail closed to avoid deadlocks.- Disposing a plugin context removes its hooks, associations, and bridge resources. Any later call through that context is rejected.
call runs on the current runtime thread. invoke takes an explicit thread policy, such as current or main. A synchronous call across threads that the bridge doesn’t support fails with a structured error.
Results and errors:
- Objective-C
NSDataresults are returned asUint8Arrayvalues. - Native bridge failures are thrown, or rejected for asynchronous calls, as JavaScript errors with a stable
codeproperty on both iOS and Android. A coded native failure keeps its own code; other native runtime failures use the codeNATIVE_BRIDGE_ERROR. - Client-side negotiation errors have their own codes for unavailable bridges, missing capabilities, disposed scopes, and unsupported minimum API versions.
Raw FFI operations
Theffi namespace resolves symbols from images that are already loaded and calls them. Call signatures must be non-variadic and can use scalar, pointer, Objective-C, and registered struct types.
cstring, pointer, and struct types. iOS adds object, class, and selector.
- A signature takes at most 16 arguments, and no argument can be
void. nullorundefinedpassed as acstringbecomes a null pointer.- Struct arguments can be
{ name, value }objects, plain field objects, orobjc.structvalues. Struct results havenameandvalueproperties, for example{ name: 'timespec', value: { tv_sec: 1n, tv_nsec: 0n } }. On iOS, UIKit and Foundation structs come back asobjc.structvalues, so you can pass them straight to Objective-C calls. - The registered structs are
CGPoint,CGSize,CGVector,CGRect,NSRange,UIEdgeInsets,CGAffineTransform,timespec, andtimeval. i64andu64results areBigIntvalues.
- Arbitrary memory access or
dlopen - Variadic calls or plugin-supplied native closures
- Unknown structs, vectors, unions, or bitfields
Fabric surfaces
Thefabric namespace mounts a registered React Native component inside an existing UIKit view. Plugins use it to show Discord’s real native message component instead of a second, JavaScript-only approximation.
mountAsync, updateAsync, setSizeAsync, setFrameAsync, measureAsync, and unmountAsync), so Android plugins declare native.fabric.async without native.fabric.mount.
Sizes and frames must be finite. Widths and heights must be between 0 and 32,768, and x and y between -100,000 and 100,000. Surface properties are limited to 8 levels of nesting, 512 entries, 16 KiB in total, 4 KiB per string, and property names of 1 to 64 bytes.
Surface handles belong to the plugin scope. They are unmounted automatically when the scope is disposed.
Capabilities and lifecycle
In the plugin manifest, declare only the capabilities the plugin needs:- Objective-C:
native.objc.classes,native.objc.invoke,native.objc.ivars,native.objc.associations,native.objc.hooks - FFI:
native.ffi.symbols,native.ffi.call - Fabric:
native.fabric.mount(iOS only),native.fabric.async
native.ffi.symbols or native.ffi.call is enabled, Unbound asks whether it may call native functions directly, and it starts only if the user allows it. It asks again when an update adds another FFI capability. Until then, an FFI plugin that was enabled before does not start. First-party plugins signed by the Unbound publish pipeline never ask.
Before calling start(context), the addon manager validates the capability names and the optional minimum API version. context.native is scoped to the plugin. context.dispose() is called during stop, unload, reload, and cleanup after a failed start.
Existing plugins whose start function takes no context still work. New plugins should use the scoped context so native resources can’t outlive the plugin’s lifecycle.