1#![allow(unsafe_code)]
52
53use std::cell::RefCell;
54use std::ffi::{c_char, CString};
55use std::ptr;
56use std::sync::OnceLock;
57
58use pamoja_core::Error;
59
60#[cfg(feature = "actuators")]
65pub mod actuators;
66#[cfg(feature = "actuators")]
67pub mod actuators_driver;
68#[cfg(feature = "audit")]
69pub mod audit;
70#[cfg(feature = "bus")]
71pub mod bus;
72#[cfg(feature = "can")]
73pub mod can;
74#[cfg(feature = "can")]
75pub mod can_bus;
76#[cfg(feature = "coap")]
77pub mod coap;
78#[cfg(feature = "codec")]
79pub mod codec;
80#[cfg(feature = "gateway")]
81pub mod gateway;
82#[cfg(feature = "gateway")]
83pub mod gateway_chirpstack;
84#[cfg(feature = "gateway")]
85pub mod gateway_network;
86#[cfg(feature = "gateway")]
87pub mod gateway_station;
88#[cfg(feature = "gpio")]
89pub mod gpio;
90#[cfg(feature = "hal")]
91pub mod hal;
92#[cfg(feature = "runtime")]
93pub mod host;
94#[cfg(feature = "kit")]
95pub mod kit;
96#[cfg(feature = "ladder")]
97pub mod ladder;
98#[cfg(feature = "loopback")]
99pub mod loopback;
100#[cfg(feature = "lora")]
101pub mod lora;
102#[cfg(feature = "radios")]
103pub mod lora_radio;
104#[cfg(feature = "lora")]
105pub mod lora_region;
106#[cfg(feature = "lorawan")]
107pub mod lorawan;
108#[cfg(all(feature = "lora", feature = "lorawan"))]
109pub mod lorawan_device;
110#[cfg(feature = "lorawan")]
111pub mod lorawan_link;
112#[cfg(feature = "lorawan")]
113pub mod lorawan_mac;
114#[cfg(all(feature = "lora", feature = "lorawan"))]
115pub mod lorawan_packages;
116#[cfg(all(feature = "lora", feature = "lorawan"))]
117pub mod lorawan_relay;
118#[cfg(all(feature = "lora", feature = "lorawan"))]
119pub mod lorawan_relay_node;
120#[cfg(feature = "mavlink")]
121pub mod mavlink;
122#[cfg(feature = "mavlink")]
123pub mod mavlink_enums;
124#[cfg(feature = "mavlink")]
125pub mod mavlink_protocol;
126#[cfg(feature = "mavlink")]
127pub mod mavlink_schema;
128#[cfg(feature = "mesh")]
129pub mod mesh;
130#[cfg(feature = "modbus")]
131pub mod modbus;
132#[cfg(feature = "modbus")]
133pub mod modbus_client;
134#[cfg(feature = "kit")]
135pub mod motion;
136#[cfg(feature = "mqtt")]
137pub mod mqtt;
138#[cfg(feature = "hal")]
139pub mod port;
140#[cfg(feature = "power")]
141pub mod power;
142#[cfg(feature = "profile")]
143pub mod profile;
144#[cfg(feature = "radios")]
145pub mod radios;
146#[cfg(feature = "ros2")]
147pub mod ros2;
148#[cfg(feature = "routing")]
149pub mod routing;
150#[cfg(feature = "profile")]
151pub mod rules;
152#[cfg(feature = "security")]
153pub mod security;
154#[cfg(feature = "sensors")]
155pub mod sensors;
156#[cfg(feature = "sensors")]
157pub mod sensors_driver;
158#[cfg(feature = "serial")]
159pub mod serial;
160#[cfg(feature = "session")]
161pub mod session;
162#[cfg(feature = "sim")]
163pub mod sim;
164#[cfg(feature = "sync")]
165pub mod sync;
166#[cfg(feature = "telemetry")]
167pub mod telemetry;
168#[cfg(feature = "runtime")]
169pub mod transport;
170#[cfg(feature = "update")]
171pub mod update;
172#[cfg(feature = "zenoh")]
173pub mod zenoh;
174
175#[repr(C)]
181#[derive(Clone, Copy, Debug, PartialEq, Eq)]
182pub enum PamojaStatus {
183 Ok = 0,
185 Transport = 1,
187 Io = 2,
189 Codec = 3,
191 Closed = 4,
193 Unsupported = 5,
195 InvalidArgument = 6,
197 Other = 7,
199 Panic = 8,
201 Auth = 9,
203}
204
205impl PamojaStatus {
206 pub(crate) fn from_error(error: &Error) -> Self {
216 match error {
217 Error::Transport(_) => Self::Transport,
218 Error::Io(_) => Self::Io,
219 Error::Codec(_) => Self::Codec,
220 Error::Closed => Self::Closed,
221 Error::Auth(_) => Self::Auth,
222 Error::Unsupported(_) => Self::Unsupported,
223 _ => Self::Other,
224 }
225 }
226}
227
228thread_local! {
229 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
231}
232
233pub(crate) fn set_last_error(message: String) {
241 let value =
242 CString::new(message).unwrap_or_else(|_| CString::new("pamoja error").expect("static"));
243 LAST_ERROR.with(|slot| *slot.borrow_mut() = Some(value));
244}
245
246#[cfg(feature = "runtime")]
248pub(crate) fn take_last_error() -> Option<String> {
249 LAST_ERROR
250 .with(|slot| slot.borrow_mut().take())
251 .and_then(|value| value.into_string().ok())
252}
253
254#[no_mangle]
263pub extern "C" fn pamoja_last_error_message() -> *const c_char {
264 LAST_ERROR.with(|slot| match &*slot.borrow() {
265 Some(value) => value.as_ptr(),
266 None => ptr::null(),
267 })
268}
269
270#[cfg(any(
276 feature = "audit",
277 feature = "can",
278 feature = "codec",
279 feature = "gateway",
280 feature = "hal",
281 feature = "lora",
282 feature = "lorawan",
283 feature = "mavlink",
284 feature = "mesh",
285 feature = "modbus",
286 feature = "mqtt",
287 feature = "ros2",
288 feature = "runtime",
289 feature = "security",
290 feature = "sensors",
291 feature = "serial",
292 feature = "session",
293 feature = "update"
294))]
295pub(crate) unsafe fn read_bytes(ptr: *const u8, len: usize) -> Result<Vec<u8>, PamojaStatus> {
296 if len == 0 {
297 Ok(Vec::new())
298 } else if ptr.is_null() {
299 set_last_error("payload must not be null when its length is non-zero".to_owned());
300 Err(PamojaStatus::InvalidArgument)
301 } else {
302 Ok(std::slice::from_raw_parts(ptr, len).to_vec())
303 }
304}
305
306#[cfg(any(
313 feature = "audit",
314 feature = "bus",
315 feature = "codec",
316 feature = "gateway",
317 feature = "hal",
318 feature = "lorawan",
319 feature = "modbus",
320 feature = "radios",
321 feature = "ros2",
322 feature = "security",
323 feature = "serial",
324 feature = "sync",
325 feature = "update"
326))]
327pub struct PamojaBuffer {
328 bytes: Vec<u8>,
329}
330
331#[cfg(any(
332 feature = "audit",
333 feature = "bus",
334 feature = "codec",
335 feature = "gateway",
336 feature = "hal",
337 feature = "lorawan",
338 feature = "modbus",
339 feature = "radios",
340 feature = "ros2",
341 feature = "security",
342 feature = "serial",
343 feature = "sync",
344 feature = "update"
345))]
346impl PamojaBuffer {
347 pub(crate) fn into_raw(bytes: Vec<u8>) -> *mut Self {
357 Box::into_raw(Box::new(Self { bytes }))
358 }
359}
360
361#[cfg(any(
374 feature = "audit",
375 feature = "bus",
376 feature = "codec",
377 feature = "gateway",
378 feature = "hal",
379 feature = "lorawan",
380 feature = "modbus",
381 feature = "radios",
382 feature = "ros2",
383 feature = "security",
384 feature = "serial",
385 feature = "sync",
386 feature = "update"
387))]
388#[no_mangle]
389pub unsafe extern "C" fn pamoja_buffer_data(buffer: *const PamojaBuffer) -> *const u8 {
390 if buffer.is_null() {
391 return ptr::null();
392 }
393 (*buffer).bytes.as_ptr()
394}
395
396#[cfg(any(
406 feature = "audit",
407 feature = "bus",
408 feature = "codec",
409 feature = "gateway",
410 feature = "hal",
411 feature = "lorawan",
412 feature = "modbus",
413 feature = "radios",
414 feature = "ros2",
415 feature = "security",
416 feature = "serial",
417 feature = "sync",
418 feature = "update"
419))]
420#[no_mangle]
421pub unsafe extern "C" fn pamoja_buffer_len(buffer: *const PamojaBuffer) -> usize {
422 if buffer.is_null() {
423 return 0;
424 }
425 (*buffer).bytes.len()
426}
427
428#[cfg(any(
437 feature = "audit",
438 feature = "bus",
439 feature = "codec",
440 feature = "gateway",
441 feature = "hal",
442 feature = "lorawan",
443 feature = "modbus",
444 feature = "radios",
445 feature = "ros2",
446 feature = "security",
447 feature = "serial",
448 feature = "sync",
449 feature = "update"
450))]
451#[no_mangle]
452pub unsafe extern "C" fn pamoja_buffer_free(buffer: *mut PamojaBuffer) {
453 if !buffer.is_null() {
454 drop(Box::from_raw(buffer));
455 }
456}
457
458#[cfg(any(
465 feature = "lora",
466 feature = "mavlink",
467 feature = "profile",
468 feature = "ros2",
469 feature = "sensors",
470 feature = "zenoh"
471))]
472pub struct PamojaString {
473 text: CString,
474}
475
476#[cfg(any(
477 feature = "lora",
478 feature = "mavlink",
479 feature = "profile",
480 feature = "ros2",
481 feature = "sensors",
482 feature = "zenoh"
483))]
484impl PamojaString {
485 pub(crate) fn into_raw(text: String) -> *mut Self {
496 match CString::new(text) {
497 Ok(text) => Box::into_raw(Box::new(Self { text })),
498 Err(_) => {
499 set_last_error("the string contains an interior null byte".to_owned());
500 ptr::null_mut()
501 }
502 }
503 }
504}
505
506#[cfg(any(
519 feature = "lora",
520 feature = "mavlink",
521 feature = "profile",
522 feature = "ros2",
523 feature = "sensors",
524 feature = "zenoh"
525))]
526#[no_mangle]
527pub unsafe extern "C" fn pamoja_string_data(string: *const PamojaString) -> *const c_char {
528 if string.is_null() {
529 return ptr::null();
530 }
531 (*string).text.as_ptr()
532}
533
534#[cfg(any(
544 feature = "lora",
545 feature = "mavlink",
546 feature = "profile",
547 feature = "ros2",
548 feature = "sensors",
549 feature = "zenoh"
550))]
551#[no_mangle]
552pub unsafe extern "C" fn pamoja_string_len(string: *const PamojaString) -> usize {
553 if string.is_null() {
554 return 0;
555 }
556 (*string).text.as_bytes().len()
557}
558
559#[cfg(any(
568 feature = "lora",
569 feature = "mavlink",
570 feature = "profile",
571 feature = "ros2",
572 feature = "sensors",
573 feature = "zenoh"
574))]
575#[no_mangle]
576pub unsafe extern "C" fn pamoja_string_free(string: *mut PamojaString) {
577 if !string.is_null() {
578 drop(Box::from_raw(string));
579 }
580}
581
582#[cfg(any(
589 feature = "coap",
590 feature = "gpio",
591 feature = "hal",
592 feature = "ladder",
593 feature = "lora",
594 feature = "mavlink",
595 feature = "mqtt",
596 feature = "profile",
597 feature = "radios",
598 feature = "ros2",
599 feature = "runtime",
600 feature = "sync",
601 feature = "zenoh"
602))]
603pub(crate) unsafe fn read_str<'a>(ptr: *const c_char, name: &str) -> Option<&'a str> {
604 if ptr.is_null() {
605 set_last_error(format!("{name} must not be null"));
606 return None;
607 }
608 match std::ffi::CStr::from_ptr(ptr).to_str() {
609 Ok(value) => Some(value),
610 Err(_) => {
611 set_last_error(format!("{name} must be valid UTF-8"));
612 None
613 }
614 }
615}
616
617#[cfg(feature = "runtime")]
619static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
620
621#[cfg(feature = "runtime")]
628pub(crate) fn runtime() -> &'static tokio::runtime::Runtime {
629 RUNTIME.get_or_init(|| {
630 tokio::runtime::Builder::new_multi_thread()
631 .enable_all()
632 .build()
633 .expect("build the pamoja tokio runtime")
634 })
635}
636
637static VERSION: OnceLock<CString> = OnceLock::new();
639
640#[no_mangle]
647pub extern "C" fn pamoja_version() -> *const c_char {
648 VERSION
649 .get_or_init(|| CString::new(env!("CARGO_PKG_VERSION")).expect("version has no null byte"))
650 .as_ptr()
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656
657 #[test]
663 fn every_exported_constant_reaches_the_header_as_a_value() {
664 let header = include_str!("../include/pamoja.h");
665 let mut defined = std::collections::HashMap::new();
666 for line in header.lines() {
667 if let Some(rest) = line.strip_prefix("#define PAMOJA_") {
668 let mut parts = rest.splitn(2, ' ');
669 let (Some(name), Some(value)) = (parts.next(), parts.next()) else {
670 continue;
671 };
672 defined.insert(format!("PAMOJA_{name}"), value.trim().to_owned());
673 }
674 }
675
676 let mut missing = Vec::new();
677 let mut unresolved = Vec::new();
678 let sources = std::fs::read_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/src"))
679 .expect("the crate's own sources");
680 for entry in sources.flatten() {
681 let path = entry.path();
682 if path.extension().is_none_or(|extension| extension != "rs") {
683 continue;
684 }
685 let source = std::fs::read_to_string(&path).expect("read a source");
686 for line in source.lines() {
687 let Some(rest) = line.trim_start().strip_prefix("pub const PAMOJA_") else {
688 continue;
689 };
690 let Some(name) = rest.split(':').next() else {
691 continue;
692 };
693 let name = format!("PAMOJA_{}", name.trim());
694 match defined.get(&name) {
695 None => missing.push(name),
696 Some(value) => {
697 if value
698 .chars()
699 .next()
700 .is_some_and(|first| first.is_ascii_alphabetic() || first == '_')
701 && !value.starts_with("PAMOJA_")
702 {
703 unresolved.push(format!("{name} = {value}"));
704 }
705 }
706 }
707 }
708 }
709 assert!(missing.is_empty(), "dropped from the header: {missing:?}");
710 assert!(
711 unresolved.is_empty(),
712 "emitted as a name C cannot resolve: {unresolved:?}"
713 );
714 }
715
716 #[test]
717 fn status_maps_each_error_variant() {
718 assert!(matches!(
719 PamojaStatus::from_error(&Error::Transport("x".into())),
720 PamojaStatus::Transport
721 ));
722 assert!(matches!(
723 PamojaStatus::from_error(&Error::Io("x".into())),
724 PamojaStatus::Io
725 ));
726 assert!(matches!(
727 PamojaStatus::from_error(&Error::Codec("x".into())),
728 PamojaStatus::Codec
729 ));
730 assert!(matches!(
731 PamojaStatus::from_error(&Error::Closed),
732 PamojaStatus::Closed
733 ));
734 assert!(matches!(
735 PamojaStatus::from_error(&Error::Unsupported("mqtt")),
736 PamojaStatus::Unsupported
737 ));
738 }
739
740 #[test]
741 fn version_is_a_non_empty_c_string() {
742 let ptr = pamoja_version();
743 assert!(!ptr.is_null());
744 let version = unsafe { std::ffi::CStr::from_ptr(ptr) };
746 assert_eq!(version.to_str().expect("utf-8"), env!("CARGO_PKG_VERSION"));
747 }
748
749 #[test]
750 fn last_error_round_trips_on_this_thread() {
751 set_last_error("transport error: boom".to_owned());
752 let ptr = pamoja_last_error_message();
753 assert!(!ptr.is_null());
754 let message = unsafe { std::ffi::CStr::from_ptr(ptr) };
756 assert_eq!(message.to_str().expect("utf-8"), "transport error: boom");
757 }
758}