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}