Skip to main content

howzat_ffi_c/
lib.rs

1#![allow(non_camel_case_types)]
2
3use std::ffi::{CStr, CString, c_char};
4use std::panic::AssertUnwindSafe;
5use std::ptr;
6use std::sync::OnceLock;
7
8use howzat_kit::{Backend, BackendGeometry, BackendRunAny, BackendRunConfig, Representation, Stats};
9use howzat_kit::backend::{AnyPolytopeCoefficients, CoefficientMatrix, RowMajorMatrix};
10use hullabaloo::AdjacencyList;
11use hullabaloo::set_family::{ListFamily, SetFamily};
12
13const DEFAULT_BACKEND_SPEC: &str = "howzat-dd[purify[snap]]:f64[eps[1e-12]]";
14static DEFAULT_BACKEND: OnceLock<Backend> = OnceLock::new();
15
16const DEFAULT_EXACT_BACKEND_SPEC: &str = "howzat-dd:gmprat";
17static DEFAULT_EXACT_BACKEND: OnceLock<Backend> = OnceLock::new();
18
19fn default_backend() -> &'static Backend {
20    DEFAULT_BACKEND.get_or_init(|| {
21        DEFAULT_BACKEND_SPEC
22            .parse()
23            .expect("default backend spec must parse")
24    })
25}
26
27fn default_exact_backend() -> &'static Backend {
28    DEFAULT_EXACT_BACKEND.get_or_init(|| {
29        DEFAULT_EXACT_BACKEND_SPEC
30            .parse()
31            .expect("default exact backend spec must parse")
32    })
33}
34
35pub struct howzat_backend_t;
36
37pub struct howzat_result_t;
38
39pub struct howzat_dense_graph_t;
40
41pub struct howzat_adjacency_list_t;
42
43#[repr(C)]
44pub struct howzat_error_t {
45    message: *mut c_char,
46}
47
48#[repr(C)]
49pub struct howzat_usize_slice_t {
50    pub ptr: *const usize,
51    pub len: usize,
52}
53
54#[repr(C)]
55pub struct howzat_f64_slice_t {
56    pub ptr: *const f64,
57    pub len: usize,
58}
59
60#[repr(C)]
61pub struct howzat_i64_slice_t {
62    pub ptr: *const i64,
63    pub len: usize,
64}
65
66#[repr(C)]
67pub struct howzat_str_slice_t {
68    pub ptr: *const *const c_char,
69    pub len: usize,
70}
71
72#[allow(non_camel_case_types)]
73/// cbindgen:no-export
74pub struct __mpq_struct;
75
76#[allow(non_camel_case_types)]
77/// cbindgen:no-export
78pub type mpq_srcptr = *const __mpq_struct;
79
80#[repr(C)]
81pub struct howzat_mpq_slice_t {
82    pub ptr: *const mpq_srcptr,
83    pub len: usize,
84}
85
86#[derive(Copy, Clone, Debug, Eq, PartialEq)]
87#[repr(C)]
88pub enum howzat_graph_kind_t {
89    HOWZAT_GRAPH_DENSE = 0,
90    HOWZAT_GRAPH_SPARSE = 1,
91}
92
93#[derive(Copy, Clone, Debug, Eq, PartialEq)]
94#[repr(C)]
95pub enum howzat_representation_t {
96    HOWZAT_REPR_EUCLIDEAN_VERTICES = 0,
97    HOWZAT_REPR_INEQUALITY = 1,
98    HOWZAT_REPR_HOMOGENEOUS_GENERATORS = 2,
99}
100
101#[derive(Copy, Clone, Debug, Eq, PartialEq)]
102#[repr(C)]
103pub enum howzat_coeff_kind_t {
104    HOWZAT_COEFF_F64 = 0,
105    HOWZAT_COEFF_GMPRAT = 1,
106}
107
108struct BackendHandle {
109    inner: Backend,
110    spec: CString,
111}
112
113struct DenseGraphHandle {
114    inner: SetFamily,
115}
116
117struct AdjacencyListHandle {
118    inner: AdjacencyList,
119}
120
121enum GraphHandle {
122    Dense(DenseGraphHandle),
123    Sparse(AdjacencyListHandle),
124}
125
126struct ResultHandle {
127    spec: CString,
128    stats: Stats,
129    total_seconds: f64,
130    fails: usize,
131    fallbacks: usize,
132    vertex_positions: Option<Vec<f64>>,
133    vertex_positions_count: usize,
134    vertex_positions_dim: usize,
135    vertex_adjacency: GraphHandle,
136    facet_adjacency: GraphHandle,
137    facets_to_vertices_offsets: Vec<usize>,
138    facets_to_vertices_data: Vec<usize>,
139    coefficients: CoefficientsHandle,
140}
141
142struct CoeffMatrixF64Handle {
143    rows: usize,
144    cols: usize,
145    data: Vec<f64>,
146}
147
148struct CoeffMatrixI64Handle {
149    data: Vec<i64>,
150}
151
152struct CoeffMatrixStrHandle {
153    #[allow(dead_code)]
154    data: Vec<CString>,
155    ptrs: Vec<*const c_char>,
156}
157
158struct CoeffMatrixRugRatHandle {
159    rows: usize,
160    cols: usize,
161    data: Vec<calculo::num::RugRat>,
162    ptrs: Vec<mpq_srcptr>,
163    strings: OnceLock<CoeffMatrixStrHandle>,
164    i64s: OnceLock<Option<CoeffMatrixI64Handle>>,
165}
166
167enum CoefficientsHandle {
168    F64 {
169        generators: CoeffMatrixF64Handle,
170        inequalities: CoeffMatrixF64Handle,
171    },
172    RugRat {
173        generators: CoeffMatrixRugRatHandle,
174        inequalities: CoeffMatrixRugRatHandle,
175    },
176}
177
178fn cstring_lossy(s: &str) -> CString {
179    if !s.as_bytes().contains(&b'\0') {
180        return CString::new(s).expect("string must not contain NUL");
181    }
182    let sanitized: String = s.chars().map(|c| if c == '\0' { '�' } else { c }).collect();
183    CString::new(sanitized).expect("sanitized string must not contain NUL")
184}
185
186fn set_error(out: *mut *mut howzat_error_t, msg: &str) {
187    if out.is_null() {
188        return;
189    }
190    let message = cstring_lossy(msg).into_raw();
191    let err = Box::new(howzat_error_t { message });
192    unsafe {
193        *out = Box::into_raw(err);
194    }
195}
196
197fn clear_error(out: *mut *mut howzat_error_t) {
198    if out.is_null() {
199        return;
200    }
201    unsafe {
202        let prev = *out;
203        *out = ptr::null_mut();
204        if !prev.is_null() {
205            let prev = Box::from_raw(prev);
206            if !prev.message.is_null() {
207                drop(CString::from_raw(prev.message));
208            }
209        }
210    }
211}
212
213unsafe fn ref_backend<'a>(backend: *const howzat_backend_t) -> Option<&'a BackendHandle> {
214    if backend.is_null() {
215        return None;
216    }
217    Some(unsafe { &*(backend.cast::<BackendHandle>()) })
218}
219
220unsafe fn ref_result<'a>(result: *const howzat_result_t) -> Option<&'a ResultHandle> {
221    if result.is_null() {
222        return None;
223    }
224    Some(unsafe { &*(result.cast::<ResultHandle>()) })
225}
226
227unsafe fn ref_dense_graph<'a>(graph: *const howzat_dense_graph_t) -> Option<&'a DenseGraphHandle> {
228    if graph.is_null() {
229        return None;
230    }
231    Some(unsafe { &*(graph.cast::<DenseGraphHandle>()) })
232}
233
234unsafe fn ref_sparse_graph<'a>(
235    graph: *const howzat_adjacency_list_t,
236) -> Option<&'a AdjacencyListHandle> {
237    if graph.is_null() {
238        return None;
239    }
240    Some(unsafe { &*(graph.cast::<AdjacencyListHandle>()) })
241}
242
243fn flatten_set_family(family: &SetFamily) -> (Vec<usize>, Vec<usize>) {
244    let sets = family.sets();
245    let mut offsets = Vec::with_capacity(sets.len() + 1);
246    offsets.push(0);
247
248    let mut total = 0usize;
249    for set in sets {
250        total = total.saturating_add(set.cardinality());
251        offsets.push(total);
252    }
253
254    let mut data = Vec::with_capacity(total);
255    for set in sets {
256        data.extend(set.iter().raw());
257    }
258    (offsets, data)
259}
260
261fn flatten_list_family(family: &ListFamily) -> (Vec<usize>, Vec<usize>) {
262    let sets = family.sets();
263    let mut offsets = Vec::with_capacity(sets.len() + 1);
264    offsets.push(0);
265
266    let mut total = 0usize;
267    for set in sets {
268        total = total.saturating_add(set.len());
269        offsets.push(total);
270    }
271
272    let mut data = Vec::with_capacity(total);
273    for set in sets {
274        data.extend_from_slice(set);
275    }
276    (offsets, data)
277}
278
279fn build_coefficients_handle(
280    coefficients: Option<AnyPolytopeCoefficients>,
281) -> Result<CoefficientsHandle, String> {
282    let coefficients = coefficients.ok_or_else(|| "backend did not return coefficients".to_string())?;
283
284    fn build_rugrat_matrix(m: RowMajorMatrix<calculo::num::RugRat>) -> CoeffMatrixRugRatHandle {
285        let RowMajorMatrix { rows, cols, data } = m;
286        let ptrs = data
287            .iter()
288            .map(|v| v.0.as_raw().cast::<__mpq_struct>())
289            .collect();
290        CoeffMatrixRugRatHandle {
291            rows,
292            cols,
293            data,
294            ptrs,
295            strings: OnceLock::new(),
296            i64s: OnceLock::new(),
297        }
298    }
299
300    fn is_rational(matrix: &CoefficientMatrix) -> bool {
301        matches!(
302            matrix,
303            CoefficientMatrix::RugRat(_) | CoefficientMatrix::DashuRat(_)
304        )
305    }
306
307    match (coefficients.generators, coefficients.inequalities) {
308        (g, h) if !is_rational(&g) && !is_rational(&h) => {
309            let g = g
310                .coerce::<f64>()
311                .map_err(|_| "coefficient matrix could not be coerced to f64".to_string())?;
312            let h = h
313                .coerce::<f64>()
314                .map_err(|_| "coefficient matrix could not be coerced to f64".to_string())?;
315            Ok(CoefficientsHandle::F64 {
316                generators: CoeffMatrixF64Handle {
317                    rows: g.rows,
318                    cols: g.cols,
319                    data: g.data,
320                },
321                inequalities: CoeffMatrixF64Handle {
322                    rows: h.rows,
323                    cols: h.cols,
324                    data: h.data,
325                },
326            })
327        }
328        (g, h) if is_rational(&g) && is_rational(&h) => {
329            let g = g.coerce::<calculo::num::RugRat>().map_err(|_| {
330                "coefficient matrix could not be coerced to rug::Rational".to_string()
331            })?;
332            let h = h.coerce::<calculo::num::RugRat>().map_err(|_| {
333                "coefficient matrix could not be coerced to rug::Rational".to_string()
334            })?;
335            Ok(CoefficientsHandle::RugRat {
336                generators: build_rugrat_matrix(g),
337                inequalities: build_rugrat_matrix(h),
338            })
339        }
340        _ => Err("mixed coefficient kinds are not supported".to_string()),
341    }
342}
343
344fn build_result_any(run: BackendRunAny) -> Result<ResultHandle, String> {
345    match run {
346        BackendRunAny::Dense(run) => {
347            let howzat_kit::BackendRun {
348                spec,
349                stats,
350                timing,
351                geometry,
352                coefficients,
353                fails,
354                fallbacks,
355                error,
356                ..
357            } = run;
358
359            if let Some(err) = error {
360                return Err(err);
361            }
362
363            let vertex_positions_count = stats.vertices;
364            let vertex_positions_dim = stats.dimension;
365
366            let (vertex_positions, vertex_adjacency, facets_to_vertices, facet_adjacency) =
367                match geometry {
368                    BackendGeometry::Baseline(b) => (
369                        Some(b.vertex_positions),
370                        b.vertex_adjacency,
371                        b.facets_to_vertices,
372                        b.facet_adjacency,
373                    ),
374                    BackendGeometry::Input(g) => (
375                        None,
376                        g.vertex_adjacency,
377                        g.facets_to_vertices,
378                        g.facet_adjacency,
379                    ),
380                };
381
382            let (facets_to_vertices_offsets, facets_to_vertices_data) =
383                flatten_set_family(&facets_to_vertices);
384
385            let coefficients = build_coefficients_handle(coefficients)?;
386
387            let vertex_positions = match vertex_positions {
388                None => None,
389                Some(m) => Some(
390                    m.coerce::<f64>()
391                        .map_err(|_| "vertex positions could not be coerced to f64".to_string())?
392                        .data,
393                ),
394            };
395
396            Ok(ResultHandle {
397                spec: cstring_lossy(&spec.to_string()),
398                stats,
399                total_seconds: timing.total.as_secs_f64(),
400                fails,
401                fallbacks,
402                vertex_positions,
403                vertex_positions_count,
404                vertex_positions_dim,
405                vertex_adjacency: GraphHandle::Dense(DenseGraphHandle {
406                    inner: vertex_adjacency,
407                }),
408                facet_adjacency: GraphHandle::Dense(DenseGraphHandle {
409                    inner: facet_adjacency,
410                }),
411                facets_to_vertices_offsets,
412                facets_to_vertices_data,
413                coefficients,
414            })
415        }
416        BackendRunAny::Sparse(run) => {
417            let howzat_kit::BackendRun {
418                spec,
419                stats,
420                timing,
421                geometry,
422                coefficients,
423                fails,
424                fallbacks,
425                error,
426                ..
427            } = run;
428
429            if let Some(err) = error {
430                return Err(err);
431            }
432
433            let vertex_positions_count = stats.vertices;
434            let vertex_positions_dim = stats.dimension;
435
436            let (vertex_positions, vertex_adjacency, facets_to_vertices, facet_adjacency) =
437                match geometry {
438                    BackendGeometry::Baseline(b) => (
439                        Some(b.vertex_positions),
440                        b.vertex_adjacency,
441                        b.facets_to_vertices,
442                        b.facet_adjacency,
443                    ),
444                    BackendGeometry::Input(g) => (
445                        None,
446                        g.vertex_adjacency,
447                        g.facets_to_vertices,
448                        g.facet_adjacency,
449                    ),
450                };
451
452            let (facets_to_vertices_offsets, facets_to_vertices_data) =
453                flatten_list_family(&facets_to_vertices);
454
455            let coefficients = build_coefficients_handle(coefficients)?;
456
457            let vertex_positions = match vertex_positions {
458                None => None,
459                Some(m) => Some(
460                    m.coerce::<f64>()
461                        .map_err(|_| "vertex positions could not be coerced to f64".to_string())?
462                        .data,
463                ),
464            };
465
466            Ok(ResultHandle {
467                spec: cstring_lossy(&spec.to_string()),
468                stats,
469                total_seconds: timing.total.as_secs_f64(),
470                fails,
471                fallbacks,
472                vertex_positions,
473                vertex_positions_count,
474                vertex_positions_dim,
475                vertex_adjacency: GraphHandle::Sparse(AdjacencyListHandle {
476                    inner: vertex_adjacency,
477                }),
478                facet_adjacency: GraphHandle::Sparse(AdjacencyListHandle {
479                    inner: facet_adjacency,
480                }),
481                facets_to_vertices_offsets,
482                facets_to_vertices_data,
483                coefficients,
484            })
485        }
486    }
487}
488
489#[unsafe(no_mangle)]
490pub unsafe extern "C" fn howzat_error_message(err: *const howzat_error_t) -> *const c_char {
491    if err.is_null() {
492        return ptr::null();
493    }
494    unsafe { (*err).message }
495}
496
497#[unsafe(no_mangle)]
498pub unsafe extern "C" fn howzat_error_free(err: *mut howzat_error_t) {
499    if err.is_null() {
500        return;
501    }
502    unsafe {
503        let err = Box::from_raw(err);
504        if !err.message.is_null() {
505            drop(CString::from_raw(err.message));
506        }
507    }
508}
509
510#[unsafe(no_mangle)]
511pub unsafe extern "C" fn howzat_backend_new(
512    spec: *const c_char,
513    out_err: *mut *mut howzat_error_t,
514) -> *mut howzat_backend_t {
515    clear_error(out_err);
516
517    let make = || -> Result<BackendHandle, String> {
518        let backend = if spec.is_null() {
519            default_backend().clone()
520        } else {
521            let raw = unsafe { CStr::from_ptr(spec) };
522            let raw = raw
523                .to_str()
524                .map_err(|_| "backend spec must be valid UTF-8".to_string())?;
525            Backend::parse(raw)?
526        };
527        Ok(BackendHandle {
528            spec: cstring_lossy(&backend.to_string()),
529            inner: backend,
530        })
531    };
532
533    let handle = match std::panic::catch_unwind(AssertUnwindSafe(make)) {
534        Ok(Ok(handle)) => handle,
535        Ok(Err(msg)) => {
536            set_error(out_err, &msg);
537            return ptr::null_mut();
538        }
539        Err(_) => {
540            set_error(out_err, "panic while constructing backend");
541            return ptr::null_mut();
542        }
543    };
544
545    Box::into_raw(Box::new(handle)).cast::<howzat_backend_t>()
546}
547
548#[unsafe(no_mangle)]
549pub unsafe extern "C" fn howzat_backend_free(backend: *mut howzat_backend_t) {
550    if backend.is_null() {
551        return;
552    }
553    unsafe {
554        drop(Box::from_raw(backend.cast::<BackendHandle>()));
555    }
556}
557
558#[unsafe(no_mangle)]
559pub unsafe extern "C" fn howzat_backend_spec(backend: *const howzat_backend_t) -> *const c_char {
560    let Some(backend) = (unsafe { ref_backend(backend) }) else {
561        return ptr::null();
562    };
563    backend.spec.as_ptr()
564}
565
566fn solve_row_major_f64(
567    backend: &Backend,
568    data: *const f64,
569    rows: usize,
570    cols: usize,
571    repr: howzat_representation_t,
572) -> Result<BackendRunAny, String> {
573    let len = rows
574        .checked_mul(cols)
575        .ok_or_else(|| "rows * cols overflow".to_string())?;
576    if data.is_null() {
577        return Err("data must not be NULL".to_string());
578    }
579    if rows == 0 || cols == 0 {
580        return Err("rows and cols must be non-zero".to_string());
581    }
582    let slice = unsafe { std::slice::from_raw_parts(data, len) };
583    let config = BackendRunConfig {
584        output_coefficients: true,
585        ..BackendRunConfig::default()
586    };
587
588    let repr = match repr {
589        howzat_representation_t::HOWZAT_REPR_EUCLIDEAN_VERTICES => Representation::EuclideanVertices,
590        howzat_representation_t::HOWZAT_REPR_INEQUALITY => Representation::Inequality,
591        howzat_representation_t::HOWZAT_REPR_HOMOGENEOUS_GENERATORS => {
592            Representation::HomogeneousGenerators
593        }
594    };
595    if repr == Representation::Inequality && cols < 2 {
596        return Err("inequality matrix must have at least 2 columns".to_string());
597    }
598    if repr == Representation::HomogeneousGenerators && cols < 2 {
599        return Err("generator matrix must have at least 2 columns".to_string());
600    }
601    backend
602        .solve_row_major(repr, slice, rows, cols, &config)
603        .map_err(|e| e.to_string())
604}
605
606fn solve_row_major_i64(
607    backend: &Backend,
608    data: *const i64,
609    rows: usize,
610    cols: usize,
611    repr: howzat_representation_t,
612) -> Result<BackendRunAny, String> {
613    let len = rows
614        .checked_mul(cols)
615        .ok_or_else(|| "rows * cols overflow".to_string())?;
616    if data.is_null() {
617        return Err("data must not be NULL".to_string());
618    }
619    if rows == 0 || cols == 0 {
620        return Err("rows and cols must be non-zero".to_string());
621    }
622    let slice = unsafe { std::slice::from_raw_parts(data, len) };
623    let config = BackendRunConfig {
624        output_coefficients: true,
625        ..BackendRunConfig::default()
626    };
627
628    let repr = match repr {
629        howzat_representation_t::HOWZAT_REPR_EUCLIDEAN_VERTICES => Representation::EuclideanVertices,
630        howzat_representation_t::HOWZAT_REPR_INEQUALITY => Representation::Inequality,
631        howzat_representation_t::HOWZAT_REPR_HOMOGENEOUS_GENERATORS => {
632            Representation::HomogeneousGenerators
633        }
634    };
635    if repr == Representation::Inequality && cols < 2 {
636        return Err("inequality matrix must have at least 2 columns".to_string());
637    }
638    if repr == Representation::HomogeneousGenerators && cols < 2 {
639        return Err("generator matrix must have at least 2 columns".to_string());
640    }
641    backend
642        .solve_row_major_exact(repr, slice, rows, cols, &config)
643        .map_err(|e| e.to_string())
644}
645
646fn solve_row_major_gmprat(
647    backend: &Backend,
648    data: *const mpq_srcptr,
649    rows: usize,
650    cols: usize,
651    repr: howzat_representation_t,
652) -> Result<BackendRunAny, String> {
653    use gmp_mpfr_sys::gmp::mpq_t;
654    use rug::rational::BorrowRational;
655
656    let len = rows
657        .checked_mul(cols)
658        .ok_or_else(|| "rows * cols overflow".to_string())?;
659    if data.is_null() {
660        return Err("data must not be NULL".to_string());
661    }
662    if rows == 0 || cols == 0 {
663        return Err("rows and cols must be non-zero".to_string());
664    }
665    let slice = unsafe { std::slice::from_raw_parts(data, len) };
666    let mut values: Vec<calculo::num::RugRat> = Vec::with_capacity(len);
667    for &ptr in slice {
668        if ptr.is_null() {
669            return Err("gmprat data entries must not be NULL".to_string());
670        }
671        let raw = unsafe { *(ptr.cast::<mpq_t>()) };
672        let borrow = unsafe { BorrowRational::from_raw(raw) };
673        values.push(calculo::num::RugRat((&*borrow).clone()));
674    }
675
676    let config = BackendRunConfig {
677        output_coefficients: true,
678        ..BackendRunConfig::default()
679    };
680
681    let repr = match repr {
682        howzat_representation_t::HOWZAT_REPR_EUCLIDEAN_VERTICES => Representation::EuclideanVertices,
683        howzat_representation_t::HOWZAT_REPR_INEQUALITY => Representation::Inequality,
684        howzat_representation_t::HOWZAT_REPR_HOMOGENEOUS_GENERATORS => {
685            Representation::HomogeneousGenerators
686        }
687    };
688    if repr == Representation::Inequality && cols < 2 {
689        return Err("inequality matrix must have at least 2 columns".to_string());
690    }
691    if repr == Representation::HomogeneousGenerators && cols < 2 {
692        return Err("generator matrix must have at least 2 columns".to_string());
693    }
694    backend
695        .solve_row_major_exact_gmprat(repr, values, rows, cols, &config)
696        .map_err(|e| e.to_string())
697}
698
699#[unsafe(no_mangle)]
700pub unsafe extern "C" fn howzat_solve(
701    data: *const f64,
702    rows: usize,
703    cols: usize,
704    repr: howzat_representation_t,
705    out_err: *mut *mut howzat_error_t,
706) -> *mut howzat_result_t {
707    clear_error(out_err);
708
709    let make = || -> Result<ResultHandle, String> {
710        let run = solve_row_major_f64(default_backend(), data, rows, cols, repr)?;
711        build_result_any(run)
712    };
713
714    let result = match std::panic::catch_unwind(AssertUnwindSafe(make)) {
715        Ok(Ok(result)) => result,
716        Ok(Err(msg)) => {
717            set_error(out_err, &msg);
718            return ptr::null_mut();
719        }
720        Err(_) => {
721            set_error(out_err, "panic while solving");
722            return ptr::null_mut();
723        }
724    };
725
726    Box::into_raw(Box::new(result)).cast::<howzat_result_t>()
727}
728
729#[unsafe(no_mangle)]
730pub unsafe extern "C" fn howzat_solve_exact(
731    data: *const i64,
732    rows: usize,
733    cols: usize,
734    repr: howzat_representation_t,
735    out_err: *mut *mut howzat_error_t,
736) -> *mut howzat_result_t {
737    clear_error(out_err);
738
739    let make = || -> Result<ResultHandle, String> {
740        let run = solve_row_major_i64(default_exact_backend(), data, rows, cols, repr)?;
741        build_result_any(run)
742    };
743
744    let result = match std::panic::catch_unwind(AssertUnwindSafe(make)) {
745        Ok(Ok(result)) => result,
746        Ok(Err(msg)) => {
747            set_error(out_err, &msg);
748            return ptr::null_mut();
749        }
750        Err(_) => {
751            set_error(out_err, "panic while solving");
752            return ptr::null_mut();
753        }
754    };
755
756    Box::into_raw(Box::new(result)).cast::<howzat_result_t>()
757}
758
759#[unsafe(no_mangle)]
760pub unsafe extern "C" fn howzat_solve_exact_gmprat(
761    data: *const mpq_srcptr,
762    rows: usize,
763    cols: usize,
764    repr: howzat_representation_t,
765    out_err: *mut *mut howzat_error_t,
766) -> *mut howzat_result_t {
767    clear_error(out_err);
768
769    let make = || -> Result<ResultHandle, String> {
770        let run = solve_row_major_gmprat(default_exact_backend(), data, rows, cols, repr)?;
771        build_result_any(run)
772    };
773
774    let result = match std::panic::catch_unwind(AssertUnwindSafe(make)) {
775        Ok(Ok(result)) => result,
776        Ok(Err(msg)) => {
777            set_error(out_err, &msg);
778            return ptr::null_mut();
779        }
780        Err(_) => {
781            set_error(out_err, "panic while solving");
782            return ptr::null_mut();
783        }
784    };
785
786    Box::into_raw(Box::new(result)).cast::<howzat_result_t>()
787}
788
789#[unsafe(no_mangle)]
790pub unsafe extern "C" fn howzat_backend_solve(
791    backend: *const howzat_backend_t,
792    data: *const f64,
793    rows: usize,
794    cols: usize,
795    repr: howzat_representation_t,
796    out_err: *mut *mut howzat_error_t,
797) -> *mut howzat_result_t {
798    clear_error(out_err);
799
800    let Some(backend_ref) = unsafe { ref_backend(backend) }.map(|b| &b.inner) else {
801        set_error(out_err, "backend must not be NULL");
802        return ptr::null_mut();
803    };
804
805    let make = || -> Result<ResultHandle, String> {
806        let run = solve_row_major_f64(backend_ref, data, rows, cols, repr)?;
807        build_result_any(run)
808    };
809
810    let result = match std::panic::catch_unwind(AssertUnwindSafe(make)) {
811        Ok(Ok(result)) => result,
812        Ok(Err(msg)) => {
813            set_error(out_err, &msg);
814            return ptr::null_mut();
815        }
816        Err(_) => {
817            set_error(out_err, "panic while solving");
818            return ptr::null_mut();
819        }
820    };
821
822    Box::into_raw(Box::new(result)).cast::<howzat_result_t>()
823}
824
825#[unsafe(no_mangle)]
826pub unsafe extern "C" fn howzat_backend_solve_exact(
827    backend: *const howzat_backend_t,
828    data: *const i64,
829    rows: usize,
830    cols: usize,
831    repr: howzat_representation_t,
832    out_err: *mut *mut howzat_error_t,
833) -> *mut howzat_result_t {
834    clear_error(out_err);
835
836    let Some(backend_ref) = unsafe { ref_backend(backend) }.map(|b| &b.inner) else {
837        set_error(out_err, "backend must not be NULL");
838        return ptr::null_mut();
839    };
840
841    let make = || -> Result<ResultHandle, String> {
842        let run = solve_row_major_i64(backend_ref, data, rows, cols, repr)?;
843        build_result_any(run)
844    };
845
846    let result = match std::panic::catch_unwind(AssertUnwindSafe(make)) {
847        Ok(Ok(result)) => result,
848        Ok(Err(msg)) => {
849            set_error(out_err, &msg);
850            return ptr::null_mut();
851        }
852        Err(_) => {
853            set_error(out_err, "panic while solving");
854            return ptr::null_mut();
855        }
856    };
857
858    Box::into_raw(Box::new(result)).cast::<howzat_result_t>()
859}
860
861#[unsafe(no_mangle)]
862pub unsafe extern "C" fn howzat_backend_solve_exact_gmprat(
863    backend: *const howzat_backend_t,
864    data: *const mpq_srcptr,
865    rows: usize,
866    cols: usize,
867    repr: howzat_representation_t,
868    out_err: *mut *mut howzat_error_t,
869) -> *mut howzat_result_t {
870    clear_error(out_err);
871
872    let Some(backend_ref) = unsafe { ref_backend(backend) }.map(|b| &b.inner) else {
873        set_error(out_err, "backend must not be NULL");
874        return ptr::null_mut();
875    };
876
877    let make = || -> Result<ResultHandle, String> {
878        let run = solve_row_major_gmprat(backend_ref, data, rows, cols, repr)?;
879        build_result_any(run)
880    };
881
882    let result = match std::panic::catch_unwind(AssertUnwindSafe(make)) {
883        Ok(Ok(result)) => result,
884        Ok(Err(msg)) => {
885            set_error(out_err, &msg);
886            return ptr::null_mut();
887        }
888        Err(_) => {
889            set_error(out_err, "panic while solving");
890            return ptr::null_mut();
891        }
892    };
893
894    Box::into_raw(Box::new(result)).cast::<howzat_result_t>()
895}
896
897#[unsafe(no_mangle)]
898pub unsafe extern "C" fn howzat_result_free(result: *mut howzat_result_t) {
899    if result.is_null() {
900        return;
901    }
902    unsafe {
903        drop(Box::from_raw(result.cast::<ResultHandle>()));
904    }
905}
906
907#[unsafe(no_mangle)]
908pub unsafe extern "C" fn howzat_result_spec(result: *const howzat_result_t) -> *const c_char {
909    let Some(result) = (unsafe { ref_result(result) }) else {
910        return ptr::null();
911    };
912    result.spec.as_ptr()
913}
914
915#[unsafe(no_mangle)]
916pub unsafe extern "C" fn howzat_result_dimension(result: *const howzat_result_t) -> usize {
917    unsafe { ref_result(result) }
918        .map(|r| r.stats.dimension)
919        .unwrap_or(0)
920}
921
922#[unsafe(no_mangle)]
923pub unsafe extern "C" fn howzat_result_vertices(result: *const howzat_result_t) -> usize {
924    unsafe { ref_result(result) }
925        .map(|r| r.stats.vertices)
926        .unwrap_or(0)
927}
928
929#[unsafe(no_mangle)]
930pub unsafe extern "C" fn howzat_result_facets(result: *const howzat_result_t) -> usize {
931    unsafe { ref_result(result) }
932        .map(|r| r.stats.facets)
933        .unwrap_or(0)
934}
935
936#[unsafe(no_mangle)]
937pub unsafe extern "C" fn howzat_result_ridges(result: *const howzat_result_t) -> usize {
938    unsafe { ref_result(result) }
939        .map(|r| r.stats.ridges)
940        .unwrap_or(0)
941}
942
943#[unsafe(no_mangle)]
944pub unsafe extern "C" fn howzat_result_total_seconds(result: *const howzat_result_t) -> f64 {
945    unsafe { ref_result(result) }
946        .map(|r| r.total_seconds)
947        .unwrap_or(0.0)
948}
949
950#[unsafe(no_mangle)]
951pub unsafe extern "C" fn howzat_result_fails(result: *const howzat_result_t) -> usize {
952    unsafe { ref_result(result) }.map(|r| r.fails).unwrap_or(0)
953}
954
955#[unsafe(no_mangle)]
956pub unsafe extern "C" fn howzat_result_fallbacks(result: *const howzat_result_t) -> usize {
957    unsafe { ref_result(result) }
958        .map(|r| r.fallbacks)
959        .unwrap_or(0)
960}
961
962#[unsafe(no_mangle)]
963pub unsafe extern "C" fn howzat_result_coeff_kind(
964    result: *const howzat_result_t,
965) -> howzat_coeff_kind_t {
966    let Some(result) = (unsafe { ref_result(result) }) else {
967        return howzat_coeff_kind_t::HOWZAT_COEFF_F64;
968    };
969    match result.coefficients {
970        CoefficientsHandle::F64 { .. } => howzat_coeff_kind_t::HOWZAT_COEFF_F64,
971        CoefficientsHandle::RugRat { .. } => howzat_coeff_kind_t::HOWZAT_COEFF_GMPRAT,
972    }
973}
974
975#[unsafe(no_mangle)]
976pub unsafe extern "C" fn howzat_result_generators_shape(
977    result: *const howzat_result_t,
978    out_rows: *mut usize,
979    out_cols: *mut usize,
980) {
981    let Some(result) = (unsafe { ref_result(result) }) else {
982        return;
983    };
984    let (rows, cols) = match &result.coefficients {
985        CoefficientsHandle::F64 { generators, .. } => (generators.rows, generators.cols),
986        CoefficientsHandle::RugRat { generators, .. } => (generators.rows, generators.cols),
987    };
988    if !out_rows.is_null() {
989        unsafe { *out_rows = rows };
990    }
991    if !out_cols.is_null() {
992        unsafe { *out_cols = cols };
993    }
994}
995
996#[unsafe(no_mangle)]
997pub unsafe extern "C" fn howzat_result_inequalities_shape(
998    result: *const howzat_result_t,
999    out_rows: *mut usize,
1000    out_cols: *mut usize,
1001) {
1002    let Some(result) = (unsafe { ref_result(result) }) else {
1003        return;
1004    };
1005    let (rows, cols) = match &result.coefficients {
1006        CoefficientsHandle::F64 { inequalities, .. } => (inequalities.rows, inequalities.cols),
1007        CoefficientsHandle::RugRat { inequalities, .. } => (inequalities.rows, inequalities.cols),
1008    };
1009    if !out_rows.is_null() {
1010        unsafe { *out_rows = rows };
1011    }
1012    if !out_cols.is_null() {
1013        unsafe { *out_cols = cols };
1014    }
1015}
1016
1017#[unsafe(no_mangle)]
1018pub unsafe extern "C" fn howzat_result_generators_f64(
1019    result: *const howzat_result_t,
1020) -> howzat_f64_slice_t {
1021    let Some(result) = (unsafe { ref_result(result) }) else {
1022        return howzat_f64_slice_t {
1023            ptr: ptr::null(),
1024            len: 0,
1025        };
1026    };
1027    let CoefficientsHandle::F64 { generators, .. } = &result.coefficients else {
1028        return howzat_f64_slice_t {
1029            ptr: ptr::null(),
1030            len: 0,
1031        };
1032    };
1033    howzat_f64_slice_t {
1034        ptr: generators.data.as_ptr(),
1035        len: generators.data.len(),
1036    }
1037}
1038
1039#[unsafe(no_mangle)]
1040pub unsafe extern "C" fn howzat_result_inequalities_f64(
1041    result: *const howzat_result_t,
1042) -> howzat_f64_slice_t {
1043    let Some(result) = (unsafe { ref_result(result) }) else {
1044        return howzat_f64_slice_t {
1045            ptr: ptr::null(),
1046            len: 0,
1047        };
1048    };
1049    let CoefficientsHandle::F64 { inequalities, .. } = &result.coefficients else {
1050        return howzat_f64_slice_t {
1051            ptr: ptr::null(),
1052            len: 0,
1053        };
1054    };
1055    howzat_f64_slice_t {
1056        ptr: inequalities.data.as_ptr(),
1057        len: inequalities.data.len(),
1058    }
1059}
1060
1061#[unsafe(no_mangle)]
1062pub unsafe extern "C" fn howzat_result_generators_i64(
1063    result: *const howzat_result_t,
1064) -> howzat_i64_slice_t {
1065    let Some(result) = (unsafe { ref_result(result) }) else {
1066        return howzat_i64_slice_t {
1067            ptr: ptr::null(),
1068            len: 0,
1069        };
1070    };
1071    let CoefficientsHandle::RugRat { generators, .. } = &result.coefficients else {
1072        return howzat_i64_slice_t {
1073            ptr: ptr::null(),
1074            len: 0,
1075        };
1076    };
1077    let i64_matrix = generators.i64s.get_or_init(|| {
1078        let mut data = Vec::with_capacity(generators.data.len());
1079        for value in &generators.data {
1080            if !value.0.is_integer() {
1081                return None;
1082            }
1083            let Some(value) = value.0.numer().to_i64() else {
1084                return None;
1085            };
1086            data.push(value);
1087        }
1088        Some(CoeffMatrixI64Handle {
1089            data,
1090        })
1091    });
1092    let Some(i64_matrix) = i64_matrix else {
1093        return howzat_i64_slice_t {
1094            ptr: ptr::null(),
1095            len: 0,
1096        };
1097    };
1098    howzat_i64_slice_t {
1099        ptr: i64_matrix.data.as_ptr(),
1100        len: i64_matrix.data.len(),
1101    }
1102}
1103
1104#[unsafe(no_mangle)]
1105pub unsafe extern "C" fn howzat_result_inequalities_i64(
1106    result: *const howzat_result_t,
1107) -> howzat_i64_slice_t {
1108    let Some(result) = (unsafe { ref_result(result) }) else {
1109        return howzat_i64_slice_t {
1110            ptr: ptr::null(),
1111            len: 0,
1112        };
1113    };
1114    let CoefficientsHandle::RugRat { inequalities, .. } = &result.coefficients else {
1115        return howzat_i64_slice_t {
1116            ptr: ptr::null(),
1117            len: 0,
1118        };
1119    };
1120    let i64_matrix = inequalities.i64s.get_or_init(|| {
1121        let mut data = Vec::with_capacity(inequalities.data.len());
1122        for value in &inequalities.data {
1123            if !value.0.is_integer() {
1124                return None;
1125            }
1126            let Some(value) = value.0.numer().to_i64() else {
1127                return None;
1128            };
1129            data.push(value);
1130        }
1131        Some(CoeffMatrixI64Handle {
1132            data,
1133        })
1134    });
1135    let Some(i64_matrix) = i64_matrix else {
1136        return howzat_i64_slice_t {
1137            ptr: ptr::null(),
1138            len: 0,
1139        };
1140    };
1141    howzat_i64_slice_t {
1142        ptr: i64_matrix.data.as_ptr(),
1143        len: i64_matrix.data.len(),
1144    }
1145}
1146
1147#[unsafe(no_mangle)]
1148pub unsafe extern "C" fn howzat_result_generators_gmprat(
1149    result: *const howzat_result_t,
1150) -> howzat_mpq_slice_t {
1151    let Some(result) = (unsafe { ref_result(result) }) else {
1152        return howzat_mpq_slice_t {
1153            ptr: ptr::null(),
1154            len: 0,
1155        };
1156    };
1157    let CoefficientsHandle::RugRat { generators, .. } = &result.coefficients else {
1158        return howzat_mpq_slice_t {
1159            ptr: ptr::null(),
1160            len: 0,
1161        };
1162    };
1163    howzat_mpq_slice_t {
1164        ptr: generators.ptrs.as_ptr(),
1165        len: generators.ptrs.len(),
1166    }
1167}
1168
1169#[unsafe(no_mangle)]
1170pub unsafe extern "C" fn howzat_result_inequalities_gmprat(
1171    result: *const howzat_result_t,
1172) -> howzat_mpq_slice_t {
1173    let Some(result) = (unsafe { ref_result(result) }) else {
1174        return howzat_mpq_slice_t {
1175            ptr: ptr::null(),
1176            len: 0,
1177        };
1178    };
1179    let CoefficientsHandle::RugRat { inequalities, .. } = &result.coefficients else {
1180        return howzat_mpq_slice_t {
1181            ptr: ptr::null(),
1182            len: 0,
1183        };
1184    };
1185    howzat_mpq_slice_t {
1186        ptr: inequalities.ptrs.as_ptr(),
1187        len: inequalities.ptrs.len(),
1188    }
1189}
1190
1191#[unsafe(no_mangle)]
1192pub unsafe extern "C" fn howzat_result_generators_str(
1193    result: *const howzat_result_t,
1194) -> howzat_str_slice_t {
1195    let Some(result) = (unsafe { ref_result(result) }) else {
1196        return howzat_str_slice_t {
1197            ptr: ptr::null(),
1198            len: 0,
1199        };
1200    };
1201    let CoefficientsHandle::RugRat { generators, .. } = &result.coefficients else {
1202        return howzat_str_slice_t {
1203            ptr: ptr::null(),
1204            len: 0,
1205        };
1206    };
1207    let str_matrix = generators.strings.get_or_init(|| {
1208        let data: Vec<CString> = generators
1209            .data
1210            .iter()
1211            .map(|v| cstring_lossy(&v.to_string()))
1212            .collect();
1213        let ptrs: Vec<*const c_char> = data.iter().map(|s| s.as_ptr()).collect();
1214        CoeffMatrixStrHandle {
1215            data,
1216            ptrs,
1217        }
1218    });
1219    howzat_str_slice_t {
1220        ptr: str_matrix.ptrs.as_ptr(),
1221        len: str_matrix.ptrs.len(),
1222    }
1223}
1224
1225#[unsafe(no_mangle)]
1226pub unsafe extern "C" fn howzat_result_inequalities_str(
1227    result: *const howzat_result_t,
1228) -> howzat_str_slice_t {
1229    let Some(result) = (unsafe { ref_result(result) }) else {
1230        return howzat_str_slice_t {
1231            ptr: ptr::null(),
1232            len: 0,
1233        };
1234    };
1235    let CoefficientsHandle::RugRat { inequalities, .. } = &result.coefficients else {
1236        return howzat_str_slice_t {
1237            ptr: ptr::null(),
1238            len: 0,
1239        };
1240    };
1241    let str_matrix = inequalities.strings.get_or_init(|| {
1242        let data: Vec<CString> = inequalities
1243            .data
1244            .iter()
1245            .map(|v| cstring_lossy(&v.to_string()))
1246            .collect();
1247        let ptrs: Vec<*const c_char> = data.iter().map(|s| s.as_ptr()).collect();
1248        CoeffMatrixStrHandle {
1249            data,
1250            ptrs,
1251        }
1252    });
1253    howzat_str_slice_t {
1254        ptr: str_matrix.ptrs.as_ptr(),
1255        len: str_matrix.ptrs.len(),
1256    }
1257}
1258
1259#[unsafe(no_mangle)]
1260pub unsafe extern "C" fn howzat_result_vertex_positions(
1261    result: *const howzat_result_t,
1262) -> howzat_f64_slice_t {
1263    let Some(result) = (unsafe { ref_result(result) }) else {
1264        return howzat_f64_slice_t {
1265            ptr: ptr::null(),
1266            len: 0,
1267        };
1268    };
1269    let Some(buf) = result.vertex_positions.as_ref() else {
1270        return howzat_f64_slice_t {
1271            ptr: ptr::null(),
1272            len: 0,
1273        };
1274    };
1275    howzat_f64_slice_t {
1276        ptr: buf.as_ptr(),
1277        len: buf.len(),
1278    }
1279}
1280
1281#[unsafe(no_mangle)]
1282pub unsafe extern "C" fn howzat_result_vertex_positions_shape(
1283    result: *const howzat_result_t,
1284    out_vertex_count: *mut usize,
1285    out_dim: *mut usize,
1286) {
1287    if let Some(result) = unsafe { ref_result(result) } {
1288        if !out_vertex_count.is_null() {
1289            unsafe {
1290                *out_vertex_count = result.vertex_positions_count;
1291            }
1292        }
1293        if !out_dim.is_null() {
1294            unsafe {
1295                *out_dim = result.vertex_positions_dim;
1296            }
1297        }
1298    }
1299}
1300
1301#[unsafe(no_mangle)]
1302pub unsafe extern "C" fn howzat_result_facets_to_vertices_offsets(
1303    result: *const howzat_result_t,
1304) -> howzat_usize_slice_t {
1305    let Some(result) = (unsafe { ref_result(result) }) else {
1306        return howzat_usize_slice_t {
1307            ptr: ptr::null(),
1308            len: 0,
1309        };
1310    };
1311    howzat_usize_slice_t {
1312        ptr: result.facets_to_vertices_offsets.as_ptr(),
1313        len: result.facets_to_vertices_offsets.len(),
1314    }
1315}
1316
1317#[unsafe(no_mangle)]
1318pub unsafe extern "C" fn howzat_result_facets_to_vertices_data(
1319    result: *const howzat_result_t,
1320) -> howzat_usize_slice_t {
1321    let Some(result) = (unsafe { ref_result(result) }) else {
1322        return howzat_usize_slice_t {
1323            ptr: ptr::null(),
1324            len: 0,
1325        };
1326    };
1327    howzat_usize_slice_t {
1328        ptr: result.facets_to_vertices_data.as_ptr(),
1329        len: result.facets_to_vertices_data.len(),
1330    }
1331}
1332
1333#[unsafe(no_mangle)]
1334pub unsafe extern "C" fn howzat_result_vertex_adjacency_kind(
1335    result: *const howzat_result_t,
1336) -> howzat_graph_kind_t {
1337    let Some(result) = (unsafe { ref_result(result) }) else {
1338        return howzat_graph_kind_t::HOWZAT_GRAPH_SPARSE;
1339    };
1340    match &result.vertex_adjacency {
1341        GraphHandle::Dense(_) => howzat_graph_kind_t::HOWZAT_GRAPH_DENSE,
1342        GraphHandle::Sparse(_) => howzat_graph_kind_t::HOWZAT_GRAPH_SPARSE,
1343    }
1344}
1345
1346#[unsafe(no_mangle)]
1347pub unsafe extern "C" fn howzat_result_facet_adjacency_kind(
1348    result: *const howzat_result_t,
1349) -> howzat_graph_kind_t {
1350    let Some(result) = (unsafe { ref_result(result) }) else {
1351        return howzat_graph_kind_t::HOWZAT_GRAPH_SPARSE;
1352    };
1353    match &result.facet_adjacency {
1354        GraphHandle::Dense(_) => howzat_graph_kind_t::HOWZAT_GRAPH_DENSE,
1355        GraphHandle::Sparse(_) => howzat_graph_kind_t::HOWZAT_GRAPH_SPARSE,
1356    }
1357}
1358
1359#[unsafe(no_mangle)]
1360pub unsafe extern "C" fn howzat_result_vertex_adjacency_dense(
1361    result: *const howzat_result_t,
1362) -> *const howzat_dense_graph_t {
1363    let Some(result) = (unsafe { ref_result(result) }) else {
1364        return ptr::null();
1365    };
1366    match &result.vertex_adjacency {
1367        GraphHandle::Dense(g) => g as *const DenseGraphHandle as *const howzat_dense_graph_t,
1368        GraphHandle::Sparse(_) => ptr::null(),
1369    }
1370}
1371
1372#[unsafe(no_mangle)]
1373pub unsafe extern "C" fn howzat_result_vertex_adjacency_sparse(
1374    result: *const howzat_result_t,
1375) -> *const howzat_adjacency_list_t {
1376    let Some(result) = (unsafe { ref_result(result) }) else {
1377        return ptr::null();
1378    };
1379    match &result.vertex_adjacency {
1380        GraphHandle::Sparse(g) => g as *const AdjacencyListHandle as *const howzat_adjacency_list_t,
1381        GraphHandle::Dense(_) => ptr::null(),
1382    }
1383}
1384
1385#[unsafe(no_mangle)]
1386pub unsafe extern "C" fn howzat_result_facet_adjacency_dense(
1387    result: *const howzat_result_t,
1388) -> *const howzat_dense_graph_t {
1389    let Some(result) = (unsafe { ref_result(result) }) else {
1390        return ptr::null();
1391    };
1392    match &result.facet_adjacency {
1393        GraphHandle::Dense(g) => g as *const DenseGraphHandle as *const howzat_dense_graph_t,
1394        GraphHandle::Sparse(_) => ptr::null(),
1395    }
1396}
1397
1398#[unsafe(no_mangle)]
1399pub unsafe extern "C" fn howzat_result_facet_adjacency_sparse(
1400    result: *const howzat_result_t,
1401) -> *const howzat_adjacency_list_t {
1402    let Some(result) = (unsafe { ref_result(result) }) else {
1403        return ptr::null();
1404    };
1405    match &result.facet_adjacency {
1406        GraphHandle::Sparse(g) => g as *const AdjacencyListHandle as *const howzat_adjacency_list_t,
1407        GraphHandle::Dense(_) => ptr::null(),
1408    }
1409}
1410
1411#[unsafe(no_mangle)]
1412pub unsafe extern "C" fn howzat_dense_graph_node_count(
1413    graph: *const howzat_dense_graph_t,
1414) -> usize {
1415    unsafe { ref_dense_graph(graph) }
1416        .map(|g| g.inner.family_size())
1417        .unwrap_or(0)
1418}
1419
1420#[unsafe(no_mangle)]
1421pub unsafe extern "C" fn howzat_dense_graph_degree(
1422    graph: *const howzat_dense_graph_t,
1423    node: usize,
1424) -> usize {
1425    let Some(graph) = (unsafe { ref_dense_graph(graph) }) else {
1426        return 0;
1427    };
1428    if node >= graph.inner.family_size() {
1429        return 0;
1430    }
1431    graph.inner.sets()[node].cardinality()
1432}
1433
1434#[unsafe(no_mangle)]
1435pub unsafe extern "C" fn howzat_dense_graph_contains(
1436    graph: *const howzat_dense_graph_t,
1437    node: usize,
1438    neighbor: usize,
1439) -> bool {
1440    let Some(graph) = (unsafe { ref_dense_graph(graph) }) else {
1441        return false;
1442    };
1443    let node_count = graph.inner.family_size();
1444    if node >= node_count || neighbor >= node_count {
1445        return false;
1446    }
1447    graph.inner.sets()[node].contains(neighbor)
1448}
1449
1450#[unsafe(no_mangle)]
1451pub unsafe extern "C" fn howzat_dense_graph_neighbors(
1452    graph: *const howzat_dense_graph_t,
1453    node: usize,
1454    out: *mut usize,
1455    out_cap: usize,
1456) -> usize {
1457    let Some(graph) = (unsafe { ref_dense_graph(graph) }) else {
1458        return 0;
1459    };
1460    let node_count = graph.inner.family_size();
1461    if node >= node_count {
1462        return 0;
1463    }
1464
1465    let row = &graph.inner.sets()[node];
1466    let degree = row.cardinality();
1467    if out.is_null() || out_cap == 0 {
1468        return degree;
1469    }
1470
1471    let mut written = 0usize;
1472    for idx in row.iter().raw() {
1473        if written >= out_cap {
1474            break;
1475        }
1476        unsafe {
1477            *out.add(written) = idx;
1478        }
1479        written += 1;
1480    }
1481    degree
1482}
1483
1484#[unsafe(no_mangle)]
1485pub unsafe extern "C" fn howzat_adjacency_list_node_count(
1486    graph: *const howzat_adjacency_list_t,
1487) -> usize {
1488    unsafe { ref_sparse_graph(graph) }
1489        .map(|g| g.inner.num_vertices())
1490        .unwrap_or(0)
1491}
1492
1493#[unsafe(no_mangle)]
1494pub unsafe extern "C" fn howzat_adjacency_list_degree(
1495    graph: *const howzat_adjacency_list_t,
1496    node: usize,
1497) -> usize {
1498    let Some(graph) = (unsafe { ref_sparse_graph(graph) }) else {
1499        return 0;
1500    };
1501    if node >= graph.inner.num_vertices() {
1502        return 0;
1503    }
1504    graph.inner.degree(node)
1505}
1506
1507#[unsafe(no_mangle)]
1508pub unsafe extern "C" fn howzat_adjacency_list_contains(
1509    graph: *const howzat_adjacency_list_t,
1510    node: usize,
1511    neighbor: usize,
1512) -> bool {
1513    let Some(graph) = (unsafe { ref_sparse_graph(graph) }) else {
1514        return false;
1515    };
1516    let node_count = graph.inner.num_vertices();
1517    if node >= node_count || neighbor >= node_count {
1518        return false;
1519    }
1520    graph.inner.contains(node, neighbor)
1521}
1522
1523#[unsafe(no_mangle)]
1524pub unsafe extern "C" fn howzat_adjacency_list_neighbors(
1525    graph: *const howzat_adjacency_list_t,
1526    node: usize,
1527) -> howzat_usize_slice_t {
1528    let Some(graph) = (unsafe { ref_sparse_graph(graph) }) else {
1529        return howzat_usize_slice_t {
1530            ptr: ptr::null(),
1531            len: 0,
1532        };
1533    };
1534    if node >= graph.inner.num_vertices() {
1535        return howzat_usize_slice_t {
1536            ptr: ptr::null(),
1537            len: 0,
1538        };
1539    }
1540    let neighbors = graph.inner.neighbors(node);
1541    howzat_usize_slice_t {
1542        ptr: neighbors.as_ptr(),
1543        len: neighbors.len(),
1544    }
1545}