axiolid_capi/
lib.rs

1//! Versioned C ABI for Axiolid's supported application facade.
2//!
3//! ABI functions never unwind. Handles are globally unique scalar tokens; ownership
4//! transfers are documented per function and no Rust allocation crosses the boundary.
5
6#![deny(unsafe_op_in_unsafe_fn)]
7
8use std::collections::HashMap;
9use std::panic::{catch_unwind, AssertUnwindSafe};
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Arc, LazyLock, Mutex, MutexGuard};
12
13use axiolid::application::Application;
14use axiolid::core::Point3;
15use axiolid::mesh::TriMesh;
16
17/// v0.4 ABI status code.
18#[repr(i32)]
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum AxiolidStatus {
21    Ok = 0,
22    NullPointer = 1,
23    InvalidArgument = 2,
24    InvalidHandle = 3,
25    LimitExceeded = 4,
26    Unsupported = 5,
27    UnsupportedExact = 6,
28    BufferTooSmall = 7,
29    OperationFailed = 8,
30    WrongResultKind = 9,
31    NoError = 10,
32    Panic = 255,
33}
34
35/// Semantic ABI and package version.
36#[repr(C)]
37#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
38pub struct AxiolidVersion {
39    pub abi_major: u16,
40    pub abi_minor: u16,
41    pub abi_patch: u16,
42    pub crate_major: u16,
43    pub crate_minor: u16,
44    pub crate_patch: u16,
45}
46
47/// Globally unique opaque context token. Zero is always invalid.
48#[repr(transparent)]
49#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct AxiolidContextHandle(pub u64);
51
52impl AxiolidContextHandle {
53    pub const INVALID: Self = Self(0);
54}
55
56/// Stable provider bundle selection. Integer form makes unknown C values rejectable.
57pub type AxiolidProviderProfile = i32;
58pub const AXIOLID_PROVIDER_PORTABLE: AxiolidProviderProfile = 1;
59
60/// Hard allocation budgets and provider selection for one context.
61#[repr(C)]
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct AxiolidContextConfig {
64    pub provider_profile: AxiolidProviderProfile,
65    pub max_meshes: u32,
66    pub max_results: u32,
67    pub max_vertices_per_mesh: u32,
68    pub max_triangles_per_mesh: u32,
69}
70
71impl Default for AxiolidContextConfig {
72    fn default() -> Self {
73        Self {
74            provider_profile: AXIOLID_PROVIDER_PORTABLE,
75            max_meshes: 1_024,
76            max_results: 1_024,
77            max_vertices_per_mesh: 10_000_000,
78            max_triangles_per_mesh: 20_000_000,
79        }
80    }
81}
82
83struct Context {
84    application: Application,
85    config: AxiolidContextConfig,
86    meshes: HandleTable<TriMesh>,
87    results: HandleTable<StoredResult>,
88    last_error: Option<ErrorRecord>,
89}
90
91enum StoredResult {
92    Mesh(TriMesh),
93    Exact(Box<axiolid::brep::ExactBRep>),
94}
95
96struct ErrorRecord {
97    status: AxiolidStatus,
98    operation: AxiolidOperation,
99    tolerance: AxiolidTolerance,
100    provider: String,
101    message: String,
102}
103
104struct HandleTable<T> {
105    values: HashMap<u64, T>,
106}
107
108impl<T> Default for HandleTable<T> {
109    fn default() -> Self {
110        Self {
111            values: HashMap::new(),
112        }
113    }
114}
115
116static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1);
117
118impl<T> HandleTable<T> {
119    fn insert(&mut self, value: T) -> u64 {
120        let handle = loop {
121            let candidate = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
122            if candidate != 0 && !self.values.contains_key(&candidate) {
123                break candidate;
124            }
125        };
126        self.values.insert(handle, value);
127        handle
128    }
129
130    fn get(&self, handle: u64) -> Option<&T> {
131        self.values.get(&handle)
132    }
133
134    fn get_mut(&mut self, handle: u64) -> Option<&mut T> {
135        self.values.get_mut(&handle)
136    }
137
138    fn remove(&mut self, handle: u64) -> Option<T> {
139        self.values.remove(&handle)
140    }
141
142    fn live_count(&self) -> usize {
143        self.values.len()
144    }
145}
146
147static CONTEXTS: LazyLock<Mutex<HandleTable<Arc<Mutex<Context>>>>> =
148    LazyLock::new(|| Mutex::new(HandleTable::default()));
149
150fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
151    mutex
152        .lock()
153        .unwrap_or_else(|poisoned| poisoned.into_inner())
154}
155
156fn contexts() -> MutexGuard<'static, HandleTable<Arc<Mutex<Context>>>> {
157    lock_unpoisoned(&CONTEXTS)
158}
159
160fn context_entry(handle: u64) -> Option<Arc<Mutex<Context>>> {
161    contexts().get(handle).cloned()
162}
163
164fn boundary(operation: impl FnOnce() -> AxiolidStatus) -> AxiolidStatus {
165    catch_unwind(AssertUnwindSafe(operation)).unwrap_or(AxiolidStatus::Panic)
166}
167
168/// Write the ABI/package version to caller-owned memory.
169///
170/// `out_version` must be null or point to writable storage for one `AxiolidVersion`.
171/// # Safety
172/// Every non-null pointer must be aligned and valid for the documented read or write extent.
173#[no_mangle]
174pub unsafe extern "C" fn axiolid_v0_4_version(out_version: *mut AxiolidVersion) -> AxiolidStatus {
175    boundary(|| {
176        if out_version.is_null() {
177            return AxiolidStatus::NullPointer;
178        }
179        let version = AxiolidVersion {
180            abi_major: 0,
181            abi_minor: 4,
182            abi_patch: 0,
183            crate_major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap_or(0),
184            crate_minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap_or(0),
185            crate_patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap_or(0),
186        };
187        // SAFETY: null was rejected; the ABI requires writable, aligned storage for this POD type.
188        unsafe { out_version.write(version) };
189        AxiolidStatus::Ok
190    })
191}
192
193/// Create and transfer ownership of a context handle to the caller.
194///
195/// Both pointers must refer to readable/writable instances for the duration of this call.
196/// # Safety
197/// Every non-null pointer must be aligned and valid for the documented read or write extent.
198#[no_mangle]
199pub unsafe extern "C" fn axiolid_v0_4_context_create(
200    config: *const AxiolidContextConfig,
201    out_context: *mut AxiolidContextHandle,
202) -> AxiolidStatus {
203    boundary(|| {
204        if config.is_null() || out_context.is_null() {
205            return AxiolidStatus::NullPointer;
206        }
207        // SAFETY: null was rejected; caller guarantees readable aligned POD storage.
208        let config = unsafe { config.read() };
209        if config.provider_profile != AXIOLID_PROVIDER_PORTABLE
210            || config.max_meshes == 0
211            || config.max_results == 0
212            || config.max_vertices_per_mesh == 0
213            || config.max_triangles_per_mesh == 0
214        {
215            return AxiolidStatus::InvalidArgument;
216        }
217        let Ok(application) = Application::portable() else {
218            return AxiolidStatus::OperationFailed;
219        };
220        let handle = AxiolidContextHandle(contexts().insert(Arc::new(Mutex::new(Context {
221            application,
222            config,
223            meshes: HandleTable::default(),
224            results: HandleTable::default(),
225            last_error: None,
226        }))));
227        // SAFETY: null was rejected; caller guarantees writable aligned POD storage.
228        unsafe { out_context.write(handle) };
229        AxiolidStatus::Ok
230    })
231}
232
233/// Destroy a context and all child objects it owns.
234///
235/// A stale or repeatedly destroyed handle is rejected without dereferencing freed memory.
236#[no_mangle]
237pub extern "C" fn axiolid_v0_4_context_destroy(context: AxiolidContextHandle) -> AxiolidStatus {
238    boundary(|| {
239        if contexts().remove(context.0).is_some() {
240            AxiolidStatus::Ok
241        } else {
242            AxiolidStatus::InvalidHandle
243        }
244    })
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn panic_is_contained() {
253        assert_eq!(boundary(|| panic!("contained")), AxiolidStatus::Panic);
254    }
255}
256
257/// Globally unique opaque mesh token owned by an Axiolid context.
258#[repr(transparent)]
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
260pub struct AxiolidMeshHandle(pub u64);
261
262impl AxiolidMeshHandle {
263    pub const INVALID: Self = Self(0);
264}
265
266/// Operation identifier in a capability descriptor.
267#[repr(i32)]
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum AxiolidOperation {
270    CurveEvaluation = 1,
271    SurfaceEvaluation = 2,
272    ProfileTriangulation = 3,
273    Sweep = 4,
274    Tessellation = 5,
275    MeshBoolean = 6,
276    MeshPlaneSection = 7,
277    SpatialQuery = 8,
278    Measurement = 9,
279    Healing = 10,
280    GraphCompilation = 11,
281    Unknown = 255,
282}
283
284/// Representation identifier in a capability descriptor.
285#[repr(i32)]
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub enum AxiolidRepresentation {
288    Scalar = 1,
289    Linear = 2,
290    Profile2d = 3,
291    AnalyticCurve = 4,
292    AnalyticSurface = 5,
293    Topology = 6,
294    ExactBrep = 7,
295    TriangleMesh = 8,
296    MeshHealth = 9,
297    Measurements = 10,
298    RayHit = 11,
299    ModelGraph = 12,
300    SampledField = 13,
301    Unknown = 255,
302}
303
304/// Exactness promise in a capability descriptor.
305#[repr(i32)]
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
307pub enum AxiolidExactness {
308    Exact = 1,
309    ToleranceBounded = 2,
310}
311
312/// Stable, pointer-free capability description. String lengths exclude padding.
313#[repr(C)]
314#[derive(Debug, Clone, Copy)]
315pub struct AxiolidCapabilityDescriptor {
316    pub operation: AxiolidOperation,
317    pub representation: AxiolidRepresentation,
318    pub exactness: AxiolidExactness,
319    pub id: [u8; 64],
320    pub provider: [u8; 64],
321    pub required_feature: [u8; 32],
322    pub id_len: u8,
323    pub provider_len: u8,
324    pub required_feature_len: u8,
325    pub deterministic: u8,
326}
327
328impl Default for AxiolidCapabilityDescriptor {
329    fn default() -> Self {
330        Self {
331            id: [0; 64],
332            id_len: 0,
333            provider: [0; 64],
334            provider_len: 0,
335            required_feature: [0; 32],
336            required_feature_len: 0,
337            operation: AxiolidOperation::Unknown,
338            representation: AxiolidRepresentation::Unknown,
339            exactness: AxiolidExactness::ToleranceBounded,
340            deterministic: 0,
341        }
342    }
343}
344
345fn copy_text<const N: usize>(text: &str, destination: &mut [u8; N]) -> u8 {
346    let count = text.len().min(N);
347    destination[..count].copy_from_slice(&text.as_bytes()[..count]);
348    count as u8
349}
350
351fn operation(value: axiolid::contracts::Operation) -> AxiolidOperation {
352    use axiolid::contracts::Operation;
353    match value {
354        Operation::CurveEvaluation => AxiolidOperation::CurveEvaluation,
355        Operation::SurfaceEvaluation => AxiolidOperation::SurfaceEvaluation,
356        Operation::ProfileTriangulation => AxiolidOperation::ProfileTriangulation,
357        Operation::Sweep => AxiolidOperation::Sweep,
358        Operation::Tessellation => AxiolidOperation::Tessellation,
359        Operation::MeshBoolean => AxiolidOperation::MeshBoolean,
360        Operation::MeshPlaneSection => AxiolidOperation::MeshPlaneSection,
361        Operation::SpatialQuery => AxiolidOperation::SpatialQuery,
362        Operation::Measurement => AxiolidOperation::Measurement,
363        Operation::Healing => AxiolidOperation::Healing,
364        Operation::GraphCompilation => AxiolidOperation::GraphCompilation,
365        _ => AxiolidOperation::Unknown,
366    }
367}
368
369fn representation(value: axiolid::contracts::Representation) -> AxiolidRepresentation {
370    use axiolid::contracts::Representation;
371    match value {
372        Representation::Scalar => AxiolidRepresentation::Scalar,
373        Representation::Linear => AxiolidRepresentation::Linear,
374        Representation::Profile2d => AxiolidRepresentation::Profile2d,
375        Representation::AnalyticCurve => AxiolidRepresentation::AnalyticCurve,
376        Representation::AnalyticSurface => AxiolidRepresentation::AnalyticSurface,
377        Representation::Topology => AxiolidRepresentation::Topology,
378        Representation::ExactBrep => AxiolidRepresentation::ExactBrep,
379        Representation::TriangleMesh => AxiolidRepresentation::TriangleMesh,
380        Representation::MeshHealth => AxiolidRepresentation::MeshHealth,
381        Representation::Measurements => AxiolidRepresentation::Measurements,
382        Representation::RayHit => AxiolidRepresentation::RayHit,
383        Representation::ModelGraph => AxiolidRepresentation::ModelGraph,
384        Representation::SampledField => AxiolidRepresentation::SampledField,
385    }
386}
387
388fn exactness(value: axiolid::contracts::Exactness) -> AxiolidExactness {
389    match value {
390        axiolid::contracts::Exactness::Exact => AxiolidExactness::Exact,
391        axiolid::contracts::Exactness::ToleranceBounded => AxiolidExactness::ToleranceBounded,
392    }
393}
394
395fn capability(value: &axiolid::contracts::CapabilityDescriptor) -> AxiolidCapabilityDescriptor {
396    let mut result = AxiolidCapabilityDescriptor::default();
397    result.id_len = copy_text(value.id.as_str(), &mut result.id);
398    result.provider_len = copy_text(value.provider.as_str(), &mut result.provider);
399    result.required_feature_len = copy_text(value.required_feature, &mut result.required_feature);
400    result.operation = operation(value.operation);
401    result.representation = representation(value.output);
402    result.exactness = exactness(value.exactness);
403    result.deterministic = u8::from(value.deterministic);
404    result
405}
406
407fn c_capabilities(
408    context: &Context,
409) -> impl Iterator<Item = &axiolid::contracts::CapabilityDescriptor> {
410    context
411        .application
412        .descriptor()
413        .capabilities
414        .iter()
415        .filter(|descriptor| {
416            matches!(
417                descriptor.operation,
418                axiolid::contracts::Operation::Healing
419                    | axiolid::contracts::Operation::Measurement
420                    | axiolid::contracts::Operation::MeshBoolean
421                    | axiolid::contracts::Operation::Sweep
422            )
423        })
424}
425
426/// Query the number of capabilities callable through this ABI context.
427/// # Safety
428/// Every non-null pointer must be aligned and valid for the documented read or write extent.
429#[no_mangle]
430pub unsafe extern "C" fn axiolid_v0_4_capability_count(
431    context: AxiolidContextHandle,
432    out_count: *mut usize,
433) -> AxiolidStatus {
434    boundary(|| {
435        if out_count.is_null() {
436            return AxiolidStatus::NullPointer;
437        }
438        let Some(context) = context_entry(context.0) else {
439            return AxiolidStatus::InvalidHandle;
440        };
441        let context = lock_unpoisoned(&context);
442        let count = c_capabilities(&context).count();
443        // SAFETY: null was rejected; the caller contract requires one writable usize.
444        unsafe { out_count.write(count) };
445        AxiolidStatus::Ok
446    })
447}
448
449/// Copy one capability descriptor into caller-owned storage.
450/// # Safety
451/// Every non-null pointer must be aligned and valid for the documented read or write extent.
452#[no_mangle]
453pub unsafe extern "C" fn axiolid_v0_4_capability_get(
454    context: AxiolidContextHandle,
455    index: usize,
456    out_descriptor: *mut AxiolidCapabilityDescriptor,
457) -> AxiolidStatus {
458    boundary(|| {
459        if out_descriptor.is_null() {
460            return AxiolidStatus::NullPointer;
461        }
462        let Some(context) = context_entry(context.0) else {
463            return AxiolidStatus::InvalidHandle;
464        };
465        let context = lock_unpoisoned(&context);
466        let Some(value) = c_capabilities(&context).nth(index) else {
467            return AxiolidStatus::InvalidArgument;
468        };
469        // SAFETY: null was rejected; the caller contract requires one writable descriptor.
470        unsafe { out_descriptor.write(capability(value)) };
471        AxiolidStatus::Ok
472    })
473}
474
475fn tolerance(value: AxiolidTolerance) -> Result<axiolid::core::Tolerance, AxiolidStatus> {
476    axiolid::core::Tolerance::new(value.linear, value.angular)
477        .map_err(|_| AxiolidStatus::InvalidArgument)
478}
479
480fn record_error(
481    mut context: MutexGuard<'_, Context>,
482    status: AxiolidStatus,
483    operation: AxiolidOperation,
484    tolerance: AxiolidTolerance,
485    provider: impl Into<String>,
486    message: impl Into<String>,
487) -> AxiolidStatus {
488    context.last_error = Some(ErrorRecord {
489        status,
490        operation,
491        tolerance,
492        provider: provider.into(),
493        message: message.into(),
494    });
495    status
496}
497
498fn record_application_error(
499    context: MutexGuard<'_, Context>,
500    error: axiolid::application::ApplicationError,
501) -> AxiolidStatus {
502    let operation = operation(error.context.operation);
503    let tolerance = AxiolidTolerance {
504        linear: error.context.tolerance.linear(),
505        angular: error.context.tolerance.angular(),
506    };
507    record_error(
508        context,
509        AxiolidStatus::OperationFailed,
510        operation,
511        tolerance,
512        error.context.provider.as_str(),
513        error.to_string(),
514    )
515}
516
517/// Structured portion of a context-owned error record.
518#[repr(C)]
519#[derive(Debug, Clone, Copy)]
520pub struct AxiolidErrorInfo {
521    pub status: AxiolidStatus,
522    pub operation: AxiolidOperation,
523    pub tolerance: AxiolidTolerance,
524    pub provider: [u8; 64],
525    pub message_len: usize,
526    pub provider_len: u8,
527    /// Must be zero; reserves layout space without exposing padding bytes.
528    pub reserved: [u8; 7],
529}
530
531impl Default for AxiolidErrorInfo {
532    fn default() -> Self {
533        Self {
534            status: AxiolidStatus::NoError,
535            operation: AxiolidOperation::Unknown,
536            tolerance: AxiolidTolerance {
537                linear: 0.0,
538                angular: 0.0,
539            },
540            provider: [0; 64],
541            message_len: 0,
542            provider_len: 0,
543            reserved: [0; 7],
544        }
545    }
546}
547
548mod diagnostics;
549mod mesh;
550mod operations;
551
552pub use diagnostics::*;
553pub use mesh::*;
554pub use operations::*;