Skip to main content

pamoja_ffi/
coap.rs

1//! The C ABI for CoAP.
2//!
3//! These functions wrap [`pamoja_coap`] for callers that reach the SDK through
4//! the flat C boundary. CoAP is the transport for links where MQTT is more than
5//! the budget allows: it runs over UDP, its headers are a handful of bytes, and
6//! a node can fire a reading and forget it rather than holding a session open.
7//!
8//! A client holds a socket and a background loop, so it crosses as an opaque
9//! handle and every call blocks on the shared runtime. To use CoAP as one rung
10//! of a ladder rather than driving it directly, build a
11//! [`PamojaTransport`] with
12//! [`pamoja_transport_coap`] instead.
13
14use std::ffi::c_char;
15use std::ptr;
16use std::sync::Arc;
17use std::time::Duration;
18
19use pamoja_coap::{CoapConfig, CoapTransport, Reliability};
20use pamoja_core::Transport;
21use tokio::sync::Mutex;
22
23use crate::transport::{status, Kind, PamojaMessage, PamojaTransport};
24use crate::{read_bytes, read_str, runtime, set_last_error, PamojaStatus};
25
26/// Whether a CoAP request is acknowledged and retried.
27#[repr(C)]
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum PamojaCoapReliability {
30    /// Fire and forget: the request is sent once and not acknowledged.
31    NonConfirmable = 0,
32    /// The request is acknowledged, and retransmitted until an ACK arrives.
33    Confirmable = 1,
34}
35
36/// The settings a CoAP endpoint is built from.
37#[repr(C)]
38#[derive(Clone, Copy, Debug)]
39pub struct PamojaCoapConfig {
40    /// The peer hostname or IP address, as null-terminated UTF-8.
41    pub host: *const c_char,
42    /// The peer UDP port, conventionally 5683 for plaintext CoAP.
43    pub port: u16,
44    /// The local address to bind, or null for the default.
45    pub bind: *const c_char,
46    /// Whether requests are acknowledged and retried.
47    pub reliability: PamojaCoapReliability,
48    /// How long to wait for an acknowledgement, in milliseconds, or 0 for the
49    /// default.
50    pub ack_timeout_ms: u32,
51    /// How many times to retransmit an unacknowledged request, or 0 for the
52    /// default.
53    pub max_retransmits: u32,
54}
55
56/// An opaque handle to a CoAP endpoint.
57pub struct PamojaCoapClient {
58    inner: Arc<Mutex<CoapTransport>>,
59}
60
61/// Creates a disconnected CoAP endpoint from the given settings.
62///
63/// # Arguments
64///
65/// * `config` - the endpoint settings.
66///
67/// # Returns
68///
69/// A handle the caller must release with [`pamoja_coap_client_free`], or null on
70/// failure with the reason available from
71/// [`pamoja_last_error_message`](crate::pamoja_last_error_message).
72///
73/// # Safety
74///
75/// `config` must point to a valid [`PamojaCoapConfig`] whose strings are valid
76/// null-terminated UTF-8 for the duration of the call.
77#[no_mangle]
78pub unsafe extern "C" fn pamoja_coap_client_new(
79    config: *const PamojaCoapConfig,
80) -> *mut PamojaCoapClient {
81    let Some(settings) = coap_settings(config) else {
82        return ptr::null_mut();
83    };
84    Box::into_raw(Box::new(PamojaCoapClient {
85        inner: Arc::new(Mutex::new(CoapTransport::new(settings))),
86    }))
87}
88
89/// Binds the local socket so the endpoint can carry traffic.
90///
91/// # Arguments
92///
93/// * `client` - the endpoint.
94///
95/// # Returns
96///
97/// [`PamojaStatus::Ok`] once bound.
98///
99/// # Safety
100///
101/// `client` must be a live handle from [`pamoja_coap_client_new`].
102#[no_mangle]
103pub unsafe extern "C" fn pamoja_coap_client_connect(client: *mut PamojaCoapClient) -> PamojaStatus {
104    let Some(client) = client_handle(client) else {
105        return PamojaStatus::InvalidArgument;
106    };
107    let inner = Arc::clone(&client.inner);
108    status(runtime().block_on(async move { inner.lock().await.connect().await }))
109}
110
111/// Sends a payload to a resource path.
112///
113/// # Arguments
114///
115/// * `client` - the endpoint.
116/// * `topic` - the resource path, as null-terminated UTF-8.
117/// * `payload` - the bytes to send.
118/// * `payload_len` - the length of `payload`.
119///
120/// # Returns
121///
122/// [`PamojaStatus::Ok`] once the request has gone out.
123///
124/// # Safety
125///
126/// `client` must be a live handle, `topic` a valid null-terminated UTF-8
127/// string, and `payload` must point to at least `payload_len` readable bytes or
128/// be null when that length is 0.
129#[no_mangle]
130pub unsafe extern "C" fn pamoja_coap_client_send(
131    client: *mut PamojaCoapClient,
132    topic: *const c_char,
133    payload: *const u8,
134    payload_len: usize,
135) -> PamojaStatus {
136    let Some(client) = client_handle(client) else {
137        return PamojaStatus::InvalidArgument;
138    };
139    let Some(topic) = read_str(topic, "topic") else {
140        return PamojaStatus::InvalidArgument;
141    };
142    let payload = match read_bytes(payload, payload_len) {
143        Ok(payload) => payload,
144        Err(status) => return status,
145    };
146    let inner = Arc::clone(&client.inner);
147    let topic = topic.to_owned();
148    status(runtime().block_on(async move { inner.lock().await.send(&topic, &payload).await }))
149}
150
151/// Observes a resource path, so messages published to it arrive at
152/// [`pamoja_coap_client_recv`].
153///
154/// # Arguments
155///
156/// * `client` - the endpoint.
157/// * `topic` - the resource path, as null-terminated UTF-8.
158///
159/// # Returns
160///
161/// [`PamojaStatus::Ok`] once observing.
162///
163/// # Safety
164///
165/// `client` must be a live handle and `topic` a valid null-terminated UTF-8
166/// string.
167#[no_mangle]
168pub unsafe extern "C" fn pamoja_coap_client_subscribe(
169    client: *mut PamojaCoapClient,
170    topic: *const c_char,
171) -> PamojaStatus {
172    let Some(client) = client_handle(client) else {
173        return PamojaStatus::InvalidArgument;
174    };
175    let Some(topic) = read_str(topic, "topic") else {
176        return PamojaStatus::InvalidArgument;
177    };
178    let inner = Arc::clone(&client.inner);
179    let topic = topic.to_owned();
180    status(runtime().block_on(async move { inner.lock().await.subscribe(&topic).await }))
181}
182
183/// Waits for the next message on an observed path.
184///
185/// # Arguments
186///
187/// * `client` - the endpoint.
188/// * `out_message` - receives a message handle, or null when the endpoint is
189///   closed.
190///
191/// # Returns
192///
193/// [`PamojaStatus::Ok`] on success. A null `out_message` with an `Ok` status
194/// means the endpoint closed rather than that anything failed.
195///
196/// # Safety
197///
198/// `client` must be a live handle and `out_message` must be writable.
199#[no_mangle]
200pub unsafe extern "C" fn pamoja_coap_client_recv(
201    client: *mut PamojaCoapClient,
202    out_message: *mut *mut PamojaMessage,
203) -> PamojaStatus {
204    let Some(client) = client_handle(client) else {
205        return PamojaStatus::InvalidArgument;
206    };
207    if out_message.is_null() {
208        set_last_error("out_message must not be null".to_owned());
209        return PamojaStatus::InvalidArgument;
210    }
211    *out_message = ptr::null_mut();
212
213    let inner = Arc::clone(&client.inner);
214    match runtime().block_on(async move { inner.lock().await.recv().await }) {
215        Ok(Some(message)) => {
216            *out_message = PamojaMessage::into_raw(message.topic, message.payload);
217            PamojaStatus::Ok
218        }
219        Ok(None) => PamojaStatus::Ok,
220        Err(error) => {
221            let code = PamojaStatus::from_error(&error);
222            set_last_error(error.to_string());
223            code
224        }
225    }
226}
227
228/// Reports whether the endpoint is bound.
229///
230/// # Arguments
231///
232/// * `client` - the endpoint.
233///
234/// # Returns
235///
236/// `true` when bound, or `false` if `client` is null.
237///
238/// # Safety
239///
240/// `client` must be a live handle from [`pamoja_coap_client_new`], or null.
241#[no_mangle]
242pub unsafe extern "C" fn pamoja_coap_client_is_connected(client: *mut PamojaCoapClient) -> bool {
243    let Some(client) = client_handle(client) else {
244        return false;
245    };
246    let inner = Arc::clone(&client.inner);
247    runtime().block_on(async move { inner.lock().await.is_connected() })
248}
249
250/// Releases the socket the endpoint holds.
251///
252/// # Arguments
253///
254/// * `client` - the endpoint.
255///
256/// # Returns
257///
258/// [`PamojaStatus::Ok`] once closed.
259///
260/// # Safety
261///
262/// `client` must be a live handle from [`pamoja_coap_client_new`].
263#[no_mangle]
264pub unsafe extern "C" fn pamoja_coap_client_disconnect(
265    client: *mut PamojaCoapClient,
266) -> PamojaStatus {
267    let Some(client) = client_handle(client) else {
268        return PamojaStatus::InvalidArgument;
269    };
270    let inner = Arc::clone(&client.inner);
271    status(runtime().block_on(async move { inner.lock().await.disconnect().await }))
272}
273
274/// Releases a CoAP endpoint handle.
275///
276/// Passing null is a no-op.
277///
278/// # Safety
279///
280/// `client` must be a handle from [`pamoja_coap_client_new`] that has not
281/// already been freed, or null. After this call it must not be used again.
282#[no_mangle]
283pub unsafe extern "C" fn pamoja_coap_client_free(client: *mut PamojaCoapClient) {
284    if !client.is_null() {
285        drop(Box::from_raw(client));
286    }
287}
288
289/// Creates a CoAP transport for composing into a ladder or a wrapper.
290///
291/// # Arguments
292///
293/// * `config` - the endpoint settings.
294///
295/// # Returns
296///
297/// A handle the caller must release with
298/// [`pamoja_transport_free`](crate::transport::pamoja_transport_free) or hand to
299/// a call that consumes it, or null on failure.
300///
301/// # Safety
302///
303/// `config` must point to a valid [`PamojaCoapConfig`] whose strings are valid
304/// null-terminated UTF-8 for the duration of the call.
305#[no_mangle]
306pub unsafe extern "C" fn pamoja_transport_coap(
307    config: *const PamojaCoapConfig,
308) -> *mut PamojaTransport {
309    let Some(settings) = coap_settings(config) else {
310        return ptr::null_mut();
311    };
312    PamojaTransport::into_raw(Kind::Coap(CoapTransport::new(settings)))
313}
314
315/// Reads the endpoint settings a config describes.
316///
317/// # Safety
318///
319/// `config` must point to a valid [`PamojaCoapConfig`] whose strings are valid
320/// null-terminated UTF-8 for the duration of the call, or be null.
321unsafe fn coap_settings(config: *const PamojaCoapConfig) -> Option<CoapConfig> {
322    if config.is_null() {
323        set_last_error("config must not be null".to_owned());
324        return None;
325    }
326    let config = &*config;
327    let host = read_str(config.host, "host")?;
328
329    let mut settings = CoapConfig::new(host, config.port);
330    if !config.bind.is_null() {
331        settings = settings.bind(read_str(config.bind, "bind")?);
332    }
333    settings = settings.reliability(match config.reliability {
334        PamojaCoapReliability::NonConfirmable => Reliability::NonConfirmable,
335        PamojaCoapReliability::Confirmable => Reliability::Confirmable,
336    });
337    if config.ack_timeout_ms != 0 {
338        settings = settings.ack_timeout(Duration::from_millis(u64::from(config.ack_timeout_ms)));
339    }
340    if config.max_retransmits != 0 {
341        settings = settings.max_retransmits(config.max_retransmits);
342    }
343    Some(settings)
344}
345
346/// Borrows a client handle, rejecting a null pointer.
347///
348/// # Safety
349///
350/// `client` must be a live handle from [`pamoja_coap_client_new`], or null.
351unsafe fn client_handle<'a>(client: *mut PamojaCoapClient) -> Option<&'a PamojaCoapClient> {
352    if client.is_null() {
353        set_last_error("client must not be null".to_owned());
354        return None;
355    }
356    Some(&*client)
357}