use std::sync::{Mutex, MutexGuard};
use crate::{BackendError, IpcBackend};
const DEFAULT_READOUT_IPC: &str = "ipc:///tmp/corpus_ipc_readout.ipc";
struct ExclusiveSocket {
socket: zmq::Socket,
}
impl ExclusiveSocket {
fn recv_bytes_dontwait(&mut self) -> zmq::Result<Vec<u8>> {
self.socket.recv_bytes(zmq::DONTWAIT)
}
}
#[deprecated(since = "0.1.0", note = "renamed to ZmqIpcBackend")]
pub type ZmqRuntimeBackend = ZmqIpcBackend;
pub struct ZmqIpcBackend {
context: zmq::Context,
sub_socket: Mutex<Option<ExclusiveSocket>>,
initialized: bool,
pub(crate) last_readout: Vec<f32>,
pub tick: i64,
}
impl ZmqIpcBackend {
pub fn new() -> Self {
Self {
context: zmq::Context::new(),
sub_socket: Mutex::new(None),
initialized: false,
last_readout: Vec::new(),
tick: 0,
}
}
pub fn tick(&self) -> i64 {
self.tick
}
fn lock_sub_socket(&self) -> MutexGuard<'_, Option<ExclusiveSocket>> {
self.sub_socket
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn connect_sub(&mut self, endpoint: &str) -> Result<(), BackendError> {
let socket = self
.context
.socket(zmq::SUB)
.map_err(|e| BackendError::InitializationError(format!("ZMQ SUB socket: {e}")))?;
socket
.set_subscribe(b"")
.map_err(|e| BackendError::InitializationError(format!("ZMQ subscribe: {e}")))?;
socket
.set_rcvhwm(16)
.map_err(|e| BackendError::InitializationError(format!("ZMQ rcvhwm: {e}")))?;
socket.connect(endpoint).map_err(|e| {
BackendError::InitializationError(format!(
"ZMQ connect to {endpoint}: {e} (is the IPC producer running?)"
))
})?;
*self.lock_sub_socket() = Some(ExclusiveSocket { socket });
Ok(())
}
#[cfg(test)]
fn initialize_at(&mut self, endpoint: &str) -> Result<(), BackendError> {
if self.initialized {
return Ok(());
}
self.connect_sub(endpoint)?;
self.initialized = true;
Ok(())
}
fn receive_readout(&mut self) -> Result<Vec<f32>, BackendError> {
let recv_result = {
let mut guard = self.lock_sub_socket();
let socket = guard.as_mut().ok_or_else(|| {
BackendError::CommunicationError("SUB socket not connected".to_string())
})?;
socket.recv_bytes_dontwait()
};
match recv_result {
Ok(buf) if buf.len() >= 8 && (buf.len() - 8).is_multiple_of(4) => {
self.tick = i64::from_le_bytes(buf[0..8].try_into().unwrap());
let num_floats = (buf.len() - 8) / 4;
self.last_readout.resize(num_floats, 0.0);
for i in 0..num_floats {
let off = 8 + i * 4;
self.last_readout[i] =
f32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
}
}
Ok(buf) => {
eprintln!("[zmq-ipc] Unexpected packet size: {} bytes", buf.len());
}
Err(zmq::Error::EAGAIN) => {
}
Err(e) => {
return Err(BackendError::CommunicationError(format!(
"ZMQ recv failed: {e}"
)));
}
}
Ok(self.last_readout.clone())
}
}
impl Default for ZmqIpcBackend {
fn default() -> Self {
Self::new()
}
}
impl IpcBackend for ZmqIpcBackend {
fn process_batch(&mut self, _inputs: &[f32]) -> Result<Vec<f32>, BackendError> {
if !self.initialized {
return Err(BackendError::InitializationError(
"ZmqIpcBackend not initialized — call initialize() first".to_string(),
));
}
self.receive_readout()
}
fn initialize(&mut self, _model_path: Option<&str>) -> Result<(), BackendError> {
if self.initialized {
return Ok(());
}
let endpoint = std::env::var("CORPUS_IPC_ZMQ_READOUT_IPC")
.unwrap_or_else(|_| DEFAULT_READOUT_IPC.to_string());
self.connect_sub(&endpoint)?;
self.initialized = true;
println!("[zmq-ipc] Connected to IPC producer at {}", endpoint);
Ok(())
}
fn save_state(&self, _model_path: &str) -> Result<(), BackendError> {
Ok(())
}
fn get_spike_states(&self) -> Vec<bool> {
self.last_readout.iter().map(|&v| v > 0.5).collect()
}
fn reset(&mut self) -> Result<(), BackendError> {
self.last_readout.clear();
self.tick = 0;
self.initialized = false;
*self.lock_sub_socket() = None;
println!("[zmq-ipc] Readout cache reset; will re-initialize on next call");
Ok(())
}
}
const _: fn() = _assert_intended_send_sync_surface;
fn _assert_intended_send_sync_surface() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn assert_ipc_backend<T: IpcBackend>() {}
assert_send::<zmq::Socket>();
assert_send::<ExclusiveSocket>();
assert_send::<ZmqIpcBackend>();
assert_sync::<ZmqIpcBackend>();
assert_ipc_backend::<ZmqIpcBackend>();
let _ = <zmq::Socket as _assert_not_sync::IfSyncThenAmbiguous<_>>::witness;
let _ = <ExclusiveSocket as _assert_not_sync::IfSyncThenAmbiguous<_>>::witness;
}
mod _assert_not_sync {
pub trait IfSyncThenAmbiguous<Marker> {
fn witness() {}
}
pub struct NotSyncToken;
pub struct SyncToken;
impl<T: ?Sized> IfSyncThenAmbiguous<NotSyncToken> for T {}
impl<T: ?Sized + Sync> IfSyncThenAmbiguous<SyncToken> for T {}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use super::*;
fn unique_test_endpoint() -> String {
static COUNTER: AtomicU64 = AtomicU64::new(0);
format!(
"ipc:///tmp/corpus_ipc_zmq_test_{}_{}.ipc",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
)
}
fn make_packet(tick: i64, readout: &[f32]) -> Vec<u8> {
let mut buf = Vec::with_capacity(8 + readout.len() * 4);
buf.extend_from_slice(&tick.to_le_bytes());
for v in readout {
buf.extend_from_slice(&v.to_le_bytes());
}
buf
}
#[test]
fn parse_dynamic_packet() {
let readout: Vec<f32> = (0..20).map(|i| i as f32 * 0.1).collect();
let tick: i64 = 42_000;
let buf = make_packet(tick, &readout);
let mut b = ZmqIpcBackend::new();
b.tick = i64::from_le_bytes(buf[0..8].try_into().unwrap());
let num_floats = (buf.len() - 8) / 4;
b.last_readout.resize(num_floats, 0.0);
for i in 0..num_floats {
let off = 8 + i * 4;
b.last_readout[i] = f32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
}
assert_eq!(b.tick, tick);
assert_eq!(b.last_readout.len(), 20);
for (i, val) in readout.iter().enumerate().take(20) {
assert!((b.last_readout[i] - val).abs() < 1e-5);
}
}
#[test]
#[allow(deprecated)]
fn deprecated_alias_is_same_type() {
fn same(backend: crate::ZmqRuntimeBackend) -> ZmqIpcBackend {
backend
}
let _ = same(ZmqIpcBackend::new());
}
#[test]
fn malformed_packet_does_not_mutate_state() {
let mut b = ZmqIpcBackend::new();
b.last_readout = vec![1.0, 2.0];
b.tick = 100;
let initial_readout = b.last_readout.clone();
let initial_tick = b.tick;
let bad_buf: &[u8] = &[0xDE, 0xAD, 0xBE, 0xEF, 0x00];
if bad_buf.len() < 8 || !(bad_buf.len() - 8).is_multiple_of(4) {
eprintln!("[test] Malformed packet received");
} else {
}
assert_eq!(b.last_readout, initial_readout);
assert_eq!(b.tick, initial_tick);
}
#[test]
fn initialize_nonblocking_recv_and_drop() {
let mut backend = ZmqIpcBackend::new();
backend
.initialize_at(&unique_test_endpoint())
.expect("ZMQ connect is asynchronous; a publisher need not be bound");
let out = backend.process_batch(&[1.0]).unwrap();
assert!(out.is_empty());
assert_eq!(backend.tick(), 0);
drop(backend);
}
#[test]
fn connected_backend_can_move_to_owner_thread() {
let mut backend = ZmqIpcBackend::new();
backend.initialize_at(&unique_test_endpoint()).unwrap();
let readout = std::thread::spawn(move || {
let out = backend.process_batch(&[]).unwrap();
drop(backend);
out
})
.join()
.expect("owner thread panicked");
assert!(readout.is_empty());
}
#[test]
fn uninitialized_backend_can_move_then_initialize() {
let backend = ZmqIpcBackend::new();
let endpoint = unique_test_endpoint();
std::thread::spawn(move || {
let mut backend = backend;
backend.initialize_at(&endpoint).unwrap();
let out = backend.process_batch(&[]).unwrap();
assert!(out.is_empty());
})
.join()
.expect("owner thread panicked");
}
#[test]
fn repeated_construct_initialize_process_drop() {
for _ in 0..8 {
let mut backend = ZmqIpcBackend::new();
backend.initialize_at(&unique_test_endpoint()).unwrap();
let out = backend.process_batch(&[]).unwrap();
assert!(out.is_empty());
backend.reset().unwrap();
drop(backend);
}
}
#[test]
fn backend_is_usable_behind_arc_mutex() {
let backend = Arc::new(Mutex::new(ZmqIpcBackend::new()));
{
let mut guard = backend.lock().unwrap();
guard.initialize_at(&unique_test_endpoint()).unwrap();
assert!(guard.process_batch(&[]).unwrap().is_empty());
}
let moved = Arc::clone(&backend);
std::thread::spawn(move || {
let mut guard = moved.lock().unwrap();
assert!(guard.process_batch(&[]).unwrap().is_empty());
guard.reset().unwrap();
})
.join()
.expect("mutex owner thread panicked");
}
#[test]
fn thread_safety_surface_is_the_documented_one() {
_assert_intended_send_sync_surface();
}
}