Bringing Fiber to Mobile Apps: FFI, Android, and iOS Native Integration

Fiber is written in Rust, while mobile apps are usually built with Swift, Kotlin, Java, or C/C++. This post explains how Fiber can be exposed through a stable FFI layer and then integrated into native Android and iOS applications.

The goal is to give developers a practical path from the FFI interface to a working mobile integration. After reading it, you should understand how to start and stop Fiber and call Fiber RPCs.

Post Structure

  1. Fiber FFI usage guide
  2. Native Android integration
  3. Native iOS integration

Some sections are intentionally repeated between the Android and iOS parts. Many readers will only care about one platform, so each platform-specific is written to be readable on its own. If you read both, you may see similar explanations around lifecycle, data directories, and event handling.


Fiber FFI User Guide

Background

This FFI layer originally came from an exploration of mobile support. The community hoped that Fiber could support mobile platforms, so we tried to run the existing Rust Fiber node inside Android/iOS app processes instead of reimplementing the node logic on mobile.

While integrating the existing Fiber implementation into mobile apps, we found that we first needed a clear FFI boundary: the app side should only handle platform integration such as JNI bridges and Swift bridging headers, while the node logic should still be carried by the Rust implementation. fiber-ffi was therefore extracted from the mobile exploration.

After this boundary was extracted, it no longer served only Android/iOS. C++, desktop apps, server processes, scripting language bindings, plugin systems, and other scenarios can also wrap their own runtimes around the same header files and dynamic libraries. The rest of this document therefore focuses on the general FFI calling rules. Mobile platforms appear mainly as the motivation and typical use cases.

Introduction

fiber-ffi is the FFI layer for Fiber nodes. It does not reimplement node logic. Instead, it wraps Rust fiber-lib as a stable C ABI, allowing non-Rust programs to embed a Fiber node in their own process through a dynamic library.

It provides a basic set of node capabilities: starting or stopping a node, reading node information, connecting peers, managing channels, handling invoices and payments, and receiving node events through callbacks.

The call chain is short: the host language wrapper calls functions declared in the C header, those functions enter the dynamic library, and then dispatch to Rust fiber-lib. The public surface mainly exposes FiberHandle, Fiber...Options, status codes, JSON return values, and error messages. Callers must release returned strings according to the FFI rules.

fiber-ffi is still in an exploratory stage. It is suitable for validating the call path, writing upper-level language bindings, and discussing SDK shape. Before using it in production, code review and sufficient testing are recommended.

The overall structure can be understood as the following layers:

+----------------------------------------------------------+
| App project code                                         |
+---------------------------^------------------------------+
                            | App projects integrate here
+---------------------------+------------------------------+
| fiber-ffi project scope                                  |
|                                                          |
| +------------------------------------------------------+ |
| | Android JNI bridge / iOS Swift bridge                | |
| +------------------------------------------------------+ |
| | C header: include/fiber_ffi.h                        | |  <- generated by fiber-ffi
| +------------------------------------------------------+ |
| | fiber-ffi Rust dynamic library                       | |  <- exports a stable C ABI
| +------------------------------------------------------+ |
+---------------------------^------------------------------+
                            |
+---------------------------+------------------------------+
| fiber-lib                                                |  <- Fiber node core logic
+----------------------------------------------------------+

Build

Source Code
Regular local build:

cargo build --release

The repository also provides mobile build entry points:

make build-android
make build-ios
make build-ios-sim

Integrators need to distribute both the dynamic library and the header file:

include/fiber_ffi.h
libfiber_ffi.*

The header file is available at include/fiber_ffi.h.

Dynamic libraries are currently the preferred distribution format. The goal is to keep the integration boundary at the C ABI: as long as the target platform, CPU architecture, and exported ABI match, the host program can load and call fiber-ffi at runtime. This means integrators only need to handle the header file, dynamic library file, and platform loading path. They do not need to bring Rust build artifacts into their own link flow.

Static libraries do have some advantages, but they push more build details onto the integrator: Rust dependencies, system libraries, link order, symbol exports, runtime initialization, whole-archive/dead strip configuration, and the platform-specific linking rules for Android, iOS, and desktop platforms. For a cross-language SDK, these factors can easily make integration more complex than the FFI itself. Therefore, the current documentation and build scripts primarily target dynamic library distribution.

Calling Details

Lifecycle

Recommended lifecycle rules:

  • The current FFI usage model is a single-node lifecycle. A process should maintain only one valid node at a time.
  • Repeated start calls should return already running at the upper layer instead of creating a second node.
  • After stop, immediately clear the handle on the host side. The old handle must no longer be used. Repeated stop calls should return already stopped at the upper layer.
  • Opening two Fiber nodes in the same process at the same time is not supported.
  • stop performs a graceful shutdown. Afterwards, the same configuration and the same data can be used to start again. If you need to switch from testnet to mainnet, or to another custom network configuration, restart the host process.

At the implementation level, note the following details:

After a successful start, the dynamic library creates a group of background tasks that handle on-chain interaction, network connections, channel state, and event dispatch. Calling fiber_stop notifies these background tasks to exit in order, waits for them to finish cleanup, and writes the relevant channel state back to local data. During shutdown, network connections are closed and a NetworkStopped event is emitted through the callback.

This means fiber_stop is not a simple thread kill. When the node is started again after shutdown, channels in the same local data are handled by Fiber’s existing recovery logic. If stop happens while channel/payment negotiation is in progress, do not treat it as canceling a business operation. The recovery result still depends on the state already written to local data and on whether the peer reconnects later.

Why switching network configuration requires restarting the process: fiber_start loads the testnet, mainnet, or custom network configuration and initializes process-level global state. This kind of state is similar to global variables. Once it is set for the first time, it is not cleared by fiber_stop. fiber-ffi also remembers the network used for the first start, and later starts with a different network configuration will fail.

After starting and then stopping, keep these points in mind:

  • fiber_stop consumes and releases the incoming FiberHandle *. The caller must immediately clear the handle stored on the host side.
  • Using an old handle after stop is a use-after-free. Repeated stop should also not call into FFI again. The upper-level wrapper should return already stopped directly.
  • stop must be mutually exclusive with other calls on the same handle. Do not close the same node concurrently while business APIs are running.
  • After stop, you can start again, but do not switch network configuration inside the same process. For example, starting with testnet first and then starting with mainnet or another custom network configuration after stop should be handled by starting a new host process.

The current FFI does not support opening two node instances at the same time. fiber-ffi does not block a second start with a global lock at the fiber_start entry point, so a second start may continue in some configurations. However, process-level global state exists, and listening ports, local data directories, private key directories, network configuration, and contract-related caches may conflict with each other. The upper-level runtime should treat the node as a singleton and must not create a second node.

Start Options

typedef struct FiberStartOptions {
  const char *config_path;
  const char *database_prefix;
  const char *log_level;
  fiber_event_callback event_callback;
  void *event_callback_user_data;
} FiberStartOptions;
Field Description
config_path Required. Path to the Fiber configuration file.
database_prefix Optional. Node data root directory. If omitted, the directory containing config_path is used.
log_level Optional. For example, info or debug. Defaults to info.
event_callback Optional. Fiber event callback.
event_callback_user_data Optional. Passed back to event_callback unchanged.

Note that config_path must be a configuration file path that the current process can read directly. It is not the configuration file contents. During startup, fiber-ffi opens the file at this path and parses YAML. If the configuration comes from Android assets, an iOS app bundle, or another packaged resource, first copy it to a real file path accessible by the app, then pass that path to fiber_start.

config_path and other string parameters must be UTF-8 C strings and must remain valid for the duration of the call. Related information can be checked through Fiber’s API documentation.

It is recommended to pass database_prefix explicitly. Point it to a private and stable data root directory owned by the host app, and isolate it by user, account, or node identity. FFI overrides fiber.base_dir to <database_prefix>/fiber and ckb.base_dir to <database_prefix>/ckb. If it is not passed, the directory containing config_path is used as the data root directory.

Do not let different Fiber node private keys, CKB private keys, or network configurations reuse the same directory. Otherwise, the node may read old fiber/sk, ckb/key, and store data, causing identity confusion, key decryption failure, or startup/recovery failure. If old channel state and on-chain funds do not match the current private key, user assets may be lost in severe cases. When switching the CKB key or Fiber node identity, use a new database_prefix, or explicitly clean up/migrate the corresponding data after shutdown.

Error Handling

The main APIs return FiberFfiStatus:

typedef enum FiberFfiStatus {
  FIBER_FFI_STATUS_OK = 0,
  FIBER_FFI_STATUS_NULL_POINTER = 1,
  FIBER_FFI_STATUS_INVALID_ARGUMENT = 2,
  FIBER_FFI_STATUS_STARTUP_FAILED = 3,
  FIBER_FFI_STATUS_ALREADY_STOPPED = 4,
  FIBER_FFI_STATUS_PANIC = 5,
} FiberFfiStatus;

After a failure, read the error details:

char buffer[4096];
size_t len = fiber_last_error_message(buffer, sizeof(buffer));
if (len > 0) {
  fprintf(stderr, "%s\n", buffer);
}

Notes:

  • Error messages are thread-local.
  • Read them on the same thread immediately after the failed call.
  • Successful calls clear the current thread’s error message.
  • The return value is the full error length, excluding the trailing \0.

String Memory

Follow the common ownership convention: whoever allocates releases. Strings allocated by the caller are released by the caller. Strings allocated by the fiber-ffi dynamic library must be returned to the dynamic library for release. The rules below are this convention applied specifically to the FFI boundary.

Several kinds of char * appear across the FFI boundary. Their ownership differs, so distinguish them by source during integration:

String source Allocator Releaser Lifetime
const char * passed by the caller Caller Caller Only needs to remain valid during this FFI call.
Strings returned by FFI through char **out_* fiber-ffi dynamic library Caller, using fiber_string_free Release after the caller copies or parses it.
Return value of fiber_version() fiber-ffi static storage Do not release Valid for the process lifetime.
event_json in an event callback Temporarily created by fiber-ffi Do not release Valid only during this callback invocation.
Buffer written by fiber_last_error_message Caller Caller The buffer is provided by the caller. FFI does not allocate memory.

Strings passed by the caller must be UTF-8, NUL-terminated C strings. For optional fields, pass NULL to mean unset. For required fields, passing NULL returns FIBER_FFI_STATUS_NULL_POINTER. During the call, FFI copies any information it needs to keep into Rust-owned objects. Therefore, Swift withCString, JNI GetStringUTFChars, and C++ std::string::c_str() can all be used, as long as the corresponding memory is not destroyed or modified before the function returns.

Any parameter shaped as char **out_* means the result string is allocated by the dynamic library. For example:

FiberFfiStatus fiber_node_info(FiberHandle *handle, char **out_json);
FiberFfiStatus fiber_new_invoice(FiberHandle *handle,
                                 const FiberNewInvoiceOptions *options,
                                 char **out_invoice_address);

These return values must first be copied into the host language’s own string object, then released with fiber_string_free:

void fiber_string_free(char *string);

Do not use free, delete, JNI, Swift, or another host language allocator to release strings returned by FFI. This memory is allocated by the Rust dynamic library and can only be released by fiber_string_free exported from the same dynamic library. Do not continue reading the original pointer after fiber_string_free. fiber_string_free(NULL) is safe, but the same non-null pointer may only be released once.

Event Callback

Callback type:

typedef void (*fiber_event_callback)(const char *event_json, void *user_data);

event_json is event JSON, for example:

{
  "kind": "PeerConnected",
  "pubkey": "02...",
  "addr": "/ip4/127.0.0.1/tcp/8228"
}

Callback rules:

  • event_json is valid only while the callback is being invoked.
  • If asynchronous processing is needed, copy the string immediately.
  • The callback may run on a background thread owned by the dynamic library, not necessarily on the host main thread.
  • Do not perform long-running work in the callback.
  • Avoid calling other FFI functions directly from the callback.

Common events include NetworkStarted, NetworkStopped, PeerConnected, ChannelCreated, ChannelReady, ChannelClosed, PreimageCreated, and so on.

Parameter Struct Rules

Most business parameter structs contain:

uint32_t struct_size;
uint32_t flags;

Use the initialization macro before calling:

FiberListChannelsOptions options = FIBER_LIST_CHANNELS_OPTIONS_INIT;

Rules:

  • struct_size must be set.
  • flags must currently be 0.
  • has_* indicates whether the corresponding field is enabled.
  • *_json fields must be valid JSON strings.
  • NULL means unset.

128-bit Integers and Amounts

Unsigned 128-bit integers are represented in the C ABI as:

typedef struct FiberU128 {
  uint64_t low;
  uint64_t high;
} FiberU128;

Full value:

(high << 64) | low

For amounts smaller than uint64_t, only low needs to be set:

FiberU128 amount = {0};
amount.low = 100000000;
amount.high = 0;

Amount fields in the FFI layer always use integer smallest units. Do not pass display amounts with decimal points. CKB amounts use shannons, where 1 CKB = 100000000 shannons. If the business layer displays or accepts decimal CKB values, the upper-level wrapper should convert them to shannons before calling FFI.

UDT amounts use the integer smallest unit of that UDT. No CKB/shannons conversion is applied. In other words, when UDT parameters such as funding_udt_type_script_json and udt_type_script_json are passed, amount / funding_amount should be handled as the UDT’s raw integer amount, and must not apply the CKB amount * 100000000 conversion.

JSON Parameters

Complex objects are still passed as JSON strings. For example:

  • funding_udt_type_script_json
  • shutdown_script_json
  • funding_lock_script_json
  • close_script_json
  • trampoline_hops_json
  • custom_records_json
  • hop_hints_json
  • router_json

Rules:

  • NULL means unset.
  • Non-null values must be valid JSON.
  • JSON contents must match the corresponding Fiber RPC parameter structure.
  • Invalid JSON returns FIBER_FFI_STATUS_INVALID_ARGUMENT.

Language Binding Recommendations

It is recommended not to expose the C ABI directly to the business layer. Wrap it in a host language runtime:

FiberRuntime
  - start(configPath, dataDir, logLevel)
  - stop()
  - nodeInfo()
  - listPeers()
  - connectPeer(...)
  - listChannels(...)
  - openChannel(...)
  - newInvoice(...)
  - sendPayment(...)
  - onEvent(callback)

The wrapper should be responsible for:

  • Loading the dynamic library.
  • Holding and releasing FiberHandle *.
  • Serializing FFI calls.
  • Converting FiberFfiStatus into host language exceptions or Result values.
  • Reading fiber_last_error_message.
  • Copying output strings and calling fiber_string_free.
  • Parsing JSON.
  • Dispatching event callbacks back to the host thread or event loop.

Threads and Safety

Recommended handling:

  • Do not execute potentially blocking FFI calls directly on the UI/main thread.
  • Calls on the same FiberHandle should preferably be serialized.
  • fiber_stop must be mutually exclusive with other calls.
  • In callbacks, only copy the event and post it to the host event loop.
  • Real applications must design their own private key, password, data directory, and log redaction strategies.

The private key file and fixed password scheme in the exploratory demo are only for validating the call path. Do not copy them into production integrations.

ABI Compatibility Recommendations

The current ABI is still exploratory. To reduce upgrade cost:

  • Always use the fiber_ffi.h published with the dynamic library.
  • Use FIBER_*_OPTIONS_INIT when initializing options.
  • Parse returned JSON leniently and ignore unknown fields.
  • Do not rely on the order of fields in event JSON.
  • Do not treat error strings as a stable protocol.
  • Upgrade the header file together with the dynamic library.

Summary

fiber-ffi provides a C ABI that lets non-Rust applications embed a Fiber node through a dynamic library. During integration, focus on five points:

  1. Use fiber_start / fiber_stop to manage FiberHandle.
  2. Release all FFI-returned strings with fiber_string_free.
  3. Read error details with fiber_last_error_message after failures.
  4. Copy events in callbacks, then hand them to the host event loop.
  5. Secure storage, the threading model, and platform lifecycle are the responsibility of the host app.
4 Likes

Fiber: Android Native Integration Guide

This document explains how to integrate fiber-ffi into an Android app. The repository provides a runnable demo:

demos/android

The demo covers starting and stopping a node, reading NodeInfo, receiving native events, connecting peers, creating and sending invoices, and listing, creating, and closing channels.

Quick Start

First build the Android native library from the fiber-ffi repository root:


ANDROID_NDK_HOME=/path/to/android-ndk make build-android

The current Makefile builds the following by default:


target = aarch64-linux-android

Android ABI = arm64-v8a

min Android API = 23

features = sqlite

page size = 16 KB max-page-size

After the build completes, only two files need to be synced:


target/aarch64-linux-android/release/libfiber_ffi.so -> demos/android/app/src/main/jniLibs/arm64-v8a/libfiber_ffi.so

include/fiber_ffi.h -> demos/android/app/src/main/cpp/include/fiber_ffi.h

Then build the demo:


cd demos/android

./gradlew clean assembleDebug

You can also open demos/android directly in Android Studio and run app. When building the demo from the command line, run cd demos/android && ./gradlew clean assembleDebug; on Windows, use gradlew.bat clean assembleDebug.

Integration Steps

Prepare Files

An Android project needs the following files. The header file is available at include/fiber_ffi.h:


app/src/main/jniLibs/arm64-v8a/libfiber_ffi.so

app/src/main/cpp/include/fiber_ffi.h

app/src/main/assets/fiber_config.yml

fiber_config.yml must include both fiber and ckb services. The demo uses a testnet configuration, which can be referenced directly at demos/android/app/src/main/assets/fiber_config.yml.

chain affects the invoice currency. The demo uses a testnet configuration, so when creating an invoice, the currency should be the testnet value Fibt (the FFI enum is FIBER_INVOICE_CURRENCY_FIBT). If the configuration is switched to mainnet or devnet, the currency must also be changed to Fibb or Fibd; otherwise newInvoice will be rejected by Fiber RPC. For the demo, fixing one network and its corresponding currency is enough. A production app should choose dynamically based on the current network.

JNI Bridge

fiber_bridge.cpp is the Android adapter layer: Java/Kotlin calls FiberRuntime, and the bridge calls the C ABI in fiber_ffi.h. The app side usually only uses FiberRuntime; it should not directly handle FiberHandle *, Fiber*Options, or char **out_json.

A bridge is needed because fiber-ffi exports a C ABI, while Android business code is better expressed with ordinary Java types. The bridge converts String, boolean, amounts, and other parameters into the format required by C, holds a single FiberHandle *, and converts returned JSON or error text into Java strings. When adding new capabilities, prefer adding methods to FiberRuntime and reusing the conversion and error handling patterns in fiber_bridge.cpp.

Pay special attention to memory boundaries:

  • char * values returned by FFI are allocated by libfiber_ffi.so; the bridge must copy them into Java String values and then call fiber_string_free.

  • Java string pointers obtained with GetStringUTFChars must be released with ReleaseStringUTFChars before the current JNI call ends.

  • event_json in event callbacks is only valid during the callback. Copy it immediately before entering Java.

  • fiber_stop consumes the handle. After Stop, the old handle must not be passed back to FFI.

CMake must let fiber_bridge find the header file and link fiber_ffi. The demo’s app/src/main/cpp/CMakeLists.txt already handles this.

Load Native Libraries

The Java/Kotlin layer must load fiber_ffi first, then fiber_bridge:


System.loadLibrary("fiber_ffi");

System.loadLibrary("fiber_bridge");

Prepare Data Directories

Before startup, copy the configuration from assets to a normal file path and prepare the data directory:


config_path = filesDir/fiber_config.yml

database_prefix = filesDir/fiber-data

The CKB private key is stored at:


filesDir/fiber-data/ckb/key

The demo writes this file through SetCKBKey: the Java layer normalizes the user-entered 32-byte hex private key and writes it to ckb/key, and the JNI layer sets FIBER_SECRET_KEY_PASSWORD before startup.

Distinguish the “plaintext file at import time” from the “persistent file after Fiber starts.” Fiber’s default logic reads FIBER_SECRET_KEY_PASSWORD when loading the CKB private key. If it finds that ckb/key is still parseable plaintext hex, it encrypts it with this password and overwrites the file in place. Later it decrypts the file with the same password. Therefore, after the first SetCKBKey and before the first successful Start, ckb/key may briefly remain plaintext. After a successful start, the file should have been migrated to encrypted contents.

If writing ckb/key manually, the contents should be a 32-byte hex string without the 0x prefix. The demo automatically removes a 0x prefix from user input. The plaintext migration logic in the Fiber source rejects key files with a 0x prefix.

A production app should use its own wallet or key management process, and must not use the fixed FIBER_SECRET_KEY_PASSWORD from the demo. This password and the encrypted ckb/key are both required to recover the ability to sign funds. If either one is lost, the CKB private key cannot be recovered from the encrypted file.

Start and Stop

Start the node by calling:


FiberRuntime.start(context)

After startup succeeds, you can continue with:


FiberRuntime.nodeInfo()

Stop the node by calling:


FiberRuntime.stop()

Do not start multiple nodes in the same process. For repeated Start calls, you can return already running directly. After Stop, clear the native handle. The old handle must not be used again.

Calling Features

The current demo wraps the following Java methods:

| Method | Purpose |

| — | — |

| start(context) | Start the node |

| stop() | Stop the node |

| nodeInfo() | Read node information |

| listPeers() | List peers |

| connectPeer(...) | Connect a peer |

| listChannels() | List channels |

| createChannel(...) | Create a channel |

| shutdownChannel(...) | Close a channel |

| newInvoice(...) | Create an invoice |

| sendPayment(...) | Pay an invoice |

Pay attention to amount units:

  • The demo’s createChannel(...) accepts a decimal CKB string for display, such as 1.5. The Java/JNI bridge converts it to shannons before filling the FFI FiberU128.

  • The demo’s newInvoice(...) accepts an integer shannons string and does not accept decimal CKB.

  • Amount fields actually passed into FFI always use integer smallest units: CKB uses shannons, where 1 CKB = 100000000 shannons; UDT uses the integer smallest unit of that UDT.

  • If UDT channels or UDT invoices are added later, pass UDT parameters such as funding_udt_type_script_json / udt_type_script_json, and pass amount as the UDT’s raw integer amount. Do not reuse the CKB amount * 100000000 conversion.

More APIs can be extended following the JNI Bridge template above.

Handling Events

Pass the native event callback during startup. The Java layer listens like this:


FiberRuntime.addNativeEventListener(eventJson -> {

// Parse eventJson, then refresh UI or local state.

});

Event contents are JSON strings. Business code should parse the JSON first, then dispatch based on the kind field. Common kind values include:


NetworkStarted

NetworkStopped

PeerConnected

PeerDisConnected

ChannelCreated

ChannelReady

ChannelClosed

PreimageCreated

The list above is only a set of common event examples, not a complete enum. When consuming events, use the kind field in the callback JSON as the source of truth. If an event kind is not currently relevant or is added in the future, it can be logged and ignored.

Do not update UI directly from the native callback thread. Switch to the main thread after receiving events.

Lifecycle

Recommended handling:

  • Do not execute potentially blocking native calls directly on the UI thread.

  • Start, Stop, and calls on the same handle must be serialized.

  • Execute Stop when the user logs out, switches accounts, or explicitly closes the node.

  • When switching network, account, or data directory, call Stop first, then restart the app process.

  • Use different database_prefix values for different users, accounts, and networks.

  • The Android demo includes a minimal FiberNodeService foreground service to demonstrate letting a service help keep the process after the node starts. Background execution strategy, notification presentation, power consumption, and restrictions across system versions must be designed and verified by production app developers.

The current document mentions the foreground service only as a demo reminder. It does not treat it as a complete persistent background node solution.

Build

  • The current demo packages only arm64-v8a, and app/build.gradle.kts also configures only this ABI. To support more ABIs, build the corresponding targets separately and put each .so into the corresponding directory.

  • Android builds currently enable only the sqlite feature. If you want to enable watchtower, change the feature to sqlite,watchtower and re-verify size, linking, and runtime behavior.

  • Some Android 15+ devices use a 16 KB page size. The current Rust .so and JNI bridge both add -Wl,-z,max-page-size=16384; do not remove it.

Demo Operations

  1. SetCKBKey: enter the CKB private key.

  2. Start: start the node.

  3. NodeInfo: read the node address and pubkey.

  4. Peers: view or connect peers.

  5. Invoice: create an invoice or pay an invoice.

  6. Channels: list, create, or close channels.

  7. Stop: stop the node.

Q&A

System.loadLibrary("fiber_ffi") fails

Check whether libfiber_ffi.so is under app/src/main/jniLibs/<ABI>/, whether the device ABI is included in abiFilters, whether the Rust target matches the Android ABI, and whether the .so has been relinked for 16 KB page size.

Loading fiber_bridge fails

Load fiber_ffi first, then fiber_bridge. Also check whether IMPORTED_LOCATION in CMake points to the .so for ${ANDROID_ABI}, and whether fiber_bridge correctly links fiber_ffi and the Android log library.

Startup fails and says the configuration file is unreadable

Do not pass the assets path directly to FFI. First copy fiber_config.yml from assets to filesDir, then pass the real file path.

Startup fails and says the CKB private key is not set

The demo requires running SetCKBKey first. The key is a 32-byte hex string, with or without a 0x prefix. The demo normalizes it and writes it to filesDir/fiber-data/ckb/key. During the first successful Start, Fiber uses FIBER_SECRET_KEY_PASSWORD to migrate this plaintext key into an encrypted file. Later starts must continue using the same password to decrypt it.

2 Likes

Fiber: iOS Native Integration Guide

This document explains how to integrate fiber-ffi into an iOS app. The repository provides a runnable demo:

demos/ios

The demo covers starting and stopping a node, reading NodeInfo, receiving native events, connecting peers, creating and sending invoices, and listing, creating, and closing channels.

iOS integration differs from Android integration. Android needs a JNI bridge, while iOS can use an Objective-C bridging header to let Swift directly access the C ABI exposed by fiber_ffi.h. What iOS really needs is a Swift runtime layer that manages the dynamic library, FiberHandle *, threading, string memory, app sandbox paths, and scene lifecycle.

Quick Start

iOS builds must be performed in a macOS + Xcode environment. First build the iOS native library from the fiber-ffi repository root:

make build-ios build-ios-sim

The current Makefile builds the following by default:

device target        = aarch64-apple-ios
simulator target     = aarch64-apple-ios-sim
deployment target    = iOS 15.0
features             = sqlite
install name         = @rpath/libfiber_ffi.dylib

After the build completes, sync the two dynamic libraries into the demo:

target/aarch64-apple-ios/release/libfiber_ffi.dylib -> demos/ios/FiberDemo/Libs/iphoneos/libfiber_ffi.dylib
target/aarch64-apple-ios-sim/release/libfiber_ffi.dylib -> demos/ios/FiberDemo/Libs/iphonesimulator/libfiber_ffi.dylib

You can also use the demo’s Makefile to build and copy them:

make -C demos/ios build
make -C demos/ios build-sim

Then open the Xcode project:

demos/ios/FiberDemo.xcodeproj

Select the FiberDemo target and run it. Running on a real device requires configuring the development team in the target signing settings, or passing it through the command line:

make -C demos/ios build DEVELOPMENT_TEAM=YOURTEAMID

If you need to specify your own bundle identifier:

make -C demos/ios build DEVELOPMENT_TEAM=YOURTEAMID PRODUCT_BUNDLE_IDENTIFIER=com.yourcompany.fiberdemo

Integration Steps

Prepare Files

An iOS project needs the following files. The header file is available at include/fiber_ffi.h:

include/fiber_ffi.h
App/Libs/iphoneos/libfiber_ffi.dylib
App/Libs/iphonesimulator/libfiber_ffi.dylib
App/Resources/fiber_config.yml

The corresponding paths in the demo are:

fiber_config.yml must include both fiber and ckb services. The demo uses a testnet configuration, which can be referenced directly at demos/ios/FiberDemo/Resources/fiber_config.yml.

chain affects the invoice currency. The demo uses a testnet configuration, so when creating an invoice, the currency should be the testnet value Fibt (the FFI enum is FIBER_INVOICE_CURRENCY_FIBT, and in Swift it is FiberInvoiceCurrency(2)). If the configuration is switched to mainnet or devnet, the currency must also be changed to Fibb or Fibd; otherwise newInvoice will be rejected by Fiber RPC. For the demo, fixing one network and its corresponding currency is enough. A production app should choose dynamically based on the current network.

Xcode Configuration

An iOS project needs four pieces of configuration:

  1. Let Swift find the C header.
  2. Let the linker find the libfiber_ffi.dylib for the current platform.
  3. Copy the dylib into the app bundle.
  4. Code sign the dylib for real-device execution.

The key settings in the demo target are:

HEADER_SEARCH_PATHS      = $(PROJECT_DIR)/../../include
LIBRARY_SEARCH_PATHS     = $(PROJECT_DIR)/FiberDemo/Libs/$(PLATFORM_NAME)
OTHER_LDFLAGS            = -lfiber_ffi
LD_RUNPATH_SEARCH_PATHS  = @executable_path/Frameworks
SWIFT_OBJC_BRIDGING_HEADER = FiberDemo/FiberDemo-Bridging-Header.h

$(PLATFORM_NAME) expands to iphoneos or iphonesimulator during the Xcode build, so the same target can automatically link the dylib from the corresponding directory for the current SDK.

The demo also adds an Embed Fiber FFI dylib build phase. It copies:

FiberDemo/Libs/${PLATFORM_NAME}/libfiber_ffi.dylib

to:

${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libfiber_ffi.dylib

If a signing identity is available for the current build, the script also runs:

codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY}" --timestamp=none libfiber_ffi.dylib

iOS cannot freely load dynamic libraries from arbitrary external paths like desktop programs can. libfiber_ffi.dylib must be bundled with the app at build time and placed where it can be found through @rpath at runtime. The dylib inside a real-device app bundle must also be signed.

The current demo directly embeds a loose dylib to validate the integration path. A production SDK can further package it as an .xcframework, dynamic framework, or Swift Package to reduce manual search path and embed script configuration for integrators.

Swift Bridging Header

Swift imports the C ABI through a bridging header:

#ifndef FIBER_DEMO_BRIDGING_HEADER_H
#define FIBER_DEMO_BRIDGING_HEADER_H

#include "fiber_ffi.h"

#endif

After configuration, Swift can directly use symbols such as FiberStartOptions, FiberFfiStatus, fiber_start, fiber_stop, and fiber_string_free.

Business code should not call these C functions in scattered places. It is recommended to wrap all FFI details in a Swift file as a FiberRuntime singleton, as the demo does.

Swift Runtime

FiberRuntime.swift is the iOS adapter layer: the UI calls FiberRuntime, and the runtime calls the C ABI in fiber_ffi.h. The app side usually only uses FiberRuntime; view controllers should not directly store FiberHandle *, assemble Fiber*Options, or handle char **out_json.

A runtime is needed because fiber-ffi exports a C ABI, while iOS business code is better expressed with Swift types and iOS lifecycle concepts. The runtime converts String, Bool, amounts, and other parameters into the format required by C, holds a single FiberHandle *, serializes calls on the same handle, and converts returned JSON or error text into Swift String values.

Pay special attention to memory boundaries:

  • Strings passed from Swift to FFI can use withCString. The generated pointer only needs to remain valid during this FFI call.
  • Strings returned by FFI through char **out_* are allocated by libfiber_ffi.dylib; Swift must copy them into String and then call fiber_string_free.
  • event_json in event callbacks is only valid during the callback. Copy it immediately after entering Swift.
  • fiber_stop consumes the handle. After Stop, the old handle must not be passed back to FFI.

The demo handles returned JSON as follows:

private func jsonResult(status: FiberFfiStatus, json: UnsafeMutablePointer<CChar>?, action: String) -> NativeResult {
    if status == fiberStatusOk {
        if let json {
            let value = String(cString: json)
            fiber_string_free(json)
            return .ok(value)
        }
        return .ok("")
    }
    if let json {
        fiber_string_free(json)
    }
    return .failed(resultMessage(action: action, status: status))
}

After a failure, read the thread-local error message on the same thread immediately after the failed call:

private func lastErrorMessage() -> String {
    let required = fiber_last_error_message(nil, 0)
    if required == 0 {
        return ""
    }

    var buffer = [CChar](repeating: 0, count: required + 1)
    let written = fiber_last_error_message(&buffer, buffer.count)
    if written == 0 {
        return ""
    }
    return String(cString: buffer)
}

Prepare Data Directories

Before startup, put the configuration file at a regular file path and prepare the data directory:

config_path      = Documents/fiber_config.yml
database_prefix = Documents/fiber-data

The demo packages fiber_config.yml as a bundle resource and copies it to Documents before startup:

let bundledConfig = Bundle.main.url(forResource: "fiber_config", withExtension: "yml")
let destination = documentsURL().appendingPathComponent("fiber_config.yml")
try fileManager.copyItem(at: bundledConfig, to: destination)

Copying the bundle resource into the sandbox has two benefits: it gives you a normal readable file path, and it lets you reuse the same flow later if configuration needs to be generated per user, network, or environment. A production app can also choose Application Support as the data root directory. The key point is that config_path must be a file path that the current process can read directly, not the YAML contents themselves.

The CKB private key is stored at:

Documents/fiber-data/ckb/key

The demo writes this file through setCkbPrivateKey: the Swift layer normalizes the user-entered 32-byte hex private key and writes it to ckb/key, then sets the following before startup:

setenv("FIBER_SECRET_KEY_PASSWORD", "fiber-demo-secret-key-password", 0)

Distinguish the “plaintext file at import time” from the “persistent file after Fiber starts.” Fiber’s default logic reads FIBER_SECRET_KEY_PASSWORD when loading the CKB private key. If it finds that ckb/key is still parseable plaintext hex, it encrypts it with this password and overwrites the file in place. Later it decrypts the file with the same password. Therefore, after the first SetCKBKey and before the first successful Start, ckb/key may briefly remain plaintext. After a successful start, the file should have been migrated to encrypted contents.

If writing ckb/key manually, the contents should be a 32-byte hex string without the 0x prefix. The demo automatically removes a 0x prefix from user input. The plaintext migration logic in the Fiber source rejects key files with a 0x prefix.

This is only a demo scheme. A production app should use its own wallet, Keychain, Secure Enclave, or key management process, and must not use the fixed FIBER_SECRET_KEY_PASSWORD from the demo. This password and the encrypted ckb/key are both required to recover the ability to sign funds. If either one is lost, the CKB private key cannot be recovered from the encrypted file.

Start and Stop

Assemble FiberStartOptions when starting the node:

var newHandle: OpaquePointer?
let status = configURL.path.withCString { configPath in
    dataURL.path.withCString { databasePrefix in
        "info".withCString { logLevel in
            var options = FiberStartOptions()
            options.config_path = configPath
            options.database_prefix = databasePrefix
            options.log_level = logLevel
            options.event_callback = fiberEventCallback
            options.event_callback_user_data = nil
            return fiber_start(&options, &newHandle)
        }
    }
}

After successful startup, save the handle:

handle = newHandle
runningFlag = true

When stopping the node, call:

let currentHandle = handle
handle = nil
runningFlag = false
let status = fiber_stop(currentHandle)

Do not start multiple nodes in the same process. For repeated Start calls, you can return already running directly. Before Stop, clear the handle in the Swift runtime first, so callbacks or concurrent calls cannot get the old pointer again. After Stop, the old handle must not be used.

Calling Features

The current demo wraps the following Swift methods:

Method Purpose
start() Start the node
stop() Stop the node
nodeInfo() Read node information
listPeers() List peers
connectPeer(...) Connect a peer
listChannels() List channels
createChannel(...) Create a channel
shutdownChannel(...) Close a channel
newInvoice(...) Create an invoice
sendPayment(...) Pay an invoice

Pay attention to amount units:

  • The demo’s createChannel(...) accepts a decimal CKB string for display, such as 1.5. The Swift runtime converts it to shannons before filling the FFI FiberU128.
  • The demo’s newInvoice(...) accepts an integer shannons string and does not accept decimal CKB.
  • Amount fields actually passed into FFI always use integer smallest units: CKB uses shannons, where 1 CKB = 100000000 shannons; UDT uses the integer smallest unit of that UDT.
  • If UDT channels or UDT invoices are added later, pass UDT parameters such as funding_udt_type_script_json / udt_type_script_json, and pass amount as the UDT’s raw integer amount. Do not reuse the CKB amount * 100000000 conversion.

Swift has no built-in UInt128. The demo uses a small UInt128Value struct to store low and high, and finally converts it to the FFI type:

FiberU128(low: low, high: high)

When adding new capabilities, prefer adding methods to FiberRuntime and reusing the existing withHandle, withOptionalCString, jsonResult, amount parsing, and error handling patterns.

Handling Events

Pass the native event callback during startup:

private let fiberEventCallback: fiber_event_callback = { eventJson, _ in
    guard let eventJson else {
        return
    }
    FiberRuntime.shared.emitNativeEvent(String(cString: eventJson))
}

The UI layer listens for events:

runtime.setEventHandler { [weak self] eventJson in
    DispatchQueue.main.async {
        self?.appendLog("event: \(eventJson)")
    }
}

Event contents are JSON strings. Business code should parse the JSON first, then dispatch based on the kind field. Common kind values include:

NetworkStarted
NetworkStopped
PeerConnected
PeerDisConnected
ChannelCreated
ChannelReady
ChannelClosed
PreimageCreated

The list above is only a set of common event examples, not a complete enum. When consuming events, use the kind field in the callback JSON as the source of truth. If an event kind is not currently relevant or is added in the future, it can be logged and ignored.

Do not update UI directly from the native callback thread. Switch to the main thread after receiving events. Also avoid long-running work or direct reverse calls into other FFI APIs inside the callback.

Lifecycle

The iOS demo only performs minimal lifecycle cleanup: when a scene disconnects or the app terminates, it calls the same stop helper:

FiberRuntime.shared.stopIfRunning()

Recommended handling:

  • Do not execute potentially blocking FFI calls directly on the UI main thread.
  • Start, Stop, and calls on the same handle must be serialized.
  • Execute Stop when the user logs out, switches accounts, or explicitly closes the node.
  • When switching network, account, or data directory, call Stop first, then restart the app process.
  • Use different database_prefix values for different users, accounts, and networks.
  • Background execution, disconnection recovery, push, wallet-hosted nodes, or server-assisted designs must be designed by production app developers according to product shape and iOS background capabilities. The demo does not attempt to implement a persistent background node.

A normal iOS app has limited execution time after entering the background. Long-lived P2P connections, channel negotiation, and on-chain monitoring may all be suspended by the system. The current demo is meant to validate the in-process embedding path while the app is active. It is not equivalent to a complete persistent background node.

Build

  • The current demo prepares one dylib for iphoneos and one for iphonesimulator. Real devices use aarch64-apple-ios, and Apple Silicon simulators use aarch64-apple-ios-sim.
  • The demo’s simulator build fixes ARCHS=arm64, so it needs the arm64 simulator dylib generated by make build-ios-sim.
  • iOS builds currently enable only the sqlite feature to avoid making RocksDB a default mobile storage dependency.
  • IOS_DEPLOYMENT_TARGET defaults to 15.0. If the Xcode target deployment target is changed to another version, pass the same value when building the Rust dylib.
  • IOS_RUSTFLAGS sets -Wl,-install_name,@rpath/libfiber_ffi.dylib. Do not change it to an absolute path, or the app may fail to find the embedded dylib at runtime.
  • The dylib in a real-device app bundle must be signed. The demo embed script re-signs the dylib with the current target’s signing identity.

Example for changing the deployment target:

make -C demos/ios build IOS_DEPLOYMENT_TARGET=16.0
make -C demos/ios build-sim IOS_DEPLOYMENT_TARGET=16.0

Demo Operations

  1. SetCKBKey: enter the CKB private key.
  2. Start: start the node.
  3. NodeInfo: read the node address and pubkey.
  4. Peers: view or connect peers.
  5. Invoice: create an invoice or pay an invoice.
  6. Channels: list, create, or close channels.
  7. Stop: stop the node.

Q&A

Library not loaded: @rpath/libfiber_ffi.dylib

Check whether libfiber_ffi.dylib is under FiberDemo/Libs/<PLATFORM_NAME>/, whether the target links -lfiber_ffi, whether LD_RUNPATH_SEARCH_PATHS contains @executable_path/Frameworks, and whether the embed script has copied the dylib into the app bundle’s Frameworks directory.

Real-device startup fails with a dylib signing error

Check the development team, bundle identifier, and signing identity. libfiber_ffi.dylib inside the real-device app bundle must be signed together with the app. The demo embed script re-signs the dylib when EXPANDED_CODE_SIGN_IDENTITY exists.

Simulator link fails or runtime reports an architecture mismatch

Confirm that the current simulator is arm64 and that you are using:

target/aarch64-apple-ios-sim/release/libfiber_ffi.dylib

Do not put the real-device aarch64-apple-ios dylib into the iphonesimulator directory.

Swift cannot find fiber_start or FiberStartOptions

Check whether SWIFT_OBJC_BRIDGING_HEADER points to the correct bridging header, whether HEADER_SEARCH_PATHS includes the directory containing include/fiber_ffi.h, and whether the bridging header contains:

#include "fiber_ffi.h"

Startup fails and says the configuration file is unreadable

Do not pass only YAML contents. fiber_start requires config_path to be a real file path. The demo copies fiber_config.yml from the bundle into Documents before passing it in.

Startup fails and says the CKB private key is not set

The demo requires running SetCKBKey first. The key is a 32-byte hex string, with or without a 0x prefix. The demo normalizes it and writes it to Documents/fiber-data/ckb/key. During the first successful Start, Fiber uses FIBER_SECRET_KEY_PASSWORD to migrate this plaintext key into an encrypted file. Later starts must continue using the same password to decrypt it.

Can the node stay online after the app enters the background?

The current demo does not guarantee persistent background execution. A normal iOS app is suspended by the system after entering the background, so P2P connections and event processing may pause. If background capabilities are needed, first clarify the product scenario, then evaluate Background Modes, push, wallet-hosted nodes, or server-assisted designs.

4 Likes

This is a really important piece of the puzzle !

Great technology only gets adopted when developers can integrate it easily into real applications. Native Android and iOS support could make a big difference for Fiber’s future.

Looking forward to seeing it come to life.

1 Like