empyrean_sys/lib.rs
1//! Raw FFI bindings to `libempyrean`, the C shared library for empyrean's
2//! astrodynamics engine.
3//!
4//! Prefer the safe wrapper crate [`empyrean`](https://docs.rs/empyrean)
5//! unless you need direct access to the C ABI.
6//!
7//! # How `libempyrean` is loaded
8//!
9//! The library is opened at run time with [`libloading`] — there is **no**
10//! link-time native dependency and no `install_name_tool` / `patchelf` / rpath /
11//! `LD_LIBRARY_PATH` setup. Its path is resolved on first use, in order:
12//!
13//! 1. `EMPYREAN_LIB` — an explicit path to the library, passed straight to
14//! `dlopen` (override / offline use / a locally built engine). A bare name
15//! defers to the OS loader search; pass a full path for an exact file.
16//! 2. A `libempyrean.{dylib,so}` sitting next to the **currently loaded module**
17//! — the `.so`/`.dylib`/executable that statically links this crate — located
18//! via `dladdr`. This makes prebuilt, relocatable artifacts self-contained:
19//! a Python wheel can bundle the engine beside its extension and it is found
20//! with no build-machine path baked in.
21//! 3. The absolute path recorded at build time (`LIB_PATH`): a sibling
22//! `../target/release` build, an `EMPYREAN_LIB_DIR` override, or a
23//! version-matched, checksum-pinned prebuilt downloaded (in pure Rust — no
24//! `curl`/`tar`) into `~/.cache/empyrean`. This covers `cargo add empyrean`,
25//! where the build script runs on the consumer's own machine.
26//!
27//! The bindings are pre-generated and committed, so building needs no C header
28//! and no `libclang` / `bindgen`. Callers use the free `empyrean_*` functions
29//! exactly as with a statically linked library; each delegates to the loaded
30//! [`EmpyreanLib`].
31#![allow(non_upper_case_globals)]
32#![allow(non_camel_case_types)]
33#![allow(non_snake_case)]
34#![allow(dead_code)]
35// The generated dynamic-loading methods call the loaded fn pointers directly in
36// their (unsafe) bodies; this is generated FFI, so allow the 2024 granularity lint.
37#![allow(unsafe_op_in_unsafe_fn)]
38// The generated bindings and the free-function shims are `pub unsafe fn`s without
39// per-function `# Safety` sections, and mirror the C ABI's wide argument lists;
40// they are generated FFI, not hand-authored API.
41#![allow(clippy::missing_safety_doc)]
42#![allow(clippy::too_many_arguments)]
43
44use std::path::{Path, PathBuf};
45use std::sync::OnceLock;
46
47// Dynamic-loading bindings: `struct EmpyreanLib` + per-function methods + the
48// shared type/const definitions.
49include!("bindings.rs");
50
51// Absolute path to libempyrean recorded by the build script — resolution
52// fallback #3 (see the module docs).
53include!(concat!(env!("OUT_DIR"), "/lib_path.rs"));
54
55static LIB: OnceLock<EmpyreanLib> = OnceLock::new();
56
57/// Platform file name of the engine library.
58const LIB_FILENAME: &str = if cfg!(target_os = "macos") {
59 "libempyrean.dylib"
60} else if cfg!(target_os = "windows") {
61 "empyrean.dll"
62} else {
63 "libempyrean.so"
64};
65
66// A data symbol whose address lands in *this* module, so `dladdr` reports the
67// shared object / executable that statically links empyrean-sys.
68#[cfg(unix)]
69static SELF_MARKER: u8 = 0;
70
71/// Directory of the currently loaded module — the `.so`/`.dylib`/executable that
72/// links this crate — via `dladdr`. `None` if it cannot be determined.
73#[cfg(unix)]
74fn self_module_dir() -> Option<PathBuf> {
75 use std::ffi::CStr;
76 use std::os::raw::c_void;
77
78 let mut info: libc::Dl_info = unsafe { std::mem::zeroed() };
79 let addr = &SELF_MARKER as *const u8 as *const c_void;
80 // SAFETY: `info` is a valid out-pointer; `dli_fname` is a loader-owned C
81 // string we only dereference when dladdr reports success (non-zero).
82 if unsafe { libc::dladdr(addr, &mut info) } == 0 || info.dli_fname.is_null() {
83 return None;
84 }
85 let cstr = unsafe { CStr::from_ptr(info.dli_fname) };
86 let dir = Path::new(cstr.to_str().ok()?).parent()?;
87 // Only trust an ABSOLUTE module directory. `dladdr` may report a relative or
88 // bare path (e.g. glibc echoes a relative `argv[0]` for the main executable),
89 // whose `parent()` can be `""`; joining the library name onto that would
90 // resolve against the current working directory and let a cwd-planted
91 // `libempyrean` load ahead of the checksum-pinned build-time path. Treat that
92 // as "cannot determine" so resolution falls through to the absolute LIB_PATH.
93 dir.is_absolute().then(|| dir.to_path_buf())
94}
95
96#[cfg(not(unix))]
97fn self_module_dir() -> Option<PathBuf> {
98 None
99}
100
101/// Resolve the engine library path at run time (see the module docs for the
102/// full resolution order).
103fn resolve_lib_path() -> PathBuf {
104 // 1. Explicit override.
105 if let Some(p) = std::env::var_os("EMPYREAN_LIB") {
106 return PathBuf::from(p);
107 }
108 // 2. Bundled next to the currently loaded module (relocatable artifacts).
109 if let Some(dir) = self_module_dir() {
110 let candidate = dir.join(LIB_FILENAME);
111 if candidate.exists() {
112 return candidate;
113 }
114 }
115 // 3. The absolute path recorded at build time.
116 PathBuf::from(LIB_PATH)
117}
118
119/// The loaded `libempyrean`, opened lazily on first use.
120///
121/// Panics if the library cannot be opened. The path is resolved from the host
122/// environment and the build, so a failure here means a broken or incomplete
123/// install (e.g. a prebuilt artifact missing its bundled engine) — surfaced
124/// loudly rather than papered over.
125pub fn lib() -> &'static EmpyreanLib {
126 LIB.get_or_init(|| {
127 let path = resolve_lib_path();
128 // SAFETY: opening a shared library by the path resolved above.
129 unsafe {
130 EmpyreanLib::new(&path)
131 .unwrap_or_else(|e| panic!("failed to load libempyrean from {path:?}: {e}"))
132 }
133 })
134}
135
136mod shims;
137pub use shims::*;