1use std::ffi::c_char;
15use std::fmt::Write as _;
16use std::ptr;
17
18use pamoja_ros2::key::entity_key;
19use pamoja_ros2::msg::{CdrReader, CdrWriter, Twist, Vector3};
20use pamoja_ros2::name::{dds_topic, is_fully_qualified, is_valid_name, percent_mangle, EntityKind};
21use pamoja_ros2::typehash::{dds_type_name, TypeHash};
22
23use crate::{read_bytes, read_str, set_last_error, PamojaBuffer, PamojaStatus, PamojaString};
24
25pub const PAMOJA_TYPE_HASH_LEN: usize = 32;
27
28#[repr(C)]
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum PamojaEntityKind {
32 Topic = 0,
34 ServiceRequest = 1,
36 ServiceResponse = 2,
38}
39
40impl From<PamojaEntityKind> for EntityKind {
41 fn from(kind: PamojaEntityKind) -> Self {
42 match kind {
43 PamojaEntityKind::Topic => EntityKind::Topic,
44 PamojaEntityKind::ServiceRequest => EntityKind::ServiceRequest,
45 PamojaEntityKind::ServiceResponse => EntityKind::ServiceResponse,
46 }
47 }
48}
49
50#[repr(C)]
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub struct PamojaTypeHash {
54 pub digest: [u8; PAMOJA_TYPE_HASH_LEN],
56}
57
58#[repr(C)]
60#[derive(Clone, Copy, Debug, PartialEq)]
61pub struct PamojaVector3 {
62 pub x: f64,
64 pub y: f64,
66 pub z: f64,
68}
69
70#[repr(C)]
75#[derive(Clone, Copy, Debug, PartialEq)]
76pub struct PamojaRos2Twist {
77 pub linear: PamojaVector3,
79 pub angular: PamojaVector3,
81}
82
83impl From<Vector3> for PamojaVector3 {
84 fn from(vector: Vector3) -> Self {
85 Self {
86 x: vector.x,
87 y: vector.y,
88 z: vector.z,
89 }
90 }
91}
92
93impl From<PamojaVector3> for Vector3 {
94 fn from(vector: PamojaVector3) -> Self {
95 Vector3::new(vector.x, vector.y, vector.z)
96 }
97}
98
99impl PamojaTypeHash {
100 fn text(&self) -> String {
102 let mut text = String::from("RIHS01_");
103 for byte in self.digest {
104 let _ = write!(text, "{byte:02x}");
105 }
106 text
107 }
108}
109
110#[no_mangle]
126pub unsafe extern "C" fn pamoja_ros2_name_is_valid(name: *const c_char) -> bool {
127 match read_str(name, "name") {
128 Some(name) => is_valid_name(name),
129 None => false,
130 }
131}
132
133#[no_mangle]
149pub unsafe extern "C" fn pamoja_ros2_name_is_fully_qualified(name: *const c_char) -> bool {
150 match read_str(name, "name") {
151 Some(name) => is_fully_qualified(name),
152 None => false,
153 }
154}
155
156#[no_mangle]
166pub extern "C" fn pamoja_ros2_entity_kind_prefix(kind: PamojaEntityKind) -> *const c_char {
167 match kind {
168 PamojaEntityKind::Topic => c"rt".as_ptr(),
169 PamojaEntityKind::ServiceRequest => c"rq".as_ptr(),
170 PamojaEntityKind::ServiceResponse => c"rr".as_ptr(),
171 }
172}
173
174#[no_mangle]
192pub unsafe extern "C" fn pamoja_ros2_dds_topic(
193 fqn: *const c_char,
194 kind: PamojaEntityKind,
195) -> *mut PamojaString {
196 let Some(fqn) = read_str(fqn, "fqn") else {
197 return ptr::null_mut();
198 };
199 match dds_topic(fqn, kind.into()) {
200 Some(topic) => PamojaString::into_raw(topic),
201 None => {
202 set_last_error(format!("`{fqn}` is not a fully qualified ROS 2 name"));
203 ptr::null_mut()
204 }
205 }
206}
207
208#[no_mangle]
224pub unsafe extern "C" fn pamoja_ros2_percent_mangle(name: *const c_char) -> *mut PamojaString {
225 let Some(name) = read_str(name, "name") else {
226 return ptr::null_mut();
227 };
228 PamojaString::into_raw(percent_mangle(name))
229}
230
231#[no_mangle]
249pub unsafe extern "C" fn pamoja_ros2_dds_type_name(ros_type: *const c_char) -> *mut PamojaString {
250 let Some(ros_type) = read_str(ros_type, "ros_type") else {
251 return ptr::null_mut();
252 };
253 match dds_type_name(ros_type) {
254 Some(name) => PamojaString::into_raw(name),
255 None => {
256 set_last_error(format!(
257 "`{ros_type}` is not a `package/namespace/Type` interface type"
258 ));
259 ptr::null_mut()
260 }
261 }
262}
263
264#[no_mangle]
282pub unsafe extern "C" fn pamoja_ros2_type_hash_parse(
283 text: *const c_char,
284 out_hash: *mut PamojaTypeHash,
285) -> PamojaStatus {
286 if out_hash.is_null() {
287 set_last_error("out_hash must not be null".to_owned());
288 return PamojaStatus::InvalidArgument;
289 }
290 let Some(text) = read_str(text, "text") else {
291 return PamojaStatus::InvalidArgument;
292 };
293 match TypeHash::parse(text) {
294 Some(hash) => {
295 *out_hash = PamojaTypeHash {
296 digest: hash.digest(),
297 };
298 PamojaStatus::Ok
299 }
300 None => {
301 set_last_error(format!("`{text}` is not a well-formed RIHS01 hash"));
302 PamojaStatus::InvalidArgument
303 }
304 }
305}
306
307#[no_mangle]
318pub extern "C" fn pamoja_ros2_type_hash_to_string(hash: PamojaTypeHash) -> *mut PamojaString {
319 PamojaString::into_raw(hash.text())
320}
321
322#[no_mangle]
344pub unsafe extern "C" fn pamoja_ros2_entity_key(
345 domain_id: u32,
346 fqn: *const c_char,
347 ros_type: *const c_char,
348 hash: PamojaTypeHash,
349) -> *mut PamojaString {
350 let (Some(fqn), Some(ros_type)) = (read_str(fqn, "fqn"), read_str(ros_type, "ros_type")) else {
351 return ptr::null_mut();
352 };
353 let Some(parsed) = TypeHash::parse(&hash.text()) else {
354 set_last_error("the type hash is malformed".to_owned());
355 return ptr::null_mut();
356 };
357 match entity_key(domain_id, fqn, ros_type, &parsed) {
358 Some(key) => PamojaString::into_raw(key),
359 None => {
360 set_last_error(format!(
361 "no entity key for `{fqn}` of type `{ros_type}` in domain {domain_id}"
362 ));
363 ptr::null_mut()
364 }
365 }
366}
367
368#[no_mangle]
379pub extern "C" fn pamoja_ros2_twist_to_cdr(twist: PamojaRos2Twist) -> *mut PamojaBuffer {
380 let twist = Twist {
381 linear: twist.linear.into(),
382 angular: twist.angular.into(),
383 };
384 PamojaBuffer::into_raw(twist.to_cdr())
385}
386
387#[no_mangle]
405pub unsafe extern "C" fn pamoja_ros2_twist_from_cdr(
406 data: *const u8,
407 data_len: usize,
408 out_twist: *mut PamojaRos2Twist,
409) -> PamojaStatus {
410 if out_twist.is_null() {
411 set_last_error("out_twist must not be null".to_owned());
412 return PamojaStatus::InvalidArgument;
413 }
414 let data = match read_bytes(data, data_len) {
415 Ok(data) => data,
416 Err(status) => return status,
417 };
418 match Twist::from_cdr(&data) {
419 Some(twist) => {
420 *out_twist = PamojaRos2Twist {
421 linear: twist.linear.into(),
422 angular: twist.angular.into(),
423 };
424 PamojaStatus::Ok
425 }
426 None => {
427 set_last_error("the bytes are not a well-formed CDR twist".to_owned());
428 PamojaStatus::InvalidArgument
429 }
430 }
431}
432
433pub struct PamojaCdrWriter {
435 inner: CdrWriter,
436}
437
438#[no_mangle]
445pub extern "C" fn pamoja_cdr_writer_new() -> *mut PamojaCdrWriter {
446 Box::into_raw(Box::new(PamojaCdrWriter {
447 inner: CdrWriter::new(),
448 }))
449}
450
451#[no_mangle]
467pub unsafe extern "C" fn pamoja_cdr_writer_write_i32(
468 writer: *mut PamojaCdrWriter,
469 value: i32,
470) -> PamojaStatus {
471 let Some(writer) = writer_handle(writer) else {
472 return PamojaStatus::InvalidArgument;
473 };
474 writer.inner.write_i32(value);
475 PamojaStatus::Ok
476}
477
478#[no_mangle]
494pub unsafe extern "C" fn pamoja_cdr_writer_write_u32(
495 writer: *mut PamojaCdrWriter,
496 value: u32,
497) -> PamojaStatus {
498 let Some(writer) = writer_handle(writer) else {
499 return PamojaStatus::InvalidArgument;
500 };
501 writer.inner.write_u32(value);
502 PamojaStatus::Ok
503}
504
505#[no_mangle]
521pub unsafe extern "C" fn pamoja_cdr_writer_write_f32(
522 writer: *mut PamojaCdrWriter,
523 value: f32,
524) -> PamojaStatus {
525 let Some(writer) = writer_handle(writer) else {
526 return PamojaStatus::InvalidArgument;
527 };
528 writer.inner.write_f32(value);
529 PamojaStatus::Ok
530}
531
532#[no_mangle]
548pub unsafe extern "C" fn pamoja_cdr_writer_write_f64(
549 writer: *mut PamojaCdrWriter,
550 value: f64,
551) -> PamojaStatus {
552 let Some(writer) = writer_handle(writer) else {
553 return PamojaStatus::InvalidArgument;
554 };
555 writer.inner.write_f64(value);
556 PamojaStatus::Ok
557}
558
559#[no_mangle]
576pub unsafe extern "C" fn pamoja_cdr_writer_into_bytes(
577 writer: *mut PamojaCdrWriter,
578) -> *mut PamojaBuffer {
579 if writer.is_null() {
580 set_last_error("writer must not be null".to_owned());
581 return ptr::null_mut();
582 }
583 let writer = Box::from_raw(writer);
584 PamojaBuffer::into_raw(writer.inner.into_bytes())
585}
586
587#[no_mangle]
596pub unsafe extern "C" fn pamoja_cdr_writer_free(writer: *mut PamojaCdrWriter) {
597 if !writer.is_null() {
598 drop(Box::from_raw(writer));
599 }
600}
601
602#[derive(Clone, Copy)]
609enum Field {
610 Word,
612 Double,
614}
615
616pub struct PamojaCdrReader {
618 data: Vec<u8>,
619 taken: Vec<Field>,
620}
621
622#[no_mangle]
639pub unsafe extern "C" fn pamoja_cdr_reader_new(
640 data: *const u8,
641 data_len: usize,
642) -> *mut PamojaCdrReader {
643 let Ok(data) = read_bytes(data, data_len) else {
644 return ptr::null_mut();
645 };
646 if CdrReader::new(&data).is_none() {
647 set_last_error("the bytes carry no valid CDR encapsulation header".to_owned());
648 return ptr::null_mut();
649 }
650 Box::into_raw(Box::new(PamojaCdrReader {
651 data,
652 taken: Vec::new(),
653 }))
654}
655
656#[no_mangle]
673pub unsafe extern "C" fn pamoja_cdr_reader_read_i32(
674 reader: *mut PamojaCdrReader,
675 out_value: *mut i32,
676) -> PamojaStatus {
677 read_field(reader, out_value, Field::Word, |cursor| cursor.read_i32())
678}
679
680#[no_mangle]
697pub unsafe extern "C" fn pamoja_cdr_reader_read_u32(
698 reader: *mut PamojaCdrReader,
699 out_value: *mut u32,
700) -> PamojaStatus {
701 read_field(reader, out_value, Field::Word, |cursor| cursor.read_u32())
702}
703
704#[no_mangle]
721pub unsafe extern "C" fn pamoja_cdr_reader_read_f32(
722 reader: *mut PamojaCdrReader,
723 out_value: *mut f32,
724) -> PamojaStatus {
725 read_field(reader, out_value, Field::Word, |cursor| cursor.read_f32())
726}
727
728#[no_mangle]
745pub unsafe extern "C" fn pamoja_cdr_reader_read_f64(
746 reader: *mut PamojaCdrReader,
747 out_value: *mut f64,
748) -> PamojaStatus {
749 read_field(reader, out_value, Field::Double, |cursor| cursor.read_f64())
750}
751
752#[no_mangle]
761pub unsafe extern "C" fn pamoja_cdr_reader_free(reader: *mut PamojaCdrReader) {
762 if !reader.is_null() {
763 drop(Box::from_raw(reader));
764 }
765}
766
767unsafe fn writer_handle<'a>(writer: *mut PamojaCdrWriter) -> Option<&'a mut PamojaCdrWriter> {
773 if writer.is_null() {
774 set_last_error("writer must not be null".to_owned());
775 return None;
776 }
777 Some(&mut *writer)
778}
779
780unsafe fn read_field<T>(
787 reader: *mut PamojaCdrReader,
788 out_value: *mut T,
789 width: Field,
790 read: impl FnOnce(&mut CdrReader<'_>) -> Option<T>,
791) -> PamojaStatus {
792 if reader.is_null() {
793 set_last_error("reader must not be null".to_owned());
794 return PamojaStatus::InvalidArgument;
795 }
796 if out_value.is_null() {
797 set_last_error("out_value must not be null".to_owned());
798 return PamojaStatus::InvalidArgument;
799 }
800 let handle = &mut *reader;
801 let Some(mut cursor) = CdrReader::new(&handle.data) else {
802 set_last_error("the bytes carry no valid CDR encapsulation header".to_owned());
803 return PamojaStatus::InvalidArgument;
804 };
805 for field in &handle.taken {
806 let stepped = match field {
807 Field::Word => cursor.read_u32().is_some(),
808 Field::Double => cursor.read_f64().is_some(),
809 };
810 if !stepped {
811 set_last_error("the CDR buffer is exhausted".to_owned());
812 return PamojaStatus::InvalidArgument;
813 }
814 }
815 match read(&mut cursor) {
816 Some(value) => {
817 *out_value = value;
818 handle.taken.push(width);
819 PamojaStatus::Ok
820 }
821 None => {
822 set_last_error("the CDR buffer is exhausted".to_owned());
823 PamojaStatus::InvalidArgument
824 }
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use std::ffi::{CStr, CString};
831
832 use super::*;
833
834 const CHATTER_HASH: &str =
835 "RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18";
836
837 fn text_of(string: *mut PamojaString) -> String {
838 assert!(!string.is_null(), "the call produced no string");
839 let text = unsafe { CStr::from_ptr(crate::pamoja_string_data(string)) }
840 .to_str()
841 .expect("utf-8")
842 .to_owned();
843 unsafe { crate::pamoja_string_free(string) };
844 text
845 }
846
847 #[test]
848 fn a_topic_maps_onto_its_dds_name() {
849 let fqn = CString::new("/robot1/cmd_vel").expect("static");
850 let topic = unsafe { pamoja_ros2_dds_topic(fqn.as_ptr(), PamojaEntityKind::Topic) };
851 assert_eq!(text_of(topic), "rt/robot1/cmd_vel");
852 }
853
854 #[test]
855 fn a_name_that_breaks_the_rules_is_refused() {
856 let bad = CString::new("/2foo").expect("static");
857 assert!(!unsafe { pamoja_ros2_name_is_valid(bad.as_ptr()) });
858 let good = CString::new("/robot1/camera_left/image_raw").expect("static");
859 assert!(unsafe { pamoja_ros2_name_is_valid(good.as_ptr()) });
860 }
861
862 #[test]
863 fn an_entity_key_matches_the_published_example() {
864 let text = CString::new(CHATTER_HASH).expect("static");
865 let mut hash = PamojaTypeHash {
866 digest: [0u8; PAMOJA_TYPE_HASH_LEN],
867 };
868 assert_eq!(
869 unsafe { pamoja_ros2_type_hash_parse(text.as_ptr(), &mut hash) },
870 PamojaStatus::Ok
871 );
872 assert_eq!(text_of(pamoja_ros2_type_hash_to_string(hash)), CHATTER_HASH);
873
874 let fqn = CString::new("/chatter").expect("static");
875 let ros_type = CString::new("std_msgs/msg/String").expect("static");
876 let key = unsafe { pamoja_ros2_entity_key(0, fqn.as_ptr(), ros_type.as_ptr(), hash) };
877 assert_eq!(
878 text_of(key),
879 format!("0/chatter/std_msgs::msg::dds_::String_/{CHATTER_HASH}")
880 );
881 }
882
883 #[test]
884 fn a_twist_survives_a_cdr_round_trip() {
885 let sent = PamojaRos2Twist {
886 linear: PamojaVector3 {
887 x: 1.5,
888 y: 0.0,
889 z: 0.0,
890 },
891 angular: PamojaVector3 {
892 x: 0.0,
893 y: 0.0,
894 z: -0.25,
895 },
896 };
897 let buffer = pamoja_ros2_twist_to_cdr(sent);
898 assert!(!buffer.is_null());
899
900 let mut received = PamojaRos2Twist {
901 linear: PamojaVector3 {
902 x: 0.0,
903 y: 0.0,
904 z: 0.0,
905 },
906 angular: PamojaVector3 {
907 x: 0.0,
908 y: 0.0,
909 z: 0.0,
910 },
911 };
912 let status = unsafe {
913 pamoja_ros2_twist_from_cdr(
914 crate::pamoja_buffer_data(buffer),
915 crate::pamoja_buffer_len(buffer),
916 &mut received,
917 )
918 };
919 unsafe { crate::pamoja_buffer_free(buffer) };
920 assert_eq!(status, PamojaStatus::Ok);
921 assert_eq!(received, sent);
922 }
923
924 #[test]
925 fn mixed_width_fields_read_back_in_order() {
926 let writer = pamoja_cdr_writer_new();
927 unsafe {
928 assert_eq!(pamoja_cdr_writer_write_u32(writer, 7), PamojaStatus::Ok);
929 assert_eq!(pamoja_cdr_writer_write_f64(writer, 2.5), PamojaStatus::Ok);
930 assert_eq!(pamoja_cdr_writer_write_i32(writer, -3), PamojaStatus::Ok);
931 }
932 let buffer = unsafe { pamoja_cdr_writer_into_bytes(writer) };
933 assert!(!buffer.is_null());
934
935 let reader = unsafe {
936 pamoja_cdr_reader_new(
937 crate::pamoja_buffer_data(buffer),
938 crate::pamoja_buffer_len(buffer),
939 )
940 };
941 assert!(!reader.is_null());
942
943 let mut word = 0u32;
944 let mut double = 0f64;
945 let mut signed = 0i32;
946 unsafe {
947 assert_eq!(
948 pamoja_cdr_reader_read_u32(reader, &mut word),
949 PamojaStatus::Ok
950 );
951 assert_eq!(
952 pamoja_cdr_reader_read_f64(reader, &mut double),
953 PamojaStatus::Ok
954 );
955 assert_eq!(
956 pamoja_cdr_reader_read_i32(reader, &mut signed),
957 PamojaStatus::Ok
958 );
959 pamoja_cdr_reader_free(reader);
960 crate::pamoja_buffer_free(buffer);
961 }
962 assert_eq!(word, 7, "the first word reads back");
963 assert_eq!(double, 2.5, "an eight-byte field keeps its alignment");
964 assert_eq!(signed, -3, "and the field after it is not skewed");
965 }
966
967 #[test]
968 fn a_null_argument_is_rejected_rather_than_dereferenced() {
969 assert!(!unsafe { pamoja_ros2_name_is_valid(ptr::null()) });
970 assert!(!unsafe { pamoja_ros2_name_is_fully_qualified(ptr::null()) });
971 assert!(unsafe { pamoja_ros2_percent_mangle(ptr::null()) }.is_null());
972 assert!(unsafe { pamoja_ros2_dds_type_name(ptr::null()) }.is_null());
973 assert_eq!(
974 unsafe { pamoja_ros2_type_hash_parse(ptr::null(), ptr::null_mut()) },
975 PamojaStatus::InvalidArgument
976 );
977 assert_eq!(
978 unsafe { pamoja_cdr_writer_write_u32(ptr::null_mut(), 0) },
979 PamojaStatus::InvalidArgument
980 );
981 assert!(unsafe { pamoja_cdr_writer_into_bytes(ptr::null_mut()) }.is_null());
982 }
983}