use rlx_runtime::dist::node::{
NodeConfig, NodeControl, NodeStopHandle, serve_trainer_here, serve_worker,
};
use std::ffi::{CStr, CString, c_char, c_int};
use std::sync::{Mutex, OnceLock, mpsc};
pub const RLX_NODE_OK: c_int = 0;
pub const RLX_NODE_ERR_BUSY: c_int = -1;
pub const RLX_NODE_ERR_ARG: c_int = -2;
pub const RLX_NODE_ERR_CONFIG: c_int = -3;
pub const RLX_NODE_ERR_SPAWN: c_int = -4;
pub const RLX_NODE_ERR_MODE: c_int = -5;
fn uri_resolver(_uri: &str) -> Vec<f32> {
Vec::new()
}
struct Slot {
stop: NodeStopHandle,
done: mpsc::Receiver<String>,
last: Option<String>,
}
fn slot() -> &'static Mutex<Option<Slot>> {
static S: OnceLock<Mutex<Option<Slot>>> = OnceLock::new();
S.get_or_init(|| Mutex::new(None))
}
fn last_error() -> &'static Mutex<String> {
static E: OnceLock<Mutex<String>> = OnceLock::new();
E.get_or_init(|| Mutex::new(String::new()))
}
fn set_error(msg: impl Into<String>) {
if let Ok(mut e) = last_error().lock() {
*e = msg.into();
}
}
unsafe fn cstr(p: *const c_char) -> Option<String> {
if p.is_null() {
return None;
}
unsafe { CStr::from_ptr(p) }
.to_str()
.ok()
.map(str::to_owned)
}
unsafe fn fill(buf: *mut c_char, cap: usize, s: &str) -> c_int {
if buf.is_null() || cap == 0 {
return RLX_NODE_ERR_ARG;
}
let Ok(c) = CString::new(s) else {
return RLX_NODE_ERR_ARG;
};
let bytes = c.as_bytes_with_nul();
let n = bytes.len().min(cap);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), buf, n);
*buf.add(n - 1) = 0;
}
(n - 1) as c_int
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rlx_node_start(
rank: c_int,
world: c_int,
peers: *const c_char,
device: *const c_char,
mode: *const c_char,
) -> c_int {
let (Some(peers), Some(device), Some(mode)) =
(unsafe { cstr(peers) }, unsafe { cstr(device) }, unsafe {
cstr(mode)
})
else {
set_error("peers/device/mode must be valid UTF-8 C strings");
return RLX_NODE_ERR_ARG;
};
let training = match mode.as_str() {
"" | "infer" => false,
"train" => true,
other => {
set_error(format!("unknown mode [{other}]; expected infer or train"));
return RLX_NODE_ERR_MODE;
}
};
let Ok(mut g) = slot().lock() else {
set_error("node state poisoned");
return RLX_NODE_ERR_BUSY;
};
if g.as_ref().is_some_and(|s| s.last.is_none()) {
set_error("a node is already running in this process");
return RLX_NODE_ERR_BUSY;
}
if rank < 0 || world < 1 || rank >= world {
set_error(format!("bad rank/world: {rank}/{world}"));
return RLX_NODE_ERR_CONFIG;
}
let addrs: Vec<String> = peers
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let base = NodeConfig::new(rank as u32, world as u32).device(device);
let cfg = if addrs.is_empty() {
base.star().discover(29600, 29500)
} else {
let star = addrs.len() == 1 && world > 1;
let base = if star { base.star() } else { base.mesh() };
match base.peers(addrs) {
Ok(c) => c,
Err(e) => {
set_error(e);
return RLX_NODE_ERR_CONFIG;
}
}
};
let ctl = NodeControl::unbounded();
let stop = ctl.stop_handle();
let (tx, rx) = mpsc::channel();
if let Err(e) = std::thread::Builder::new()
.name("rlx-node".into())
.spawn(move || {
let msg = match cfg.connect() {
Err(e) => format!("error: connect failed: {e}"),
Ok(group) if training => match serve_trainer_here(&group, uri_resolver, false) {
Ok(r) => format!(
"ok: rank {} trained on {} ({}), {} sample(s), loss {:.4}->{:.4}",
r.rank,
r.metrics.device.name(),
r.platform,
r.metrics.samples,
r.metrics.first_loss,
r.metrics.last_loss
),
Err(e) => format!("error: {e}"),
},
Ok(group) => match serve_worker(&group, uri_resolver) {
Ok(r) => format!(
"ok: rank {} on {} ({}), {} activation(s)",
r.rank,
r.device.name(),
r.platform,
r.activations
),
Err(e) => format!("error: {e}"),
},
};
let _ = tx.send(msg);
})
{
set_error(format!("spawn: {e}"));
return RLX_NODE_ERR_SPAWN;
}
*g = Some(Slot {
stop,
done: rx,
last: None,
});
RLX_NODE_OK
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rlx_node_status(buf: *mut c_char, cap: usize) -> c_int {
let Ok(mut g) = slot().lock() else {
return unsafe { fill(buf, cap, "error: node state poisoned") };
};
let s = match g.as_mut() {
None => "idle".to_string(),
Some(s) => {
if s.last.is_none()
&& let Ok(msg) = s.done.try_recv()
{
s.last = Some(msg);
}
match &s.last {
Some(m) => m.clone(),
None if s.stop.is_stopped() => "stopping".into(),
None => "running".into(),
}
}
};
unsafe { fill(buf, cap, &s) }
}
#[unsafe(no_mangle)]
pub extern "C" fn rlx_node_stop() -> c_int {
let Ok(g) = slot().lock() else {
return RLX_NODE_ERR_BUSY;
};
match g.as_ref() {
Some(s) => {
s.stop.stop();
RLX_NODE_OK
}
None => RLX_NODE_OK,
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rlx_node_last_error(buf: *mut c_char, cap: usize) -> c_int {
let msg = last_error().lock().map(|e| e.clone()).unwrap_or_default();
unsafe { fill(buf, cap, &msg) }
}
#[unsafe(no_mangle)]
pub extern "C" fn rlx_node_platform() -> *const c_char {
static P: OnceLock<CString> = OnceLock::new();
P.get_or_init(|| CString::new(rlx_runtime::dist::node::platform_tag()).unwrap_or_default())
.as_ptr()
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
static FFI_TEST_LOCK: Mutex<()> = Mutex::new(());
fn ffi_lock() -> std::sync::MutexGuard<'static, ()> {
FFI_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn status() -> String {
let mut buf = [0 as c_char; 256];
let n = unsafe { rlx_node_status(buf.as_mut_ptr(), buf.len()) };
assert!(n >= 0, "status returned {n}");
unsafe { CStr::from_ptr(buf.as_ptr()) }
.to_string_lossy()
.into_owned()
}
#[test]
fn rejects_bad_rank_world() {
let _lock = ffi_lock();
let peers = CString::new("127.0.0.1:1").unwrap();
let dev = CString::new("cpu").unwrap();
let mode = CString::new("infer").unwrap();
let rc = unsafe { rlx_node_start(2, 2, peers.as_ptr(), dev.as_ptr(), mode.as_ptr()) };
assert_eq!(rc, RLX_NODE_ERR_CONFIG);
let mut buf = [0 as c_char; 256];
let n = unsafe { rlx_node_last_error(buf.as_mut_ptr(), buf.len()) };
assert!(n > 0);
let msg = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_string_lossy();
assert!(msg.contains("2/2"), "unexpected error: {msg}");
}
#[test]
fn rejects_peer_count_mismatch() {
let _lock = ffi_lock();
let peers = CString::new("127.0.0.1:1,127.0.0.1:2").unwrap();
let dev = CString::new("cpu").unwrap();
let mode = CString::new("infer").unwrap();
let rc = unsafe { rlx_node_start(1, 3, peers.as_ptr(), dev.as_ptr(), mode.as_ptr()) };
assert_eq!(rc, RLX_NODE_ERR_CONFIG);
}
#[test]
fn rejects_an_unknown_mode() {
let _lock = ffi_lock();
let peers = CString::new("127.0.0.1:1").unwrap();
let dev = CString::new("cpu").unwrap();
let mode = CString::new("trian").unwrap(); let rc = unsafe { rlx_node_start(0, 1, peers.as_ptr(), dev.as_ptr(), mode.as_ptr()) };
assert_eq!(rc, RLX_NODE_ERR_MODE);
let mut buf = [0 as c_char; 256];
let n = unsafe { rlx_node_last_error(buf.as_mut_ptr(), buf.len()) };
assert!(n > 0);
let msg = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_string_lossy();
assert!(
msg.contains("trian"),
"error should name the bad mode: {msg}"
);
}
#[test]
fn rejects_null_strings() {
let _lock = ffi_lock();
let dev = CString::new("cpu").unwrap();
let mode = CString::new("infer").unwrap();
let rc = unsafe { rlx_node_start(0, 1, std::ptr::null(), dev.as_ptr(), mode.as_ptr()) };
assert_eq!(rc, RLX_NODE_ERR_ARG);
}
#[test]
fn status_is_idle_before_start() {
let _lock = ffi_lock();
assert_eq!(status(), "idle");
}
#[test]
fn stop_is_safe_with_no_node() {
let _lock = ffi_lock();
assert_eq!(rlx_node_stop(), RLX_NODE_OK);
}
#[test]
fn platform_tag_is_nonempty() {
let p = rlx_node_platform();
assert!(!p.is_null());
let s = unsafe { CStr::from_ptr(p) }.to_string_lossy();
assert!(!s.is_empty() && s != "unknown", "platform tag: {s}");
}
#[test]
fn status_buffer_truncates_safely() {
let _lock = ffi_lock();
let mut buf = [0 as c_char; 3];
let n = unsafe { rlx_node_status(buf.as_mut_ptr(), buf.len()) };
assert_eq!(n, 2);
let s = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_string_lossy();
assert_eq!(s, "id"); }
}