1use std::ffi::c_char;
15use std::ptr;
16
17use pamoja_ladder::{Delivery, TransportLadder};
18
19use crate::sync::{take_store, PamojaStore, StoreKind};
20use crate::transport::{receive_within, take_transport, PamojaMessage, PamojaTransport};
21use crate::{read_bytes, read_str, runtime, set_last_error, PamojaStatus};
22
23#[repr(C)]
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum PamojaDelivery {
27 Sent = 0,
29 Buffered = 1,
31}
32
33pub struct PamojaLadder {
40 inner: Option<TransportLadder<StoreKind>>,
41}
42
43#[no_mangle]
60pub unsafe extern "C" fn pamoja_ladder_new(store: *mut PamojaStore) -> *mut PamojaLadder {
61 let Some(store) = take_store(store) else {
62 return ptr::null_mut();
63 };
64 Box::into_raw(Box::new(PamojaLadder {
65 inner: Some(TransportLadder::new(store)),
66 }))
67}
68
69#[no_mangle]
91pub unsafe extern "C" fn pamoja_ladder_rung(
92 ladder: *mut PamojaLadder,
93 transport: *mut PamojaTransport,
94) -> PamojaStatus {
95 add(ladder, transport, false)
96}
97
98#[no_mangle]
119pub unsafe extern "C" fn pamoja_ladder_uplink(
120 ladder: *mut PamojaLadder,
121 transport: *mut PamojaTransport,
122) -> PamojaStatus {
123 add(ladder, transport, true)
124}
125
126unsafe fn add(
133 ladder: *mut PamojaLadder,
134 transport: *mut PamojaTransport,
135 uplink: bool,
136) -> PamojaStatus {
137 let Some(handle) = ladder_handle(ladder) else {
138 drop(take_transport(transport));
141 return PamojaStatus::InvalidArgument;
142 };
143 let Some(transport) = take_transport(transport) else {
144 return PamojaStatus::InvalidArgument;
145 };
146 let Some(inner) = handle.inner.take() else {
147 set_last_error("this ladder is no longer usable".to_owned());
148 return PamojaStatus::InvalidArgument;
149 };
150 handle.inner = Some(if uplink || !transport.listens() {
151 inner.uplink(transport)
152 } else {
153 inner.rung(transport)
154 });
155 PamojaStatus::Ok
156}
157
158#[no_mangle]
175pub unsafe extern "C" fn pamoja_ladder_connect(ladder: *mut PamojaLadder) -> PamojaStatus {
176 let Some(inner) = ladder_inner(ladder) else {
177 return PamojaStatus::InvalidArgument;
178 };
179 match runtime().block_on(inner.connect()) {
180 Ok(()) => PamojaStatus::Ok,
181 Err(error) => fail(error),
182 }
183}
184
185#[no_mangle]
206pub unsafe extern "C" fn pamoja_ladder_send(
207 ladder: *mut PamojaLadder,
208 topic: *const c_char,
209 payload: *const u8,
210 payload_len: usize,
211 out_delivery: *mut PamojaDelivery,
212) -> PamojaStatus {
213 let Some(topic) = read_str(topic, "topic") else {
214 return PamojaStatus::InvalidArgument;
215 };
216 let payload = match read_bytes(payload, payload_len) {
217 Ok(payload) => payload,
218 Err(status) => return status,
219 };
220 let Some(inner) = ladder_inner(ladder) else {
221 return PamojaStatus::InvalidArgument;
222 };
223 match runtime().block_on(inner.send(topic, &payload)) {
224 Ok(delivery) => {
225 if !out_delivery.is_null() {
226 *out_delivery = match delivery {
227 Delivery::Sent => PamojaDelivery::Sent,
228 Delivery::Buffered => PamojaDelivery::Buffered,
229 };
230 }
231 PamojaStatus::Ok
232 }
233 Err(error) => fail(error),
234 }
235}
236
237#[no_mangle]
252pub unsafe extern "C" fn pamoja_ladder_flush(
253 ladder: *mut PamojaLadder,
254 out_sent: *mut usize,
255) -> PamojaStatus {
256 let Some(inner) = ladder_inner(ladder) else {
257 return PamojaStatus::InvalidArgument;
258 };
259 match runtime().block_on(inner.flush()) {
260 Ok(sent) => {
261 if !out_sent.is_null() {
262 *out_sent = sent;
263 }
264 PamojaStatus::Ok
265 }
266 Err(error) => fail(error),
267 }
268}
269
270#[no_mangle]
285pub unsafe extern "C" fn pamoja_ladder_buffered(
286 ladder: *mut PamojaLadder,
287 out_count: *mut usize,
288) -> PamojaStatus {
289 let Some(inner) = ladder_inner(ladder) else {
290 return PamojaStatus::InvalidArgument;
291 };
292 if out_count.is_null() {
293 set_last_error("out_count must not be null".to_owned());
294 return PamojaStatus::InvalidArgument;
295 }
296 match runtime().block_on(inner.buffered()) {
297 Ok(count) => {
298 *out_count = count;
299 PamojaStatus::Ok
300 }
301 Err(error) => fail(error),
302 }
303}
304
305#[no_mangle]
326pub unsafe extern "C" fn pamoja_ladder_subscribe(
327 ladder: *mut PamojaLadder,
328 topic: *const c_char,
329) -> PamojaStatus {
330 let Some(topic) = read_str(topic, "topic") else {
331 return PamojaStatus::InvalidArgument;
332 };
333 let Some(inner) = ladder_inner(ladder) else {
334 return PamojaStatus::InvalidArgument;
335 };
336 match runtime().block_on(inner.subscribe(topic)) {
337 Ok(()) => PamojaStatus::Ok,
338 Err(error) => fail(error),
339 }
340}
341
342#[no_mangle]
364pub unsafe extern "C" fn pamoja_ladder_recv(
365 ladder: *mut PamojaLadder,
366 out_message: *mut *mut PamojaMessage,
367) -> PamojaStatus {
368 let Some(inner) = ladder_inner(ladder) else {
369 return PamojaStatus::InvalidArgument;
370 };
371 if out_message.is_null() {
372 set_last_error("out_message must not be null".to_owned());
373 return PamojaStatus::InvalidArgument;
374 }
375 *out_message = ptr::null_mut();
376 match runtime().block_on(inner.recv()) {
377 Ok(Some(message)) => {
378 *out_message = PamojaMessage::into_raw(message.topic, message.payload);
379 PamojaStatus::Ok
380 }
381 Ok(None) => PamojaStatus::Ok,
382 Err(error) => fail(error),
383 }
384}
385
386#[no_mangle]
410pub unsafe extern "C" fn pamoja_ladder_recv_within(
411 ladder: *mut PamojaLadder,
412 timeout_ms: u64,
413 out_message: *mut *mut PamojaMessage,
414 out_timed_out: *mut bool,
415) -> PamojaStatus {
416 let Some(inner) = ladder_inner(ladder) else {
417 return PamojaStatus::InvalidArgument;
418 };
419 receive_within(timeout_ms, out_message, out_timed_out, || inner.recv())
420}
421
422#[no_mangle]
431pub unsafe extern "C" fn pamoja_ladder_free(ladder: *mut PamojaLadder) {
432 if !ladder.is_null() {
433 drop(Box::from_raw(ladder));
434 }
435}
436
437unsafe fn ladder_handle<'a>(ladder: *mut PamojaLadder) -> Option<&'a mut PamojaLadder> {
443 if ladder.is_null() {
444 set_last_error("ladder must not be null".to_owned());
445 return None;
446 }
447 Some(&mut *ladder)
448}
449
450unsafe fn ladder_inner<'a>(
456 ladder: *mut PamojaLadder,
457) -> Option<&'a mut TransportLadder<StoreKind>> {
458 let handle = ladder_handle(ladder)?;
459 match handle.inner.as_mut() {
460 Some(inner) => Some(inner),
461 None => {
462 set_last_error("this ladder is no longer usable".to_owned());
463 None
464 }
465 }
466}
467
468fn fail(error: pamoja_core::Error) -> PamojaStatus {
470 let status = PamojaStatus::from_error(&error);
471 set_last_error(error.to_string());
472 status
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use crate::loopback::{
479 pamoja_loopback_broker_free, pamoja_loopback_broker_is_reachable,
480 pamoja_loopback_broker_new, pamoja_loopback_broker_set_reachable,
481 pamoja_loopback_transport_connect, pamoja_loopback_transport_free,
482 pamoja_loopback_transport_new, pamoja_loopback_transport_recv,
483 pamoja_loopback_transport_send, pamoja_loopback_transport_subscribe,
484 pamoja_transport_loopback,
485 };
486 use crate::sync::pamoja_store_memory;
487 use crate::transport::{
488 pamoja_message_free, pamoja_message_payload, pamoja_message_payload_len,
489 pamoja_transport_faulty,
490 };
491
492 #[test]
493 fn a_message_no_rung_takes_is_buffered_rather_than_lost() {
494 unsafe {
495 let ladder = pamoja_ladder_new(pamoja_store_memory(0));
496 let topic = std::ffi::CString::new("sensors/1").expect("static");
497
498 let mut delivery = PamojaDelivery::Sent;
499 assert_eq!(
500 pamoja_ladder_send(ladder, topic.as_ptr(), b"21.5".as_ptr(), 4, &mut delivery),
501 PamojaStatus::Ok,
502 "buffering is a success, not a failure"
503 );
504 assert_eq!(delivery, PamojaDelivery::Buffered);
505
506 let mut waiting = 0;
507 assert_eq!(
508 pamoja_ladder_buffered(ladder, &mut waiting),
509 PamojaStatus::Ok
510 );
511 assert_eq!(waiting, 1);
512
513 pamoja_ladder_free(ladder);
514 }
515 }
516
517 #[test]
518 fn a_rung_that_refuses_falls_through_to_the_next() {
519 unsafe {
520 let broker = pamoja_loopback_broker_new();
521 let listener = pamoja_loopback_transport_new(broker);
522 pamoja_loopback_transport_connect(listener);
523 let topic = std::ffi::CString::new("sensors/1").expect("static");
524 pamoja_loopback_transport_subscribe(listener, topic.as_ptr());
525
526 let failing = pamoja_transport_faulty(pamoja_transport_loopback(broker), 1);
528 let working = pamoja_transport_loopback(broker);
529
530 let ladder = pamoja_ladder_new(pamoja_store_memory(0));
531 assert_eq!(pamoja_ladder_rung(ladder, failing), PamojaStatus::Ok);
532 assert_eq!(pamoja_ladder_rung(ladder, working), PamojaStatus::Ok);
533 assert_eq!(pamoja_ladder_connect(ladder), PamojaStatus::Ok);
534
535 let mut delivery = PamojaDelivery::Buffered;
536 assert_eq!(
537 pamoja_ladder_send(ladder, topic.as_ptr(), b"21.5".as_ptr(), 4, &mut delivery),
538 PamojaStatus::Ok
539 );
540 assert_eq!(
541 delivery,
542 PamojaDelivery::Sent,
543 "the second rung carried what the first refused"
544 );
545
546 let mut message = ptr::null_mut();
547 pamoja_loopback_transport_recv(listener, &mut message);
548 assert!(!message.is_null());
549 let payload = std::slice::from_raw_parts(
550 pamoja_message_payload(message),
551 pamoja_message_payload_len(message),
552 )
553 .to_vec();
554 assert_eq!(payload, b"21.5");
555 pamoja_message_free(message);
556
557 pamoja_ladder_free(ladder);
558 pamoja_loopback_transport_free(listener);
559 pamoja_loopback_broker_free(broker);
560 }
561 }
562
563 #[test]
564 fn a_flush_replays_what_was_buffered() {
565 unsafe {
566 let broker = pamoja_loopback_broker_new();
567 let listener = pamoja_loopback_transport_new(broker);
568 pamoja_loopback_transport_connect(listener);
569 let topic = std::ffi::CString::new("sensors/1").expect("static");
570 pamoja_loopback_transport_subscribe(listener, topic.as_ptr());
571
572 let ladder = pamoja_ladder_new(pamoja_store_memory(0));
573
574 pamoja_ladder_send(ladder, topic.as_ptr(), b"one".as_ptr(), 3, ptr::null_mut());
576 pamoja_ladder_send(ladder, topic.as_ptr(), b"two".as_ptr(), 3, ptr::null_mut());
577
578 assert_eq!(
580 pamoja_ladder_rung(ladder, pamoja_transport_loopback(broker)),
581 PamojaStatus::Ok
582 );
583 assert_eq!(pamoja_ladder_connect(ladder), PamojaStatus::Ok);
584
585 let mut sent = 0;
586 assert_eq!(pamoja_ladder_flush(ladder, &mut sent), PamojaStatus::Ok);
587 assert_eq!(sent, 2, "both buffered messages went out");
588
589 let mut waiting = 1;
590 pamoja_ladder_buffered(ladder, &mut waiting);
591 assert_eq!(waiting, 0, "and the buffer is empty");
592
593 pamoja_ladder_free(ladder);
594 pamoja_loopback_transport_free(listener);
595 pamoja_loopback_broker_free(broker);
596 }
597 }
598
599 #[test]
600 fn a_command_comes_back_through_the_ladder() {
601 unsafe {
602 let broker = pamoja_loopback_broker_new();
603 let ladder = pamoja_ladder_new(pamoja_store_memory(0));
604 assert_eq!(
605 pamoja_ladder_rung(ladder, pamoja_transport_loopback(broker)),
606 PamojaStatus::Ok
607 );
608 assert_eq!(pamoja_ladder_connect(ladder), PamojaStatus::Ok);
609 let topic = std::ffi::CString::new("commands/1").expect("static");
610 assert_eq!(
611 pamoja_ladder_subscribe(ladder, topic.as_ptr()),
612 PamojaStatus::Ok
613 );
614
615 let upstream = pamoja_loopback_transport_new(broker);
616 pamoja_loopback_transport_connect(upstream);
617 pamoja_loopback_transport_send(upstream, topic.as_ptr(), b"open".as_ptr(), 4);
618
619 let mut message = ptr::null_mut();
620 assert_eq!(pamoja_ladder_recv(ladder, &mut message), PamojaStatus::Ok);
621 assert!(
622 !message.is_null(),
623 "the command came back through the ladder"
624 );
625 let payload = std::slice::from_raw_parts(
626 pamoja_message_payload(message),
627 pamoja_message_payload_len(message),
628 )
629 .to_vec();
630 assert_eq!(payload, b"open");
631 pamoja_message_free(message);
632
633 pamoja_ladder_free(ladder);
634 pamoja_loopback_transport_free(upstream);
635 pamoja_loopback_broker_free(broker);
636 }
637 }
638
639 #[test]
640 fn a_timed_receive_runs_out_and_leaves_the_next_message_queued() {
641 unsafe {
642 let broker = pamoja_loopback_broker_new();
643 let ladder = pamoja_ladder_new(pamoja_store_memory(0));
644 pamoja_ladder_rung(ladder, pamoja_transport_loopback(broker));
645 pamoja_ladder_connect(ladder);
646 let topic = std::ffi::CString::new("commands/1").expect("static");
647 pamoja_ladder_subscribe(ladder, topic.as_ptr());
648
649 let mut message = ptr::null_mut();
650 let mut timed_out = false;
651 assert_eq!(
652 pamoja_ladder_recv_within(ladder, 20, &mut message, &mut timed_out),
653 PamojaStatus::Ok
654 );
655 assert!(timed_out && message.is_null());
656
657 let upstream = pamoja_loopback_transport_new(broker);
658 pamoja_loopback_transport_connect(upstream);
659 pamoja_loopback_transport_send(upstream, topic.as_ptr(), b"open".as_ptr(), 4);
660 assert_eq!(
661 pamoja_ladder_recv_within(ladder, 1000, &mut message, &mut timed_out),
662 PamojaStatus::Ok
663 );
664 assert!(!timed_out && !message.is_null());
665 let payload = std::slice::from_raw_parts(
666 pamoja_message_payload(message),
667 pamoja_message_payload_len(message),
668 )
669 .to_vec();
670 assert_eq!(payload, b"open");
671 pamoja_message_free(message);
672
673 pamoja_ladder_free(ladder);
674 pamoja_loopback_transport_free(upstream);
675 pamoja_loopback_broker_free(broker);
676 }
677 }
678
679 #[test]
680 fn an_uplink_carries_what_an_unreachable_rung_refuses_and_is_never_listened_on() {
681 unsafe {
682 let near = pamoja_loopback_broker_new();
683 let far = pamoja_loopback_broker_new();
684 let ashore = pamoja_loopback_transport_new(far);
685 pamoja_loopback_transport_connect(ashore);
686 let reports = std::ffi::CString::new("reports").expect("static");
687 let orders = std::ffi::CString::new("orders").expect("static");
688 pamoja_loopback_transport_subscribe(ashore, reports.as_ptr());
689
690 let ladder = pamoja_ladder_new(pamoja_store_memory(0));
691 assert_eq!(
692 pamoja_ladder_rung(ladder, pamoja_transport_loopback(near)),
693 PamojaStatus::Ok
694 );
695 assert_eq!(
696 pamoja_ladder_uplink(ladder, pamoja_transport_loopback(far)),
697 PamojaStatus::Ok
698 );
699 pamoja_ladder_connect(ladder);
700 assert_eq!(
701 pamoja_ladder_subscribe(ladder, orders.as_ptr()),
702 PamojaStatus::Ok
703 );
704
705 assert_eq!(
706 pamoja_loopback_broker_set_reachable(near, false),
707 PamojaStatus::Ok
708 );
709 assert!(!pamoja_loopback_broker_is_reachable(near));
710 let mut delivery = PamojaDelivery::Buffered;
711 pamoja_ladder_send(ladder, reports.as_ptr(), b"1".as_ptr(), 1, &mut delivery);
712 assert_eq!(delivery, PamojaDelivery::Sent, "the uplink carried it");
713 let mut message = ptr::null_mut();
714 pamoja_loopback_transport_recv(ashore, &mut message);
715 assert!(!message.is_null());
716 pamoja_message_free(message);
717
718 pamoja_loopback_transport_send(ashore, orders.as_ptr(), b"stop".as_ptr(), 4);
719 let mut timed_out = false;
720 pamoja_ladder_recv_within(ladder, 20, &mut message, &mut timed_out);
721 assert!(timed_out, "an uplink is never listened on");
722
723 pamoja_loopback_broker_set_reachable(far, false);
724 pamoja_ladder_send(ladder, reports.as_ptr(), b"2".as_ptr(), 1, &mut delivery);
725 assert_eq!(
726 delivery,
727 PamojaDelivery::Buffered,
728 "with every link out of reach"
729 );
730
731 pamoja_ladder_free(ladder);
732 pamoja_loopback_transport_free(ashore);
733 pamoja_loopback_broker_free(near);
734 pamoja_loopback_broker_free(far);
735 }
736 }
737
738 #[test]
739 fn a_receive_with_no_listening_rung_reports_closed() {
740 unsafe {
741 let ladder = pamoja_ladder_new(pamoja_store_memory(0));
742 let mut message = ptr::null_mut();
743 assert_eq!(
744 pamoja_ladder_recv(ladder, &mut message),
745 PamojaStatus::Closed
746 );
747 assert!(message.is_null());
748 pamoja_ladder_free(ladder);
749 }
750 }
751
752 #[test]
753 fn a_null_handle_is_refused_rather_than_dereferenced() {
754 unsafe {
755 assert!(pamoja_ladder_new(ptr::null_mut()).is_null());
756 assert_eq!(
757 pamoja_ladder_connect(ptr::null_mut()),
758 PamojaStatus::InvalidArgument
759 );
760 assert_eq!(
761 pamoja_ladder_buffered(ptr::null_mut(), ptr::null_mut()),
762 PamojaStatus::InvalidArgument
763 );
764 pamoja_ladder_free(ptr::null_mut());
765 }
766 }
767}