This example shows how to expose a small piece of Subxt functionality, in our case, a single balance-transfer call, as a native C-ABI library, consumable from Python and Node.js.
- We want to let non-Rust clients interact with any Substrate-based node (Polkadot in this example) via a tiny FFI layer.
- Instead of exposing Subxt’s full, Rust-centric API, we build a thin facade crate that:
- Calls Subxt under the hood
- Exposes just the functions we need via
pub extern "C" fn …
- Client languages (Python, JavaScript, Swift, Kotlin, etc.) load the compiled
.so/.dylib/.dlland call these C-ABI functions directly.
flowchart LR
subgraph Rust side
subxt[Subxt Public API]
facade[Facade crate]
node[Substrate node]
cabi[C ABI library]
subxt --> facade
facade --> node
facade --> cabi
end
subgraph Client side
swift[Swift client]
python[Python client]
kotlin[Kotlin client]
js[JavaScript client]
swift --> cabi
python --> cabi
kotlin --> cabi
js --> cabi
end
Our one example function is:
pub extern "C" fn do_transfer(dest_hex: *const c_char, amount: u64) -> i32which does a single balance transfer and returns 0 on success, –1 on error.
- Rust toolchain (with cargo)
- Python 3
- Node.js (for the JS example)
- A running Substrate node (Polkadot) on ws://127.0.0.1:8000. We recommend using Chopsticks for a quick local Polkadot node:
npx @acala-network/chopsticks \
--config=https://raw.githubusercontent.com/AcalaNetwork/chopsticks/master/configs/polkadot.yml
- In our Python and Javascript files, we introduce a dest variable that represents the destination account for the transfer, we gave it a hard coded value (Bob's account public key) from the running Chopsticks node. Feel free to change it to any other account, or better yet, make it generic!
cargo buildThis will produce a dynamic library in target/debug/ (or target/release/ if you pass --release):
- macOS: libsubxt_ffi.dylib
- Linux: libsubxt_ffi.so
- Windows: subxt_ffi.dll
To run the example files (main.py and main.js) on Windows, just copy the dll file to the folder where main files are.
export LD_LIBRARY_PATH=$PWD/target/debug # or DYLD_LIBRARY_PATH on macOS
python3 src/main.pyExpected output:
✓ transfer succeeded
In the root of the project run:
npm installthen:
export LD_LIBRARY_PATH=$PWD/target/debug # or DYLD_LIBRARY_PATH on macOS
node src/main.jsExpected output:
✓ transfer succeeded
- Hex handling: We strip an optional 0x prefix and decode into 32 bytes.
- FFI safety: We only pass pointers and primitive types (u64, i32) across the boundary.
- Error codes: We return 0 on success, -1 on any kind of failure (decode error, RPC error, etc.).
- You can extend this facade crate with any additional functions you need—just expose them as pub extern "C" and follow the same pattern.
Translating a complex Rust API like Subxt to a bare bones C ABI ready to be consumed by foreign languages has its limitations. Here's a few of them:
- Complex types (strings, structs) require to design C-safe representations.
- Only C primitive types (integers, pointers) are FFI-safe; anything else must be translated.
- Manual memory management glue code is needed if owned data is returned.
- Needs a manual translation to every foreign language we export to, every time the Rust library changes.