Skip to main content

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}