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.
15
16use std::ffi::CString;
17use std::future::Future;
18use std::pin::Pin;
19use std::ptr;
20
21use pamoja_core::{Result, Transport};
22
23use crate::{read_bytes, set_last_error, PamojaStatus};
24
25/// Object-safe erasure of a transport, so a wrapper can hold any of them.
26///
27/// The core trait returns `impl Future`, which is not dyn-compatible; this one
28/// boxes the future so a wrapping kind can hold a transport without naming its
29/// concrete type. That is what keeps the union below from naming itself: a
30/// nested transport is reached through this trait, whose futures are already a
31/// type the compiler can name.
32trait DynTransport: Send {
33    /// Connects the erased transport.
34    fn connect(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
35
36    /// Sends a payload over the erased transport.
37    fn send<'a>(
38        &'a mut self,
39        topic: &'a str,
40        payload: &'a [u8],
41    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
42
43    /// Subscribes the erased transport to a topic.
44    fn subscribe<'a>(
45        &'a mut self,
46        topic: &'a str,
47    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
48}
49
50/// Newtype carrying one concrete transport behind [`DynTransport`].
51struct Erased<T>(T);
52
53impl<T: Transport + Send> DynTransport for Erased<T> {
54    fn connect(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
55        Box::pin(Transport::connect(&mut self.0))
56    }
57
58    fn send<'a>(
59        &'a mut self,
60        topic: &'a str,
61        payload: &'a [u8],
62    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
63        Box::pin(Transport::send(&mut self.0, topic, payload))
64    }
65
66    fn subscribe<'a>(
67        &'a mut self,
68        topic: &'a str,
69    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
70        Box::pin(Transport::subscribe(&mut self.0, topic))
71    }
72}
73
74/// A transport of any kind, ready to be nested inside a wrapper.
75pub(crate) struct AnyTransport(Box<dyn DynTransport>);
76
77impl AnyTransport {
78    /// Erases one transport so a wrapper can hold it.
79    fn new(transport: Kind) -> Self {
80        Self(Box::new(Erased(transport)))
81    }
82}
83
84impl Transport for AnyTransport {
85    async fn connect(&mut self) -> Result<()> {
86        self.0.connect().await
87    }
88
89    async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
90        self.0.send(topic, payload).await
91    }
92
93    async fn subscribe(&mut self, topic: &str) -> Result<()> {
94        self.0.subscribe(topic).await
95    }
96}
97
98/// One transport, whichever kind it was built as.
99///
100/// A wrapping kind holds its inner transport erased rather than as this enum, so
101/// a faulty link can wrap a degraded one to any depth without the enum naming
102/// itself. Naming itself would make the hidden type of each method depend on
103/// knowing that same type, which is a cycle rather than recursion.
104pub(crate) enum Kind {
105    /// An MQTT broker connection.
106    #[cfg(feature = "mqtt")]
107    Mqtt(pamoja_mqtt::MqttTransport),
108    /// A CoAP endpoint.
109    #[cfg(feature = "coap")]
110    Coap(pamoja_coap::CoapTransport),
111    /// An in-process link to a loopback broker.
112    #[cfg(feature = "loopback")]
113    Loopback(pamoja_loopback::LoopbackTransport),
114    /// Another transport with a set number of sends made to fail.
115    #[cfg(feature = "loopback")]
116    Faulty(pamoja_loopback::Faulty<AnyTransport>),
117    /// Another transport carrying loss and outages.
118    #[cfg(feature = "sim")]
119    Degraded(pamoja_sim::DegradedLink<AnyTransport>),
120}
121
122impl Transport for Kind {
123    async fn connect(&mut self) -> Result<()> {
124        match self {
125            #[cfg(feature = "mqtt")]
126            Kind::Mqtt(inner) => inner.connect().await,
127            #[cfg(feature = "coap")]
128            Kind::Coap(inner) => inner.connect().await,
129            #[cfg(feature = "loopback")]
130            Kind::Loopback(inner) => inner.connect().await,
131            #[cfg(feature = "loopback")]
132            Kind::Faulty(inner) => inner.connect().await,
133            #[cfg(feature = "sim")]
134            Kind::Degraded(inner) => inner.connect().await,
135        }
136    }
137
138    async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
139        match self {
140            #[cfg(feature = "mqtt")]
141            Kind::Mqtt(inner) => inner.send(topic, payload).await,
142            #[cfg(feature = "coap")]
143            Kind::Coap(inner) => inner.send(topic, payload).await,
144            #[cfg(feature = "loopback")]
145            Kind::Loopback(inner) => inner.send(topic, payload).await,
146            #[cfg(feature = "loopback")]
147            Kind::Faulty(inner) => inner.send(topic, payload).await,
148            #[cfg(feature = "sim")]
149            Kind::Degraded(inner) => inner.send(topic, payload).await,
150        }
151    }
152
153    async fn subscribe(&mut self, topic: &str) -> Result<()> {
154        match self {
155            #[cfg(feature = "mqtt")]
156            Kind::Mqtt(inner) => inner.subscribe(topic).await,
157            #[cfg(feature = "coap")]
158            Kind::Coap(inner) => inner.subscribe(topic).await,
159            #[cfg(feature = "loopback")]
160            Kind::Loopback(inner) => inner.subscribe(topic).await,
161            #[cfg(feature = "loopback")]
162            Kind::Faulty(inner) => inner.subscribe(topic).await,
163            #[cfg(feature = "sim")]
164            Kind::Degraded(inner) => inner.subscribe(topic).await,
165        }
166    }
167}
168
169/// An opaque handle to one message that arrived on a subscribed topic.
170///
171/// CoAP and the loopback broker both hand back a topic and a payload, so one
172/// handle serves them rather than a near-identical type per transport.
173pub struct PamojaMessage {
174    topic: CString,
175    payload: Vec<u8>,
176}
177
178impl PamojaMessage {
179    /// Wraps a received message in a handle the caller owns.
180    ///
181    /// A topic carrying an interior null cannot cross as a C string, so it is
182    /// replaced with an empty one rather than truncated at the null, which would
183    /// hand the caller a plausible-looking wrong topic.
184    pub(crate) fn into_raw(topic: String, payload: Vec<u8>) -> *mut Self {
185        let topic = CString::new(topic).unwrap_or_default();
186        Box::into_raw(Box::new(Self { topic, payload }))
187    }
188}
189
190/// Returns the topic a message arrived on.
191///
192/// # Arguments
193///
194/// * `message` - the message.
195///
196/// # Returns
197///
198/// A null-terminated UTF-8 string owned by the message and valid until it is
199/// freed, or null if `message` is null.
200///
201/// # Safety
202///
203/// `message` must be a live handle from a call that produced one, or null.
204#[no_mangle]
205pub unsafe extern "C" fn pamoja_message_topic(
206    message: *const PamojaMessage,
207) -> *const std::ffi::c_char {
208    if message.is_null() {
209        return ptr::null();
210    }
211    (*message).topic.as_ptr()
212}
213
214/// Returns a pointer to a message payload.
215///
216/// # Arguments
217///
218/// * `message` - the message.
219///
220/// # Returns
221///
222/// A pointer to the bytes, valid until the message is freed, or null if
223/// `message` is null.
224///
225/// # Safety
226///
227/// `message` must be a live handle from a call that produced one, or null.
228#[no_mangle]
229pub unsafe extern "C" fn pamoja_message_payload(message: *const PamojaMessage) -> *const u8 {
230    if message.is_null() {
231        return ptr::null();
232    }
233    (*message).payload.as_ptr()
234}
235
236/// Returns the length in bytes of a message payload.
237///
238/// # Arguments
239///
240/// * `message` - the message.
241///
242/// # Returns
243///
244/// The length, or 0 if `message` is null.
245///
246/// # Safety
247///
248/// `message` must be a live handle from a call that produced one, or null.
249#[no_mangle]
250pub unsafe extern "C" fn pamoja_message_payload_len(message: *const PamojaMessage) -> usize {
251    if message.is_null() {
252        return 0;
253    }
254    (*message).payload.len()
255}
256
257/// Releases a message handle.
258///
259/// Passing null is a no-op.
260///
261/// # Safety
262///
263/// `message` must be a handle from a call that produced one and that has not
264/// already been freed, or null. After this call it must not be used again.
265#[no_mangle]
266pub unsafe extern "C" fn pamoja_message_free(message: *mut PamojaMessage) {
267    if !message.is_null() {
268        drop(Box::from_raw(message));
269    }
270}
271
272/// An opaque handle to one transport, ready to drive or to compose.
273///
274/// Release it with [`pamoja_transport_free`] unless it has been consumed by a
275/// call that takes ownership, such as adding it to a ladder.
276pub struct PamojaTransport {
277    pub(crate) kind: Kind,
278}
279
280impl PamojaTransport {
281    /// Wraps a transport kind in a handle the caller owns.
282    pub(crate) fn into_raw(kind: Kind) -> *mut Self {
283        Box::into_raw(Box::new(Self { kind }))
284    }
285}
286
287/// Creates an MQTT transport from broker settings.
288///
289/// # Arguments
290///
291/// * `config` - the broker settings, read the same way a client reads them.
292///
293/// # Returns
294///
295/// A handle the caller must release with [`pamoja_transport_free`] or hand to a
296/// call that consumes it, or null on failure.
297///
298/// # Safety
299///
300/// `config` must point to a valid config whose strings are valid
301/// null-terminated UTF-8 for the duration of the call.
302#[cfg(feature = "mqtt")]
303#[no_mangle]
304pub unsafe extern "C" fn pamoja_transport_mqtt(
305    config: *const crate::mqtt::PamojaMqttConfig,
306) -> *mut PamojaTransport {
307    let Some(settings) = crate::mqtt::mqtt_settings(config) else {
308        return ptr::null_mut();
309    };
310    PamojaTransport::into_raw(Kind::Mqtt(pamoja_mqtt::MqttTransport::new(settings)))
311}
312
313/// Wraps a transport so a set number of its next sends fail.
314///
315/// This is how a caller checks that a ladder falls through to its next rung, or
316/// that a buffer fills, without unplugging anything.
317///
318/// # Arguments
319///
320/// * `transport` - the transport to wrap, consumed by this call.
321/// * `failures` - how many upcoming sends to fail.
322///
323/// # Returns
324///
325/// A new handle owning the wrapped transport, or null if `transport` is null.
326///
327/// # Safety
328///
329/// `transport` must be a live handle that has not been freed or consumed. After
330/// this call it must not be used again, whatever the result.
331#[cfg(feature = "loopback")]
332#[no_mangle]
333pub unsafe extern "C" fn pamoja_transport_faulty(
334    transport: *mut PamojaTransport,
335    failures: usize,
336) -> *mut PamojaTransport {
337    let Some(inner) = take_transport(transport) else {
338        return ptr::null_mut();
339    };
340    PamojaTransport::into_raw(Kind::Faulty(pamoja_loopback::Faulty::new(
341        AnyTransport::new(inner),
342        failures,
343    )))
344}
345
346/// Wraps a transport in a link that loses packets and goes down.
347///
348/// # Arguments
349///
350/// * `transport` - the transport to wrap, consumed by this call.
351/// * `drop_every` - lose one send in every this many, or 0 to lose none.
352/// * `up` - how many sends the link stays up for, or 0 to never go down.
353/// * `down` - how many sends it then stays down for.
354///
355/// # Returns
356///
357/// A new handle owning the wrapped transport, or null if `transport` is null.
358///
359/// # Safety
360///
361/// `transport` must be a live handle that has not been freed or consumed. After
362/// this call it must not be used again, whatever the result.
363#[cfg(feature = "sim")]
364#[no_mangle]
365pub unsafe extern "C" fn pamoja_transport_degraded(
366    transport: *mut PamojaTransport,
367    drop_every: u32,
368    up: u32,
369    down: u32,
370) -> *mut PamojaTransport {
371    let Some(inner) = take_transport(transport) else {
372        return ptr::null_mut();
373    };
374    let mut link = pamoja_sim::DegradedLink::new(AnyTransport::new(inner));
375    if drop_every != 0 {
376        link = link.drop_every(drop_every);
377    }
378    if up != 0 {
379        link = link.intermittent(up, down);
380    }
381    PamojaTransport::into_raw(Kind::Degraded(link))
382}
383
384/// Connects a transport.
385///
386/// # Arguments
387///
388/// * `transport` - the transport to connect.
389///
390/// # Returns
391///
392/// [`PamojaStatus::Ok`] once connected.
393///
394/// # Safety
395///
396/// `transport` must be a live handle that has not been freed or consumed.
397#[no_mangle]
398pub unsafe extern "C" fn pamoja_transport_connect(transport: *mut PamojaTransport) -> PamojaStatus {
399    let Some(transport) = transport_handle(transport) else {
400        return PamojaStatus::InvalidArgument;
401    };
402    status(crate::runtime().block_on(transport.kind.connect()))
403}
404
405/// Sends a payload to a topic over a transport.
406///
407/// # Arguments
408///
409/// * `transport` - the transport to send over.
410/// * `topic` - the destination topic, as null-terminated UTF-8.
411/// * `payload` - the bytes to send.
412/// * `payload_len` - the length of `payload`.
413///
414/// # Returns
415///
416/// [`PamojaStatus::Ok`] once the transport has taken the payload.
417///
418/// # Safety
419///
420/// `transport` must be a live handle, `topic` a valid null-terminated UTF-8
421/// string, and `payload` must point to at least `payload_len` readable bytes or
422/// be null when that length is 0.
423#[no_mangle]
424pub unsafe extern "C" fn pamoja_transport_send(
425    transport: *mut PamojaTransport,
426    topic: *const std::ffi::c_char,
427    payload: *const u8,
428    payload_len: usize,
429) -> PamojaStatus {
430    let Some(transport) = transport_handle(transport) else {
431        return PamojaStatus::InvalidArgument;
432    };
433    let Some(topic) = crate::read_str(topic, "topic") else {
434        return PamojaStatus::InvalidArgument;
435    };
436    let payload = match read_bytes(payload, payload_len) {
437        Ok(payload) => payload,
438        Err(status) => return status,
439    };
440    status(crate::runtime().block_on(transport.kind.send(topic, &payload)))
441}
442
443/// Subscribes a transport to a topic.
444///
445/// # Arguments
446///
447/// * `transport` - the transport to subscribe.
448/// * `topic` - the topic to subscribe to, as null-terminated UTF-8.
449///
450/// # Returns
451///
452/// [`PamojaStatus::Ok`] once subscribed.
453///
454/// # Safety
455///
456/// `transport` must be a live handle and `topic` a valid null-terminated UTF-8
457/// string.
458#[no_mangle]
459pub unsafe extern "C" fn pamoja_transport_subscribe(
460    transport: *mut PamojaTransport,
461    topic: *const std::ffi::c_char,
462) -> PamojaStatus {
463    let Some(transport) = transport_handle(transport) else {
464        return PamojaStatus::InvalidArgument;
465    };
466    let Some(topic) = crate::read_str(topic, "topic") else {
467        return PamojaStatus::InvalidArgument;
468    };
469    status(crate::runtime().block_on(transport.kind.subscribe(topic)))
470}
471
472/// Releases a transport handle.
473///
474/// Passing null is a no-op.
475///
476/// # Safety
477///
478/// `transport` must be a handle that has not already been freed or consumed by
479/// a call that takes ownership, or null. After this call it must not be used
480/// again.
481#[no_mangle]
482pub unsafe extern "C" fn pamoja_transport_free(transport: *mut PamojaTransport) {
483    if !transport.is_null() {
484        drop(Box::from_raw(transport));
485    }
486}
487
488/// Borrows a transport handle, rejecting a null pointer.
489///
490/// # Safety
491///
492/// `transport` must be a live handle from a call that produced one, or null.
493unsafe fn transport_handle<'a>(transport: *mut PamojaTransport) -> Option<&'a mut PamojaTransport> {
494    if transport.is_null() {
495        set_last_error("transport must not be null".to_owned());
496        return None;
497    }
498    Some(&mut *transport)
499}
500
501/// Takes ownership of a transport handle, leaving the caller nothing to free.
502///
503/// # Safety
504///
505/// `transport` must be a live handle that has not been freed or consumed, or
506/// null. After this call the caller must not use it again.
507pub(crate) unsafe fn take_transport(transport: *mut PamojaTransport) -> Option<Kind> {
508    if transport.is_null() {
509        set_last_error("transport must not be null".to_owned());
510        return None;
511    }
512    Some(Box::from_raw(transport).kind)
513}
514
515/// Maps a transport result onto a status, recording any failure.
516pub(crate) fn status(result: Result<()>) -> PamojaStatus {
517    match result {
518        Ok(()) => PamojaStatus::Ok,
519        Err(error) => {
520            let status = PamojaStatus::from_error(&error);
521            set_last_error(error.to_string());
522            status
523        }
524    }
525}