use crate::transport::{MeshTransport, NodeId, PeerEvent};
use serde::{Deserialize, Serialize};
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock, RwLock};
pub const PEAT_OK: c_int = 0;
pub const PEAT_ERR_NOT_INITIALIZED: c_int = -1;
pub const PEAT_ERR_INVALID_PEER: c_int = -2;
pub const PEAT_ERR_CONNECTION_FAILED: c_int = -3;
pub const PEAT_ERR_ALREADY_CONNECTED: c_int = -4;
pub const PEAT_ERR_NOT_CONNECTED: c_int = -5;
pub const PEAT_ERR_INVALID_ARGUMENT: c_int = -6;
pub const PEAT_ERR_INTERNAL: c_int = -99;
pub const PEAT_EVENT_CONNECTED: c_int = 1;
pub const PEAT_EVENT_DISCONNECTED: c_int = 2;
pub const PEAT_EVENT_DEGRADED: c_int = 3;
pub const PEAT_EVENT_RECONNECTING: c_int = 4;
pub const PEAT_EVENT_RECONNECT_FAILED: c_int = 5;
static GLOBAL_TRANSPORT: OnceLock<Arc<dyn MeshTransport>> = OnceLock::new();
static LAST_ERROR: RwLock<Option<String>> = RwLock::new(None);
static PEER_CALLBACK: RwLock<Option<PeerEventCallback>> = RwLock::new(None);
static CALLBACK_CANCEL: OnceLock<Arc<AtomicBool>> = OnceLock::new();
fn get_or_init_cancel_token() -> &'static Arc<AtomicBool> {
CALLBACK_CANCEL.get_or_init(|| Arc::new(AtomicBool::new(false)))
}
pub type PeerEventCallback =
extern "C" fn(event_type: c_int, peer_id: *const c_char, reason: *const c_char);
#[derive(Debug, Serialize, Deserialize)]
pub struct PeerInfo {
pub peer_id: String,
pub connected_since: String, pub status: String, }
#[derive(Debug, Serialize, Deserialize)]
pub struct ConnectedPeersResponse {
pub peers: Vec<PeerInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PeerStatusResponse {
pub peer_id: String,
pub connected: bool,
pub connection_type: String, }
#[derive(Debug, Serialize, Deserialize)]
pub struct PeerHealthResponse {
pub peer_id: String,
pub rtt_ms: u32,
pub packet_loss_percent: u8,
pub state: String, }
pub fn initialize_ffi(transport: Arc<dyn MeshTransport>) -> Result<(), &'static str> {
GLOBAL_TRANSPORT
.set(transport)
.map_err(|_| "FFI already initialized")
}
pub fn is_initialized() -> bool {
GLOBAL_TRANSPORT.get().is_some()
}
fn get_transport() -> Option<&'static Arc<dyn MeshTransport>> {
GLOBAL_TRANSPORT.get()
}
fn set_last_error(error: String) {
if let Ok(mut last) = LAST_ERROR.write() {
*last = Some(error);
}
}
#[no_mangle]
pub extern "C" fn peat_get_last_error() -> *mut c_char {
let error = LAST_ERROR.read().ok().and_then(|e| e.clone());
match error {
Some(msg) => CString::new(msg)
.map(|s| s.into_raw())
.unwrap_or(std::ptr::null_mut()),
None => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn peat_free_string(s: *mut c_char) {
if !s.is_null() {
drop(CString::from_raw(s));
}
}
#[no_mangle]
pub extern "C" fn peat_get_connected_peers() -> *mut c_char {
let transport = match get_transport() {
Some(t) => t,
None => {
set_last_error("Peat not initialized".to_string());
return std::ptr::null_mut();
}
};
let peer_ids = transport.connected_peers();
let peers: Vec<PeerInfo> = peer_ids
.into_iter()
.map(|peer_id| {
let (status, connected_since) = transport
.get_connection(&peer_id)
.map(|c| {
let status = if c.is_alive() { "healthy" } else { "dead" };
let duration_ago = c.connected_at().elapsed();
let connected_at = chrono::Utc::now()
- chrono::Duration::from_std(duration_ago).unwrap_or_default();
(status, connected_at.to_rfc3339())
})
.unwrap_or(("unknown", chrono::Utc::now().to_rfc3339()));
PeerInfo {
peer_id: peer_id.to_string(),
connected_since,
status: status.to_string(),
}
})
.collect();
let response = ConnectedPeersResponse { peers };
match serde_json::to_string(&response) {
Ok(json) => CString::new(json)
.map(|s| s.into_raw())
.unwrap_or(std::ptr::null_mut()),
Err(e) => {
set_last_error(format!("JSON serialization failed: {}", e));
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn peat_get_peer_status(peer_id: *const c_char) -> *mut c_char {
if peer_id.is_null() {
set_last_error("peer_id is null".to_string());
return std::ptr::null_mut();
}
let peer_id_str = match CStr::from_ptr(peer_id).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("Invalid UTF-8 in peer_id".to_string());
return std::ptr::null_mut();
}
};
let transport = match get_transport() {
Some(t) => t,
None => {
set_last_error("Peat not initialized".to_string());
return std::ptr::null_mut();
}
};
let node_id = NodeId::new(peer_id_str.to_string());
let connected = transport.is_connected(&node_id);
let response = PeerStatusResponse {
peer_id: peer_id_str.to_string(),
connected,
connection_type: if connected { "quic" } else { "unknown" }.to_string(),
};
match serde_json::to_string(&response) {
Ok(json) => CString::new(json)
.map(|s| s.into_raw())
.unwrap_or(std::ptr::null_mut()),
Err(e) => {
set_last_error(format!("JSON serialization failed: {}", e));
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn peat_get_peer_health(peer_id: *const c_char) -> *mut c_char {
if peer_id.is_null() {
set_last_error("peer_id is null".to_string());
return std::ptr::null_mut();
}
let peer_id_str = match CStr::from_ptr(peer_id).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("Invalid UTF-8 in peer_id".to_string());
return std::ptr::null_mut();
}
};
let transport = match get_transport() {
Some(t) => t,
None => {
set_last_error("Peat not initialized".to_string());
return std::ptr::null_mut();
}
};
let node_id = NodeId::new(peer_id_str.to_string());
if !transport.is_connected(&node_id) {
set_last_error(format!("Peer not found: {}", peer_id_str));
return std::ptr::null_mut();
}
let health = transport.get_peer_health(&node_id);
let response = match health {
Some(h) => PeerHealthResponse {
peer_id: peer_id_str.to_string(),
rtt_ms: h.rtt_ms,
packet_loss_percent: h.packet_loss_percent,
state: format!("{}", h.state),
},
None => {
PeerHealthResponse {
peer_id: peer_id_str.to_string(),
rtt_ms: 0,
packet_loss_percent: 0,
state: "unknown".to_string(),
}
}
};
match serde_json::to_string(&response) {
Ok(json) => CString::new(json)
.map(|s| s.into_raw())
.unwrap_or(std::ptr::null_mut()),
Err(e) => {
set_last_error(format!("JSON serialization failed: {}", e));
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn peat_register_peer_callback(callback: Option<PeerEventCallback>) -> c_int {
let transport = match get_transport() {
Some(t) => t,
None => {
set_last_error("Peat not initialized".to_string());
return PEAT_ERR_NOT_INITIALIZED;
}
};
let cancel_token = get_or_init_cancel_token();
cancel_token.store(false, Ordering::SeqCst);
if let Ok(mut cb) = PEER_CALLBACK.write() {
*cb = callback;
}
if callback.is_some() {
let mut rx = transport.subscribe_peer_events();
let cancel = Arc::clone(cancel_token);
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create runtime");
rt.block_on(async move {
loop {
if cancel.load(Ordering::SeqCst) {
break;
}
match tokio::time::timeout(std::time::Duration::from_millis(100), rx.recv())
.await
{
Ok(Some(event)) => {
if let Ok(cb_guard) = PEER_CALLBACK.read() {
if let Some(cb) = *cb_guard {
invoke_callback(cb, &event);
} else {
break;
}
}
}
Ok(None) => {
break;
}
Err(_) => {
}
}
}
});
});
}
PEAT_OK
}
#[no_mangle]
pub extern "C" fn peat_unregister_peer_callback() -> c_int {
get_or_init_cancel_token().store(true, Ordering::SeqCst);
if let Ok(mut cb) = PEER_CALLBACK.write() {
*cb = None;
}
PEAT_OK
}
fn invoke_callback(callback: PeerEventCallback, event: &PeerEvent) {
match event {
PeerEvent::Connected { peer_id, .. } => {
let peer_id_cstr = CString::new(peer_id.to_string()).unwrap_or_default();
callback(
PEAT_EVENT_CONNECTED,
peer_id_cstr.as_ptr(),
std::ptr::null(),
);
}
PeerEvent::Disconnected {
peer_id, reason, ..
} => {
let peer_id_cstr = CString::new(peer_id.to_string()).unwrap_or_default();
let reason_cstr = CString::new(format!("{}", reason)).unwrap_or_default();
callback(
PEAT_EVENT_DISCONNECTED,
peer_id_cstr.as_ptr(),
reason_cstr.as_ptr(),
);
}
PeerEvent::Degraded { peer_id, health } => {
let peer_id_cstr = CString::new(peer_id.to_string()).unwrap_or_default();
let health_cstr = CString::new(format!(
"rtt={}ms, loss={}%",
health.rtt_ms, health.packet_loss_percent
))
.unwrap_or_default();
callback(
PEAT_EVENT_DEGRADED,
peer_id_cstr.as_ptr(),
health_cstr.as_ptr(),
);
}
PeerEvent::Reconnecting {
peer_id,
attempt,
max_attempts,
} => {
let peer_id_cstr = CString::new(peer_id.to_string()).unwrap_or_default();
let info_cstr = CString::new(format!(
"attempt={}/{}",
attempt,
max_attempts.map_or("∞".to_string(), |m| m.to_string())
))
.unwrap_or_default();
callback(
PEAT_EVENT_RECONNECTING,
peer_id_cstr.as_ptr(),
info_cstr.as_ptr(),
);
}
PeerEvent::ReconnectFailed {
peer_id,
attempt,
error,
will_retry,
} => {
let peer_id_cstr = CString::new(peer_id.to_string()).unwrap_or_default();
let info_cstr = CString::new(format!(
"attempt={}, error={}, will_retry={}",
attempt, error, will_retry
))
.unwrap_or_default();
callback(
PEAT_EVENT_RECONNECT_FAILED,
peer_id_cstr.as_ptr(),
info_cstr.as_ptr(),
);
}
}
}
pub struct PeatAsyncContext {
runtime: tokio::runtime::Runtime,
}
impl PeatAsyncContext {
pub fn new() -> Result<Self, std::io::Error> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
Ok(Self { runtime })
}
pub fn connect_peer(&self, peer_id: &str) -> c_int {
let transport = match get_transport() {
Some(t) => t.clone(),
None => {
set_last_error("Peat not initialized".to_string());
return PEAT_ERR_NOT_INITIALIZED;
}
};
let node_id = NodeId::new(peer_id.to_string());
match self.runtime.block_on(transport.connect(&node_id)) {
Ok(_) => PEAT_OK,
Err(e) => {
set_last_error(format!("Connection failed: {}", e));
PEAT_ERR_CONNECTION_FAILED
}
}
}
pub fn disconnect_peer(&self, peer_id: &str) -> c_int {
let transport = match get_transport() {
Some(t) => t.clone(),
None => {
set_last_error("Peat not initialized".to_string());
return PEAT_ERR_NOT_INITIALIZED;
}
};
let node_id = NodeId::new(peer_id.to_string());
match self.runtime.block_on(transport.disconnect(&node_id)) {
Ok(_) => PEAT_OK,
Err(e) => {
set_last_error(format!("Disconnect failed: {}", e));
PEAT_ERR_INTERNAL
}
}
}
}
static ASYNC_CONTEXT: OnceLock<PeatAsyncContext> = OnceLock::new();
fn get_async_context() -> Option<&'static PeatAsyncContext> {
ASYNC_CONTEXT.get()
}
#[no_mangle]
pub extern "C" fn peat_init_async() -> c_int {
match PeatAsyncContext::new() {
Ok(ctx) => match ASYNC_CONTEXT.set(ctx) {
Ok(_) => PEAT_OK,
Err(_) => {
set_last_error("Async context already initialized".to_string());
PEAT_OK }
},
Err(e) => {
set_last_error(format!("Failed to create async runtime: {}", e));
PEAT_ERR_INTERNAL
}
}
}
#[no_mangle]
pub unsafe extern "C" fn peat_connect_peer(peer_id: *const c_char) -> c_int {
if peer_id.is_null() {
set_last_error("peer_id is null".to_string());
return PEAT_ERR_INVALID_ARGUMENT;
}
let peer_id_str = match CStr::from_ptr(peer_id).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("Invalid UTF-8 in peer_id".to_string());
return PEAT_ERR_INVALID_ARGUMENT;
}
};
let ctx = match get_async_context() {
Some(c) => c,
None => {
set_last_error("Async context not initialized, call peat_init_async first".to_string());
return PEAT_ERR_NOT_INITIALIZED;
}
};
ctx.connect_peer(peer_id_str)
}
#[no_mangle]
pub unsafe extern "C" fn peat_disconnect_peer(peer_id: *const c_char) -> c_int {
if peer_id.is_null() {
set_last_error("peer_id is null".to_string());
return PEAT_ERR_INVALID_ARGUMENT;
}
let peer_id_str = match CStr::from_ptr(peer_id).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("Invalid UTF-8 in peer_id".to_string());
return PEAT_ERR_INVALID_ARGUMENT;
}
};
let ctx = match get_async_context() {
Some(c) => c,
None => {
set_last_error("Async context not initialized, call peat_init_async first".to_string());
return PEAT_ERR_NOT_INITIALIZED;
}
};
ctx.disconnect_peer(peer_id_str)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_codes() {
assert_eq!(PEAT_OK, 0);
assert_eq!(PEAT_ERR_NOT_INITIALIZED, -1);
assert_eq!(PEAT_ERR_INVALID_PEER, -2);
}
#[test]
fn test_event_types() {
assert_eq!(PEAT_EVENT_CONNECTED, 1);
assert_eq!(PEAT_EVENT_DISCONNECTED, 2);
assert_eq!(PEAT_EVENT_DEGRADED, 3);
assert_eq!(PEAT_EVENT_RECONNECTING, 4);
assert_eq!(PEAT_EVENT_RECONNECT_FAILED, 5);
}
#[test]
fn test_not_initialized() {
let result = peat_get_connected_peers();
assert!(result.is_null());
}
#[test]
fn test_peer_info_serialization() {
let info = PeerInfo {
peer_id: "test-peer".to_string(),
connected_since: "2025-01-01T00:00:00Z".to_string(),
status: "healthy".to_string(),
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("test-peer"));
assert!(json.contains("healthy"));
}
#[test]
fn test_connected_peers_response_serialization() {
let response = ConnectedPeersResponse {
peers: vec![PeerInfo {
peer_id: "node-1".to_string(),
connected_since: "2025-01-01T00:00:00Z".to_string(),
status: "healthy".to_string(),
}],
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("peers"));
assert!(json.contains("node-1"));
}
}