1use std::ffi::{c_char, c_void, CString};
41use std::ptr;
42use std::sync::Arc;
43
44use pamoja_core::{Error, Message, Receive, Result, Transport};
45use tokio::sync::mpsc;
46use tokio::task::JoinHandle;
47
48use crate::transport::{Kind, PamojaMessage, PamojaTransport};
49use crate::{read_bytes, read_str, set_last_error, take_last_error, PamojaStatus};
50
51#[repr(C)]
57#[derive(Clone, Copy)]
58pub struct PamojaTransportCallbacks {
59 pub connect: Option<unsafe extern "C" fn(user_data: *mut c_void) -> PamojaStatus>,
61 pub send: Option<
64 unsafe extern "C" fn(
65 user_data: *mut c_void,
66 topic: *const c_char,
67 payload: *const u8,
68 payload_len: usize,
69 ) -> PamojaStatus,
70 >,
71 pub subscribe:
74 Option<unsafe extern "C" fn(user_data: *mut c_void, topic: *const c_char) -> PamojaStatus>,
75 pub recv: Option<
79 unsafe extern "C" fn(
80 user_data: *mut c_void,
81 out_message: *mut *mut PamojaMessage,
82 ) -> PamojaStatus,
83 >,
84 pub release: Option<unsafe extern "C" fn(user_data: *mut c_void)>,
86}
87
88struct Host {
93 callbacks: PamojaTransportCallbacks,
94 user_data: *mut c_void,
95}
96
97unsafe impl Send for Host {}
101unsafe impl Sync for Host {}
102
103impl Drop for Host {
104 fn drop(&mut self) {
105 if let Some(release) = self.callbacks.release {
106 unsafe { release(self.user_data) }
110 }
111 }
112}
113
114fn outcome(status: PamojaStatus) -> Result<()> {
118 if status == PamojaStatus::Ok {
119 return Ok(());
120 }
121 let reason =
122 take_last_error().unwrap_or_else(|| format!("the host transport reported {status:?}"));
123 Err(match status {
124 PamojaStatus::Io => Error::Io(reason),
125 PamojaStatus::Codec => Error::Codec(reason),
126 PamojaStatus::Closed => Error::Closed,
127 PamojaStatus::Auth => Error::Auth(reason),
128 _ => Error::Transport(reason),
129 })
130}
131
132async fn blocking<T: Send + 'static>(call: impl FnOnce() -> T + Send + 'static) -> Result<T> {
135 tokio::task::spawn_blocking(call)
136 .await
137 .map_err(|error| Error::Transport(format!("the host callback thread failed: {error}")))
138}
139
140fn c_topic(topic: &str) -> Result<CString> {
142 CString::new(topic)
143 .map_err(|_| Error::Transport("a topic cannot carry an interior null byte".to_owned()))
144}
145
146pub struct CallbackTransport {
151 host: Arc<Host>,
152 connected: bool,
153 inbox: Option<mpsc::UnboundedReceiver<Result<Message>>>,
154 pump: Option<JoinHandle<()>>,
155}
156
157impl CallbackTransport {
158 pub(crate) fn listens(&self) -> bool {
161 self.host.callbacks.recv.is_some()
162 }
163
164 fn start_pump(&mut self) {
167 let Some(recv) = self.host.callbacks.recv else {
168 return;
169 };
170 if let Some(pump) = self.pump.take() {
171 pump.abort();
172 }
173 let (sender, receiver) = mpsc::unbounded_channel();
174 let host = Arc::clone(&self.host);
175 self.inbox = Some(receiver);
176 self.pump = Some(tokio::spawn(async move {
177 loop {
178 let host = Arc::clone(&host);
179 let next = blocking(move || {
180 let mut out_message: *mut PamojaMessage = ptr::null_mut();
181 let status = unsafe { recv(host.user_data, &mut out_message) };
185 let delivered = (!out_message.is_null())
186 .then(|| unsafe { Box::from_raw(out_message) }.into_message());
187 outcome(status).map(|()| delivered)
188 })
189 .await;
190 let delivered = match next {
191 Ok(Ok(Some(message))) => Ok(message),
192 Ok(Ok(None)) => break,
193 Ok(Err(error)) | Err(error) => Err(error),
194 };
195 let failed = delivered.is_err();
196 if sender.send(delivered).is_err() || failed {
197 break;
198 }
199 }
200 }));
201 }
202}
203
204impl Drop for CallbackTransport {
205 fn drop(&mut self) {
206 if let Some(pump) = self.pump.take() {
207 pump.abort();
208 }
209 }
210}
211
212impl Transport for CallbackTransport {
213 async fn connect(&mut self) -> Result<()> {
214 let host = Arc::clone(&self.host);
215 blocking(move || {
216 let connect = host.callbacks.connect.expect("required at creation");
217 outcome(unsafe { connect(host.user_data) })
219 })
220 .await??;
221 self.connected = true;
222 self.start_pump();
223 Ok(())
224 }
225
226 async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
227 if !self.connected {
228 return Err(Error::Closed);
229 }
230 let host = Arc::clone(&self.host);
231 let topic = c_topic(topic)?;
232 let payload = payload.to_vec();
233 blocking(move || {
234 let send = host.callbacks.send.expect("required at creation");
235 outcome(unsafe {
238 send(
239 host.user_data,
240 topic.as_ptr(),
241 payload.as_ptr(),
242 payload.len(),
243 )
244 })
245 })
246 .await?
247 }
248
249 async fn subscribe(&mut self, topic: &str) -> Result<()> {
250 if !self.connected {
251 return Err(Error::Closed);
252 }
253 let host = Arc::clone(&self.host);
254 let topic = c_topic(topic)?;
255 blocking(move || {
256 let subscribe = host.callbacks.subscribe.expect("required at creation");
257 outcome(unsafe { subscribe(host.user_data, topic.as_ptr()) })
260 })
261 .await?
262 }
263}
264
265impl Receive for CallbackTransport {
266 async fn recv(&mut self) -> Result<Option<Message>> {
280 if !self.connected {
281 return Err(Error::Closed);
282 }
283 match self.inbox.as_mut() {
284 Some(inbox) => inbox.recv().await.transpose(),
285 None => Ok(None),
286 }
287 }
288}
289
290#[no_mangle]
314pub unsafe extern "C" fn pamoja_transport_from_callbacks(
315 callbacks: *const PamojaTransportCallbacks,
316 user_data: *mut c_void,
317) -> *mut PamojaTransport {
318 if callbacks.is_null() {
319 set_last_error("callbacks must not be null".to_owned());
320 return ptr::null_mut();
321 }
322 let callbacks = *callbacks;
323 if callbacks.connect.is_none() || callbacks.send.is_none() || callbacks.subscribe.is_none() {
324 set_last_error("a host transport needs connect, send, and subscribe callbacks".to_owned());
325 return ptr::null_mut();
326 }
327 let transport = CallbackTransport {
328 host: Arc::new(Host {
329 callbacks,
330 user_data,
331 }),
332 connected: false,
333 inbox: None,
334 pump: None,
335 };
336 PamojaTransport::into_raw(Kind::Host(transport))
337}
338
339#[no_mangle]
360pub unsafe extern "C" fn pamoja_message_new(
361 topic: *const c_char,
362 payload: *const u8,
363 payload_len: usize,
364) -> *mut PamojaMessage {
365 let Some(topic) = read_str(topic, "topic") else {
366 return ptr::null_mut();
367 };
368 let Ok(payload) = read_bytes(payload, payload_len) else {
369 return ptr::null_mut();
370 };
371 PamojaMessage::into_raw(topic.to_owned(), payload)
372}
373
374#[no_mangle]
390pub unsafe extern "C" fn pamoja_last_error_set(message: *const c_char) {
391 match read_str(message, "message") {
392 Some(message) => set_last_error(message.to_owned()),
393 None => drop(take_last_error()),
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 use std::collections::VecDeque;
402 use std::ffi::CStr;
403 use std::sync::atomic::{AtomicBool, Ordering};
404 use std::sync::{Condvar, Mutex};
405
406 use crate::transport::{
407 pamoja_message_free, pamoja_message_payload, pamoja_message_payload_len,
408 pamoja_message_topic, pamoja_transport_connect, pamoja_transport_free,
409 pamoja_transport_recv, pamoja_transport_send, pamoja_transport_subscribe,
410 };
411 use crate::{pamoja_last_error_message, PamojaStatus};
412
413 #[derive(Default)]
416 struct FakeHost {
417 connects: Mutex<usize>,
418 sent: Mutex<Vec<(String, Vec<u8>)>>,
419 filters: Mutex<Vec<String>>,
420 inbox: Mutex<VecDeque<std::result::Result<Option<Message>, &'static str>>>,
421 ready: Condvar,
422 refuse_sends: AtomicBool,
423 released: Arc<AtomicBool>,
424 }
425
426 impl FakeHost {
427 fn deliver(&self, next: Option<Message>) {
428 self.inbox.lock().expect("inbox").push_back(Ok(next));
429 self.ready.notify_all();
430 }
431
432 fn fail(&self, reason: &'static str) {
433 self.inbox.lock().expect("inbox").push_back(Err(reason));
434 self.ready.notify_all();
435 }
436 }
437
438 unsafe extern "C" fn connect(user_data: *mut c_void) -> PamojaStatus {
439 let host = &*(user_data as *const FakeHost);
440 *host.connects.lock().expect("connects") += 1;
441 PamojaStatus::Ok
442 }
443
444 unsafe extern "C" fn send(
445 user_data: *mut c_void,
446 topic: *const c_char,
447 payload: *const u8,
448 payload_len: usize,
449 ) -> PamojaStatus {
450 let host = &*(user_data as *const FakeHost);
451 if host.refuse_sends.load(Ordering::SeqCst) {
452 let reason = CString::new("the radio is out of range").expect("static");
453 pamoja_last_error_set(reason.as_ptr());
454 return PamojaStatus::Transport;
455 }
456 let topic = CStr::from_ptr(topic).to_str().expect("utf-8").to_owned();
457 let payload = std::slice::from_raw_parts(payload, payload_len).to_vec();
458 host.sent.lock().expect("sent").push((topic, payload));
459 PamojaStatus::Ok
460 }
461
462 unsafe extern "C" fn subscribe(user_data: *mut c_void, topic: *const c_char) -> PamojaStatus {
463 let host = &*(user_data as *const FakeHost);
464 let topic = CStr::from_ptr(topic).to_str().expect("utf-8").to_owned();
465 host.filters.lock().expect("filters").push(topic);
466 PamojaStatus::Ok
467 }
468
469 unsafe extern "C" fn recv(
470 user_data: *mut c_void,
471 out_message: *mut *mut PamojaMessage,
472 ) -> PamojaStatus {
473 let host = &*(user_data as *const FakeHost);
474 let mut inbox = host.inbox.lock().expect("inbox");
475 while inbox.is_empty() {
476 inbox = host.ready.wait(inbox).expect("inbox");
477 }
478 *out_message = match inbox.pop_front().expect("non-empty") {
479 Ok(Some(message)) => {
480 let topic = CString::new(message.topic).expect("topic");
481 pamoja_message_new(
482 topic.as_ptr(),
483 message.payload.as_ptr(),
484 message.payload.len(),
485 )
486 }
487 Ok(None) => ptr::null_mut(),
488 Err(reason) => {
489 let reason = CString::new(reason).expect("static");
490 pamoja_last_error_set(reason.as_ptr());
491 return PamojaStatus::Transport;
492 }
493 };
494 PamojaStatus::Ok
495 }
496
497 unsafe extern "C" fn release(user_data: *mut c_void) {
498 let host = Box::from_raw(user_data as *mut FakeHost);
499 host.released.store(true, Ordering::SeqCst);
500 }
501
502 fn fake_host(listens: bool) -> (*mut FakeHost, PamojaTransportCallbacks, Arc<AtomicBool>) {
504 let host = Box::new(FakeHost::default());
505 let released = Arc::clone(&host.released);
506 let host = Box::into_raw(host);
507 let callbacks = PamojaTransportCallbacks {
508 connect: Some(connect),
509 send: Some(send),
510 subscribe: Some(subscribe),
511 recv: listens.then_some(recv as unsafe extern "C" fn(_, _) -> _),
512 release: Some(release),
513 };
514 (host, callbacks, released)
515 }
516
517 fn last_error() -> String {
518 unsafe {
519 CStr::from_ptr(pamoja_last_error_message())
520 .to_str()
521 .expect("utf-8")
522 .to_owned()
523 }
524 }
525
526 #[test]
527 fn the_host_carries_sends_and_delivers_what_it_receives() {
528 unsafe {
529 let (host, callbacks, released) = fake_host(true);
530 let transport = pamoja_transport_from_callbacks(&callbacks, host.cast());
531 assert!(!transport.is_null());
532 let topic = CString::new("sensors/1").expect("static");
533
534 assert_eq!(pamoja_transport_connect(transport), PamojaStatus::Ok);
535 assert_eq!(
536 pamoja_transport_subscribe(transport, topic.as_ptr()),
537 PamojaStatus::Ok
538 );
539 assert_eq!(
540 pamoja_transport_send(transport, topic.as_ptr(), b"21.5".as_ptr(), 4),
541 PamojaStatus::Ok
542 );
543 assert_eq!(*(*host).connects.lock().expect("connects"), 1);
544 assert_eq!(
545 (*host).filters.lock().expect("filters").as_slice(),
546 ["sensors/1"]
547 );
548 assert_eq!(
549 (*host).sent.lock().expect("sent").as_slice(),
550 [("sensors/1".to_owned(), b"21.5".to_vec())]
551 );
552
553 (*host).deliver(Some(Message::new("commands/1", b"open")));
555 (*host).deliver(Some(Message::new("commands/1", b"close")));
556 for expected in [b"open".as_slice(), b"close".as_slice()] {
557 let mut message = ptr::null_mut();
558 assert_eq!(
559 pamoja_transport_recv(transport, &mut message),
560 PamojaStatus::Ok
561 );
562 assert!(!message.is_null());
563 assert_eq!(
564 CStr::from_ptr(pamoja_message_topic(message)).to_bytes(),
565 b"commands/1"
566 );
567 let payload = std::slice::from_raw_parts(
568 pamoja_message_payload(message),
569 pamoja_message_payload_len(message),
570 );
571 assert_eq!(payload, expected);
572 pamoja_message_free(message);
573 }
574
575 (*host).deliver(None);
578 let mut message = ptr::null_mut();
579 assert_eq!(
580 pamoja_transport_recv(transport, &mut message),
581 PamojaStatus::Ok
582 );
583 assert!(message.is_null());
584 assert!(!released.load(Ordering::SeqCst));
585 pamoja_transport_free(transport);
586 for _ in 0..200 {
587 if released.load(Ordering::SeqCst) {
588 break;
589 }
590 std::thread::sleep(std::time::Duration::from_millis(5));
591 }
592 assert!(
593 released.load(Ordering::SeqCst),
594 "release runs after the pump ends"
595 );
596 }
597 }
598
599 #[test]
600 fn a_host_that_refuses_a_send_explains_itself() {
601 unsafe {
602 let (host, callbacks, _released) = fake_host(false);
603 let transport = pamoja_transport_from_callbacks(&callbacks, host.cast());
604 let topic = CString::new("t").expect("static");
605 assert_eq!(pamoja_transport_connect(transport), PamojaStatus::Ok);
606
607 (*host).refuse_sends.store(true, Ordering::SeqCst);
608 assert_eq!(
609 pamoja_transport_send(transport, topic.as_ptr(), b"x".as_ptr(), 1),
610 PamojaStatus::Transport
611 );
612 assert_eq!(last_error(), "transport error: the radio is out of range");
613
614 let mut message = ptr::null_mut();
616 assert_eq!(
617 pamoja_transport_recv(transport, &mut message),
618 PamojaStatus::Ok
619 );
620 assert!(message.is_null());
621 pamoja_transport_free(transport);
622 }
623 }
624
625 #[test]
626 fn a_failed_recv_is_reported_once_and_the_next_connect_listens_again() {
627 unsafe {
628 let (host, callbacks, _released) = fake_host(true);
629 let transport = pamoja_transport_from_callbacks(&callbacks, host.cast());
630 assert_eq!(pamoja_transport_connect(transport), PamojaStatus::Ok);
631
632 (*host).fail("the modem lost its session");
633 let mut message = ptr::null_mut();
634 assert_eq!(
635 pamoja_transport_recv(transport, &mut message),
636 PamojaStatus::Transport
637 );
638 assert_eq!(last_error(), "transport error: the modem lost its session");
639 assert_eq!(
640 pamoja_transport_recv(transport, &mut message),
641 PamojaStatus::Ok
642 );
643 assert!(message.is_null());
644
645 assert_eq!(pamoja_transport_connect(transport), PamojaStatus::Ok);
646 (*host).deliver(Some(Message::new("commands/interval", b"900")));
647 assert_eq!(
648 pamoja_transport_recv(transport, &mut message),
649 PamojaStatus::Ok
650 );
651 assert!(!message.is_null());
652 pamoja_message_free(message);
653
654 (*host).deliver(None);
655 pamoja_transport_free(transport);
656 }
657 }
658
659 #[test]
660 fn operations_before_connect_report_closed() {
661 unsafe {
662 let (host, callbacks, _released) = fake_host(true);
663 let transport = pamoja_transport_from_callbacks(&callbacks, host.cast());
664 let topic = CString::new("t").expect("static");
665 assert_eq!(
666 pamoja_transport_send(transport, topic.as_ptr(), b"x".as_ptr(), 1),
667 PamojaStatus::Closed
668 );
669 let mut message = ptr::null_mut();
670 assert_eq!(
671 pamoja_transport_recv(transport, &mut message),
672 PamojaStatus::Closed
673 );
674 pamoja_transport_free(transport);
675 }
676 }
677
678 #[test]
679 fn incomplete_callbacks_are_refused_and_the_host_keeps_its_data() {
680 unsafe {
681 let (host, mut callbacks, released) = fake_host(true);
682 callbacks.send = None;
683 assert!(pamoja_transport_from_callbacks(&callbacks, host.cast()).is_null());
684 assert_eq!(
685 last_error(),
686 "a host transport needs connect, send, and subscribe callbacks"
687 );
688 assert!(pamoja_transport_from_callbacks(ptr::null(), host.cast()).is_null());
689 assert!(!released.load(Ordering::SeqCst));
690 drop(Box::from_raw(host));
691 }
692 }
693
694 #[test]
695 fn a_message_needs_a_topic_and_a_readable_payload() {
696 unsafe {
697 let topic = CString::new("t").expect("static");
698 assert!(pamoja_message_new(ptr::null(), ptr::null(), 0).is_null());
699 assert!(pamoja_message_new(topic.as_ptr(), ptr::null(), 3).is_null());
700 let empty = pamoja_message_new(topic.as_ptr(), ptr::null(), 0);
701 assert!(!empty.is_null());
702 assert_eq!(pamoja_message_payload_len(empty), 0);
703 pamoja_message_free(empty);
704 }
705 }
706}