coolprop_sys/lib.rs
1//! [<img alt="GitHub" src="https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github" height="22">](https://github.com/portyanikhin/rfluids)
2//! [<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="22">](https://docs.rs/coolprop-sys)
3//! [<img alt="crates.io" src="https://img.shields.io/crates/v/coolprop-sys?style=for-the-badge&logo=rust&labelColor=555555&color=fc8d62" height="22">](https://crates.io/crates/coolprop-sys)
4//! [<img alt="CI" src="https://img.shields.io/github/actions/workflow/status/portyanikhin/rfluids/ci.yml?style=for-the-badge&logo=githubactions&logoColor=ffffff&label=ci&labelColor=555555" height="22">](https://github.com/portyanikhin/rfluids/actions/workflows/ci.yml)
5//!
6//! Raw FFI bindings to [`CoolProp`](https://coolprop.org)
7//!
8//! ## Supported platforms
9//!
10//! - `Linux AArch64`
11//! - `Linux x86-64`
12//! - `macOS AArch64`
13//! - `macOS x86-64`
14//! - `Windows AArch64`
15//! - `Windows x86-64`
16//!
17//! ## MSRV
18//!
19//! `coolprop-sys` requires `rustc` 1.85.0 or later.
20//!
21//! ## How to install
22//!
23//! Add this to your `Cargo.toml`:
24//!
25//! ```toml
26//! [dependencies]
27//! coolprop-sys = "8"
28//! ```
29//!
30//! Or via command line:
31//!
32//! ```shell
33//! cargo add coolprop-sys
34//! ```
35//!
36//! 🎁 It comes with native `CoolProp` dynamic libraries for supported platforms. The library
37//! required for your platform will be automatically copied to the target directory during build.
38//!
39//! It also includes pre-generated FFI bindings, so `libclang` is not required for normal builds.
40//!
41//! ### Regenerating bindings
42//!
43//! If you need to regenerate the FFI bindings (requires `libclang`), enable the
44//! **`regen-bindings`** feature.
45//!
46//! Add this to your `Cargo.toml`:
47//!
48//! ```toml
49//! [dependencies]
50//! coolprop-sys = { version = "8", features = ["regen-bindings"] }
51//! ```
52//!
53//! Or via command line:
54//!
55//! ```shell
56//! cargo add coolprop-sys --features regen-bindings
57//! ```
58//!
59//! ## Accessing the native library
60//!
61//! Use the process-wide [`COOLPROP`] handle:
62//!
63//! ```rust
64//! use coolprop_sys::COOLPROP;
65//!
66//! let coolprop = COOLPROP.shared_access();
67//! let critical_temperature = unsafe { coolprop.Props1SI(c"Water".as_ptr(), c"Tcrit".as_ptr()) };
68//! assert!(critical_temperature.is_finite());
69//! ```
70//!
71//! - Use [`shared_access()`](CoolPropLib::shared_access) only for native operations known to
72//! support concurrent execution.
73//! - Use [`exclusive_access()`](CoolPropLib::exclusive_access) for configuration and debug changes,
74//! global error or warning handling, `REFPROP` operations, `VTPR` construction or reload, tabular
75//! backends, and operations whose concurrency guarantees are unknown. When in doubt, use
76//! exclusive access.
77//!
78//! Some native functions report failure through a sentinel value and store details in the
79//! process-global `errstring`. After such a failure with shared access, release the shared guard,
80//! then acquire exclusive access. If the caller needs error details for that operation, read and
81//! discard the stale `errstring` with
82//! [`get_global_param_string`](bindings::CoolProp::get_global_param_string) (which clears it),
83//! repeat the complete native call, and read the new `errstring` before releasing the exclusive
84//! guard. If the caller does not need error details, clear the stale `errstring` before releasing
85//! the exclusive guard; no retry is required.
86//!
87//! When an exclusive native call may set a process-global error or warning, keep the same
88//! exclusive guard from that call through retrieval of its `errstring` or `warnstring`.
89//!
90//! Do not acquire another access guard while one is already held by the same thread. For this
91//! synchronization boundary to be effective, all access to the bundled native library in a
92//! process must go through [`COOLPROP`]. Constructing [`bindings::CoolProp`] directly bypasses it
93//! and requires equivalent process-wide synchronization from the caller.
94//!
95//! #### License
96//!
97//! <sup>
98//! This project is licensed under
99//! <a href="https://github.com/portyanikhin/rfluids/blob/main/LICENSE">MIT License</a>
100//! </sup>
101
102use std::{
103 ops::Deref,
104 sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard},
105};
106
107pub mod bindings;
108
109/// `CoolProp` dynamic library absolute path.
110#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
111pub const COOLPROP_PATH: &str = coolprop_sys_linux_aarch64::COOLPROP_PATH;
112#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
113pub const COOLPROP_PATH: &str = coolprop_sys_linux_x86_64::COOLPROP_PATH;
114#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
115pub const COOLPROP_PATH: &str = coolprop_sys_macos_aarch64::COOLPROP_PATH;
116#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
117pub const COOLPROP_PATH: &str = coolprop_sys_macos_x86_64::COOLPROP_PATH;
118#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
119pub const COOLPROP_PATH: &str = coolprop_sys_windows_aarch64::COOLPROP_PATH;
120#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
121pub const COOLPROP_PATH: &str = coolprop_sys_windows_x86_64::COOLPROP_PATH;
122
123/// Process-wide synchronization boundary for the loaded `CoolProp` dynamic library.
124///
125/// Use [`CoolPropLib::shared_access`] only for native operations known to support concurrent
126/// execution. Use [`CoolPropLib::exclusive_access`] for configuration and debug changes, global
127/// error or warning handling, `REFPROP` operations, `VTPR` construction or reload, tabular
128/// backends, and operations whose concurrency guarantees are unknown. When in doubt, use
129/// exclusive access.
130///
131/// Do not acquire a second access guard while another guard is held by the same thread. Drop the
132/// current guard before changing access modes.
133///
134/// For this synchronization boundary to be effective, all access to the bundled native library in
135/// a process must go through [`COOLPROP`]. Constructing [`bindings::CoolProp`] directly bypasses
136/// this boundary and requires equivalent process-wide synchronization from the caller.
137pub struct CoolPropLib(RwLock<bindings::CoolProp>);
138
139impl CoolPropLib {
140 /// Acquires shared access to the native library.
141 ///
142 /// A shared guard does not make an arbitrary native function or backend reentrant. Use it only
143 /// for operations explicitly known to support concurrent execution, such as calculations on
144 /// independent states backed by `HEOS`, `INCOMP`, `IF97`, `SRK`, `PR`, `PCSAFT`, or an
145 /// already-constructed `VTPR` state.
146 ///
147 /// Some native functions report failure through a sentinel value and store details in the
148 /// process-global `errstring`. Do not release shared access and then treat that string as the
149 /// error from the failed call: another failure may replace it first. To retrieve attributable
150 /// error details:
151 ///
152 /// 1. Release the shared guard.
153 /// 2. Acquire exclusive access.
154 /// 3. Read and discard any stale `errstring` with
155 /// [`get_global_param_string`](bindings::CoolProp::get_global_param_string), which clears
156 /// the stored message.
157 /// 4. Repeat the complete native call.
158 /// 5. Read `errstring` with
159 /// [`get_global_param_string`](bindings::CoolProp::get_global_param_string) before releasing
160 /// the same exclusive guard.
161 ///
162 /// If error details are not needed, perform only steps 1–3; no retry is required.
163 ///
164 /// Lock poisoning is recovered transparently; access does not panic solely because a previous
165 /// guard holder panicked.
166 ///
167 /// # Examples
168 ///
169 /// ```no_run
170 /// use coolprop_sys::COOLPROP;
171 ///
172 /// let coolprop = COOLPROP.shared_access();
173 /// let critical_temperature = unsafe { coolprop.Props1SI(c"Water".as_ptr(), c"Tcrit".as_ptr()) };
174 /// assert!(critical_temperature.is_finite());
175 /// ```
176 pub fn shared_access(&self) -> SharedAccess<'_> {
177 SharedAccess(self.0.read().unwrap_or_else(|err| err.into_inner()))
178 }
179
180 /// Acquires exclusive access to the native library.
181 ///
182 /// Use this for configuration changes, pending-error retrieval, `REFPROP` calls, `VTPR` state
183 /// construction, tabular backends, and other calls that touch mutable process-global state.
184 /// When a native call may set a process-global error or warning, keep the same exclusive guard
185 /// from that call through retrieval of its `errstring` or `warnstring`.
186 ///
187 /// Lock poisoning is recovered transparently; access does not panic solely because a previous
188 /// guard holder panicked.
189 ///
190 /// # Examples
191 ///
192 /// ```no_run
193 /// use coolprop_sys::COOLPROP;
194 ///
195 /// let coolprop = COOLPROP.exclusive_access();
196 /// unsafe {
197 /// coolprop.set_debug_level(0);
198 /// }
199 /// ```
200 pub fn exclusive_access(&self) -> ExclusiveAccess<'_> {
201 ExclusiveAccess(self.0.write().unwrap_or_else(|err| err.into_inner()))
202 }
203}
204
205/// Shared access to native operations known to support concurrent execution.
206#[must_use]
207pub struct SharedAccess<'a>(RwLockReadGuard<'a, bindings::CoolProp>);
208
209impl Deref for SharedAccess<'_> {
210 type Target = bindings::CoolProp;
211
212 fn deref(&self) -> &Self::Target {
213 &self.0
214 }
215}
216
217/// Exclusive access to native `CoolProp` calls that must not overlap other calls.
218///
219/// This type intentionally does not implement [`DerefMut`](std::ops::DerefMut): exclusive access
220/// is an execution mode, not permission to replace or mutate the loaded function table.
221#[must_use]
222pub struct ExclusiveAccess<'a>(RwLockWriteGuard<'a, bindings::CoolProp>);
223
224impl Deref for ExclusiveAccess<'_> {
225 type Target = bindings::CoolProp;
226
227 fn deref(&self) -> &Self::Target {
228 &self.0
229 }
230}
231
232/// Global instance of the `CoolProp` dynamic library.
233///
234/// The library is loaded lazily. Before the handle is published, an internal probe initializes
235/// native process-global configuration. This is automatic; callers do not need to perform a
236/// special first native call.
237///
238/// # Panics
239///
240/// Panics on initialization if the `CoolProp` dynamic library cannot be loaded or its
241/// initialization probe does not produce a finite value.
242///
243/// # Safety
244///
245/// Methods exposed by [`bindings::CoolProp`] remain unsafe. Callers must uphold each function's
246/// pointer and lifetime requirements and select the access mode required by the native operation.
247/// Loading and the initialization probe occur once, but synchronization is effective only for
248/// calls made through this handle.
249///
250/// # See Also
251///
252/// - [`CoolPropLib.h` Reference](https://coolprop.org/_static/doxygen/html/_cool_prop_2_cool_prop_lib_8h.html)
253pub static COOLPROP: LazyLock<CoolPropLib> = LazyLock::new(load_coolprop);
254
255fn load_coolprop() -> CoolPropLib {
256 let coolprop = unsafe { bindings::CoolProp::new(COOLPROP_PATH) }
257 .expect("CoolProp dynamic library should load from `COOLPROP_PATH`");
258 let probe = unsafe { coolprop.Props1SI(c"Water".as_ptr(), c"Tcrit".as_ptr()) };
259 assert!(
260 probe.is_finite(),
261 "CoolProp initialization probe `Props1SI(\"Water\", \"Tcrit\")` should return a finite value"
262 );
263 CoolPropLib(RwLock::new(coolprop))
264}
265
266#[cfg(test)]
267mod tests {
268 use std::{sync::TryLockError, thread};
269
270 use static_assertions::assert_not_impl_any;
271
272 use super::*;
273
274 assert_not_impl_any!(ExclusiveAccess<'static>: std::ops::DerefMut);
275
276 fn test_lib() -> CoolPropLib {
277 LazyLock::force(&COOLPROP);
278 let coolprop = unsafe { bindings::CoolProp::new(COOLPROP_PATH) }
279 .expect("CoolProp dynamic library should load from `COOLPROP_PATH`");
280 CoolPropLib(RwLock::new(coolprop))
281 }
282
283 fn shared_access_is_available(lib: &CoolPropLib) -> bool {
284 match lib.0.try_read() {
285 Ok(_) | Err(TryLockError::Poisoned(_)) => true,
286 Err(TryLockError::WouldBlock) => false,
287 }
288 }
289
290 fn exclusive_access_is_available(lib: &CoolPropLib) -> bool {
291 match lib.0.try_write() {
292 Ok(_) | Err(TryLockError::Poisoned(_)) => true,
293 Err(TryLockError::WouldBlock) => false,
294 }
295 }
296
297 #[test]
298 fn access_types_deref_to_coolprop() {
299 // Given
300 let lib = test_lib();
301
302 // When
303 let shared = lib.shared_access();
304 let shared_target = std::ptr::from_ref::<bindings::CoolProp>(&shared);
305 drop(shared);
306 let exclusive = lib.exclusive_access();
307 let exclusive_target = std::ptr::from_ref::<bindings::CoolProp>(&exclusive);
308
309 // Then
310 assert_eq!(shared_target, exclusive_target);
311 }
312
313 #[test]
314 fn poisoned_lock_is_recovered() {
315 // Given
316 let lib = test_lib();
317
318 // When
319 let panic_result = thread::scope(|scope| {
320 scope
321 .spawn(|| {
322 let _access = lib.exclusive_access();
323 panic!("poison the test lock");
324 })
325 .join()
326 });
327 let shared = lib.shared_access();
328 let shared_level = unsafe { shared.get_debug_level() };
329 drop(shared);
330 let exclusive = lib.exclusive_access();
331 let exclusive_level = unsafe { exclusive.get_debug_level() };
332
333 // Then
334 assert!(panic_result.is_err());
335 assert!((0..=10).contains(&shared_level));
336 assert!((0..=10).contains(&exclusive_level));
337 }
338
339 #[test]
340 fn shared_access_allows_another_reader_and_blocks_a_writer() {
341 // Given
342 let lib = test_lib();
343 let _shared = lib.shared_access();
344
345 // When
346 let another_reader_is_available = shared_access_is_available(&lib);
347 let writer_is_available = exclusive_access_is_available(&lib);
348
349 // Then
350 assert!(another_reader_is_available);
351 assert!(!writer_is_available);
352 }
353
354 #[test]
355 fn exclusive_access_blocks_other_access() {
356 // Given
357 let lib = test_lib();
358 let _exclusive = lib.exclusive_access();
359
360 // When
361 let reader_is_available = shared_access_is_available(&lib);
362 let writer_is_available = exclusive_access_is_available(&lib);
363
364 // Then
365 assert!(!reader_is_available);
366 assert!(!writer_is_available);
367 }
368
369 #[test]
370 fn unlocked_lib_allows_exclusive_access() {
371 // Given
372 let lib = test_lib();
373
374 // When
375 let writer_is_available = exclusive_access_is_available(&lib);
376
377 // Then
378 assert!(writer_is_available);
379 }
380}