1use std::panic::{catch_unwind, AssertUnwindSafe};
16use std::ptr;
17
18use pamoja_codec::{cbor_to_json, decode_deltas, encode_deltas, json_to_cbor, Quantizer};
19use pamoja_core::Result as CoreResult;
20
21use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
22
23pub struct PamojaSamples {
28 samples: Vec<i64>,
29}
30
31pub struct PamojaReadings {
36 readings: Vec<f32>,
37}
38
39#[no_mangle]
55pub unsafe extern "C" fn pamoja_codec_json_to_cbor(
56 json: *const u8,
57 json_len: usize,
58 out_buffer: *mut *mut PamojaBuffer,
59) -> PamojaStatus {
60 transcode(json, json_len, out_buffer, json_to_cbor)
61}
62
63#[no_mangle]
77pub unsafe extern "C" fn pamoja_codec_cbor_to_json(
78 cbor: *const u8,
79 cbor_len: usize,
80 out_buffer: *mut *mut PamojaBuffer,
81) -> PamojaStatus {
82 transcode(cbor, cbor_len, out_buffer, cbor_to_json)
83}
84
85#[no_mangle]
99pub unsafe extern "C" fn pamoja_codec_encode_deltas(
100 samples: *const i64,
101 count: usize,
102 out_buffer: *mut *mut PamojaBuffer,
103) -> PamojaStatus {
104 let out_buffer = match out_slot(out_buffer, "out_buffer") {
105 Ok(slot) => slot,
106 Err(status) => return status,
107 };
108 let samples = match read_slice(samples, count, "samples") {
109 Ok(samples) => samples,
110 Err(status) => return status,
111 };
112 match catch_unwind(AssertUnwindSafe(|| encode_deltas(&samples))) {
113 Ok(bytes) => {
114 *out_buffer = PamojaBuffer::into_raw(bytes);
115 PamojaStatus::Ok
116 }
117 Err(_) => panicked(),
118 }
119}
120
121#[no_mangle]
135pub unsafe extern "C" fn pamoja_codec_decode_deltas(
136 bytes: *const u8,
137 bytes_len: usize,
138 out_samples: *mut *mut PamojaSamples,
139) -> PamojaStatus {
140 let out_samples = match out_slot(out_samples, "out_samples") {
141 Ok(slot) => slot,
142 Err(status) => return status,
143 };
144 let bytes = match read_bytes(bytes, bytes_len) {
145 Ok(bytes) => bytes,
146 Err(status) => return status,
147 };
148 match catch_unwind(AssertUnwindSafe(|| decode_deltas(&bytes))) {
149 Ok(Ok(samples)) => {
150 *out_samples = Box::into_raw(Box::new(PamojaSamples { samples }));
151 PamojaStatus::Ok
152 }
153 Ok(Err(error)) => failed(&error),
154 Err(_) => panicked(),
155 }
156}
157
158#[no_mangle]
175pub unsafe extern "C" fn pamoja_codec_quantizer_encode(
176 scale: f32,
177 readings: *const f32,
178 count: usize,
179 out_buffer: *mut *mut PamojaBuffer,
180) -> PamojaStatus {
181 let out_buffer = match out_slot(out_buffer, "out_buffer") {
182 Ok(slot) => slot,
183 Err(status) => return status,
184 };
185 if let Err(status) = check_scale(scale) {
186 return status;
187 }
188 let readings = match read_slice(readings, count, "readings") {
189 Ok(readings) => readings,
190 Err(status) => return status,
191 };
192 match catch_unwind(AssertUnwindSafe(|| Quantizer::new(scale).encode(&readings))) {
193 Ok(Ok(bytes)) => {
194 *out_buffer = PamojaBuffer::into_raw(bytes);
195 PamojaStatus::Ok
196 }
197 Ok(Err(error)) => failed(&error),
198 Err(_) => panicked(),
199 }
200}
201
202#[no_mangle]
219pub unsafe extern "C" fn pamoja_codec_quantizer_decode(
220 scale: f32,
221 bytes: *const u8,
222 bytes_len: usize,
223 out_readings: *mut *mut PamojaReadings,
224) -> PamojaStatus {
225 let out_readings = match out_slot(out_readings, "out_readings") {
226 Ok(slot) => slot,
227 Err(status) => return status,
228 };
229 if let Err(status) = check_scale(scale) {
230 return status;
231 }
232 let bytes = match read_bytes(bytes, bytes_len) {
233 Ok(bytes) => bytes,
234 Err(status) => return status,
235 };
236 match catch_unwind(AssertUnwindSafe(|| Quantizer::new(scale).decode(&bytes))) {
237 Ok(Ok(readings)) => {
238 *out_readings = Box::into_raw(Box::new(PamojaReadings { readings }));
239 PamojaStatus::Ok
240 }
241 Ok(Err(error)) => failed(&error),
242 Err(_) => panicked(),
243 }
244}
245
246#[no_mangle]
259pub unsafe extern "C" fn pamoja_samples_data(samples: *const PamojaSamples) -> *const i64 {
260 if samples.is_null() {
261 return ptr::null();
262 }
263 (*samples).samples.as_ptr()
264}
265
266#[no_mangle]
276pub unsafe extern "C" fn pamoja_samples_len(samples: *const PamojaSamples) -> usize {
277 if samples.is_null() {
278 return 0;
279 }
280 (*samples).samples.len()
281}
282
283#[no_mangle]
292pub unsafe extern "C" fn pamoja_samples_free(samples: *mut PamojaSamples) {
293 if !samples.is_null() {
294 drop(Box::from_raw(samples));
295 }
296}
297
298#[no_mangle]
312pub unsafe extern "C" fn pamoja_readings_data(readings: *const PamojaReadings) -> *const f32 {
313 if readings.is_null() {
314 return ptr::null();
315 }
316 (*readings).readings.as_ptr()
317}
318
319#[no_mangle]
330pub unsafe extern "C" fn pamoja_readings_len(readings: *const PamojaReadings) -> usize {
331 if readings.is_null() {
332 return 0;
333 }
334 (*readings).readings.len()
335}
336
337#[no_mangle]
346pub unsafe extern "C" fn pamoja_readings_free(readings: *mut PamojaReadings) {
347 if !readings.is_null() {
348 drop(Box::from_raw(readings));
349 }
350}
351
352unsafe fn transcode(
360 input: *const u8,
361 input_len: usize,
362 out_buffer: *mut *mut PamojaBuffer,
363 convert: fn(&[u8]) -> CoreResult<Vec<u8>>,
364) -> PamojaStatus {
365 let out_buffer = match out_slot(out_buffer, "out_buffer") {
366 Ok(slot) => slot,
367 Err(status) => return status,
368 };
369 let input = match read_bytes(input, input_len) {
370 Ok(input) => input,
371 Err(status) => return status,
372 };
373 match catch_unwind(AssertUnwindSafe(|| convert(&input))) {
374 Ok(Ok(bytes)) => {
375 *out_buffer = PamojaBuffer::into_raw(bytes);
376 PamojaStatus::Ok
377 }
378 Ok(Err(error)) => failed(&error),
379 Err(_) => panicked(),
380 }
381}
382
383unsafe fn out_slot<'a, T>(out: *mut *mut T, name: &str) -> Result<&'a mut *mut T, PamojaStatus> {
394 if out.is_null() {
395 set_last_error(format!("{name} must not be null"));
396 return Err(PamojaStatus::InvalidArgument);
397 }
398 let slot = &mut *out;
399 *slot = ptr::null_mut();
400 Ok(slot)
401}
402
403unsafe fn read_slice<T: Copy>(
410 ptr: *const T,
411 count: usize,
412 name: &str,
413) -> Result<Vec<T>, PamojaStatus> {
414 if count == 0 {
415 Ok(Vec::new())
416 } else if ptr.is_null() {
417 set_last_error(format!(
418 "{name} must not be null when its count is non-zero"
419 ));
420 Err(PamojaStatus::InvalidArgument)
421 } else {
422 Ok(std::slice::from_raw_parts(ptr, count).to_vec())
423 }
424}
425
426fn check_scale(scale: f32) -> Result<(), PamojaStatus> {
428 if scale.is_finite() && scale > 0.0 {
429 Ok(())
430 } else {
431 set_last_error(format!(
432 "a quantizer's scale must be a positive, finite number, not {scale}"
433 ));
434 Err(PamojaStatus::InvalidArgument)
435 }
436}
437
438fn failed(error: &pamoja_core::Error) -> PamojaStatus {
440 set_last_error(error.to_string());
441 PamojaStatus::from_error(error)
442}
443
444fn panicked() -> PamojaStatus {
446 set_last_error("panic at the FFI boundary".to_owned());
447 PamojaStatus::Panic
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
454
455 unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
461 let bytes =
462 std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
463 .to_vec();
464 pamoja_buffer_free(buffer);
465 bytes
466 }
467
468 #[test]
469 fn a_document_round_trips_through_cbor() {
470 let json = br#"{"c":21.5}"#;
471 let mut cbor = ptr::null_mut();
472 let mut back = ptr::null_mut();
473
474 unsafe {
476 assert_eq!(
477 pamoja_codec_json_to_cbor(json.as_ptr(), json.len(), &mut cbor),
478 PamojaStatus::Ok
479 );
480 let cbor_bytes = take(cbor);
481 assert!(cbor_bytes.len() < json.len());
482 assert_eq!(
483 pamoja_codec_cbor_to_json(cbor_bytes.as_ptr(), cbor_bytes.len(), &mut back),
484 PamojaStatus::Ok
485 );
486 assert_eq!(take(back), json);
487 }
488 }
489
490 #[test]
491 fn invalid_json_reports_a_codec_status() {
492 let json = b"not json";
493 let mut out = ptr::null_mut();
494 let status = unsafe { pamoja_codec_json_to_cbor(json.as_ptr(), json.len(), &mut out) };
496 assert_eq!(status, PamojaStatus::Codec);
497 assert!(out.is_null());
498 }
499
500 #[test]
501 fn samples_round_trip_through_delta_encoding() {
502 let samples = [10i64, 11, 13, 12, 900];
503 let mut encoded = ptr::null_mut();
504 let mut decoded = ptr::null_mut();
505
506 unsafe {
508 assert_eq!(
509 pamoja_codec_encode_deltas(samples.as_ptr(), samples.len(), &mut encoded),
510 PamojaStatus::Ok
511 );
512 let bytes = take(encoded);
513 assert_eq!(
514 pamoja_codec_decode_deltas(bytes.as_ptr(), bytes.len(), &mut decoded),
515 PamojaStatus::Ok
516 );
517 let restored = std::slice::from_raw_parts(
518 pamoja_samples_data(decoded),
519 pamoja_samples_len(decoded),
520 )
521 .to_vec();
522 pamoja_samples_free(decoded);
523 assert_eq!(restored, samples);
524 }
525 }
526
527 #[test]
528 fn readings_round_trip_to_within_the_quantizer_precision() {
529 let readings = [20.0f32, 20.1, 20.2, 20.3];
530 let mut encoded = ptr::null_mut();
531 let mut decoded = ptr::null_mut();
532
533 unsafe {
535 assert_eq!(
536 pamoja_codec_quantizer_encode(
537 100.0,
538 readings.as_ptr(),
539 readings.len(),
540 &mut encoded
541 ),
542 PamojaStatus::Ok
543 );
544 let bytes = take(encoded);
545 assert!(bytes.len() < readings.len() * 4);
546 assert_eq!(
547 pamoja_codec_quantizer_decode(100.0, bytes.as_ptr(), bytes.len(), &mut decoded),
548 PamojaStatus::Ok
549 );
550 let restored = std::slice::from_raw_parts(
551 pamoja_readings_data(decoded),
552 pamoja_readings_len(decoded),
553 )
554 .to_vec();
555 pamoja_readings_free(decoded);
556 for (got, want) in restored.iter().zip(readings.iter()) {
557 assert!((got - want).abs() < 0.05);
558 }
559 }
560 }
561
562 #[test]
563 fn a_non_positive_scale_is_rejected() {
564 let readings = [1.0f32];
565 let mut out = ptr::null_mut();
566 let status = unsafe {
568 pamoja_codec_quantizer_encode(0.0, readings.as_ptr(), readings.len(), &mut out)
569 };
570 assert_eq!(status, PamojaStatus::InvalidArgument);
571 assert!(out.is_null());
572 }
573
574 #[test]
575 fn a_null_out_pointer_is_rejected() {
576 let json = br#"{}"#;
577 let status =
579 unsafe { pamoja_codec_json_to_cbor(json.as_ptr(), json.len(), ptr::null_mut()) };
580 assert_eq!(status, PamojaStatus::InvalidArgument);
581 }
582
583 #[test]
584 fn an_empty_series_encodes_and_decodes_as_empty() {
585 let mut encoded = ptr::null_mut();
586 let mut decoded = ptr::null_mut();
587 unsafe {
589 assert_eq!(
590 pamoja_codec_encode_deltas(ptr::null(), 0, &mut encoded),
591 PamojaStatus::Ok
592 );
593 let bytes = take(encoded);
594 assert_eq!(
595 pamoja_codec_decode_deltas(bytes.as_ptr(), bytes.len(), &mut decoded),
596 PamojaStatus::Ok
597 );
598 assert_eq!(pamoja_samples_len(decoded), 0);
599 pamoja_samples_free(decoded);
600 }
601 }
602}