pamoja_ffi/transport.rs
1//! The C ABI for composing transports.
2//!
3//! A ladder rung, a fault injector, and a degraded link all take "some
4//! transport", which in Rust is any `impl Transport`. A C ABI has no generics,
5//! so this module carries one tagged handle that holds whichever transport was
6//! built and dispatches to it. One handle keeps the composing calls to a single
7//! function each, rather than one per transport kind, and lets the set of kinds
8//! grow without reshaping the surface.
9//!
10//! A transport built here is separate from a client handle such as
11//! [`PamojaMqttClient`](crate::mqtt::PamojaMqttClient). A client is for driving
12//! a link directly; a transport is for composition, and is consumed by whatever
13//! it is composed into. Keeping them apart means nothing has to move out of a
14//! live, shared handle. A link the host implements itself enters through
15//! [`pamoja_transport_from_callbacks`](crate::host::pamoja_transport_from_callbacks)
16//! and is one of these handles from then on.
17
18use std::ffi::CString;
19use std::future::Future;
20use std::pin::Pin;
21use std::ptr;
22
23use pamoja_core::{Message, Receive, Result, Transport};
24
25use crate::{read_bytes, set_last_error, PamojaStatus};
26
27/// Object-safe erasure of a transport, so a wrapper can hold any of them.
28///
29/// The core trait returns `impl Future`, which is not dyn-compatible; this one
30/// boxes the future so a wrapping kind can hold a transport without naming its
31/// concrete type. That is what keeps the union below from naming itself: a
32/// nested transport is reached through this trait, whose futures are already a
33/// type the compiler can name.
34trait DynTransport: Send {
35 /// Connects the erased transport.
36 fn connect(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
37
38 /// Sends a payload over the erased transport.
39 fn send<'a>(
40 &'a mut self,
41 topic: &'a str,
42 payload: &'a [u8],
43 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
44
45 /// Subscribes the erased transport to a topic.
46 fn subscribe<'a>(
47 &'a mut self,
48 topic: &'a str,
49 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
50
51 /// Awaits the next message the erased transport delivers.
52 fn recv(&mut self) -> Pin<Box<dyn Future<Output = Result<Option<Message>>> + Send + '_>>;
53}
54
55/// Newtype carrying one concrete transport behind [`DynTransport`].
56struct Erased<T>(T);
57
58impl<T: Transport + Receive + Send> DynTransport for Erased<T> {
59 fn connect(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
60 Box::pin(Transport::connect(&mut self.0))
61 }
62
63 fn send<'a>(
64 &'a mut self,
65 topic: &'a str,
66 payload: &'a [u8],
67 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
68 Box::pin(Transport::send(&mut self.0, topic, payload))
69 }
70
71 fn subscribe<'a>(
72 &'a mut self,
73 topic: &'a str,
74 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
75 Box::pin(Transport::subscribe(&mut self.0, topic))
76 }
77
78 fn recv(&mut self) -> Pin<Box<dyn Future<Output = Result<Option<Message>>> + Send + '_>> {
79 Box::pin(Receive::recv(&mut self.0))
80 }
81}
82
83/// A transport of any kind, ready to be nested inside a wrapper.
84pub(crate) struct AnyTransport(Box<dyn DynTransport>);
85
86impl AnyTransport {
87 /// Erases one transport so a wrapper can hold it.
88 fn new(transport: Kind) -> Self {
89 Self(Box::new(Erased(transport)))
90 }
91}
92
93impl Transport for AnyTransport {
94 async fn connect(&mut self) -> Result<()> {
95 self.0.connect().await
96 }
97
98 async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
99 self.0.send(topic, payload).await
100 }
101
102 async fn subscribe(&mut self, topic: &str) -> Result<()> {
103 self.0.subscribe(topic).await
104 }
105}
106
107impl Receive for AnyTransport {
108 async fn recv(&mut self) -> Result<Option<Message>> {
109 self.0.recv().await
110 }
111}
112
113/// One transport, whichever kind it was built as.
114///
115/// A wrapping kind holds its inner transport erased rather than as this enum, so
116/// a faulty link can wrap a degraded one to any depth without the enum naming
117/// itself. Naming itself would make the hidden type of each method depend on
118/// knowing that same type, which is a cycle rather than recursion.
119pub(crate) enum Kind {
120 /// An MQTT broker connection.
121 #[cfg(feature = "mqtt")]
122 Mqtt(pamoja_mqtt::MqttTransport),
123 /// A CoAP endpoint.
124 #[cfg(feature = "coap")]
125 Coap(pamoja_coap::CoapTransport),
126 /// An in-process link to a loopback broker.
127 #[cfg(feature = "loopback")]
128 Loopback(pamoja_loopback::LoopbackTransport),
129 /// Another transport with a set number of sends made to fail.
130 #[cfg(feature = "loopback")]
131 Faulty(pamoja_loopback::Faulty<AnyTransport>),
132 /// Another transport carrying loss and outages.
133 #[cfg(feature = "sim")]
134 Degraded(pamoja_sim::DegradedLink<AnyTransport>),
135 /// A link the host implements through callbacks.
136 Host(crate::host::CallbackTransport),
137}
138
139impl Kind {
140 /// Whether the transport delivers messages, so a ladder listens on it rather
141 /// than treating it as an uplink.
142 pub(crate) fn listens(&self) -> bool {
143 match self {
144 Kind::Host(inner) => inner.listens(),
145 _ => true,
146 }
147 }
148}
149
150impl Transport for Kind {
151 async fn connect(&mut self) -> Result<()> {
152 match self {
153 #[cfg(feature = "mqtt")]
154 Kind::Mqtt(inner) => inner.connect().await,
155 #[cfg(feature = "coap")]
156 Kind::Coap(inner) => inner.connect().await,
157 #[cfg(feature = "loopback")]
158 Kind::Loopback(inner) => inner.connect().await,
159 #[cfg(feature = "loopback")]
160 Kind::Faulty(inner) => inner.connect().await,
161 #[cfg(feature = "sim")]
162 Kind::Degraded(inner) => inner.connect().await,
163 Kind::Host(inner) => inner.connect().await,
164 }
165 }
166
167 async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
168 match self {
169 #[cfg(feature = "mqtt")]
170 Kind::Mqtt(inner) => inner.send(topic, payload).await,
171 #[cfg(feature = "coap")]
172 Kind::Coap(inner) => inner.send(topic, payload).await,
173 #[cfg(feature = "loopback")]
174 Kind::Loopback(inner) => inner.send(topic, payload).await,
175 #[cfg(feature = "loopback")]
176 Kind::Faulty(inner) => inner.send(topic, payload).await,
177 #[cfg(feature = "sim")]
178 Kind::Degraded(inner) => inner.send(topic, payload).await,
179 Kind::Host(inner) => inner.send(topic, payload).await,
180 }
181 }
182
183 async fn subscribe(&mut self, topic: &str) -> Result<()> {
184 match self {
185 #[cfg(feature = "mqtt")]
186 Kind::Mqtt(inner) => inner.subscribe(topic).await,
187 #[cfg(feature = "coap")]
188 Kind::Coap(inner) => inner.subscribe(topic).await,
189 #[cfg(feature = "loopback")]
190 Kind::Loopback(inner) => inner.subscribe(topic).await,
191 #[cfg(feature = "loopback")]
192 Kind::Faulty(inner) => inner.subscribe(topic).await,
193 #[cfg(feature = "sim")]
194 Kind::Degraded(inner) => inner.subscribe(topic).await,
195 Kind::Host(inner) => inner.subscribe(topic).await,
196 }
197 }
198}
199
200impl Receive for Kind {
201 async fn recv(&mut self) -> Result<Option<Message>> {
202 match self {
203 #[cfg(feature = "mqtt")]
204 Kind::Mqtt(inner) => inner.recv().await,
205 #[cfg(feature = "coap")]
206 Kind::Coap(inner) => inner.recv().await,
207 #[cfg(feature = "loopback")]
208 Kind::Loopback(inner) => inner.recv().await,
209 #[cfg(feature = "loopback")]
210 Kind::Faulty(inner) => inner.recv().await,
211 #[cfg(feature = "sim")]
212 Kind::Degraded(inner) => inner.recv().await,
213 Kind::Host(inner) => inner.recv().await,
214 }
215 }
216}
217
218/// An opaque handle to one message that arrived on a subscribed topic.
219///
220/// CoAP and the loopback broker both hand back a topic and a payload, so one
221/// handle serves them rather than a near-identical type per transport.
222pub struct PamojaMessage {
223 topic: CString,
224 payload: Vec<u8>,
225}
226
227impl PamojaMessage {
228 /// Wraps a received message in a handle the caller owns.
229 ///
230 /// A topic carrying an interior null cannot cross as a C string, so it is
231 /// replaced with an empty one rather than truncated at the null, which would
232 /// hand the caller a plausible-looking wrong topic.
233 pub(crate) fn into_raw(topic: String, payload: Vec<u8>) -> *mut Self {
234 let topic = CString::new(topic).unwrap_or_default();
235 Box::into_raw(Box::new(Self { topic, payload }))
236 }
237
238 /// Takes the message back into the core type, for a message a host built.
239 pub(crate) fn into_message(self) -> Message {
240 Message::new(self.topic.into_string().unwrap_or_default(), self.payload)
241 }
242}
243
244/// Returns the topic a message arrived on.
245///
246/// # Arguments
247///
248/// * `message` - the message.
249///
250/// # Returns
251///
252/// A null-terminated UTF-8 string owned by the message and valid until it is
253/// freed, or null if `message` is null.
254///
255/// # Safety
256///
257/// `message` must be a live handle from a call that produced one, or null.
258#[no_mangle]
259pub unsafe extern "C" fn pamoja_message_topic(
260 message: *const PamojaMessage,
261) -> *const std::ffi::c_char {
262 if message.is_null() {
263 return ptr::null();
264 }
265 (*message).topic.as_ptr()
266}
267
268/// Returns a pointer to a message payload.
269///
270/// # Arguments
271///
272/// * `message` - the message.
273///
274/// # Returns
275///
276/// A pointer to the bytes, valid until the message is freed, or null if
277/// `message` is null.
278///
279/// # Safety
280///
281/// `message` must be a live handle from a call that produced one, or null.
282#[no_mangle]
283pub unsafe extern "C" fn pamoja_message_payload(message: *const PamojaMessage) -> *const u8 {
284 if message.is_null() {
285 return ptr::null();
286 }
287 (*message).payload.as_ptr()
288}
289
290/// Returns the length in bytes of a message payload.
291///
292/// # Arguments
293///
294/// * `message` - the message.
295///
296/// # Returns
297///
298/// The length, or 0 if `message` is null.
299///
300/// # Safety
301///
302/// `message` must be a live handle from a call that produced one, or null.
303#[no_mangle]
304pub unsafe extern "C" fn pamoja_message_payload_len(message: *const PamojaMessage) -> usize {
305 if message.is_null() {
306 return 0;
307 }
308 (*message).payload.len()
309}
310
311/// Releases a message handle.
312///
313/// Passing null is a no-op.
314///
315/// # Safety
316///
317/// `message` must be a handle from a call that produced one and that has not
318/// already been freed, or null. After this call it must not be used again.
319#[no_mangle]
320pub unsafe extern "C" fn pamoja_message_free(message: *mut PamojaMessage) {
321 if !message.is_null() {
322 drop(Box::from_raw(message));
323 }
324}
325
326/// An opaque handle to one transport, ready to drive or to compose.
327///
328/// Release it with [`pamoja_transport_free`] unless it has been consumed by a
329/// call that takes ownership, such as adding it to a ladder.
330pub struct PamojaTransport {
331 pub(crate) kind: Kind,
332}
333
334impl PamojaTransport {
335 /// Wraps a transport kind in a handle the caller owns.
336 pub(crate) fn into_raw(kind: Kind) -> *mut Self {
337 Box::into_raw(Box::new(Self { kind }))
338 }
339}
340
341/// Creates an MQTT transport from broker settings.
342///
343/// # Arguments
344///
345/// * `config` - the broker settings, read the same way a client reads them.
346///
347/// # Returns
348///
349/// A handle the caller must release with [`pamoja_transport_free`] or hand to a
350/// call that consumes it, or null on failure.
351///
352/// # Safety
353///
354/// `config` must point to a valid config whose strings are valid
355/// null-terminated UTF-8 for the duration of the call.
356#[cfg(feature = "mqtt")]
357#[no_mangle]
358pub unsafe extern "C" fn pamoja_transport_mqtt(
359 config: *const crate::mqtt::PamojaMqttConfig,
360) -> *mut PamojaTransport {
361 let Some(settings) = crate::mqtt::mqtt_settings(config) else {
362 return ptr::null_mut();
363 };
364 PamojaTransport::into_raw(Kind::Mqtt(pamoja_mqtt::MqttTransport::new(settings)))
365}
366
367/// Wraps a transport so a set number of its next sends fail.
368///
369/// This is how a caller checks that a ladder falls through to its next rung, or
370/// that a buffer fills, without unplugging anything.
371///
372/// # Arguments
373///
374/// * `transport` - the transport to wrap, consumed by this call.
375/// * `failures` - how many upcoming sends to fail.
376///
377/// # Returns
378///
379/// A new handle owning the wrapped transport, or null if `transport` is null.
380///
381/// # Safety
382///
383/// `transport` must be a live handle that has not been freed or consumed. After
384/// this call it must not be used again, whatever the result.
385#[cfg(feature = "loopback")]
386#[no_mangle]
387pub unsafe extern "C" fn pamoja_transport_faulty(
388 transport: *mut PamojaTransport,
389 failures: usize,
390) -> *mut PamojaTransport {
391 let Some(inner) = take_transport(transport) else {
392 return ptr::null_mut();
393 };
394 PamojaTransport::into_raw(Kind::Faulty(pamoja_loopback::Faulty::new(
395 AnyTransport::new(inner),
396 failures,
397 )))
398}
399
400/// Wraps a transport in a link that loses packets and goes down.
401///
402/// # Arguments
403///
404/// * `transport` - the transport to wrap, consumed by this call.
405/// * `drop_every` - lose one send in every this many, or 0 to lose none.
406/// * `up` - how many sends the link stays up for, or 0 to never go down.
407/// * `down` - how many sends it then stays down for.
408///
409/// # Returns
410///
411/// A new handle owning the wrapped transport, or null if `transport` is null.
412///
413/// # Safety
414///
415/// `transport` must be a live handle that has not been freed or consumed. After
416/// this call it must not be used again, whatever the result.
417#[cfg(feature = "sim")]
418#[no_mangle]
419pub unsafe extern "C" fn pamoja_transport_degraded(
420 transport: *mut PamojaTransport,
421 drop_every: u32,
422 up: u32,
423 down: u32,
424) -> *mut PamojaTransport {
425 let Some(inner) = take_transport(transport) else {
426 return ptr::null_mut();
427 };
428 let mut link = pamoja_sim::DegradedLink::new(AnyTransport::new(inner));
429 if drop_every != 0 {
430 link = link.drop_every(drop_every);
431 }
432 if up != 0 {
433 link = link.intermittent(up, down);
434 }
435 PamojaTransport::into_raw(Kind::Degraded(link))
436}
437
438/// Connects a transport.
439///
440/// # Arguments
441///
442/// * `transport` - the transport to connect.
443///
444/// # Returns
445///
446/// [`PamojaStatus::Ok`] once connected.
447///
448/// # Safety
449///
450/// `transport` must be a live handle that has not been freed or consumed.
451#[no_mangle]
452pub unsafe extern "C" fn pamoja_transport_connect(transport: *mut PamojaTransport) -> PamojaStatus {
453 let Some(transport) = transport_handle(transport) else {
454 return PamojaStatus::InvalidArgument;
455 };
456 status(crate::runtime().block_on(transport.kind.connect()))
457}
458
459/// Sends a payload to a topic over a transport.
460///
461/// # Arguments
462///
463/// * `transport` - the transport to send over.
464/// * `topic` - the destination topic, as null-terminated UTF-8.
465/// * `payload` - the bytes to send.
466/// * `payload_len` - the length of `payload`.
467///
468/// # Returns
469///
470/// [`PamojaStatus::Ok`] once the transport has taken the payload.
471///
472/// # Safety
473///
474/// `transport` must be a live handle, `topic` a valid null-terminated UTF-8
475/// string, and `payload` must point to at least `payload_len` readable bytes or
476/// be null when that length is 0.
477#[no_mangle]
478pub unsafe extern "C" fn pamoja_transport_send(
479 transport: *mut PamojaTransport,
480 topic: *const std::ffi::c_char,
481 payload: *const u8,
482 payload_len: usize,
483) -> PamojaStatus {
484 let Some(transport) = transport_handle(transport) else {
485 return PamojaStatus::InvalidArgument;
486 };
487 let Some(topic) = crate::read_str(topic, "topic") else {
488 return PamojaStatus::InvalidArgument;
489 };
490 let payload = match read_bytes(payload, payload_len) {
491 Ok(payload) => payload,
492 Err(status) => return status,
493 };
494 status(crate::runtime().block_on(transport.kind.send(topic, &payload)))
495}
496
497/// Subscribes a transport to a topic.
498///
499/// # Arguments
500///
501/// * `transport` - the transport to subscribe.
502/// * `topic` - the topic to subscribe to, as null-terminated UTF-8.
503///
504/// # Returns
505///
506/// [`PamojaStatus::Ok`] once subscribed.
507///
508/// # Safety
509///
510/// `transport` must be a live handle and `topic` a valid null-terminated UTF-8
511/// string.
512#[no_mangle]
513pub unsafe extern "C" fn pamoja_transport_subscribe(
514 transport: *mut PamojaTransport,
515 topic: *const std::ffi::c_char,
516) -> PamojaStatus {
517 let Some(transport) = transport_handle(transport) else {
518 return PamojaStatus::InvalidArgument;
519 };
520 let Some(topic) = crate::read_str(topic, "topic") else {
521 return PamojaStatus::InvalidArgument;
522 };
523 status(crate::runtime().block_on(transport.kind.subscribe(topic)))
524}
525
526/// Waits for the next message a transport delivers on a subscribed topic.
527///
528/// # Arguments
529///
530/// * `transport` - the transport to receive from.
531/// * `out_message` - receives a message handle to release with
532/// [`pamoja_message_free`], or null once the link has ended.
533///
534/// # Returns
535///
536/// [`PamojaStatus::Ok`] with a message or with null once the link has ended, or
537/// [`PamojaStatus::Closed`] if the transport is not connected.
538///
539/// # Safety
540///
541/// `transport` must be a live handle and `out_message` must be writable.
542#[no_mangle]
543pub unsafe extern "C" fn pamoja_transport_recv(
544 transport: *mut PamojaTransport,
545 out_message: *mut *mut PamojaMessage,
546) -> PamojaStatus {
547 let Some(transport) = transport_handle(transport) else {
548 return PamojaStatus::InvalidArgument;
549 };
550 if out_message.is_null() {
551 set_last_error("out_message must not be null".to_owned());
552 return PamojaStatus::InvalidArgument;
553 }
554 *out_message = ptr::null_mut();
555 match crate::runtime().block_on(transport.kind.recv()) {
556 Ok(Some(message)) => {
557 *out_message = PamojaMessage::into_raw(message.topic, message.payload);
558 PamojaStatus::Ok
559 }
560 Ok(None) => PamojaStatus::Ok,
561 Err(error) => status(Err(error)),
562 }
563}
564
565/// Waits a limited time for the next message a transport delivers.
566///
567/// Running out of time loses nothing: a message that arrives afterwards waits for
568/// the next receive. This is the call to use rather than abandoning a receive that
569/// is still waiting, which would take that message instead.
570///
571/// # Arguments
572///
573/// * `transport` - the transport to receive from.
574/// * `timeout_ms` - how long to wait, in milliseconds.
575/// * `out_message` - receives a message handle to release with
576/// [`pamoja_message_free`], or null when the time ran out or the link has ended.
577/// * `out_timed_out` - receives whether the time ran out before a message arrived.
578///
579/// # Returns
580///
581/// [`PamojaStatus::Ok`] with a message, with the time run out, or with null once
582/// the link has ended, or [`PamojaStatus::Closed`] if the transport is not
583/// connected.
584///
585/// # Safety
586///
587/// `transport` must be a live handle, and `out_message` and `out_timed_out` must
588/// be writable.
589#[no_mangle]
590pub unsafe extern "C" fn pamoja_transport_recv_within(
591 transport: *mut PamojaTransport,
592 timeout_ms: u64,
593 out_message: *mut *mut PamojaMessage,
594 out_timed_out: *mut bool,
595) -> PamojaStatus {
596 let Some(transport) = transport_handle(transport) else {
597 return PamojaStatus::InvalidArgument;
598 };
599 receive_within(timeout_ms, out_message, out_timed_out, || {
600 transport.kind.recv()
601 })
602}
603
604/// Releases a transport handle.
605///
606/// Passing null is a no-op.
607///
608/// # Safety
609///
610/// `transport` must be a handle that has not already been freed or consumed by
611/// a call that takes ownership, or null. After this call it must not be used
612/// again.
613#[no_mangle]
614pub unsafe extern "C" fn pamoja_transport_free(transport: *mut PamojaTransport) {
615 if !transport.is_null() {
616 drop(Box::from_raw(transport));
617 }
618}
619
620/// Borrows a transport handle, rejecting a null pointer.
621///
622/// # Safety
623///
624/// `transport` must be a live handle from a call that produced one, or null.
625unsafe fn transport_handle<'a>(transport: *mut PamojaTransport) -> Option<&'a mut PamojaTransport> {
626 if transport.is_null() {
627 set_last_error("transport must not be null".to_owned());
628 return None;
629 }
630 Some(&mut *transport)
631}
632
633/// Takes ownership of a transport handle, leaving the caller nothing to free.
634///
635/// # Safety
636///
637/// `transport` must be a live handle that has not been freed or consumed, or
638/// null. After this call the caller must not use it again.
639pub(crate) unsafe fn take_transport(transport: *mut PamojaTransport) -> Option<Kind> {
640 if transport.is_null() {
641 set_last_error("transport must not be null".to_owned());
642 return None;
643 }
644 Some(Box::from_raw(transport).kind)
645}
646
647/// Runs a receive against a time limit and writes what came of it through the
648/// out-parameters every timed receive in this crate shares.
649///
650/// A receive is cancel-safe, so the one dropped when the time runs out leaves the
651/// next message queued rather than taking it.
652///
653/// # Safety
654///
655/// `out_message` and `out_timed_out` must be writable, or null to be refused.
656pub(crate) unsafe fn receive_within<F>(
657 timeout_ms: u64,
658 out_message: *mut *mut PamojaMessage,
659 out_timed_out: *mut bool,
660 receive: impl FnOnce() -> F,
661) -> PamojaStatus
662where
663 F: Future<Output = Result<Option<Message>>>,
664{
665 if out_message.is_null() || out_timed_out.is_null() {
666 set_last_error("out_message and out_timed_out must not be null".to_owned());
667 return PamojaStatus::InvalidArgument;
668 }
669 *out_message = ptr::null_mut();
670 *out_timed_out = false;
671 let limit = std::time::Duration::from_millis(timeout_ms);
672 let receive = receive();
673 match crate::runtime().block_on(async move { tokio::time::timeout(limit, receive).await }) {
674 Err(_) => {
675 *out_timed_out = true;
676 PamojaStatus::Ok
677 }
678 Ok(Ok(Some(message))) => {
679 *out_message = PamojaMessage::into_raw(message.topic, message.payload);
680 PamojaStatus::Ok
681 }
682 Ok(Ok(None)) => PamojaStatus::Ok,
683 Ok(Err(error)) => status(Err(error)),
684 }
685}
686
687/// Maps a transport result onto a status, recording any failure.
688pub(crate) fn status(result: Result<()>) -> PamojaStatus {
689 match result {
690 Ok(()) => PamojaStatus::Ok,
691 Err(error) => {
692 let status = PamojaStatus::from_error(&error);
693 set_last_error(error.to_string());
694 status
695 }
696 }
697}