Add FFI example (#2037)

* Add FFI example

* Remove unnecessary dependency (libc)

* Tweak python example and add CI
CI Tweak; separate task for ffi-example run

* Remove OnceCell dep; use std

---------

Co-authored-by: wassimans <wassim@wassimans.com>
This commit is contained in:
James Wilson
2025-07-08 12:06:13 +01:00
committed by GitHub
parent 17b98d0d9e
commit ff6fc1585e
11 changed files with 4241 additions and 1 deletions
+73
View File
@@ -0,0 +1,73 @@
use hex::decode;
use std::{ffi::CStr, os::raw::c_char, sync::OnceLock};
use subxt::{OnlineClient, PolkadotConfig, dynamic::Value, ext::scale_value::Composite, tx};
use subxt_signer::sr25519::dev;
use tokio::runtime::Runtime;
static TOKIO: OnceLock<Runtime> = OnceLock::new();
fn tokio_rt() -> &'static Runtime {
TOKIO.get_or_init(|| Runtime::new().expect("failed to start tokio"))
}
/// A simple CABI function to transfer `amount` to a hexencoded `dest`.
/// Assumes a running nodes WS endpoint is at ws://127.0.0.1:8000
#[unsafe(no_mangle)]
pub extern "C" fn do_transfer(dest_hex: *const c_char, amount: u64) -> i32 {
let amount = amount as u128;
// We need to convert C string to Rust str
let raw_s = unsafe { CStr::from_ptr(dest_hex).to_str().unwrap_or_default() };
// Strip optional 0x prefix
let s = raw_s.strip_prefix("0x").unwrap_or(raw_s);
// Decode hex, force a 32byte AccountId
let raw = decode(s).expect("hex decode");
let arr: [u8; 32] = raw.as_slice().try_into().expect("must be 32 bytes");
// Wrap into a MultiAddress::Id variant for dynamic calls:
let dst = Value::variant(
"Id",
Composite::unnamed(vec![
// scale encode
Value::from_bytes(arr.to_vec()),
]),
);
// Spin up (or reuse) our Tokio runtime and connect:
let client = tokio_rt().block_on(async {
OnlineClient::<PolkadotConfig>::from_url("ws://127.0.0.1:8000")
.await
.unwrap()
});
let signer = dev::alice();
// Build the dynamic metadata extrinsic:
let tx = tx::dynamic(
"Balances",
"transfer_keep_alive",
vec![
dst.clone(),
// primitive numeric value
Value::u128(amount),
],
);
// Submit and wait for finalize
let res: Result<_, subxt::Error> = tokio_rt().block_on(async {
let progress = client
.tx()
.sign_and_submit_then_watch_default(&tx, &signer)
.await?;
progress.wait_for_finalized_success().await
});
// Return code
match res {
Ok(_) => 0,
Err(e) => {
// print the Subxt error
eprintln!("do_transfer failed: {:#?}", e);
-1
}
}
}
+28
View File
@@ -0,0 +1,28 @@
const path = require("path");
const ffi = require("ffi-napi");
// Pick the correct library file name depending on the platform
const libPath = path.resolve(__dirname, "../target/debug", {
darwin: "libsubxt_ffi.dylib",
linux: "libsubxt_ffi.so",
win32: "subxt_ffi.dll"
}[process.platform]);
// Declare the FFI interface
const lib = ffi.Library(libPath, {
"do_transfer": ["int", ["string", "uint64"]]
});
function doTransfer(destHex, amount) {
const code = lib.do_transfer(destHex, amount);
if (code === 0) {
console.log("✓ transfer succeeded");
} else {
console.error("✗ transfer failed, code =", code);
}
}
// Example usage:
const dest = "0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48";
const amount = 1_000_000_000_000; // fits in u64
doTransfer(dest, amount);
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
import os, sys, ctypes, platform
from ctypes import c_char_p, c_uint64, c_int
# The library name depends on the playform type
if platform.system() == "Linux":
libname = "libsubxt_ffi.so"
elif platform.system() == "Darwin":
libname = "libsubxt_ffi.dylib"
elif platform.system() == "Windows":
libname = "subxt_ffi.dll"
else:
raise RuntimeError(f"Unsupported platform: {platform.system()}")
# Load the library
lib_path = os.path.join(os.path.dirname(__file__), "..", "target", "debug", libname)
lib = ctypes.CDLL(lib_path)
# Tell ctypes about our function signature, the one we defined in the Rust library
lib.do_transfer.argtypes = (c_char_p, c_uint64)
lib.do_transfer.restype = c_int
def do_transfer(dest_hex: str, amount: int) -> int:
"""
Perform a transfer.
dest_hex: hex-string of the 32-byte AccountId (e.g. "0x...")
amount: integer amount (fits in u64)
Returns 0 on success, 1 on error.
"""
# ensure we pass a C-string pointer
dest_bytes = dest_hex.encode("utf8")
return lib.do_transfer(dest_bytes, amount)
if __name__ == "__main__":
# example usage
dest = "0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"
amt = 1_000_000_000_000
code = do_transfer(dest, amt)
if code == 0:
print("✓ transfer succeeded")
else:
print("✗ transfer failed")