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]
186pub extern "C" fn pamoja_ros2_entity_kind_suffix(kind: PamojaEntityKind) -> *const c_char {
187 match kind {
188 PamojaEntityKind::Topic => c"".as_ptr(),
189 PamojaEntityKind::ServiceRequest => c"Request".as_ptr(),
190 PamojaEntityKind::ServiceResponse => c"Reply".as_ptr(),
191 }
192}
193
194#[no_mangle]
214pub unsafe extern "C" fn pamoja_ros2_dds_topic(
215 fqn: *const c_char,
216 kind: PamojaEntityKind,
217) -> *mut PamojaString {
218 let Some(fqn) = read_str(fqn, "fqn") else {
219 return ptr::null_mut();
220 };
221 match dds_topic(fqn, kind.into()) {
222 Some(topic) => PamojaString::into_raw(topic),
223 None => {
224 set_last_error(format!("`{fqn}` is not a fully qualified ROS 2 name"));
225 ptr::null_mut()
226 }
227 }
228}
229
230#[no_mangle]
246pub unsafe extern "C" fn pamoja_ros2_percent_mangle(name: *const c_char) -> *mut PamojaString {
247 let Some(name) = read_str(name, "name") else {
248 return ptr::null_mut();
249 };
250 PamojaString::into_raw(percent_mangle(name))
251}
252
253#[no_mangle]
271pub unsafe extern "C" fn pamoja_ros2_dds_type_name(ros_type: *const c_char) -> *mut PamojaString {
272 let Some(ros_type) = read_str(ros_type, "ros_type") else {
273 return ptr::null_mut();
274 };
275 match dds_type_name(ros_type) {
276 Some(name) => PamojaString::into_raw(name),
277 None => {
278 set_last_error(format!(
279 "`{ros_type}` is not a `package/namespace/Type` interface type"
280 ));
281 ptr::null_mut()
282 }
283 }
284}
285
286#[no_mangle]
304pub unsafe extern "C" fn pamoja_ros2_type_hash_parse(
305 text: *const c_char,
306 out_hash: *mut PamojaTypeHash,
307) -> PamojaStatus {
308 if out_hash.is_null() {
309 set_last_error("out_hash must not be null".to_owned());
310 return PamojaStatus::InvalidArgument;
311 }
312 let Some(text) = read_str(text, "text") else {
313 return PamojaStatus::InvalidArgument;
314 };
315 match TypeHash::parse(text) {
316 Some(hash) => {
317 *out_hash = PamojaTypeHash {
318 digest: hash.digest(),
319 };
320 PamojaStatus::Ok
321 }
322 None => {
323 set_last_error(format!("`{text}` is not a well-formed RIHS01 hash"));
324 PamojaStatus::InvalidArgument
325 }
326 }
327}
328
329#[no_mangle]
340pub extern "C" fn pamoja_ros2_type_hash_to_string(hash: PamojaTypeHash) -> *mut PamojaString {
341 PamojaString::into_raw(hash.text())
342}
343
344#[no_mangle]
366pub unsafe extern "C" fn pamoja_ros2_entity_key(
367 domain_id: u32,
368 fqn: *const c_char,
369 ros_type: *const c_char,
370 hash: PamojaTypeHash,
371) -> *mut PamojaString {
372 let (Some(fqn), Some(ros_type)) = (read_str(fqn, "fqn"), read_str(ros_type, "ros_type")) else {
373 return ptr::null_mut();
374 };
375 let Some(parsed) = TypeHash::parse(&hash.text()) else {
376 set_last_error("the type hash is malformed".to_owned());
377 return ptr::null_mut();
378 };
379 match entity_key(domain_id, fqn, ros_type, &parsed) {
380 Some(key) => PamojaString::into_raw(key),
381 None => {
382 set_last_error(format!(
383 "no entity key for `{fqn}` of type `{ros_type}` in domain {domain_id}"
384 ));
385 ptr::null_mut()
386 }
387 }
388}
389
390#[no_mangle]
401pub extern "C" fn pamoja_ros2_twist_to_cdr(twist: PamojaRos2Twist) -> *mut PamojaBuffer {
402 let twist = Twist {
403 linear: twist.linear.into(),
404 angular: twist.angular.into(),
405 };
406 PamojaBuffer::into_raw(twist.to_cdr())
407}
408
409#[no_mangle]
427pub unsafe extern "C" fn pamoja_ros2_twist_from_cdr(
428 data: *const u8,
429 data_len: usize,
430 out_twist: *mut PamojaRos2Twist,
431) -> PamojaStatus {
432 if out_twist.is_null() {
433 set_last_error("out_twist must not be null".to_owned());
434 return PamojaStatus::InvalidArgument;
435 }
436 let data = match read_bytes(data, data_len) {
437 Ok(data) => data,
438 Err(status) => return status,
439 };
440 match Twist::from_cdr(&data) {
441 Some(twist) => {
442 *out_twist = PamojaRos2Twist {
443 linear: twist.linear.into(),
444 angular: twist.angular.into(),
445 };
446 PamojaStatus::Ok
447 }
448 None => {
449 set_last_error("the bytes are not a well-formed CDR twist".to_owned());
450 PamojaStatus::InvalidArgument
451 }
452 }
453}
454
455pub struct PamojaCdrWriter {
457 inner: CdrWriter,
458}
459
460#[no_mangle]
467pub extern "C" fn pamoja_cdr_writer_new() -> *mut PamojaCdrWriter {
468 Box::into_raw(Box::new(PamojaCdrWriter {
469 inner: CdrWriter::new(),
470 }))
471}
472
473#[no_mangle]
489pub unsafe extern "C" fn pamoja_cdr_writer_write_i32(
490 writer: *mut PamojaCdrWriter,
491 value: i32,
492) -> PamojaStatus {
493 let Some(writer) = writer_handle(writer) else {
494 return PamojaStatus::InvalidArgument;
495 };
496 writer.inner.write_i32(value);
497 PamojaStatus::Ok
498}
499
500#[no_mangle]
516pub unsafe extern "C" fn pamoja_cdr_writer_write_u32(
517 writer: *mut PamojaCdrWriter,
518 value: u32,
519) -> PamojaStatus {
520 let Some(writer) = writer_handle(writer) else {
521 return PamojaStatus::InvalidArgument;
522 };
523 writer.inner.write_u32(value);
524 PamojaStatus::Ok
525}
526
527#[no_mangle]
543pub unsafe extern "C" fn pamoja_cdr_writer_write_f32(
544 writer: *mut PamojaCdrWriter,
545 value: f32,
546) -> PamojaStatus {
547 let Some(writer) = writer_handle(writer) else {
548 return PamojaStatus::InvalidArgument;
549 };
550 writer.inner.write_f32(value);
551 PamojaStatus::Ok
552}
553
554#[no_mangle]
570pub unsafe extern "C" fn pamoja_cdr_writer_write_f64(
571 writer: *mut PamojaCdrWriter,
572 value: f64,
573) -> PamojaStatus {
574 let Some(writer) = writer_handle(writer) else {
575 return PamojaStatus::InvalidArgument;
576 };
577 writer.inner.write_f64(value);
578 PamojaStatus::Ok
579}
580
581#[no_mangle]
598pub unsafe extern "C" fn pamoja_cdr_writer_into_bytes(
599 writer: *mut PamojaCdrWriter,
600) -> *mut PamojaBuffer {
601 if writer.is_null() {
602 set_last_error("writer must not be null".to_owned());
603 return ptr::null_mut();
604 }
605 let writer = Box::from_raw(writer);
606 PamojaBuffer::into_raw(writer.inner.into_bytes())
607}
608
609#[no_mangle]
618pub unsafe extern "C" fn pamoja_cdr_writer_free(writer: *mut PamojaCdrWriter) {
619 if !writer.is_null() {
620 drop(Box::from_raw(writer));
621 }
622}
623
624#[derive(Clone, Copy)]
631enum Field {
632 Word,
634 Double,
636}
637
638pub struct PamojaCdrReader {
640 data: Vec<u8>,
641 taken: Vec<Field>,
642}
643
644#[no_mangle]
661pub unsafe extern "C" fn pamoja_cdr_reader_new(
662 data: *const u8,
663 data_len: usize,
664) -> *mut PamojaCdrReader {
665 let Ok(data) = read_bytes(data, data_len) else {
666 return ptr::null_mut();
667 };
668 if CdrReader::new(&data).is_none() {
669 set_last_error("the bytes carry no valid CDR encapsulation header".to_owned());
670 return ptr::null_mut();
671 }
672 Box::into_raw(Box::new(PamojaCdrReader {
673 data,
674 taken: Vec::new(),
675 }))
676}
677
678#[no_mangle]
695pub unsafe extern "C" fn pamoja_cdr_reader_read_i32(
696 reader: *mut PamojaCdrReader,
697 out_value: *mut i32,
698) -> PamojaStatus {
699 read_field(reader, out_value, Field::Word, |cursor| cursor.read_i32())
700}
701
702#[no_mangle]
719pub unsafe extern "C" fn pamoja_cdr_reader_read_u32(
720 reader: *mut PamojaCdrReader,
721 out_value: *mut u32,
722) -> PamojaStatus {
723 read_field(reader, out_value, Field::Word, |cursor| cursor.read_u32())
724}
725
726#[no_mangle]
743pub unsafe extern "C" fn pamoja_cdr_reader_read_f32(
744 reader: *mut PamojaCdrReader,
745 out_value: *mut f32,
746) -> PamojaStatus {
747 read_field(reader, out_value, Field::Word, |cursor| cursor.read_f32())
748}
749
750#[no_mangle]
767pub unsafe extern "C" fn pamoja_cdr_reader_read_f64(
768 reader: *mut PamojaCdrReader,
769 out_value: *mut f64,
770) -> PamojaStatus {
771 read_field(reader, out_value, Field::Double, |cursor| cursor.read_f64())
772}
773
774#[no_mangle]
783pub unsafe extern "C" fn pamoja_cdr_reader_free(reader: *mut PamojaCdrReader) {
784 if !reader.is_null() {
785 drop(Box::from_raw(reader));
786 }
787}
788
789unsafe fn writer_handle<'a>(writer: *mut PamojaCdrWriter) -> Option<&'a mut PamojaCdrWriter> {
795 if writer.is_null() {
796 set_last_error("writer must not be null".to_owned());
797 return None;
798 }
799 Some(&mut *writer)
800}
801
802unsafe fn read_field<T>(
809 reader: *mut PamojaCdrReader,
810 out_value: *mut T,
811 width: Field,
812 read: impl FnOnce(&mut CdrReader<'_>) -> Option<T>,
813) -> PamojaStatus {
814 if reader.is_null() {
815 set_last_error("reader must not be null".to_owned());
816 return PamojaStatus::InvalidArgument;
817 }
818 if out_value.is_null() {
819 set_last_error("out_value must not be null".to_owned());
820 return PamojaStatus::InvalidArgument;
821 }
822 let handle = &mut *reader;
823 let Some(mut cursor) = CdrReader::new(&handle.data) else {
824 set_last_error("the bytes carry no valid CDR encapsulation header".to_owned());
825 return PamojaStatus::InvalidArgument;
826 };
827 for field in &handle.taken {
828 let stepped = match field {
829 Field::Word => cursor.read_u32().is_some(),
830 Field::Double => cursor.read_f64().is_some(),
831 };
832 if !stepped {
833 set_last_error("the CDR buffer is exhausted".to_owned());
834 return PamojaStatus::InvalidArgument;
835 }
836 }
837 match read(&mut cursor) {
838 Some(value) => {
839 *out_value = value;
840 handle.taken.push(width);
841 PamojaStatus::Ok
842 }
843 None => {
844 set_last_error("the CDR buffer is exhausted".to_owned());
845 PamojaStatus::InvalidArgument
846 }
847 }
848}
849
850#[cfg(test)]
851mod tests {
852 use std::ffi::{CStr, CString};
853
854 use super::*;
855
856 const CHATTER_HASH: &str =
857 "RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18";
858
859 fn text_of(string: *mut PamojaString) -> String {
860 assert!(!string.is_null(), "the call produced no string");
861 let text = unsafe { CStr::from_ptr(crate::pamoja_string_data(string)) }
862 .to_str()
863 .expect("utf-8")
864 .to_owned();
865 unsafe { crate::pamoja_string_free(string) };
866 text
867 }
868
869 #[test]
870 fn a_topic_maps_onto_its_dds_name() {
871 let fqn = CString::new("/robot1/cmd_vel").expect("static");
872 let topic = unsafe { pamoja_ros2_dds_topic(fqn.as_ptr(), PamojaEntityKind::Topic) };
873 assert_eq!(text_of(topic), "rt/robot1/cmd_vel");
874 }
875
876 #[test]
877 fn a_service_travels_on_a_request_and_a_reply_topic() {
878 let fqn = CString::new("/add_two_ints").expect("static");
879 let (asked, answered) = unsafe {
881 (
882 pamoja_ros2_dds_topic(fqn.as_ptr(), PamojaEntityKind::ServiceRequest),
883 pamoja_ros2_dds_topic(fqn.as_ptr(), PamojaEntityKind::ServiceResponse),
884 )
885 };
886 assert_eq!(text_of(asked), "rq/add_two_intsRequest");
887 assert_eq!(text_of(answered), "rr/add_two_intsReply");
888 let suffix = unsafe {
890 CStr::from_ptr(pamoja_ros2_entity_kind_suffix(
891 PamojaEntityKind::ServiceRequest,
892 ))
893 };
894 assert_eq!(suffix.to_str(), Ok("Request"));
895 }
896
897 #[test]
898 fn a_name_that_breaks_the_rules_is_refused() {
899 let bad = CString::new("/2foo").expect("static");
900 assert!(!unsafe { pamoja_ros2_name_is_valid(bad.as_ptr()) });
901 let good = CString::new("/robot1/camera_left/image_raw").expect("static");
902 assert!(unsafe { pamoja_ros2_name_is_valid(good.as_ptr()) });
903 }
904
905 #[test]
906 fn an_entity_key_matches_the_published_example() {
907 let text = CString::new(CHATTER_HASH).expect("static");
908 let mut hash = PamojaTypeHash {
909 digest: [0u8; PAMOJA_TYPE_HASH_LEN],
910 };
911 assert_eq!(
912 unsafe { pamoja_ros2_type_hash_parse(text.as_ptr(), &mut hash) },
913 PamojaStatus::Ok
914 );
915 assert_eq!(text_of(pamoja_ros2_type_hash_to_string(hash)), CHATTER_HASH);
916
917 let fqn = CString::new("/chatter").expect("static");
918 let ros_type = CString::new("std_msgs/msg/String").expect("static");
919 let key = unsafe { pamoja_ros2_entity_key(0, fqn.as_ptr(), ros_type.as_ptr(), hash) };
920 assert_eq!(
921 text_of(key),
922 format!("0/chatter/std_msgs::msg::dds_::String_/{CHATTER_HASH}")
923 );
924 }
925
926 #[test]
927 fn a_twist_survives_a_cdr_round_trip() {
928 let sent = PamojaRos2Twist {
929 linear: PamojaVector3 {
930 x: 1.5,
931 y: 0.0,
932 z: 0.0,
933 },
934 angular: PamojaVector3 {
935 x: 0.0,
936 y: 0.0,
937 z: -0.25,
938 },
939 };
940 let buffer = pamoja_ros2_twist_to_cdr(sent);
941 assert!(!buffer.is_null());
942
943 let mut received = PamojaRos2Twist {
944 linear: PamojaVector3 {
945 x: 0.0,
946 y: 0.0,
947 z: 0.0,
948 },
949 angular: PamojaVector3 {
950 x: 0.0,
951 y: 0.0,
952 z: 0.0,
953 },
954 };
955 let status = unsafe {
956 pamoja_ros2_twist_from_cdr(
957 crate::pamoja_buffer_data(buffer),
958 crate::pamoja_buffer_len(buffer),
959 &mut received,
960 )
961 };
962 unsafe { crate::pamoja_buffer_free(buffer) };
963 assert_eq!(status, PamojaStatus::Ok);
964 assert_eq!(received, sent);
965 }
966
967 #[test]
968 fn mixed_width_fields_read_back_in_order() {
969 let writer = pamoja_cdr_writer_new();
970 unsafe {
971 assert_eq!(pamoja_cdr_writer_write_u32(writer, 7), PamojaStatus::Ok);
972 assert_eq!(pamoja_cdr_writer_write_f64(writer, 2.5), PamojaStatus::Ok);
973 assert_eq!(pamoja_cdr_writer_write_i32(writer, -3), PamojaStatus::Ok);
974 }
975 let buffer = unsafe { pamoja_cdr_writer_into_bytes(writer) };
976 assert!(!buffer.is_null());
977
978 let reader = unsafe {
979 pamoja_cdr_reader_new(
980 crate::pamoja_buffer_data(buffer),
981 crate::pamoja_buffer_len(buffer),
982 )
983 };
984 assert!(!reader.is_null());
985
986 let mut word = 0u32;
987 let mut double = 0f64;
988 let mut signed = 0i32;
989 unsafe {
990 assert_eq!(
991 pamoja_cdr_reader_read_u32(reader, &mut word),
992 PamojaStatus::Ok
993 );
994 assert_eq!(
995 pamoja_cdr_reader_read_f64(reader, &mut double),
996 PamojaStatus::Ok
997 );
998 assert_eq!(
999 pamoja_cdr_reader_read_i32(reader, &mut signed),
1000 PamojaStatus::Ok
1001 );
1002 pamoja_cdr_reader_free(reader);
1003 crate::pamoja_buffer_free(buffer);
1004 }
1005 assert_eq!(word, 7, "the first word reads back");
1006 assert_eq!(double, 2.5, "an eight-byte field keeps its alignment");
1007 assert_eq!(signed, -3, "and the field after it is not skewed");
1008 }
1009
1010 #[test]
1011 fn a_null_argument_is_rejected_rather_than_dereferenced() {
1012 assert!(!unsafe { pamoja_ros2_name_is_valid(ptr::null()) });
1013 assert!(!unsafe { pamoja_ros2_name_is_fully_qualified(ptr::null()) });
1014 assert!(unsafe { pamoja_ros2_percent_mangle(ptr::null()) }.is_null());
1015 assert!(unsafe { pamoja_ros2_dds_type_name(ptr::null()) }.is_null());
1016 assert_eq!(
1017 unsafe { pamoja_ros2_type_hash_parse(ptr::null(), ptr::null_mut()) },
1018 PamojaStatus::InvalidArgument
1019 );
1020 assert_eq!(
1021 unsafe { pamoja_cdr_writer_write_u32(ptr::null_mut(), 0) },
1022 PamojaStatus::InvalidArgument
1023 );
1024 assert!(unsafe { pamoja_cdr_writer_into_bytes(ptr::null_mut()) }.is_null());
1025 }
1026}