1use std::ffi::c_char;
15use std::ptr;
16use std::sync::Arc;
17
18use pamoja_core::{Receive, Transport};
19use pamoja_loopback::{LoopbackBroker, LoopbackTransport};
20use tokio::sync::Mutex;
21
22use crate::transport::{receive_within, status, Kind, PamojaMessage, PamojaTransport};
23use crate::{read_bytes, read_str, runtime, set_last_error, PamojaStatus};
24
25pub struct PamojaLoopbackBroker {
30 inner: LoopbackBroker,
31}
32
33pub struct PamojaLoopbackTransport {
35 inner: Arc<Mutex<LoopbackTransport>>,
36}
37
38#[no_mangle]
44pub extern "C" fn pamoja_loopback_broker_new() -> *mut PamojaLoopbackBroker {
45 Box::into_raw(Box::new(PamojaLoopbackBroker {
46 inner: LoopbackBroker::new(),
47 }))
48}
49
50#[no_mangle]
71pub unsafe extern "C" fn pamoja_loopback_broker_set_reachable(
72 broker: *const PamojaLoopbackBroker,
73 reachable: bool,
74) -> PamojaStatus {
75 match broker_handle(broker) {
76 Some(broker) => {
77 broker.inner.set_reachable(reachable);
78 PamojaStatus::Ok
79 }
80 None => PamojaStatus::InvalidArgument,
81 }
82}
83
84#[no_mangle]
98pub unsafe extern "C" fn pamoja_loopback_broker_is_reachable(
99 broker: *const PamojaLoopbackBroker,
100) -> bool {
101 broker_handle(broker).is_some_and(|broker| broker.inner.is_reachable())
102}
103
104#[no_mangle]
116pub unsafe extern "C" fn pamoja_loopback_broker_free(broker: *mut PamojaLoopbackBroker) {
117 if !broker.is_null() {
118 drop(Box::from_raw(broker));
119 }
120}
121
122#[no_mangle]
138pub unsafe extern "C" fn pamoja_loopback_transport_new(
139 broker: *const PamojaLoopbackBroker,
140) -> *mut PamojaLoopbackTransport {
141 let Some(broker) = broker_handle(broker) else {
142 return ptr::null_mut();
143 };
144 Box::into_raw(Box::new(PamojaLoopbackTransport {
145 inner: Arc::new(Mutex::new(LoopbackTransport::new(broker.inner.clone()))),
146 }))
147}
148
149#[no_mangle]
163pub unsafe extern "C" fn pamoja_loopback_transport_connect(
164 transport: *mut PamojaLoopbackTransport,
165) -> PamojaStatus {
166 let Some(transport) = transport_handle(transport) else {
167 return PamojaStatus::InvalidArgument;
168 };
169 let inner = Arc::clone(&transport.inner);
170 status(runtime().block_on(async move { inner.lock().await.connect().await }))
171}
172
173#[no_mangle]
192pub unsafe extern "C" fn pamoja_loopback_transport_send(
193 transport: *mut PamojaLoopbackTransport,
194 topic: *const c_char,
195 payload: *const u8,
196 payload_len: usize,
197) -> PamojaStatus {
198 let Some(transport) = transport_handle(transport) else {
199 return PamojaStatus::InvalidArgument;
200 };
201 let Some(topic) = read_str(topic, "topic") else {
202 return PamojaStatus::InvalidArgument;
203 };
204 let payload = match read_bytes(payload, payload_len) {
205 Ok(payload) => payload,
206 Err(status) => return status,
207 };
208 let inner = Arc::clone(&transport.inner);
209 let topic = topic.to_owned();
210 status(runtime().block_on(async move { inner.lock().await.send(&topic, &payload).await }))
211}
212
213#[no_mangle]
229pub unsafe extern "C" fn pamoja_loopback_transport_subscribe(
230 transport: *mut PamojaLoopbackTransport,
231 topic: *const c_char,
232) -> PamojaStatus {
233 let Some(transport) = transport_handle(transport) else {
234 return PamojaStatus::InvalidArgument;
235 };
236 let Some(topic) = read_str(topic, "topic") else {
237 return PamojaStatus::InvalidArgument;
238 };
239 let inner = Arc::clone(&transport.inner);
240 let topic = topic.to_owned();
241 status(runtime().block_on(async move { inner.lock().await.subscribe(&topic).await }))
242}
243
244#[no_mangle]
260pub unsafe extern "C" fn pamoja_loopback_transport_recv(
261 transport: *mut PamojaLoopbackTransport,
262 out_message: *mut *mut PamojaMessage,
263) -> PamojaStatus {
264 let Some(transport) = transport_handle(transport) else {
265 return PamojaStatus::InvalidArgument;
266 };
267 if out_message.is_null() {
268 set_last_error("out_message must not be null".to_owned());
269 return PamojaStatus::InvalidArgument;
270 }
271 *out_message = ptr::null_mut();
272
273 let inner = Arc::clone(&transport.inner);
274 match runtime().block_on(async move { inner.lock().await.recv().await }) {
275 Ok(Some(message)) => {
276 *out_message = PamojaMessage::into_raw(message.topic, message.payload);
277 PamojaStatus::Ok
278 }
279 Ok(None) => PamojaStatus::Ok,
280 Err(error) => {
281 let code = PamojaStatus::from_error(&error);
282 set_last_error(error.to_string());
283 code
284 }
285 }
286}
287
288#[no_mangle]
310pub unsafe extern "C" fn pamoja_loopback_transport_recv_within(
311 transport: *mut PamojaLoopbackTransport,
312 timeout_ms: u64,
313 out_message: *mut *mut PamojaMessage,
314 out_timed_out: *mut bool,
315) -> PamojaStatus {
316 let Some(transport) = transport_handle(transport) else {
317 return PamojaStatus::InvalidArgument;
318 };
319 let inner = Arc::clone(&transport.inner);
320 receive_within(timeout_ms, out_message, out_timed_out, || async move {
321 inner.lock().await.recv().await
322 })
323}
324
325#[no_mangle]
340pub unsafe extern "C" fn pamoja_loopback_transport_is_connected(
341 transport: *mut PamojaLoopbackTransport,
342) -> bool {
343 let Some(transport) = transport_handle(transport) else {
344 return false;
345 };
346 let inner = Arc::clone(&transport.inner);
347 runtime().block_on(async move { inner.lock().await.is_connected() })
348}
349
350#[no_mangle]
361pub unsafe extern "C" fn pamoja_loopback_transport_disconnect(
362 transport: *mut PamojaLoopbackTransport,
363) {
364 let Some(transport) = transport_handle(transport) else {
365 return;
366 };
367 let inner = Arc::clone(&transport.inner);
368 runtime().block_on(async move { inner.lock().await.disconnect() });
369}
370
371#[no_mangle]
380pub unsafe extern "C" fn pamoja_loopback_transport_free(transport: *mut PamojaLoopbackTransport) {
381 if !transport.is_null() {
382 drop(Box::from_raw(transport));
383 }
384}
385
386#[no_mangle]
402pub unsafe extern "C" fn pamoja_transport_loopback(
403 broker: *const PamojaLoopbackBroker,
404) -> *mut PamojaTransport {
405 let Some(broker) = broker_handle(broker) else {
406 return ptr::null_mut();
407 };
408 PamojaTransport::into_raw(Kind::Loopback(LoopbackTransport::new(broker.inner.clone())))
409}
410
411unsafe fn broker_handle<'a>(
417 broker: *const PamojaLoopbackBroker,
418) -> Option<&'a PamojaLoopbackBroker> {
419 if broker.is_null() {
420 set_last_error("broker must not be null".to_owned());
421 return None;
422 }
423 Some(&*broker)
424}
425
426unsafe fn transport_handle<'a>(
433 transport: *mut PamojaLoopbackTransport,
434) -> Option<&'a PamojaLoopbackTransport> {
435 if transport.is_null() {
436 set_last_error("transport must not be null".to_owned());
437 return None;
438 }
439 Some(&*transport)
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use crate::transport::{
446 pamoja_message_free, pamoja_message_payload, pamoja_message_payload_len,
447 };
448
449 unsafe fn take(message: *mut PamojaMessage) -> Vec<u8> {
451 assert!(!message.is_null());
452 let bytes = std::slice::from_raw_parts(
453 pamoja_message_payload(message),
454 pamoja_message_payload_len(message),
455 )
456 .to_vec();
457 pamoja_message_free(message);
458 bytes
459 }
460
461 #[test]
462 fn a_published_message_reaches_a_subscriber() {
463 unsafe {
464 let broker = pamoja_loopback_broker_new();
465 let publisher = pamoja_loopback_transport_new(broker);
466 let subscriber = pamoja_loopback_transport_new(broker);
467
468 assert_eq!(
469 pamoja_loopback_transport_connect(publisher),
470 PamojaStatus::Ok
471 );
472 assert_eq!(
473 pamoja_loopback_transport_connect(subscriber),
474 PamojaStatus::Ok
475 );
476 assert!(pamoja_loopback_transport_is_connected(publisher));
477
478 let topic = std::ffi::CString::new("sensors/1").expect("static");
479 assert_eq!(
480 pamoja_loopback_transport_subscribe(subscriber, topic.as_ptr()),
481 PamojaStatus::Ok
482 );
483 assert_eq!(
484 pamoja_loopback_transport_send(publisher, topic.as_ptr(), b"21.5".as_ptr(), 4),
485 PamojaStatus::Ok
486 );
487
488 let mut message = ptr::null_mut();
489 assert_eq!(
490 pamoja_loopback_transport_recv(subscriber, &mut message),
491 PamojaStatus::Ok
492 );
493 assert_eq!(take(message), b"21.5");
494
495 pamoja_loopback_transport_free(subscriber);
496 pamoja_loopback_transport_free(publisher);
497 pamoja_loopback_broker_free(broker);
498 }
499 }
500
501 #[test]
502 fn a_disconnected_link_refuses_to_send() {
503 unsafe {
504 let broker = pamoja_loopback_broker_new();
505 let transport = pamoja_loopback_transport_new(broker);
506 pamoja_loopback_transport_connect(transport);
507 pamoja_loopback_transport_disconnect(transport);
508 assert!(!pamoja_loopback_transport_is_connected(transport));
509
510 let topic = std::ffi::CString::new("sensors/1").expect("static");
511 assert_ne!(
512 pamoja_loopback_transport_send(transport, topic.as_ptr(), b"x".as_ptr(), 1),
513 PamojaStatus::Ok
514 );
515
516 pamoja_loopback_transport_free(transport);
517 pamoja_loopback_broker_free(broker);
518 }
519 }
520
521 #[test]
522 fn a_timed_receive_runs_out_and_leaves_the_next_message_queued() {
523 use crate::transport::{
524 pamoja_transport_connect, pamoja_transport_free, pamoja_transport_recv_within,
525 pamoja_transport_subscribe,
526 };
527
528 unsafe {
529 let broker = pamoja_loopback_broker_new();
530 let topic = std::ffi::CString::new("sensors/1").expect("static");
531 let publisher = pamoja_loopback_transport_new(broker);
532 let link = pamoja_loopback_transport_new(broker);
533 let composed = pamoja_transport_loopback(broker);
534 pamoja_loopback_transport_connect(publisher);
535 pamoja_loopback_transport_connect(link);
536 pamoja_loopback_transport_subscribe(link, topic.as_ptr());
537 pamoja_transport_connect(composed);
538 pamoja_transport_subscribe(composed, topic.as_ptr());
539
540 let mut message = ptr::null_mut();
541 let mut timed_out = false;
542 assert_eq!(
543 pamoja_loopback_transport_recv_within(link, 20, &mut message, &mut timed_out),
544 PamojaStatus::Ok
545 );
546 assert!(timed_out && message.is_null());
547 assert_eq!(
548 pamoja_transport_recv_within(composed, 20, &mut message, &mut timed_out),
549 PamojaStatus::Ok
550 );
551 assert!(timed_out && message.is_null());
552
553 pamoja_loopback_transport_send(publisher, topic.as_ptr(), b"21.5".as_ptr(), 4);
554 assert_eq!(
555 pamoja_loopback_transport_recv_within(link, 1000, &mut message, &mut timed_out),
556 PamojaStatus::Ok
557 );
558 assert!(!timed_out);
559 assert_eq!(take(message), b"21.5");
560 assert_eq!(
561 pamoja_transport_recv_within(composed, 1000, &mut message, &mut timed_out),
562 PamojaStatus::Ok
563 );
564 assert!(!timed_out);
565 assert_eq!(take(message), b"21.5");
566
567 pamoja_loopback_transport_disconnect(link);
568 assert_eq!(
569 pamoja_loopback_transport_recv_within(link, 20, &mut message, &mut timed_out),
570 PamojaStatus::Closed
571 );
572 assert_eq!(
573 pamoja_loopback_transport_recv_within(link, 20, &mut message, ptr::null_mut()),
574 PamojaStatus::InvalidArgument
575 );
576
577 pamoja_transport_free(composed);
578 pamoja_loopback_transport_free(link);
579 pamoja_loopback_transport_free(publisher);
580 pamoja_loopback_broker_free(broker);
581 }
582 }
583
584 #[test]
585 fn a_null_handle_is_refused_rather_than_dereferenced() {
586 unsafe {
587 assert!(pamoja_loopback_transport_new(ptr::null()).is_null());
588 assert!(!pamoja_loopback_transport_is_connected(ptr::null_mut()));
589 assert_eq!(
590 pamoja_loopback_transport_connect(ptr::null_mut()),
591 PamojaStatus::InvalidArgument
592 );
593 pamoja_loopback_transport_free(ptr::null_mut());
594 pamoja_loopback_broker_free(ptr::null_mut());
595 }
596 }
597}