Skip to main content

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