qtty_ffi/
lib.rs

1//! C-compatible FFI bindings for `qtty` physical quantities and unit conversions.
2//!
3//! `qtty-ffi` provides a stable C ABI for `qtty`, enabling interoperability with C/C++ code
4//! and other languages with C FFI support. It also provides helper types and macros for
5//! downstream Rust crates that need to expose their own FFI APIs using `qtty` quantities.
6//!
7//! # Features
8//!
9//! - **ABI-stable types**: `#[repr(C)]` and `#[repr(u32)]` types safe for FFI
10//! - **Unit registry**: Mapping between FFI unit IDs and conversion factors
11//! - **C API**: `extern "C"` functions for quantity construction and conversion
12//! - **Rust helpers**: Macros and trait implementations for downstream integration
13//!
14//! # Quick Start (C/C++)
15//!
16//! Include the generated header and link against the library:
17//!
18//! ```c
19//! #include "qtty_ffi.h"
20//!
21//! // Create a quantity
22//! QttyQuantity meters;
23//! qtty_quantity_make(1000.0, UnitId_Meter, &meters);
24//!
25//! // Convert to kilometers
26//! QttyQuantity kilometers;
27//! int32_t status = qtty_quantity_convert(meters, UnitId_Kilometer, &kilometers);
28//! if (status == QTTY_OK) {
29//!     // kilometers.value == 1.0
30//! }
31//! ```
32//!
33//! # Quick Start (Rust)
34//!
35//! Use the helper traits and macros for seamless conversion:
36//!
37//! ```rust
38//! use qtty::length::{Meters, Kilometers};
39//! use qtty_ffi::{QttyQuantity, UnitId};
40//!
41//! // Convert Rust type to FFI
42//! let meters = Meters::new(1000.0);
43//! let ffi_qty: QttyQuantity = meters.into();
44//!
45//! // Convert FFI back to Rust type (with automatic unit conversion)
46//! let km: Kilometers = ffi_qty.try_into().unwrap();
47//! assert!((km.value() - 1.0).abs() < 1e-12);
48//! ```
49//!
50//! # ABI Stability
51//!
52//! The following are part of the ABI contract and will never change:
53//!
54//! - [`UnitId`] discriminant values (existing variants)
55//! - [`DimensionId`] discriminant values (existing variants)
56//! - [`QttyQuantity`] memory layout
57//! - Status code values ([`QTTY_OK`], [`QTTY_ERR_UNKNOWN_UNIT`], etc.)
58//! - Function signatures of exported `extern "C"` functions
59//!
60//! New variants may be added to enums (with new discriminant values), and new functions
61//! may be added, but existing items will remain stable.
62//!
63//! # Supported Units (v1)
64//!
65//! ## Length
66//! - [`UnitId::Meter`] - SI base unit
67//! - [`UnitId::Kilometer`] - 1000 meters
68//!
69//! ## Time
70//! - [`UnitId::Second`] - SI base unit
71//! - [`UnitId::Minute`] - 60 seconds
72//! - [`UnitId::Hour`] - 3600 seconds
73//! - [`UnitId::Day`] - 86400 seconds
74//!
75//! ## Angle
76//! - [`UnitId::Radian`] - SI unit
77//! - [`UnitId::Degree`] - π/180 radians
78//!
79//! # Error Handling
80//!
81//! All FFI functions return status codes:
82//!
83//! - [`QTTY_OK`] (0): Success
84//! - [`QTTY_ERR_UNKNOWN_UNIT`] (-1): Invalid unit ID
85//! - [`QTTY_ERR_INCOMPATIBLE_DIM`] (-2): Dimension mismatch
86//! - [`QTTY_ERR_NULL_OUT`] (-3): Null output pointer
87//! - [`QTTY_ERR_INVALID_VALUE`] (-4): Invalid value (reserved)
88//!
89//! # Thread Safety
90//!
91//! All functions are thread-safe. The library contains no global mutable state.
92
93#![deny(missing_docs)]
94// PyO3 generated code contains unsafe operations, so we can't enforce this when pyo3 feature is enabled
95#![cfg_attr(not(feature = "pyo3"), deny(unsafe_op_in_unsafe_fn))]
96
97// Core modules
98mod ffi;
99pub mod helpers;
100#[macro_use]
101pub mod macros;
102pub mod registry;
103mod types;
104
105// Re-export FFI functions
106pub use ffi::{
107    qtty_ffi_version, qtty_quantity_convert, qtty_quantity_convert_value, qtty_quantity_make,
108    qtty_unit_dimension, qtty_unit_is_valid, qtty_unit_name, qtty_units_compatible,
109};
110
111// Re-export types
112pub use types::{
113    DimensionId, QttyDerivedQuantity, QttyQuantity, UnitId, QTTY_ERR_INCOMPATIBLE_DIM,
114    QTTY_ERR_INVALID_VALUE, QTTY_ERR_NULL_OUT, QTTY_ERR_UNKNOWN_UNIT, QTTY_OK,
115};
116
117// The impl_unit_ffi! macro is automatically exported at crate root by #[macro_export]
118
119// Re-export helper functions
120pub use helpers::{
121    days_into_ffi, degrees_into_ffi, hours_into_ffi, kilometers_into_ffi, meters_into_ffi,
122    minutes_into_ffi, radians_into_ffi, seconds_into_ffi, try_into_days, try_into_degrees,
123    try_into_hours, try_into_kilometers, try_into_meters, try_into_minutes, try_into_radians,
124    try_into_seconds,
125};
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use core::mem::{align_of, size_of};
131
132    /// Test that QttyQuantity has the expected size and alignment for FFI.
133    #[test]
134    fn test_qtty_quantity_layout() {
135        // QttyQuantity should be:
136        // - f64 (8 bytes) + UnitId (4 bytes) + padding (4 bytes) = 16 bytes
137        // - Aligned to 8 bytes (alignment of f64)
138        assert_eq!(size_of::<QttyQuantity>(), 16);
139        assert_eq!(align_of::<QttyQuantity>(), 8);
140    }
141
142    /// Test that UnitId has the expected size.
143    #[test]
144    fn test_unit_id_layout() {
145        assert_eq!(size_of::<UnitId>(), 4);
146        assert_eq!(align_of::<UnitId>(), 4);
147    }
148
149    /// Test that DimensionId has the expected size.
150    #[test]
151    fn test_dimension_id_layout() {
152        assert_eq!(size_of::<DimensionId>(), 4);
153        assert_eq!(align_of::<DimensionId>(), 4);
154    }
155
156    /// Test known conversion: 1000 meters → 1 kilometer
157    #[test]
158    fn test_known_conversion_meters_to_kilometers() {
159        let mut out = QttyQuantity::default();
160        let src = QttyQuantity::new(1000.0, UnitId::Meter);
161
162        let status = unsafe { qtty_quantity_convert(src, UnitId::Kilometer, &mut out) };
163
164        assert_eq!(status, QTTY_OK);
165        assert!((out.value - 1.0).abs() < 1e-12);
166        assert_eq!(out.unit, UnitId::Kilometer);
167    }
168
169    /// Test known conversion: 3600 seconds → 1 hour
170    #[test]
171    fn test_known_conversion_seconds_to_hours() {
172        let mut out = QttyQuantity::default();
173        let src = QttyQuantity::new(3600.0, UnitId::Second);
174
175        let status = unsafe { qtty_quantity_convert(src, UnitId::Hour, &mut out) };
176
177        assert_eq!(status, QTTY_OK);
178        assert!((out.value - 1.0).abs() < 1e-12);
179        assert_eq!(out.unit, UnitId::Hour);
180    }
181
182    /// Test known conversion: 180 degrees → π radians
183    #[test]
184    fn test_known_conversion_degrees_to_radians() {
185        use core::f64::consts::PI;
186
187        let mut out = QttyQuantity::default();
188        let src = QttyQuantity::new(180.0, UnitId::Degree);
189
190        let status = unsafe { qtty_quantity_convert(src, UnitId::Radian, &mut out) };
191
192        assert_eq!(status, QTTY_OK);
193        assert!((out.value - PI).abs() < 1e-12);
194        assert_eq!(out.unit, UnitId::Radian);
195    }
196
197    /// Test incompatible conversion: meters → seconds fails
198    #[test]
199    fn test_incompatible_conversion_fails() {
200        let mut out = QttyQuantity::default();
201        let src = QttyQuantity::new(100.0, UnitId::Meter);
202
203        let status = unsafe { qtty_quantity_convert(src, UnitId::Second, &mut out) };
204
205        assert_eq!(status, QTTY_ERR_INCOMPATIBLE_DIM);
206    }
207
208    /// Test null output pointer handling
209    #[test]
210    fn test_null_out_pointer() {
211        let src = QttyQuantity::new(100.0, UnitId::Meter);
212
213        let status =
214            unsafe { qtty_quantity_convert(src, UnitId::Kilometer, core::ptr::null_mut()) };
215
216        assert_eq!(status, QTTY_ERR_NULL_OUT);
217    }
218}