1use std::panic::{catch_unwind, AssertUnwindSafe};
15use std::ptr;
16
17use pamoja_modbus::{crc16, Adu, ModbusError, Pdu};
18
19use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
20
21pub struct PamojaModbusFrame {
26 adu: Adu,
27}
28
29pub struct PamojaRegisters {
34 registers: Vec<u16>,
35}
36
37#[no_mangle]
49pub unsafe extern "C" fn pamoja_modbus_crc16(bytes: *const u8, bytes_len: usize) -> u16 {
50 match read_bytes(bytes, bytes_len) {
51 Ok(bytes) => crc16(&bytes),
52 Err(_) => 0,
53 }
54}
55
56#[no_mangle]
68pub unsafe extern "C" fn pamoja_modbus_read_coils(
69 address: u8,
70 start: u16,
71 count: u16,
72 out_buffer: *mut *mut PamojaBuffer,
73) -> PamojaStatus {
74 let out_buffer = match out_slot(out_buffer, "out_buffer") {
75 Ok(slot) => slot,
76 Err(status) => return status,
77 };
78 request(out_buffer, address, || Ok(Pdu::read_coils(start, count)))
79}
80
81#[no_mangle]
92pub unsafe extern "C" fn pamoja_modbus_read_discrete_inputs(
93 address: u8,
94 start: u16,
95 count: u16,
96 out_buffer: *mut *mut PamojaBuffer,
97) -> PamojaStatus {
98 let out_buffer = match out_slot(out_buffer, "out_buffer") {
99 Ok(slot) => slot,
100 Err(status) => return status,
101 };
102 request(out_buffer, address, || {
103 Ok(Pdu::read_discrete_inputs(start, count))
104 })
105}
106
107#[no_mangle]
118pub unsafe extern "C" fn pamoja_modbus_read_holding_registers(
119 address: u8,
120 start: u16,
121 count: u16,
122 out_buffer: *mut *mut PamojaBuffer,
123) -> PamojaStatus {
124 let out_buffer = match out_slot(out_buffer, "out_buffer") {
125 Ok(slot) => slot,
126 Err(status) => return status,
127 };
128 request(out_buffer, address, || {
129 Ok(Pdu::read_holding_registers(start, count))
130 })
131}
132
133#[no_mangle]
145pub unsafe extern "C" fn pamoja_modbus_read_holding_registers_reply(
146 address: u8,
147 values: *const u16,
148 values_len: usize,
149 out_buffer: *mut *mut PamojaBuffer,
150) -> PamojaStatus {
151 registers_reply(address, values, values_len, out_buffer, true)
152}
153
154#[no_mangle]
166pub unsafe extern "C" fn pamoja_modbus_read_input_registers_reply(
167 address: u8,
168 values: *const u16,
169 values_len: usize,
170 out_buffer: *mut *mut PamojaBuffer,
171) -> PamojaStatus {
172 registers_reply(address, values, values_len, out_buffer, false)
173}
174
175unsafe fn registers_reply(
176 address: u8,
177 values: *const u16,
178 values_len: usize,
179 out_buffer: *mut *mut PamojaBuffer,
180 holding: bool,
181) -> PamojaStatus {
182 let out_buffer = match out_slot(out_buffer, "out_buffer") {
183 Ok(slot) => slot,
184 Err(status) => return status,
185 };
186 let values = match read_values(values, values_len, "values") {
187 Ok(values) => values,
188 Err(status) => return status,
189 };
190 request(out_buffer, address, || {
191 if holding {
192 Pdu::read_holding_registers_reply(&values)
193 } else {
194 Pdu::read_input_registers_reply(&values)
195 }
196 })
197}
198
199#[no_mangle]
210pub unsafe extern "C" fn pamoja_modbus_read_input_registers(
211 address: u8,
212 start: u16,
213 count: u16,
214 out_buffer: *mut *mut PamojaBuffer,
215) -> PamojaStatus {
216 let out_buffer = match out_slot(out_buffer, "out_buffer") {
217 Ok(slot) => slot,
218 Err(status) => return status,
219 };
220 request(out_buffer, address, || {
221 Ok(Pdu::read_input_registers(start, count))
222 })
223}
224
225#[no_mangle]
236pub unsafe extern "C" fn pamoja_modbus_write_single_coil(
237 address: u8,
238 coil: u16,
239 on: bool,
240 out_buffer: *mut *mut PamojaBuffer,
241) -> PamojaStatus {
242 let out_buffer = match out_slot(out_buffer, "out_buffer") {
243 Ok(slot) => slot,
244 Err(status) => return status,
245 };
246 request(out_buffer, address, || Ok(Pdu::write_single_coil(coil, on)))
247}
248
249#[no_mangle]
260pub unsafe extern "C" fn pamoja_modbus_write_single_register(
261 address: u8,
262 register: u16,
263 value: u16,
264 out_buffer: *mut *mut PamojaBuffer,
265) -> PamojaStatus {
266 let out_buffer = match out_slot(out_buffer, "out_buffer") {
267 Ok(slot) => slot,
268 Err(status) => return status,
269 };
270 request(out_buffer, address, || {
271 Ok(Pdu::write_single_register(register, value))
272 })
273}
274
275#[no_mangle]
289pub unsafe extern "C" fn pamoja_modbus_write_multiple_registers(
290 address: u8,
291 start: u16,
292 values: *const u16,
293 count: usize,
294 out_buffer: *mut *mut PamojaBuffer,
295) -> PamojaStatus {
296 let out_buffer = match out_slot(out_buffer, "out_buffer") {
297 Ok(slot) => slot,
298 Err(status) => return status,
299 };
300 let values = match read_values(values, count, "values") {
301 Ok(values) => values,
302 Err(status) => return status,
303 };
304 request(out_buffer, address, || {
305 Pdu::write_multiple_registers(start, &values)
306 })
307}
308
309#[no_mangle]
323pub unsafe extern "C" fn pamoja_modbus_write_multiple_coils(
324 address: u8,
325 start: u16,
326 values: *const u8,
327 count: usize,
328 out_buffer: *mut *mut PamojaBuffer,
329) -> PamojaStatus {
330 let out_buffer = match out_slot(out_buffer, "out_buffer") {
331 Ok(slot) => slot,
332 Err(status) => return status,
333 };
334 let values = match read_values(values, count, "values") {
335 Ok(values) => values,
336 Err(status) => return status,
337 };
338 let coils: Vec<bool> = values.into_iter().map(|value| value != 0).collect();
339 request(out_buffer, address, || {
340 Pdu::write_multiple_coils(start, &coils)
341 })
342}
343
344#[no_mangle]
360pub unsafe extern "C" fn pamoja_modbus_raw(
361 address: u8,
362 function: u8,
363 data: *const u8,
364 data_len: usize,
365 out_buffer: *mut *mut PamojaBuffer,
366) -> PamojaStatus {
367 let out_buffer = match out_slot(out_buffer, "out_buffer") {
368 Ok(slot) => slot,
369 Err(status) => return status,
370 };
371 let data = match read_bytes(data, data_len) {
372 Ok(data) => data,
373 Err(status) => return status,
374 };
375 request(out_buffer, address, || Pdu::raw(function, &data))
376}
377
378#[no_mangle]
393pub unsafe extern "C" fn pamoja_modbus_frame_parse(
394 bytes: *const u8,
395 bytes_len: usize,
396 out_frame: *mut *mut PamojaModbusFrame,
397) -> PamojaStatus {
398 let out_frame = match out_slot(out_frame, "out_frame") {
399 Ok(slot) => slot,
400 Err(status) => return status,
401 };
402 let bytes = match read_bytes(bytes, bytes_len) {
403 Ok(bytes) => bytes,
404 Err(status) => return status,
405 };
406 match catch_unwind(AssertUnwindSafe(|| Adu::parse(&bytes))) {
407 Ok(Ok(adu)) => {
408 *out_frame = Box::into_raw(Box::new(PamojaModbusFrame { adu }));
409 PamojaStatus::Ok
410 }
411 Ok(Err(error)) => failed(error),
412 Err(_) => panicked(),
413 }
414}
415
416#[no_mangle]
426pub unsafe extern "C" fn pamoja_modbus_frame_address(frame: *const PamojaModbusFrame) -> u8 {
427 if frame.is_null() {
428 return 0;
429 }
430 (*frame).adu.address()
431}
432
433#[no_mangle]
446pub unsafe extern "C" fn pamoja_modbus_frame_function(frame: *const PamojaModbusFrame) -> u8 {
447 if frame.is_null() {
448 return 0;
449 }
450 (*frame).adu.function_code()
451}
452
453#[no_mangle]
465pub unsafe extern "C" fn pamoja_modbus_frame_exception(frame: *const PamojaModbusFrame) -> u8 {
466 if frame.is_null() {
467 return 0;
468 }
469 (*frame)
470 .adu
471 .exception()
472 .map_or(0, pamoja_modbus::Exception::code)
473}
474
475#[no_mangle]
489pub unsafe extern "C" fn pamoja_modbus_frame_pdu(frame: *const PamojaModbusFrame) -> *const u8 {
490 if frame.is_null() {
491 return ptr::null();
492 }
493 (*frame).adu.pdu().as_ptr()
494}
495
496#[no_mangle]
506pub unsafe extern "C" fn pamoja_modbus_frame_pdu_len(frame: *const PamojaModbusFrame) -> usize {
507 if frame.is_null() {
508 return 0;
509 }
510 (*frame).adu.pdu().len()
511}
512
513#[no_mangle]
527pub unsafe extern "C" fn pamoja_modbus_frame_registers(
528 frame: *const PamojaModbusFrame,
529 out_registers: *mut *mut PamojaRegisters,
530) -> PamojaStatus {
531 let out_registers = match out_slot(out_registers, "out_registers") {
532 Ok(slot) => slot,
533 Err(status) => return status,
534 };
535 if frame.is_null() {
536 set_last_error("frame must not be null".to_owned());
537 return PamojaStatus::InvalidArgument;
538 }
539 let adu = (*frame).adu;
540 match catch_unwind(AssertUnwindSafe(|| {
541 adu.response()
542 .registers()
543 .map(|registers| registers.collect::<Vec<u16>>())
544 })) {
545 Ok(Ok(registers)) => {
546 *out_registers = Box::into_raw(Box::new(PamojaRegisters { registers }));
547 PamojaStatus::Ok
548 }
549 Ok(Err(error)) => failed(error),
550 Err(_) => panicked(),
551 }
552}
553
554#[no_mangle]
568pub unsafe extern "C" fn pamoja_modbus_frame_coils(
569 frame: *const PamojaModbusFrame,
570 count: u16,
571 out_buffer: *mut *mut PamojaBuffer,
572) -> PamojaStatus {
573 let out_buffer = match out_slot(out_buffer, "out_buffer") {
574 Ok(slot) => slot,
575 Err(status) => return status,
576 };
577 if frame.is_null() {
578 set_last_error("frame must not be null".to_owned());
579 return PamojaStatus::InvalidArgument;
580 }
581 let adu = (*frame).adu;
582 match catch_unwind(AssertUnwindSafe(|| {
583 adu.response()
584 .coils(count)
585 .map(|coils| coils.map(u8::from).collect::<Vec<u8>>())
586 })) {
587 Ok(Ok(coils)) => {
588 *out_buffer = PamojaBuffer::into_raw(coils);
589 PamojaStatus::Ok
590 }
591 Ok(Err(error)) => failed(error),
592 Err(_) => panicked(),
593 }
594}
595
596#[no_mangle]
605pub unsafe extern "C" fn pamoja_modbus_frame_free(frame: *mut PamojaModbusFrame) {
606 if !frame.is_null() {
607 drop(Box::from_raw(frame));
608 }
609}
610
611#[no_mangle]
625pub unsafe extern "C" fn pamoja_registers_data(registers: *const PamojaRegisters) -> *const u16 {
626 if registers.is_null() {
627 return ptr::null();
628 }
629 (*registers).registers.as_ptr()
630}
631
632#[no_mangle]
643pub unsafe extern "C" fn pamoja_registers_len(registers: *const PamojaRegisters) -> usize {
644 if registers.is_null() {
645 return 0;
646 }
647 (*registers).registers.len()
648}
649
650#[no_mangle]
659pub unsafe extern "C" fn pamoja_registers_free(registers: *mut PamojaRegisters) {
660 if !registers.is_null() {
661 drop(Box::from_raw(registers));
662 }
663}
664
665unsafe fn request(
671 out_buffer: &mut *mut PamojaBuffer,
672 address: u8,
673 build: impl FnOnce() -> Result<Pdu, ModbusError>,
674) -> PamojaStatus {
675 match catch_unwind(AssertUnwindSafe(|| {
676 build().map(|pdu| pdu.to_adu(address).as_bytes().to_vec())
677 })) {
678 Ok(Ok(bytes)) => {
679 *out_buffer = PamojaBuffer::into_raw(bytes);
680 PamojaStatus::Ok
681 }
682 Ok(Err(error)) => failed(error),
683 Err(_) => panicked(),
684 }
685}
686
687pub(crate) unsafe fn out_slot<'a, T>(
693 out: *mut *mut T,
694 name: &str,
695) -> Result<&'a mut *mut T, PamojaStatus> {
696 if out.is_null() {
697 set_last_error(format!("{name} must not be null"));
698 return Err(PamojaStatus::InvalidArgument);
699 }
700 let slot = &mut *out;
701 *slot = ptr::null_mut();
702 Ok(slot)
703}
704
705pub(crate) unsafe fn read_values<T: Copy>(
712 ptr: *const T,
713 count: usize,
714 name: &str,
715) -> Result<Vec<T>, PamojaStatus> {
716 if count == 0 {
717 Ok(Vec::new())
718 } else if ptr.is_null() {
719 set_last_error(format!(
720 "{name} must not be null when its count is non-zero"
721 ));
722 Err(PamojaStatus::InvalidArgument)
723 } else {
724 Ok(std::slice::from_raw_parts(ptr, count).to_vec())
725 }
726}
727
728pub(crate) fn failed(error: ModbusError) -> PamojaStatus {
730 set_last_error(error.to_string());
731 match error {
732 ModbusError::InvalidValueCount | ModbusError::UnitOutOfRange { .. } => {
733 PamojaStatus::InvalidArgument
734 }
735 ModbusError::FrameTooShort
736 | ModbusError::FrameTooLong
737 | ModbusError::CrcMismatch { .. }
738 | ModbusError::MalformedResponse => PamojaStatus::Codec,
739 }
740}
741
742pub(crate) fn panicked() -> PamojaStatus {
744 set_last_error("panic at the FFI boundary".to_owned());
745 PamojaStatus::Panic
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
752
753 unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
759 let bytes =
760 std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
761 .to_vec();
762 pamoja_buffer_free(buffer);
763 bytes
764 }
765
766 #[test]
767 fn a_read_request_matches_the_specification_example() {
768 let mut out = ptr::null_mut();
769 let frame = unsafe {
771 assert_eq!(
772 pamoja_modbus_read_holding_registers(0x11, 0x006B, 3, &mut out),
773 PamojaStatus::Ok
774 );
775 take(out)
776 };
777 assert_eq!(
778 frame,
779 vec![0x11, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x76, 0x87],
780 "the frame carries the address, the PDU, and the CRC"
781 );
782 }
783
784 #[test]
785 fn a_reply_parses_into_its_registers() {
786 let on_wire = Adu::from_pdu(0x11, &[0x03, 0x06, 0x02, 0x2B, 0x00, 0x00, 0x00, 0x64])
787 .expect("assemble");
788 let bytes = on_wire.as_bytes();
789 let mut frame = ptr::null_mut();
790 let mut registers = ptr::null_mut();
791
792 unsafe {
794 assert_eq!(
795 pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame),
796 PamojaStatus::Ok
797 );
798 assert_eq!(pamoja_modbus_frame_address(frame), 0x11);
799 assert_eq!(pamoja_modbus_frame_function(frame), 0x03);
800 assert_eq!(pamoja_modbus_frame_exception(frame), 0);
801 assert_eq!(
802 pamoja_modbus_frame_registers(frame, &mut registers),
803 PamojaStatus::Ok
804 );
805 let values = std::slice::from_raw_parts(
806 pamoja_registers_data(registers),
807 pamoja_registers_len(registers),
808 )
809 .to_vec();
810 pamoja_registers_free(registers);
811 pamoja_modbus_frame_free(frame);
812 assert_eq!(values, vec![0x022B, 0x0000, 0x0064]);
813 }
814 }
815
816 #[test]
817 fn a_bit_reply_unpacks_one_byte_per_coil() {
818 let on_wire = Adu::from_pdu(0x11, &[0x01, 0x01, 0b0000_1101]).expect("assemble");
820 let bytes = on_wire.as_bytes();
821 let mut frame = ptr::null_mut();
822 let mut coils = ptr::null_mut();
823
824 unsafe {
826 assert_eq!(
827 pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame),
828 PamojaStatus::Ok
829 );
830 assert_eq!(
831 pamoja_modbus_frame_coils(frame, 4, &mut coils),
832 PamojaStatus::Ok
833 );
834 let values = take(coils);
835 pamoja_modbus_frame_free(frame);
836 assert_eq!(values, vec![1, 0, 1, 1]);
837 }
838 }
839
840 #[test]
841 fn a_corrupt_frame_is_refused() {
842 let mut bytes = Adu::from_pdu(0x11, &[0x03, 0x00, 0x6B, 0x00, 0x03])
843 .expect("assemble")
844 .as_bytes()
845 .to_vec();
846 bytes[2] ^= 0xFF;
847 let mut frame = ptr::null_mut();
848
849 let status = unsafe { pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame) };
851 assert_eq!(
852 status,
853 PamojaStatus::Codec,
854 "a frame mangled on the wire must not reach the application"
855 );
856 assert!(frame.is_null());
857 }
858
859 #[test]
860 fn an_exception_reply_reports_its_code() {
861 let on_wire = Adu::from_pdu(0x11, &[0x83, 0x02]).expect("assemble");
863 let bytes = on_wire.as_bytes();
864 let mut frame = ptr::null_mut();
865
866 unsafe {
868 assert_eq!(
869 pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame),
870 PamojaStatus::Ok
871 );
872 assert_eq!(pamoja_modbus_frame_exception(frame), 0x02);
873 assert_eq!(pamoja_modbus_frame_pdu_len(frame), 2);
874 pamoja_modbus_frame_free(frame);
875 }
876 }
877
878 #[test]
879 fn an_empty_write_is_rejected() {
880 let mut out = ptr::null_mut();
881 let status =
883 unsafe { pamoja_modbus_write_multiple_registers(0x11, 0, ptr::null(), 0, &mut out) };
884 assert_eq!(status, PamojaStatus::InvalidArgument);
885 assert!(out.is_null());
886 }
887
888 #[test]
889 fn the_checksum_matches_the_frame_it_ends() {
890 let frame = Adu::from_pdu(0x11, &[0x03, 0x00, 0x6B, 0x00, 0x03]).expect("assemble");
891 let bytes = frame.as_bytes();
892 let split = bytes.len() - 2;
893 let computed = unsafe { pamoja_modbus_crc16(bytes.as_ptr(), split) };
895 assert_eq!(computed.to_le_bytes(), bytes[split..]);
896 }
897}