1#![allow(clippy::missing_safety_doc)]
2
3use std::cell::{Cell, RefCell};
4use std::ffi::{c_char, c_int, c_void};
5use std::panic::{catch_unwind, AssertUnwindSafe};
6use std::ptr;
7
8use metricchrono_core::{
9 adaptive_ladder_distance, carry_rules, classify_regime, custom_ladder, geometric_ladder,
10 ladder_distance, ladder_pair, normalize_ticks, progress_efficiency, simple_weight_update,
11 smooth_tick_distance, tick_distance, tick_pair, validate_ladder, weighted_consensus, Absolute,
12 CoverageMeter, Euclidean, EventLog, Metric, MetricChronoError, MetricFn, Normalization,
13 OperatingRegime, PromotionCounter, SmoothParams, Tier,
14};
15
16const MC_METRIC_EUCLIDEAN: c_int = 0;
17const MC_METRIC_ABSOLUTE: c_int = 1;
18
19const MC_REGIME_QUIESCENT: c_int = 0;
20const MC_REGIME_PROGRESS: c_int = 1;
21const MC_REGIME_CHURN: c_int = 2;
22const MC_REGIME_CREEP: c_int = 3;
23
24const MC_NORMALIZATION_NONE: c_int = 0;
25const MC_NORMALIZATION_UNIT_MAX: c_int = 1;
26const MC_NORMALIZATION_TANH: c_int = 2;
27
28thread_local! {
29 static LAST_ERROR: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
30 static LAST_ERROR_SET_IN_CALL: Cell<bool> = const { Cell::new(false) };
31}
32
33#[repr(C)]
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum MCStatus {
36 Ok = 0,
37 Null = 1,
38 InvalidArgument = 2,
39 BufferTooSmall = 3,
40 Panic = 255,
41}
42
43#[repr(C)]
44#[derive(Clone, Copy, Debug)]
45pub struct MCTier {
46 pub epsilon: f64,
47 pub delta: f64,
48 pub p: f64,
49 pub epsilon_ref: f64,
50}
51
52#[repr(C)]
53#[derive(Clone, Copy, Debug)]
54pub struct MCZoomDecision {
55 pub evaluated_tiers: usize,
56 pub first_inactive_tier: usize,
57 pub has_first_inactive_tier: bool,
58 pub stopped_early: bool,
59}
60
61pub struct MCEventLog {
62 inner: EventLog<u64>,
63}
64
65pub struct MCLadder {
66 tiers: Vec<Tier>,
67}
68
69pub struct MCPromotionCounter {
70 inner: PromotionCounter,
71}
72
73pub type MCDistanceFn =
80 unsafe extern "C" fn(a: *const f64, b: *const f64, dim: usize, user_data: *mut c_void) -> f64;
81
82#[derive(Clone, Copy)]
83enum CoverageMetric {
84 Builtin(c_int),
85 Callback {
86 callback: MCDistanceFn,
87 user_data: *mut c_void,
88 },
89}
90
91pub struct MCCoverageMeter {
92 inner: CoverageMeter<Vec<f64>>,
93 dim: usize,
94 metric: CoverageMetric,
95 scratch: Vec<f64>,
97}
98
99impl From<MCTier> for Tier {
100 fn from(value: MCTier) -> Self {
101 Self {
102 epsilon: value.epsilon,
103 delta: value.delta,
104 p: value.p,
105 epsilon_ref: value.epsilon_ref,
106 }
107 }
108}
109
110impl From<Tier> for MCTier {
111 fn from(value: Tier) -> Self {
112 Self {
113 epsilon: value.epsilon,
114 delta: value.delta,
115 p: value.p,
116 epsilon_ref: value.epsilon_ref,
117 }
118 }
119}
120
121fn ffi_status<F>(func: F) -> MCStatus
122where
123 F: FnOnce() -> MCStatus,
124{
125 begin_ffi_call();
126 match catch_unwind(AssertUnwindSafe(func)) {
127 Ok(status) => finish_status(status),
128 Err(_) => {
129 set_last_error("panic");
130 MCStatus::Panic
131 }
132 }
133}
134
135fn begin_ffi_call() {
136 LAST_ERROR_SET_IN_CALL.with(|flag| flag.set(false));
137}
138
139fn set_last_error(message: impl AsRef<str>) {
140 LAST_ERROR.with(|slot| {
141 let mut slot = slot.borrow_mut();
142 slot.clear();
143 slot.extend_from_slice(message.as_ref().as_bytes());
144 });
145 LAST_ERROR_SET_IN_CALL.with(|flag| flag.set(true));
146}
147
148fn finish_status(status: MCStatus) -> MCStatus {
149 if status != MCStatus::Ok {
150 let already_set = LAST_ERROR_SET_IN_CALL.with(Cell::get);
151 if !already_set {
152 set_last_error(status_message(status));
153 }
154 }
155 status
156}
157
158fn status_message(status: MCStatus) -> &'static str {
159 match status {
160 MCStatus::Ok => "ok",
161 MCStatus::Null => "null pointer",
162 MCStatus::InvalidArgument => "invalid argument",
163 MCStatus::BufferTooSmall => "buffer too small",
164 MCStatus::Panic => "panic",
165 }
166}
167
168fn status_from_error(error: MetricChronoError) -> MCStatus {
169 let status = match error {
170 MetricChronoError::OutputTooSmall { .. } => MCStatus::BufferTooSmall,
171 _ => MCStatus::InvalidArgument,
172 };
173 set_last_error(error.to_string());
174 status
175}
176
177fn invalid_argument(message: &'static str) -> MCStatus {
178 status_from_error(MetricChronoError::InvalidArgument(message))
179}
180
181fn buffer_too_small(needed: usize, actual: usize) -> MCStatus {
182 status_from_error(MetricChronoError::OutputTooSmall { needed, actual })
183}
184
185fn normalization_from_id(id: c_int) -> Result<Normalization, MetricChronoError> {
186 match id {
187 MC_NORMALIZATION_NONE => Ok(Normalization::None),
188 MC_NORMALIZATION_UNIT_MAX => Ok(Normalization::UnitMax),
189 MC_NORMALIZATION_TANH => Ok(Normalization::Tanh),
190 _ => Err(MetricChronoError::InvalidArgument(
191 "unknown normalization id",
192 )),
193 }
194}
195
196#[no_mangle]
197pub extern "C" fn mc_error_message(status: c_int) -> *const c_char {
198 match status {
199 0 => b"ok\0".as_ptr().cast(),
200 1 => b"null pointer\0".as_ptr().cast(),
201 2 => b"invalid argument\0".as_ptr().cast(),
202 3 => b"buffer too small\0".as_ptr().cast(),
203 255 => b"panic\0".as_ptr().cast(),
204 _ => b"unknown status\0".as_ptr().cast(),
205 }
206}
207
208#[no_mangle]
209pub unsafe extern "C" fn mc_last_error_message(
210 buf: *mut c_char,
211 cap: usize,
212 out_len: *mut usize,
213) -> MCStatus {
214 match catch_unwind(AssertUnwindSafe(|| {
215 let Some(out_len) = (unsafe { out_len.as_mut() }) else {
216 set_last_error("null pointer");
217 return MCStatus::Null;
218 };
219
220 let needed = LAST_ERROR.with(|slot| slot.borrow().len() + 1);
221 *out_len = needed;
222 if cap < needed {
223 return MCStatus::BufferTooSmall;
224 }
225
226 if buf.is_null() {
227 set_last_error("null pointer");
228 return MCStatus::Null;
229 }
230
231 LAST_ERROR.with(|slot| {
232 let message = slot.borrow();
233 unsafe {
234 ptr::copy_nonoverlapping(message.as_ptr().cast::<c_char>(), buf, message.len());
235 *buf.add(message.len()) = 0;
236 }
237 });
238 MCStatus::Ok
239 })) {
240 Ok(status) => status,
241 Err(_) => {
242 set_last_error("panic");
243 MCStatus::Panic
244 }
245 }
246}
247
248unsafe fn slice_from_ptr<'a, T>(ptr: *const T, len: usize) -> Option<&'a [T]> {
249 if len == 0 {
250 Some(&[])
251 } else if ptr.is_null() {
252 None
253 } else {
254 Some(std::slice::from_raw_parts(ptr, len))
255 }
256}
257
258unsafe fn slice_from_mut_ptr<'a, T>(ptr: *mut T, len: usize) -> Option<&'a mut [T]> {
259 if len == 0 {
260 Some(&mut [])
261 } else if ptr.is_null() {
262 None
263 } else {
264 Some(std::slice::from_raw_parts_mut(ptr, len))
265 }
266}
267
268fn create_ladder(tiers: &[MCTier], out: &mut *mut MCLadder) -> MCStatus {
269 let tiers: Vec<Tier> = tiers.iter().copied().map(Tier::from).collect();
270 match custom_ladder(tiers) {
271 Ok(tiers) => {
272 *out = Box::into_raw(Box::new(MCLadder { tiers }));
273 MCStatus::Ok
274 }
275 Err(error) => {
276 *out = ptr::null_mut();
277 status_from_error(error)
278 }
279 }
280}
281
282#[no_mangle]
283pub unsafe extern "C" fn mc_tier_new(
284 epsilon: f64,
285 delta: f64,
286 p: f64,
287 epsilon_ref: f64,
288 out: *mut MCTier,
289) -> MCStatus {
290 ffi_status(|| {
291 let Some(out) = (unsafe { out.as_mut() }) else {
292 return MCStatus::Null;
293 };
294 match Tier::new(epsilon, delta, p, epsilon_ref) {
295 Ok(tier) => {
296 *out = tier.into();
297 MCStatus::Ok
298 }
299 Err(error) => status_from_error(error),
300 }
301 })
302}
303
304#[no_mangle]
305pub unsafe extern "C" fn mc_ladder_new(
306 tiers: *const MCTier,
307 len: usize,
308 out: *mut *mut MCLadder,
309) -> MCStatus {
310 ffi_status(|| {
311 let Some(out) = (unsafe { out.as_mut() }) else {
312 return MCStatus::Null;
313 };
314 let Some(tiers) = (unsafe { slice_from_ptr(tiers, len) }) else {
315 return MCStatus::Null;
316 };
317 create_ladder(tiers, out)
318 })
319}
320
321#[no_mangle]
322pub unsafe extern "C" fn mc_custom_ladder(
323 tiers: *const MCTier,
324 len: usize,
325 out: *mut *mut MCLadder,
326) -> MCStatus {
327 ffi_status(|| {
328 let Some(out) = (unsafe { out.as_mut() }) else {
329 return MCStatus::Null;
330 };
331 let Some(tiers) = (unsafe { slice_from_ptr(tiers, len) }) else {
332 return MCStatus::Null;
333 };
334 create_ladder(tiers, out)
335 })
336}
337
338#[no_mangle]
339pub unsafe extern "C" fn mc_ladder_free(ladder: *mut MCLadder) {
340 if ladder.is_null() {
341 return;
342 }
343 let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
344 drop(Box::from_raw(ladder));
345 }));
346}
347
348#[no_mangle]
349pub unsafe extern "C" fn mc_ladder_len(ladder: *const MCLadder, out_len: *mut usize) -> MCStatus {
350 ffi_status(|| {
351 let Some(ladder) = (unsafe { ladder.as_ref() }) else {
352 return MCStatus::Null;
353 };
354 let Some(out_len) = (unsafe { out_len.as_mut() }) else {
355 return MCStatus::Null;
356 };
357 *out_len = ladder.tiers.len();
358 MCStatus::Ok
359 })
360}
361
362#[no_mangle]
363pub unsafe extern "C" fn mc_validate_ladder(ladder: *const MCLadder) -> MCStatus {
364 ffi_status(|| {
365 let Some(ladder) = (unsafe { ladder.as_ref() }) else {
366 return MCStatus::Null;
367 };
368 validate_ladder(&ladder.tiers).map_or_else(status_from_error, |_| MCStatus::Ok)
369 })
370}
371
372#[no_mangle]
373pub unsafe extern "C" fn mc_ladder_distance_owned(
374 ladder: *const MCLadder,
375 distance: f64,
376 out: *mut f64,
377 out_len: usize,
378) -> MCStatus {
379 ffi_status(|| {
380 let Some(ladder) = (unsafe { ladder.as_ref() }) else {
381 return MCStatus::Null;
382 };
383 let Some(out) = (unsafe { slice_from_mut_ptr(out, out_len) }) else {
384 return MCStatus::Null;
385 };
386 ladder_distance(distance, &ladder.tiers, out)
387 .map_or_else(status_from_error, |_| MCStatus::Ok)
388 })
389}
390
391#[no_mangle]
392pub unsafe extern "C" fn mc_tick_distance(distance: f64, tier: MCTier, out: *mut f64) -> MCStatus {
393 ffi_status(|| {
394 let Some(out) = (unsafe { out.as_mut() }) else {
395 return MCStatus::Null;
396 };
397 let tier = Tier::from(tier);
398 match metricchrono_core::try_tick_distance(distance, tier) {
399 Ok(value) => {
400 *out = value;
401 MCStatus::Ok
402 }
403 Err(error) => status_from_error(error),
404 }
405 })
406}
407
408#[no_mangle]
409pub unsafe extern "C" fn mc_euclidean_distance(
410 a: *const f64,
411 b: *const f64,
412 len: usize,
413 out: *mut f64,
414) -> MCStatus {
415 ffi_status(|| {
416 let Some(out) = (unsafe { out.as_mut() }) else {
417 return MCStatus::Null;
418 };
419 let Some(a) = (unsafe { slice_from_ptr(a, len) }) else {
420 return MCStatus::Null;
421 };
422 let Some(b) = (unsafe { slice_from_ptr(b, len) }) else {
423 return MCStatus::Null;
424 };
425 *out = Euclidean.distance(a, b);
426 MCStatus::Ok
427 })
428}
429
430#[no_mangle]
431pub unsafe extern "C" fn mc_absolute_distance(
432 a: *const f64,
433 b: *const f64,
434 len: usize,
435 out: *mut f64,
436) -> MCStatus {
437 ffi_status(|| {
438 if len != 1 {
439 return invalid_argument("absolute metric requires len == 1");
440 }
441 let Some(out) = (unsafe { out.as_mut() }) else {
442 return MCStatus::Null;
443 };
444 let Some(a) = (unsafe { slice_from_ptr(a, len) }) else {
445 return MCStatus::Null;
446 };
447 let Some(b) = (unsafe { slice_from_ptr(b, len) }) else {
448 return MCStatus::Null;
449 };
450 *out = Absolute.distance(&a[0], &b[0]);
451 MCStatus::Ok
452 })
453}
454
455#[no_mangle]
456pub unsafe extern "C" fn mc_tick_pair(
457 metric_id: c_int,
458 a: *const f64,
459 b: *const f64,
460 len: usize,
461 tier: MCTier,
462 out: *mut f64,
463) -> MCStatus {
464 ffi_status(|| {
465 let Some(out) = (unsafe { out.as_mut() }) else {
466 return MCStatus::Null;
467 };
468 match metric_id {
469 MC_METRIC_EUCLIDEAN => {
470 let Some(a) = (unsafe { slice_from_ptr(a, len) }) else {
471 return MCStatus::Null;
472 };
473 let Some(b) = (unsafe { slice_from_ptr(b, len) }) else {
474 return MCStatus::Null;
475 };
476 match tick_pair(a, b, &Euclidean, Tier::from(tier)) {
477 Ok(value) => {
478 *out = value;
479 MCStatus::Ok
480 }
481 Err(error) => status_from_error(error),
482 }
483 }
484 MC_METRIC_ABSOLUTE => {
485 if len != 1 {
486 return invalid_argument("absolute metric requires len == 1");
487 }
488 let Some(a) = (unsafe { slice_from_ptr(a, len) }) else {
489 return MCStatus::Null;
490 };
491 let Some(b) = (unsafe { slice_from_ptr(b, len) }) else {
492 return MCStatus::Null;
493 };
494 match tick_pair(&a[0], &b[0], &Absolute, Tier::from(tier)) {
495 Ok(value) => {
496 *out = value;
497 MCStatus::Ok
498 }
499 Err(error) => status_from_error(error),
500 }
501 }
502 _ => invalid_argument("unknown metric id"),
503 }
504 })
505}
506
507#[no_mangle]
508pub unsafe extern "C" fn mc_ladder_distance(
509 distance: f64,
510 tiers: *const MCTier,
511 len: usize,
512 out: *mut f64,
513 out_len: usize,
514) -> MCStatus {
515 ffi_status(|| {
516 let Some(tiers) = (unsafe { slice_from_ptr(tiers, len) }) else {
517 return MCStatus::Null;
518 };
519 let Some(out) = (unsafe { slice_from_mut_ptr(out, out_len) }) else {
520 return MCStatus::Null;
521 };
522 let tiers: Vec<Tier> = tiers.iter().copied().map(Tier::from).collect();
523 ladder_distance(distance, &tiers, out).map_or_else(status_from_error, |_| MCStatus::Ok)
524 })
525}
526
527#[no_mangle]
528pub unsafe extern "C" fn mc_ladder_pair(
529 metric_id: c_int,
530 a: *const f64,
531 b: *const f64,
532 len: usize,
533 ladder: *const MCLadder,
534 out: *mut f64,
535 cap: usize,
536 out_len: *mut usize,
537) -> MCStatus {
538 ffi_status(|| {
539 if metric_id != MC_METRIC_EUCLIDEAN && metric_id != MC_METRIC_ABSOLUTE {
540 return invalid_argument("unknown metric id");
541 }
542 if metric_id == MC_METRIC_ABSOLUTE && len != 1 {
543 return invalid_argument("absolute metric requires len == 1");
544 }
545 let Some(ladder) = (unsafe { ladder.as_ref() }) else {
546 return MCStatus::Null;
547 };
548 let Some(out_len) = (unsafe { out_len.as_mut() }) else {
549 return MCStatus::Null;
550 };
551 let needed = ladder.tiers.len();
552 *out_len = needed;
553 if cap < needed {
554 return buffer_too_small(needed, cap);
555 }
556 let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
557 return MCStatus::Null;
558 };
559 let Some(a) = (unsafe { slice_from_ptr(a, len) }) else {
560 return MCStatus::Null;
561 };
562 let Some(b) = (unsafe { slice_from_ptr(b, len) }) else {
563 return MCStatus::Null;
564 };
565 let values = match metric_id {
566 MC_METRIC_EUCLIDEAN => ladder_pair(a, b, &Euclidean, &ladder.tiers),
567 MC_METRIC_ABSOLUTE => ladder_pair(&a[0], &b[0], &Absolute, &ladder.tiers),
568 _ => unreachable!(),
569 };
570 match values {
571 Ok(values) => {
572 out[..needed].copy_from_slice(&values);
573 MCStatus::Ok
574 }
575 Err(error) => status_from_error(error),
576 }
577 })
578}
579
580#[no_mangle]
581pub unsafe extern "C" fn mc_adaptive_ladder_distance(
582 distance: f64,
583 tiers: *const MCTier,
584 len: usize,
585 out: *mut f64,
586 out_len: usize,
587 decision: *mut MCZoomDecision,
588) -> MCStatus {
589 ffi_status(|| {
590 let Some(tiers) = (unsafe { slice_from_ptr(tiers, len) }) else {
591 return MCStatus::Null;
592 };
593 let Some(out) = (unsafe { slice_from_mut_ptr(out, out_len) }) else {
594 return MCStatus::Null;
595 };
596 let Some(decision) = (unsafe { decision.as_mut() }) else {
597 return MCStatus::Null;
598 };
599 let tiers: Vec<Tier> = tiers.iter().copied().map(Tier::from).collect();
600 match adaptive_ladder_distance(distance, &tiers, out) {
601 Ok(value) => {
602 decision.evaluated_tiers = value.evaluated_tiers;
603 decision.first_inactive_tier = value.first_inactive_tier.unwrap_or(0);
604 decision.has_first_inactive_tier = value.first_inactive_tier.is_some();
605 decision.stopped_early = value.stopped_early;
606 MCStatus::Ok
607 }
608 Err(error) => status_from_error(error),
609 }
610 })
611}
612
613#[no_mangle]
614pub unsafe extern "C" fn mc_smooth_tick_distance(
615 distance: f64,
616 tier: MCTier,
617 sharpness: f64,
618 out: *mut f64,
619) -> MCStatus {
620 ffi_status(|| {
621 let Some(out) = (unsafe { out.as_mut() }) else {
622 return MCStatus::Null;
623 };
624 let params = match SmoothParams::sharpness(sharpness) {
625 Ok(params) => params,
626 Err(error) => return status_from_error(error),
627 };
628 match smooth_tick_distance(distance, Tier::from(tier), params) {
629 Ok(value) => {
630 *out = value;
631 MCStatus::Ok
632 }
633 Err(error) => status_from_error(error),
634 }
635 })
636}
637
638#[no_mangle]
639pub unsafe extern "C" fn mc_geometric_ladder(
640 epsilon0: f64,
641 delta0: f64,
642 ratio: f64,
643 tiers: usize,
644 p: f64,
645 epsilon_ref: f64,
646 out: *mut MCTier,
647 out_len: usize,
648) -> MCStatus {
649 ffi_status(|| {
650 if out_len < tiers {
651 return buffer_too_small(tiers, out_len);
652 }
653 let Some(out) = (unsafe { slice_from_mut_ptr(out, out_len) }) else {
654 return MCStatus::Null;
655 };
656 match geometric_ladder(epsilon0, delta0, ratio, tiers, p, epsilon_ref) {
657 Ok(values) => {
658 for (slot, tier) in out.iter_mut().zip(values) {
659 *slot = tier.into();
660 }
661 MCStatus::Ok
662 }
663 Err(error) => status_from_error(error),
664 }
665 })
666}
667
668#[no_mangle]
669pub unsafe extern "C" fn mc_normalize_ticks(
670 ticks: *const f64,
671 len: usize,
672 normalization_id: c_int,
673 out: *mut f64,
674) -> MCStatus {
675 ffi_status(|| {
676 let mode = match normalization_from_id(normalization_id) {
677 Ok(mode) => mode,
678 Err(error) => return status_from_error(error),
679 };
680 let Some(ticks) = (unsafe { slice_from_ptr(ticks, len) }) else {
681 return MCStatus::Null;
682 };
683 let Some(out) = (unsafe { slice_from_mut_ptr(out, len) }) else {
684 return MCStatus::Null;
685 };
686 normalize_ticks(ticks, mode, out).map_or_else(status_from_error, |_| MCStatus::Ok)
687 })
688}
689
690#[no_mangle]
691pub unsafe extern "C" fn mc_carry_rules(
692 epsilons: *const f64,
693 len: usize,
694 out: *mut u64,
695 cap: usize,
696 out_len: *mut usize,
697) -> MCStatus {
698 ffi_status(|| {
699 let Some(epsilons) = (unsafe { slice_from_ptr(epsilons, len) }) else {
700 return MCStatus::Null;
701 };
702 let Some(out_len) = (unsafe { out_len.as_mut() }) else {
703 return MCStatus::Null;
704 };
705 let rules = match carry_rules(epsilons) {
706 Ok(rules) => rules,
707 Err(error) => return status_from_error(error),
708 };
709 let needed = rules.len();
710 *out_len = needed;
711 if cap < needed {
712 return buffer_too_small(needed, cap);
713 }
714 let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
715 return MCStatus::Null;
716 };
717 out[..needed].copy_from_slice(&rules);
718 MCStatus::Ok
719 })
720}
721
722#[no_mangle]
723pub unsafe extern "C" fn mc_weighted_consensus(
724 vectors: *const f64,
725 rows: usize,
726 cols: usize,
727 weights: *const f64,
728 out: *mut f64,
729 out_len: usize,
730) -> MCStatus {
731 ffi_status(|| {
732 if rows == 0 || cols == 0 {
733 return invalid_argument("rows and cols must be > 0");
734 }
735 let Some(vector_len) = rows.checked_mul(cols) else {
736 return invalid_argument("rows * cols overflow");
737 };
738 const MAX_F64_SLICE_LEN: usize = isize::MAX as usize / std::mem::size_of::<f64>();
739 if vector_len > MAX_F64_SLICE_LEN || out_len > MAX_F64_SLICE_LEN {
740 return invalid_argument("slice byte length exceeds isize::MAX");
741 }
742 let Some(vectors) = (unsafe { slice_from_ptr(vectors, vector_len) }) else {
743 return MCStatus::Null;
744 };
745 let Some(weights) = (unsafe { slice_from_ptr(weights, rows) }) else {
746 return MCStatus::Null;
747 };
748 let Some(out) = (unsafe { slice_from_mut_ptr(out, out_len) }) else {
749 return MCStatus::Null;
750 };
751 let rows: Vec<&[f64]> = vectors.chunks(cols).collect();
752 weighted_consensus(&rows, weights, out).map_or_else(status_from_error, |_| MCStatus::Ok)
753 })
754}
755
756#[no_mangle]
757pub unsafe extern "C" fn mc_simple_weight_update(
758 weights: *mut f64,
759 residuals: *const f64,
760 len: usize,
761 learning_rate: f64,
762 floor: f64,
763) -> MCStatus {
764 ffi_status(|| {
765 let Some(weights) = (unsafe { slice_from_mut_ptr(weights, len) }) else {
766 return MCStatus::Null;
767 };
768 let Some(residuals) = (unsafe { slice_from_ptr(residuals, len) }) else {
769 return MCStatus::Null;
770 };
771 simple_weight_update(weights, residuals, learning_rate, floor)
772 .map_or_else(status_from_error, |_| MCStatus::Ok)
773 })
774}
775
776#[no_mangle]
777pub unsafe extern "C" fn mc_promotion_counter_new(
778 quotas: *const u64,
779 len: usize,
780 out: *mut *mut MCPromotionCounter,
781) -> MCStatus {
782 ffi_status(|| {
783 let Some(out) = (unsafe { out.as_mut() }) else {
784 return MCStatus::Null;
785 };
786 let Some(quotas) = (unsafe { slice_from_ptr(quotas, len) }) else {
787 return MCStatus::Null;
788 };
789 match PromotionCounter::new(quotas.to_vec()) {
790 Ok(inner) => {
791 *out = Box::into_raw(Box::new(MCPromotionCounter { inner }));
792 MCStatus::Ok
793 }
794 Err(error) => {
795 *out = ptr::null_mut();
796 status_from_error(error)
797 }
798 }
799 })
800}
801
802#[no_mangle]
803pub unsafe extern "C" fn mc_promotion_counter_from_epsilons(
804 epsilons: *const f64,
805 len: usize,
806 out: *mut *mut MCPromotionCounter,
807) -> MCStatus {
808 ffi_status(|| {
809 let Some(out) = (unsafe { out.as_mut() }) else {
810 return MCStatus::Null;
811 };
812 let Some(epsilons) = (unsafe { slice_from_ptr(epsilons, len) }) else {
813 return MCStatus::Null;
814 };
815 match PromotionCounter::from_epsilons(epsilons) {
816 Ok(inner) => {
817 *out = Box::into_raw(Box::new(MCPromotionCounter { inner }));
818 MCStatus::Ok
819 }
820 Err(error) => {
821 *out = ptr::null_mut();
822 status_from_error(error)
823 }
824 }
825 })
826}
827
828#[no_mangle]
829pub unsafe extern "C" fn mc_promotion_counter_step(
830 counter: *mut MCPromotionCounter,
831 event_flags: *const bool,
832 flags_len: usize,
833 out: *mut bool,
834 cap: usize,
835 out_len: *mut usize,
836) -> MCStatus {
837 ffi_status(|| {
838 let Some(counter) = (unsafe { counter.as_mut() }) else {
839 return MCStatus::Null;
840 };
841 let Some(out_len) = (unsafe { out_len.as_mut() }) else {
842 return MCStatus::Null;
843 };
844 let needed = counter.inner.len();
845 *out_len = needed;
846 if cap < needed {
847 return buffer_too_small(needed, cap);
848 }
849 let flags = if event_flags.is_null() && flags_len == 0 {
850 None
851 } else {
852 let Some(flags) = (unsafe { slice_from_ptr(event_flags, flags_len) }) else {
853 return MCStatus::Null;
854 };
855 Some(flags)
856 };
857 let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
858 return MCStatus::Null;
859 };
860 counter
861 .inner
862 .step(flags, out)
863 .map_or_else(status_from_error, |_| MCStatus::Ok)
864 })
865}
866
867#[no_mangle]
868pub unsafe extern "C" fn mc_promotion_counter_counters(
869 counter: *const MCPromotionCounter,
870 out: *mut u64,
871 cap: usize,
872 out_len: *mut usize,
873) -> MCStatus {
874 ffi_status(|| {
875 let Some(counter) = (unsafe { counter.as_ref() }) else {
876 return MCStatus::Null;
877 };
878 let Some(out_len) = (unsafe { out_len.as_mut() }) else {
879 return MCStatus::Null;
880 };
881 let counters = counter.inner.counters();
882 let needed = counters.len();
883 *out_len = needed;
884 if cap < needed {
885 return buffer_too_small(needed, cap);
886 }
887 let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
888 return MCStatus::Null;
889 };
890 out[..needed].copy_from_slice(counters);
891 MCStatus::Ok
892 })
893}
894
895#[no_mangle]
896pub unsafe extern "C" fn mc_promotion_counter_quotas(
897 counter: *const MCPromotionCounter,
898 out: *mut u64,
899 cap: usize,
900 out_len: *mut usize,
901) -> MCStatus {
902 ffi_status(|| {
903 let Some(counter) = (unsafe { counter.as_ref() }) else {
904 return MCStatus::Null;
905 };
906 let Some(out_len) = (unsafe { out_len.as_mut() }) else {
907 return MCStatus::Null;
908 };
909 let quotas = counter.inner.quotas();
910 let needed = quotas.len();
911 *out_len = needed;
912 if cap < needed {
913 return buffer_too_small(needed, cap);
914 }
915 let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
916 return MCStatus::Null;
917 };
918 out[..needed].copy_from_slice(quotas);
919 MCStatus::Ok
920 })
921}
922
923#[no_mangle]
924pub unsafe extern "C" fn mc_promotion_counter_reset(counter: *mut MCPromotionCounter) -> MCStatus {
925 ffi_status(|| {
926 let Some(counter) = (unsafe { counter.as_mut() }) else {
927 return MCStatus::Null;
928 };
929 counter.inner.reset();
930 MCStatus::Ok
931 })
932}
933
934#[no_mangle]
935pub unsafe extern "C" fn mc_promotion_counter_free(counter: *mut MCPromotionCounter) {
936 if counter.is_null() {
937 return;
938 }
939 let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
940 drop(Box::from_raw(counter));
941 }));
942}
943
944fn coverage_distance(metric: CoverageMetric, a: &Vec<f64>, b: &Vec<f64>) -> f64 {
945 match metric {
946 CoverageMetric::Builtin(MC_METRIC_ABSOLUTE) => (a[0] - b[0]).abs(),
947 CoverageMetric::Builtin(_) => Euclidean.distance(a.as_slice(), b.as_slice()),
948 CoverageMetric::Callback {
949 callback,
950 user_data,
951 } => unsafe { callback(a.as_ptr(), b.as_ptr(), a.len(), user_data) },
952 }
953}
954
955#[no_mangle]
956pub unsafe extern "C" fn mc_coverage_meter_new(
957 epsilons: *const f64,
958 len: usize,
959 dim: usize,
960 metric: c_int,
961 out: *mut *mut MCCoverageMeter,
962) -> MCStatus {
963 ffi_status(|| {
964 let Some(out) = (unsafe { out.as_mut() }) else {
965 return MCStatus::Null;
966 };
967 *out = ptr::null_mut();
968 let Some(epsilons) = (unsafe { slice_from_ptr(epsilons, len) }) else {
969 return MCStatus::Null;
970 };
971 if dim == 0 {
972 set_last_error("coverage state dimension must be > 0");
973 return MCStatus::InvalidArgument;
974 }
975 if metric != MC_METRIC_EUCLIDEAN && metric != MC_METRIC_ABSOLUTE {
976 set_last_error("unknown metric id");
977 return MCStatus::InvalidArgument;
978 }
979 if metric == MC_METRIC_ABSOLUTE && dim != 1 {
980 set_last_error("absolute metric requires dimension 1");
981 return MCStatus::InvalidArgument;
982 }
983 match CoverageMeter::from_epsilons(epsilons) {
984 Ok(inner) => {
985 *out = Box::into_raw(Box::new(MCCoverageMeter {
986 inner,
987 dim,
988 metric: CoverageMetric::Builtin(metric),
989 scratch: Vec::with_capacity(dim),
990 }));
991 MCStatus::Ok
992 }
993 Err(error) => status_from_error(error),
994 }
995 })
996}
997
998#[no_mangle]
999pub unsafe extern "C" fn mc_coverage_meter_new_with_callback(
1000 epsilons: *const f64,
1001 len: usize,
1002 dim: usize,
1003 callback: Option<MCDistanceFn>,
1004 user_data: *mut c_void,
1005 out: *mut *mut MCCoverageMeter,
1006) -> MCStatus {
1007 ffi_status(|| {
1008 let Some(out) = (unsafe { out.as_mut() }) else {
1009 return MCStatus::Null;
1010 };
1011 *out = ptr::null_mut();
1012 let Some(epsilons) = (unsafe { slice_from_ptr(epsilons, len) }) else {
1013 return MCStatus::Null;
1014 };
1015 let Some(callback) = callback else {
1016 return MCStatus::Null;
1017 };
1018 if dim == 0 {
1019 set_last_error("coverage state dimension must be > 0");
1020 return MCStatus::InvalidArgument;
1021 }
1022 match CoverageMeter::from_epsilons(epsilons) {
1023 Ok(inner) => {
1024 *out = Box::into_raw(Box::new(MCCoverageMeter {
1025 inner,
1026 dim,
1027 metric: CoverageMetric::Callback {
1028 callback,
1029 user_data,
1030 },
1031 scratch: Vec::with_capacity(dim),
1032 }));
1033 MCStatus::Ok
1034 }
1035 Err(error) => status_from_error(error),
1036 }
1037 })
1038}
1039
1040#[no_mangle]
1041pub unsafe extern "C" fn mc_coverage_meter_observe(
1042 meter: *mut MCCoverageMeter,
1043 state: *const f64,
1044 state_len: usize,
1045 out: *mut bool,
1046 cap: usize,
1047 out_len: *mut usize,
1048) -> MCStatus {
1049 ffi_status(|| {
1050 let Some(meter) = (unsafe { meter.as_mut() }) else {
1051 return MCStatus::Null;
1052 };
1053 let Some(out_len) = (unsafe { out_len.as_mut() }) else {
1054 return MCStatus::Null;
1055 };
1056 let needed = meter.inner.tier_count();
1057 *out_len = needed;
1058 if cap < needed {
1059 return buffer_too_small(needed, cap);
1060 }
1061 let Some(state) = (unsafe { slice_from_ptr(state, state_len) }) else {
1062 return MCStatus::Null;
1063 };
1064 if state_len != meter.dim {
1065 return status_from_error(MetricChronoError::ShapeMismatch {
1066 expected: meter.dim,
1067 actual: state_len,
1068 context: "coverage state dimension",
1069 });
1070 }
1071 let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
1072 return MCStatus::Null;
1073 };
1074 let metric_kind = meter.metric;
1075 let metric =
1076 MetricFn(move |a: &Vec<f64>, b: &Vec<f64>| coverage_distance(metric_kind, a, b));
1077 let MCCoverageMeter { inner, scratch, .. } = meter;
1078 scratch.clear();
1079 scratch.extend_from_slice(state);
1080 inner
1081 .observe_into(&metric, scratch, &mut out[..needed])
1082 .map_or_else(status_from_error, |_| MCStatus::Ok)
1083 })
1084}
1085
1086#[no_mangle]
1087pub unsafe extern "C" fn mc_coverage_meter_counts(
1088 meter: *const MCCoverageMeter,
1089 out: *mut u64,
1090 cap: usize,
1091 out_len: *mut usize,
1092) -> MCStatus {
1093 ffi_status(|| {
1094 let Some(meter) = (unsafe { meter.as_ref() }) else {
1095 return MCStatus::Null;
1096 };
1097 let Some(out_len) = (unsafe { out_len.as_mut() }) else {
1098 return MCStatus::Null;
1099 };
1100 let needed = meter.inner.tier_count();
1101 *out_len = needed;
1102 if cap < needed {
1103 return buffer_too_small(needed, cap);
1104 }
1105 let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
1106 return MCStatus::Null;
1107 };
1108 for (slot, count) in out.iter_mut().zip(meter.inner.counts()) {
1109 *slot = count as u64;
1110 }
1111 MCStatus::Ok
1112 })
1113}
1114
1115#[no_mangle]
1116pub unsafe extern "C" fn mc_coverage_meter_unique_representatives(
1117 meter: *const MCCoverageMeter,
1118 out: *mut u64,
1119) -> MCStatus {
1120 ffi_status(|| {
1121 let Some(meter) = (unsafe { meter.as_ref() }) else {
1122 return MCStatus::Null;
1123 };
1124 let Some(out) = (unsafe { out.as_mut() }) else {
1125 return MCStatus::Null;
1126 };
1127 *out = meter.inner.unique_representatives() as u64;
1128 MCStatus::Ok
1129 })
1130}
1131
1132#[no_mangle]
1133pub unsafe extern "C" fn mc_coverage_meter_tier_count(
1134 meter: *const MCCoverageMeter,
1135 out: *mut usize,
1136) -> MCStatus {
1137 ffi_status(|| {
1138 let Some(meter) = (unsafe { meter.as_ref() }) else {
1139 return MCStatus::Null;
1140 };
1141 let Some(out) = (unsafe { out.as_mut() }) else {
1142 return MCStatus::Null;
1143 };
1144 *out = meter.inner.tier_count();
1145 MCStatus::Ok
1146 })
1147}
1148
1149#[no_mangle]
1150pub unsafe extern "C" fn mc_coverage_meter_free(meter: *mut MCCoverageMeter) {
1151 if meter.is_null() {
1152 return;
1153 }
1154 let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
1155 drop(Box::from_raw(meter));
1156 }));
1157}
1158
1159#[no_mangle]
1160pub unsafe extern "C" fn mc_progress_efficiency(
1161 coverage: u64,
1162 epsilon: f64,
1163 path_length: f64,
1164 out: *mut f64,
1165) -> MCStatus {
1166 ffi_status(|| {
1167 let Some(out) = (unsafe { out.as_mut() }) else {
1168 return MCStatus::Null;
1169 };
1170 match progress_efficiency(coverage as usize, epsilon, path_length) {
1171 Some(value) => {
1172 *out = value;
1173 MCStatus::Ok
1174 }
1175 None => {
1176 set_last_error("path_length must be finite and positive");
1177 MCStatus::InvalidArgument
1178 }
1179 }
1180 })
1181}
1182
1183#[no_mangle]
1184pub extern "C" fn mc_classify_regime(throughput_delta: f64, coverage_delta: u64) -> c_int {
1185 match classify_regime(throughput_delta, coverage_delta as usize) {
1186 OperatingRegime::Quiescent => MC_REGIME_QUIESCENT,
1187 OperatingRegime::Progress => MC_REGIME_PROGRESS,
1188 OperatingRegime::Churn => MC_REGIME_CHURN,
1189 OperatingRegime::Creep => MC_REGIME_CREEP,
1190 }
1191}
1192
1193#[no_mangle]
1194pub extern "C" fn mc_event_log_new(tier_count: usize) -> *mut MCEventLog {
1195 begin_ffi_call();
1196 match catch_unwind(AssertUnwindSafe(|| match EventLog::new(tier_count) {
1197 Ok(inner) => Box::into_raw(Box::new(MCEventLog { inner })),
1198 Err(error) => {
1199 status_from_error(error);
1200 ptr::null_mut()
1201 }
1202 })) {
1203 Ok(ptr) => ptr,
1204 Err(_) => {
1205 set_last_error("panic");
1206 ptr::null_mut()
1207 }
1208 }
1209}
1210
1211#[no_mangle]
1212pub unsafe extern "C" fn mc_event_log_free(log: *mut MCEventLog) {
1213 if log.is_null() {
1214 return;
1215 }
1216 let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
1217 drop(Box::from_raw(log));
1218 }));
1219}
1220
1221#[no_mangle]
1222pub unsafe extern "C" fn mc_event_log_append(
1223 log: *mut MCEventLog,
1224 state_id: u64,
1225 ticks: *const f64,
1226 len: usize,
1227 out_index: *mut usize,
1228) -> MCStatus {
1229 ffi_status(|| {
1230 let Some(log) = (unsafe { log.as_mut() }) else {
1231 return MCStatus::Null;
1232 };
1233 let Some(ticks) = (unsafe { slice_from_ptr(ticks, len) }) else {
1234 return MCStatus::Null;
1235 };
1236 let Some(out_index) = (unsafe { out_index.as_mut() }) else {
1237 return MCStatus::Null;
1238 };
1239 match log.inner.append(state_id, ticks.to_vec()) {
1240 Ok(index) => {
1241 *out_index = index;
1242 MCStatus::Ok
1243 }
1244 Err(error) => status_from_error(error),
1245 }
1246 })
1247}
1248
1249#[no_mangle]
1250pub unsafe extern "C" fn mc_event_log_first_event(
1251 log: *const MCEventLog,
1252 tier: usize,
1253 out_index: *mut usize,
1254 out_has: *mut bool,
1255) -> MCStatus {
1256 ffi_status(|| {
1257 let Some(log) = (unsafe { log.as_ref() }) else {
1258 return MCStatus::Null;
1259 };
1260 let Some(out_index) = (unsafe { out_index.as_mut() }) else {
1261 return MCStatus::Null;
1262 };
1263 let Some(out_has) = (unsafe { out_has.as_mut() }) else {
1264 return MCStatus::Null;
1265 };
1266 if tier >= log.inner.tier_count() {
1267 return invalid_argument("event log tier is out of bounds");
1268 }
1269 if let Some(index) = log.inner.first_event(tier) {
1270 *out_index = index;
1271 *out_has = true;
1272 } else {
1273 *out_index = 0;
1274 *out_has = false;
1275 }
1276 MCStatus::Ok
1277 })
1278}
1279
1280#[no_mangle]
1281pub unsafe extern "C" fn mc_event_log_next_event(
1282 log: *const MCEventLog,
1283 index: usize,
1284 tier: usize,
1285 out_index: *mut usize,
1286 has_event: *mut bool,
1287) -> MCStatus {
1288 ffi_status(|| {
1289 let Some(log) = (unsafe { log.as_ref() }) else {
1290 return MCStatus::Null;
1291 };
1292 let Some(out_index) = (unsafe { out_index.as_mut() }) else {
1293 return MCStatus::Null;
1294 };
1295 let Some(has_event) = (unsafe { has_event.as_mut() }) else {
1296 return MCStatus::Null;
1297 };
1298 if tier >= log.inner.tier_count() || index >= log.inner.len() {
1299 return invalid_argument("event log index or tier is out of bounds");
1300 }
1301 if let Some(next) = log.inner.next_event(index, tier) {
1302 *out_index = next;
1303 *has_event = true;
1304 } else {
1305 *out_index = 0;
1306 *has_event = false;
1307 }
1308 MCStatus::Ok
1309 })
1310}
1311
1312#[no_mangle]
1313pub unsafe extern "C" fn mc_event_log_record(
1314 log: *const MCEventLog,
1315 index: usize,
1316 out_state_id: *mut u64,
1317 ticks_out: *mut f64,
1318 ticks_cap: usize,
1319 out_ticks_len: *mut usize,
1320) -> MCStatus {
1321 ffi_status(|| {
1322 let Some(log) = (unsafe { log.as_ref() }) else {
1323 return MCStatus::Null;
1324 };
1325 let Some(out_ticks_len) = (unsafe { out_ticks_len.as_mut() }) else {
1326 return MCStatus::Null;
1327 };
1328 let Some(record) = log.inner.record(index) else {
1329 return invalid_argument("event log index is out of bounds");
1330 };
1331 let needed = record.ticks.len();
1332 *out_ticks_len = needed;
1333 if ticks_cap < needed {
1334 return buffer_too_small(needed, ticks_cap);
1335 }
1336 let Some(out_state_id) = (unsafe { out_state_id.as_mut() }) else {
1337 return MCStatus::Null;
1338 };
1339 let Some(ticks_out) = (unsafe { slice_from_mut_ptr(ticks_out, ticks_cap) }) else {
1340 return MCStatus::Null;
1341 };
1342 *out_state_id = record.state_id;
1343 ticks_out[..needed].copy_from_slice(&record.ticks);
1344 MCStatus::Ok
1345 })
1346}
1347
1348#[no_mangle]
1349pub unsafe extern "C" fn mc_event_log_compact_summary(
1350 log: *const MCEventLog,
1351 tier: usize,
1352 idx_out: *mut usize,
1353 state_out: *mut u64,
1354 tick_out: *mut f64,
1355 cap: usize,
1356 out_len: *mut usize,
1357) -> MCStatus {
1358 ffi_status(|| {
1359 let Some(log) = (unsafe { log.as_ref() }) else {
1360 return MCStatus::Null;
1361 };
1362 let Some(out_len) = (unsafe { out_len.as_mut() }) else {
1363 return MCStatus::Null;
1364 };
1365 if tier >= log.inner.tier_count() {
1366 return invalid_argument("event log tier is out of bounds");
1367 }
1368 let summary = log.inner.compact_summary(tier);
1369 let needed = summary.len();
1370 *out_len = needed;
1371 if cap < needed {
1372 return buffer_too_small(needed, cap);
1373 }
1374 let Some(idx_out) = (unsafe { slice_from_mut_ptr(idx_out, cap) }) else {
1375 return MCStatus::Null;
1376 };
1377 let Some(state_out) = (unsafe { slice_from_mut_ptr(state_out, cap) }) else {
1378 return MCStatus::Null;
1379 };
1380 let Some(tick_out) = (unsafe { slice_from_mut_ptr(tick_out, cap) }) else {
1381 return MCStatus::Null;
1382 };
1383 for (offset, item) in summary.iter().enumerate() {
1384 idx_out[offset] = item.index;
1385 state_out[offset] = item.state_id;
1386 tick_out[offset] = item.tick;
1387 }
1388 MCStatus::Ok
1389 })
1390}
1391
1392#[no_mangle]
1393pub unsafe extern "C" fn mc_event_log_len(log: *const MCEventLog, out_len: *mut usize) -> MCStatus {
1394 ffi_status(|| {
1395 let Some(log) = (unsafe { log.as_ref() }) else {
1396 return MCStatus::Null;
1397 };
1398 let Some(out_len) = (unsafe { out_len.as_mut() }) else {
1399 return MCStatus::Null;
1400 };
1401 *out_len = log.inner.len();
1402 MCStatus::Ok
1403 })
1404}
1405
1406#[no_mangle]
1407pub unsafe extern "C" fn mc_event_log_tier_count(
1408 log: *const MCEventLog,
1409 out: *mut usize,
1410) -> MCStatus {
1411 ffi_status(|| {
1412 let Some(log) = (unsafe { log.as_ref() }) else {
1413 return MCStatus::Null;
1414 };
1415 let Some(out) = (unsafe { out.as_mut() }) else {
1416 return MCStatus::Null;
1417 };
1418 *out = log.inner.tier_count();
1419 MCStatus::Ok
1420 })
1421}
1422
1423#[no_mangle]
1424pub unsafe extern "C" fn mc_event_log_is_empty(log: *const MCEventLog, out: *mut bool) -> MCStatus {
1425 ffi_status(|| {
1426 let Some(log) = (unsafe { log.as_ref() }) else {
1427 return MCStatus::Null;
1428 };
1429 let Some(out) = (unsafe { out.as_mut() }) else {
1430 return MCStatus::Null;
1431 };
1432 *out = log.inner.is_empty();
1433 MCStatus::Ok
1434 })
1435}
1436
1437#[no_mangle]
1438pub extern "C" fn mc_tick_distance_raw(
1439 distance: f64,
1440 epsilon: f64,
1441 delta: f64,
1442 p: f64,
1443 epsilon_ref: f64,
1444) -> f64 {
1445 tick_distance(
1446 distance,
1447 Tier {
1448 epsilon,
1449 delta,
1450 p,
1451 epsilon_ref,
1452 },
1453 )
1454}
1455
1456#[cfg(test)]
1457mod tests {
1458 use super::*;
1459
1460 #[test]
1461 fn ffi_tick_and_ladder_return_stable_values() {
1462 let mut tier = MCTier {
1463 epsilon: 0.0,
1464 delta: 0.0,
1465 p: 0.0,
1466 epsilon_ref: 0.0,
1467 };
1468 assert_eq!(
1469 unsafe { mc_tier_new(0.5, 1.0, 0.5, 1.0, &mut tier) },
1470 MCStatus::Ok
1471 );
1472 assert_eq!(
1473 unsafe { mc_tier_new(1.0, 1.0, 0.0, 1.0, &mut tier) },
1474 MCStatus::InvalidArgument
1475 );
1476 let mut out = 0.0;
1477 assert_eq!(
1478 unsafe { mc_tick_distance(1.2, tier, &mut out) },
1479 MCStatus::Ok
1480 );
1481 assert_eq!(out, 2.0_f64.sqrt());
1482
1483 let tiers = [
1484 tier,
1485 MCTier {
1486 epsilon: 1.0,
1487 delta: 2.0,
1488 p: 0.5,
1489 epsilon_ref: 1.0,
1490 },
1491 ];
1492 let mut values = [0.0; 2];
1493 assert_eq!(
1494 unsafe {
1495 mc_ladder_distance(
1496 1.2,
1497 tiers.as_ptr(),
1498 tiers.len(),
1499 values.as_mut_ptr(),
1500 values.len(),
1501 )
1502 },
1503 MCStatus::Ok
1504 );
1505 assert!(values[0] > 0.0 && values[1] > 0.0);
1506
1507 let mut ladder = std::ptr::null_mut();
1508 assert_eq!(
1509 unsafe { mc_ladder_new(tiers.as_ptr(), tiers.len(), &mut ladder) },
1510 MCStatus::Ok
1511 );
1512 assert!(!ladder.is_null());
1513 let mut len = 0;
1514 assert_eq!(unsafe { mc_ladder_len(ladder, &mut len) }, MCStatus::Ok);
1515 assert_eq!(len, 2);
1516 let mut owned_values = [0.0; 2];
1517 assert_eq!(
1518 unsafe {
1519 mc_ladder_distance_owned(ladder, 1.2, owned_values.as_mut_ptr(), owned_values.len())
1520 },
1521 MCStatus::Ok
1522 );
1523 assert_eq!(values, owned_values);
1524 unsafe { mc_ladder_free(ladder) };
1525 }
1526
1527 #[test]
1528 fn ffi_event_log_exposes_next_pointers() {
1529 let log = mc_event_log_new(2);
1530 assert!(!log.is_null());
1531 let mut index = usize::MAX;
1532 assert_eq!(
1533 unsafe { mc_event_log_append(log, 10, [1.0, 0.0].as_ptr(), 2, &mut index) },
1534 MCStatus::Ok
1535 );
1536 assert_eq!(index, 0);
1537 assert_eq!(
1538 unsafe { mc_event_log_append(log, 11, [1.0, 1.0].as_ptr(), 2, &mut index) },
1539 MCStatus::Ok
1540 );
1541 let mut next = 0;
1542 let mut has = false;
1543 assert_eq!(
1544 unsafe { mc_event_log_next_event(log, 0, 0, &mut next, &mut has) },
1545 MCStatus::Ok
1546 );
1547 assert!(has);
1548 assert_eq!(next, 1);
1549 unsafe { mc_event_log_free(log) };
1550 }
1551
1552 #[test]
1553 fn ffi_coverage_meter_round_trip() {
1554 let epsilons = [0.1, 0.2];
1555 let mut meter: *mut MCCoverageMeter = ptr::null_mut();
1556 assert_eq!(
1557 unsafe {
1558 mc_coverage_meter_new(epsilons.as_ptr(), 2, 2, MC_METRIC_EUCLIDEAN, &mut meter)
1559 },
1560 MCStatus::Ok
1561 );
1562 assert!(!meter.is_null());
1563
1564 let mut admitted = [false; 2];
1565 let mut out_len = 0usize;
1566 assert_eq!(
1568 unsafe {
1569 mc_coverage_meter_observe(
1570 meter,
1571 [0.0, 0.0].as_ptr(),
1572 2,
1573 admitted.as_mut_ptr(),
1574 2,
1575 &mut out_len,
1576 )
1577 },
1578 MCStatus::Ok
1579 );
1580 assert_eq!(out_len, 2);
1581 assert_eq!(admitted, [true, true]);
1582 assert_eq!(
1584 unsafe {
1585 mc_coverage_meter_observe(
1586 meter,
1587 [0.15, 0.0].as_ptr(),
1588 2,
1589 admitted.as_mut_ptr(),
1590 2,
1591 &mut out_len,
1592 )
1593 },
1594 MCStatus::Ok
1595 );
1596 assert_eq!(admitted, [true, false]);
1597
1598 let mut counts = [0u64; 2];
1599 assert_eq!(
1600 unsafe { mc_coverage_meter_counts(meter, counts.as_mut_ptr(), 2, &mut out_len) },
1601 MCStatus::Ok
1602 );
1603 assert_eq!(counts, [2, 1]);
1604
1605 let mut unique = 0u64;
1606 assert_eq!(
1607 unsafe { mc_coverage_meter_unique_representatives(meter, &mut unique) },
1608 MCStatus::Ok
1609 );
1610 assert_eq!(unique, 2);
1611
1612 assert_eq!(
1614 unsafe {
1615 mc_coverage_meter_observe(
1616 meter,
1617 [0.0].as_ptr(),
1618 1,
1619 admitted.as_mut_ptr(),
1620 2,
1621 &mut out_len,
1622 )
1623 },
1624 MCStatus::InvalidArgument
1625 );
1626 unsafe { mc_coverage_meter_free(meter) };
1627
1628 let mut bad: *mut MCCoverageMeter = ptr::null_mut();
1630 assert_eq!(
1631 unsafe {
1632 mc_coverage_meter_new(epsilons.as_ptr(), 2, 0, MC_METRIC_EUCLIDEAN, &mut bad)
1633 },
1634 MCStatus::InvalidArgument
1635 );
1636 assert_eq!(
1637 unsafe { mc_coverage_meter_new(epsilons.as_ptr(), 2, 3, MC_METRIC_ABSOLUTE, &mut bad) },
1638 MCStatus::InvalidArgument
1639 );
1640
1641 unsafe extern "C" fn chebyshev(
1645 a: *const f64,
1646 b: *const f64,
1647 dim: usize,
1648 _user_data: *mut c_void,
1649 ) -> f64 {
1650 let a = unsafe { std::slice::from_raw_parts(a, dim) };
1651 let b = unsafe { std::slice::from_raw_parts(b, dim) };
1652 a.iter()
1653 .zip(b)
1654 .map(|(left, right)| (left - right).abs())
1655 .fold(0.0, f64::max)
1656 }
1657 let mut cb_meter: *mut MCCoverageMeter = ptr::null_mut();
1658 assert_eq!(
1659 unsafe {
1660 mc_coverage_meter_new_with_callback(
1661 [0.1].as_ptr(),
1662 1,
1663 2,
1664 Some(chebyshev),
1665 ptr::null_mut(),
1666 &mut cb_meter,
1667 )
1668 },
1669 MCStatus::Ok
1670 );
1671 let mut flag = [false; 1];
1672 assert_eq!(
1673 unsafe {
1674 mc_coverage_meter_observe(
1675 cb_meter,
1676 [0.0, 0.0].as_ptr(),
1677 2,
1678 flag.as_mut_ptr(),
1679 1,
1680 &mut out_len,
1681 )
1682 },
1683 MCStatus::Ok
1684 );
1685 assert_eq!(
1686 unsafe {
1687 mc_coverage_meter_observe(
1688 cb_meter,
1689 [0.05, 0.09].as_ptr(),
1690 2,
1691 flag.as_mut_ptr(),
1692 1,
1693 &mut out_len,
1694 )
1695 },
1696 MCStatus::Ok
1697 );
1698 assert_eq!(flag, [false], "chebyshev 0.09 < 0.1 must reject");
1699 unsafe { mc_coverage_meter_free(cb_meter) };
1700 assert_eq!(
1702 unsafe {
1703 mc_coverage_meter_new_with_callback(
1704 [0.1].as_ptr(),
1705 1,
1706 2,
1707 None,
1708 ptr::null_mut(),
1709 &mut cb_meter,
1710 )
1711 },
1712 MCStatus::Null
1713 );
1714
1715 assert_eq!(mc_classify_regime(0.0, 0), MC_REGIME_QUIESCENT);
1717 assert_eq!(mc_classify_regime(1.0, 1), MC_REGIME_PROGRESS);
1718 assert_eq!(mc_classify_regime(1.0, 0), MC_REGIME_CHURN);
1719 assert_eq!(mc_classify_regime(0.0, 1), MC_REGIME_CREEP);
1720 let mut efficiency = -1.0;
1721 assert_eq!(
1722 unsafe { mc_progress_efficiency(11, 0.1, 2.0, &mut efficiency) },
1723 MCStatus::Ok
1724 );
1725 assert!((efficiency - 0.5).abs() < 1e-12);
1726 assert_eq!(
1727 unsafe { mc_progress_efficiency(11, 0.1, 0.0, &mut efficiency) },
1728 MCStatus::InvalidArgument
1729 );
1730 }
1731}