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]
173pub unsafe extern "C" fn pamoja_codec_quantizer_encode(
174 scale: f32,
175 readings: *const f32,
176 count: usize,
177 out_buffer: *mut *mut PamojaBuffer,
178) -> PamojaStatus {
179 let out_buffer = match out_slot(out_buffer, "out_buffer") {
180 Ok(slot) => slot,
181 Err(status) => return status,
182 };
183 if let Err(status) = check_scale(scale) {
184 return status;
185 }
186 let readings = match read_slice(readings, count, "readings") {
187 Ok(readings) => readings,
188 Err(status) => return status,
189 };
190 match catch_unwind(AssertUnwindSafe(|| Quantizer::new(scale).encode(&readings))) {
191 Ok(bytes) => {
192 *out_buffer = PamojaBuffer::into_raw(bytes);
193 PamojaStatus::Ok
194 }
195 Err(_) => panicked(),
196 }
197}
198
199#[no_mangle]
216pub unsafe extern "C" fn pamoja_codec_quantizer_decode(
217 scale: f32,
218 bytes: *const u8,
219 bytes_len: usize,
220 out_readings: *mut *mut PamojaReadings,
221) -> PamojaStatus {
222 let out_readings = match out_slot(out_readings, "out_readings") {
223 Ok(slot) => slot,
224 Err(status) => return status,
225 };
226 if let Err(status) = check_scale(scale) {
227 return status;
228 }
229 let bytes = match read_bytes(bytes, bytes_len) {
230 Ok(bytes) => bytes,
231 Err(status) => return status,
232 };
233 match catch_unwind(AssertUnwindSafe(|| Quantizer::new(scale).decode(&bytes))) {
234 Ok(Ok(readings)) => {
235 *out_readings = Box::into_raw(Box::new(PamojaReadings { readings }));
236 PamojaStatus::Ok
237 }
238 Ok(Err(error)) => failed(&error),
239 Err(_) => panicked(),
240 }
241}
242
243#[no_mangle]
256pub unsafe extern "C" fn pamoja_samples_data(samples: *const PamojaSamples) -> *const i64 {
257 if samples.is_null() {
258 return ptr::null();
259 }
260 (*samples).samples.as_ptr()
261}
262
263#[no_mangle]
273pub unsafe extern "C" fn pamoja_samples_len(samples: *const PamojaSamples) -> usize {
274 if samples.is_null() {
275 return 0;
276 }
277 (*samples).samples.len()
278}
279
280#[no_mangle]
289pub unsafe extern "C" fn pamoja_samples_free(samples: *mut PamojaSamples) {
290 if !samples.is_null() {
291 drop(Box::from_raw(samples));
292 }
293}
294
295#[no_mangle]
309pub unsafe extern "C" fn pamoja_readings_data(readings: *const PamojaReadings) -> *const f32 {
310 if readings.is_null() {
311 return ptr::null();
312 }
313 (*readings).readings.as_ptr()
314}
315
316#[no_mangle]
327pub unsafe extern "C" fn pamoja_readings_len(readings: *const PamojaReadings) -> usize {
328 if readings.is_null() {
329 return 0;
330 }
331 (*readings).readings.len()
332}
333
334#[no_mangle]
343pub unsafe extern "C" fn pamoja_readings_free(readings: *mut PamojaReadings) {
344 if !readings.is_null() {
345 drop(Box::from_raw(readings));
346 }
347}
348
349unsafe fn transcode(
357 input: *const u8,
358 input_len: usize,
359 out_buffer: *mut *mut PamojaBuffer,
360 convert: fn(&[u8]) -> CoreResult<Vec<u8>>,
361) -> PamojaStatus {
362 let out_buffer = match out_slot(out_buffer, "out_buffer") {
363 Ok(slot) => slot,
364 Err(status) => return status,
365 };
366 let input = match read_bytes(input, input_len) {
367 Ok(input) => input,
368 Err(status) => return status,
369 };
370 match catch_unwind(AssertUnwindSafe(|| convert(&input))) {
371 Ok(Ok(bytes)) => {
372 *out_buffer = PamojaBuffer::into_raw(bytes);
373 PamojaStatus::Ok
374 }
375 Ok(Err(error)) => failed(&error),
376 Err(_) => panicked(),
377 }
378}
379
380unsafe fn out_slot<'a, T>(out: *mut *mut T, name: &str) -> Result<&'a mut *mut T, PamojaStatus> {
391 if out.is_null() {
392 set_last_error(format!("{name} must not be null"));
393 return Err(PamojaStatus::InvalidArgument);
394 }
395 let slot = &mut *out;
396 *slot = ptr::null_mut();
397 Ok(slot)
398}
399
400unsafe fn read_slice<T: Copy>(
407 ptr: *const T,
408 count: usize,
409 name: &str,
410) -> Result<Vec<T>, PamojaStatus> {
411 if count == 0 {
412 Ok(Vec::new())
413 } else if ptr.is_null() {
414 set_last_error(format!(
415 "{name} must not be null when its count is non-zero"
416 ));
417 Err(PamojaStatus::InvalidArgument)
418 } else {
419 Ok(std::slice::from_raw_parts(ptr, count).to_vec())
420 }
421}
422
423fn check_scale(scale: f32) -> Result<(), PamojaStatus> {
425 if scale.is_finite() && scale > 0.0 {
426 Ok(())
427 } else {
428 set_last_error("scale must be positive and finite".to_owned());
429 Err(PamojaStatus::InvalidArgument)
430 }
431}
432
433fn failed(error: &pamoja_core::Error) -> PamojaStatus {
435 set_last_error(error.to_string());
436 PamojaStatus::from_error(error)
437}
438
439fn panicked() -> PamojaStatus {
441 set_last_error("panic at the FFI boundary".to_owned());
442 PamojaStatus::Panic
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
449
450 unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
456 let bytes =
457 std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
458 .to_vec();
459 pamoja_buffer_free(buffer);
460 bytes
461 }
462
463 #[test]
464 fn a_document_round_trips_through_cbor() {
465 let json = br#"{"c":21.5}"#;
466 let mut cbor = ptr::null_mut();
467 let mut back = ptr::null_mut();
468
469 unsafe {
471 assert_eq!(
472 pamoja_codec_json_to_cbor(json.as_ptr(), json.len(), &mut cbor),
473 PamojaStatus::Ok
474 );
475 let cbor_bytes = take(cbor);
476 assert!(cbor_bytes.len() < json.len());
477 assert_eq!(
478 pamoja_codec_cbor_to_json(cbor_bytes.as_ptr(), cbor_bytes.len(), &mut back),
479 PamojaStatus::Ok
480 );
481 assert_eq!(take(back), json);
482 }
483 }
484
485 #[test]
486 fn invalid_json_reports_a_codec_status() {
487 let json = b"not json";
488 let mut out = ptr::null_mut();
489 let status = unsafe { pamoja_codec_json_to_cbor(json.as_ptr(), json.len(), &mut out) };
491 assert_eq!(status, PamojaStatus::Codec);
492 assert!(out.is_null());
493 }
494
495 #[test]
496 fn samples_round_trip_through_delta_encoding() {
497 let samples = [10i64, 11, 13, 12, 900];
498 let mut encoded = ptr::null_mut();
499 let mut decoded = ptr::null_mut();
500
501 unsafe {
503 assert_eq!(
504 pamoja_codec_encode_deltas(samples.as_ptr(), samples.len(), &mut encoded),
505 PamojaStatus::Ok
506 );
507 let bytes = take(encoded);
508 assert_eq!(
509 pamoja_codec_decode_deltas(bytes.as_ptr(), bytes.len(), &mut decoded),
510 PamojaStatus::Ok
511 );
512 let restored = std::slice::from_raw_parts(
513 pamoja_samples_data(decoded),
514 pamoja_samples_len(decoded),
515 )
516 .to_vec();
517 pamoja_samples_free(decoded);
518 assert_eq!(restored, samples);
519 }
520 }
521
522 #[test]
523 fn readings_round_trip_to_within_the_quantizer_precision() {
524 let readings = [20.0f32, 20.1, 20.2, 20.3];
525 let mut encoded = ptr::null_mut();
526 let mut decoded = ptr::null_mut();
527
528 unsafe {
530 assert_eq!(
531 pamoja_codec_quantizer_encode(
532 100.0,
533 readings.as_ptr(),
534 readings.len(),
535 &mut encoded
536 ),
537 PamojaStatus::Ok
538 );
539 let bytes = take(encoded);
540 assert!(bytes.len() < readings.len() * 4);
541 assert_eq!(
542 pamoja_codec_quantizer_decode(100.0, bytes.as_ptr(), bytes.len(), &mut decoded),
543 PamojaStatus::Ok
544 );
545 let restored = std::slice::from_raw_parts(
546 pamoja_readings_data(decoded),
547 pamoja_readings_len(decoded),
548 )
549 .to_vec();
550 pamoja_readings_free(decoded);
551 for (got, want) in restored.iter().zip(readings.iter()) {
552 assert!((got - want).abs() < 0.05);
553 }
554 }
555 }
556
557 #[test]
558 fn a_non_positive_scale_is_rejected() {
559 let readings = [1.0f32];
560 let mut out = ptr::null_mut();
561 let status = unsafe {
563 pamoja_codec_quantizer_encode(0.0, readings.as_ptr(), readings.len(), &mut out)
564 };
565 assert_eq!(status, PamojaStatus::InvalidArgument);
566 assert!(out.is_null());
567 }
568
569 #[test]
570 fn a_null_out_pointer_is_rejected() {
571 let json = br#"{}"#;
572 let status =
574 unsafe { pamoja_codec_json_to_cbor(json.as_ptr(), json.len(), ptr::null_mut()) };
575 assert_eq!(status, PamojaStatus::InvalidArgument);
576 }
577
578 #[test]
579 fn an_empty_series_encodes_and_decodes_as_empty() {
580 let mut encoded = ptr::null_mut();
581 let mut decoded = ptr::null_mut();
582 unsafe {
584 assert_eq!(
585 pamoja_codec_encode_deltas(ptr::null(), 0, &mut encoded),
586 PamojaStatus::Ok
587 );
588 let bytes = take(encoded);
589 assert_eq!(
590 pamoja_codec_decode_deltas(bytes.as_ptr(), bytes.len(), &mut decoded),
591 PamojaStatus::Ok
592 );
593 assert_eq!(pamoja_samples_len(decoded), 0);
594 pamoja_samples_free(decoded);
595 }
596 }
597}