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
687unsafe fn out_slot<'a, T>(out: *mut *mut T, name: &str) -> Result<&'a mut *mut T, PamojaStatus> {
693 if out.is_null() {
694 set_last_error(format!("{name} must not be null"));
695 return Err(PamojaStatus::InvalidArgument);
696 }
697 let slot = &mut *out;
698 *slot = ptr::null_mut();
699 Ok(slot)
700}
701
702unsafe fn read_values<T: Copy>(
709 ptr: *const T,
710 count: usize,
711 name: &str,
712) -> Result<Vec<T>, PamojaStatus> {
713 if count == 0 {
714 Ok(Vec::new())
715 } else if ptr.is_null() {
716 set_last_error(format!(
717 "{name} must not be null when its count is non-zero"
718 ));
719 Err(PamojaStatus::InvalidArgument)
720 } else {
721 Ok(std::slice::from_raw_parts(ptr, count).to_vec())
722 }
723}
724
725fn failed(error: ModbusError) -> PamojaStatus {
727 set_last_error(error.to_string());
728 match error {
729 ModbusError::InvalidValueCount => PamojaStatus::InvalidArgument,
730 ModbusError::FrameTooShort
731 | ModbusError::FrameTooLong
732 | ModbusError::CrcMismatch { .. }
733 | ModbusError::MalformedResponse => PamojaStatus::Codec,
734 }
735}
736
737fn panicked() -> PamojaStatus {
739 set_last_error("panic at the FFI boundary".to_owned());
740 PamojaStatus::Panic
741}
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
747
748 unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
754 let bytes =
755 std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
756 .to_vec();
757 pamoja_buffer_free(buffer);
758 bytes
759 }
760
761 #[test]
762 fn a_read_request_matches_the_specification_example() {
763 let mut out = ptr::null_mut();
764 let frame = unsafe {
766 assert_eq!(
767 pamoja_modbus_read_holding_registers(0x11, 0x006B, 3, &mut out),
768 PamojaStatus::Ok
769 );
770 take(out)
771 };
772 assert_eq!(
773 frame,
774 vec![0x11, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x76, 0x87],
775 "the frame carries the address, the PDU, and the CRC"
776 );
777 }
778
779 #[test]
780 fn a_reply_parses_into_its_registers() {
781 let on_wire = Adu::from_pdu(0x11, &[0x03, 0x06, 0x02, 0x2B, 0x00, 0x00, 0x00, 0x64])
782 .expect("assemble");
783 let bytes = on_wire.as_bytes();
784 let mut frame = ptr::null_mut();
785 let mut registers = ptr::null_mut();
786
787 unsafe {
789 assert_eq!(
790 pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame),
791 PamojaStatus::Ok
792 );
793 assert_eq!(pamoja_modbus_frame_address(frame), 0x11);
794 assert_eq!(pamoja_modbus_frame_function(frame), 0x03);
795 assert_eq!(pamoja_modbus_frame_exception(frame), 0);
796 assert_eq!(
797 pamoja_modbus_frame_registers(frame, &mut registers),
798 PamojaStatus::Ok
799 );
800 let values = std::slice::from_raw_parts(
801 pamoja_registers_data(registers),
802 pamoja_registers_len(registers),
803 )
804 .to_vec();
805 pamoja_registers_free(registers);
806 pamoja_modbus_frame_free(frame);
807 assert_eq!(values, vec![0x022B, 0x0000, 0x0064]);
808 }
809 }
810
811 #[test]
812 fn a_bit_reply_unpacks_one_byte_per_coil() {
813 let on_wire = Adu::from_pdu(0x11, &[0x01, 0x01, 0b0000_1101]).expect("assemble");
815 let bytes = on_wire.as_bytes();
816 let mut frame = ptr::null_mut();
817 let mut coils = ptr::null_mut();
818
819 unsafe {
821 assert_eq!(
822 pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame),
823 PamojaStatus::Ok
824 );
825 assert_eq!(
826 pamoja_modbus_frame_coils(frame, 4, &mut coils),
827 PamojaStatus::Ok
828 );
829 let values = take(coils);
830 pamoja_modbus_frame_free(frame);
831 assert_eq!(values, vec![1, 0, 1, 1]);
832 }
833 }
834
835 #[test]
836 fn a_corrupt_frame_is_refused() {
837 let mut bytes = Adu::from_pdu(0x11, &[0x03, 0x00, 0x6B, 0x00, 0x03])
838 .expect("assemble")
839 .as_bytes()
840 .to_vec();
841 bytes[2] ^= 0xFF;
842 let mut frame = ptr::null_mut();
843
844 let status = unsafe { pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame) };
846 assert_eq!(
847 status,
848 PamojaStatus::Codec,
849 "a frame mangled on the wire must not reach the application"
850 );
851 assert!(frame.is_null());
852 }
853
854 #[test]
855 fn an_exception_reply_reports_its_code() {
856 let on_wire = Adu::from_pdu(0x11, &[0x83, 0x02]).expect("assemble");
858 let bytes = on_wire.as_bytes();
859 let mut frame = ptr::null_mut();
860
861 unsafe {
863 assert_eq!(
864 pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame),
865 PamojaStatus::Ok
866 );
867 assert_eq!(pamoja_modbus_frame_exception(frame), 0x02);
868 assert_eq!(pamoja_modbus_frame_pdu_len(frame), 2);
869 pamoja_modbus_frame_free(frame);
870 }
871 }
872
873 #[test]
874 fn an_empty_write_is_rejected() {
875 let mut out = ptr::null_mut();
876 let status =
878 unsafe { pamoja_modbus_write_multiple_registers(0x11, 0, ptr::null(), 0, &mut out) };
879 assert_eq!(status, PamojaStatus::InvalidArgument);
880 assert!(out.is_null());
881 }
882
883 #[test]
884 fn the_checksum_matches_the_frame_it_ends() {
885 let frame = Adu::from_pdu(0x11, &[0x03, 0x00, 0x6B, 0x00, 0x03]).expect("assemble");
886 let bytes = frame.as_bytes();
887 let split = bytes.len() - 2;
888 let computed = unsafe { pamoja_modbus_crc16(bytes.as_ptr(), split) };
890 assert_eq!(computed.to_le_bytes(), bytes[split..]);
891 }
892}