1use std::{
4 env,
5 ops::Deref,
6 path::{Path, PathBuf},
7 sync::OnceLock,
8};
9
10use crate::ffi::CbfBridge;
11
12#[cfg(target_os = "macos")]
13const BRIDGE_LIB_FILE_NAME: &str = "libcbf_bridge.dylib";
14#[cfg(target_os = "linux")]
15const BRIDGE_LIB_FILE_NAME: &str = "libcbf_bridge.so";
16#[cfg(target_os = "windows")]
17const BRIDGE_LIB_FILE_NAME: &str = "cbf_bridge.dll";
18
19#[derive(Debug, Clone, Default)]
21pub struct BridgeLoadOptions {
22 pub explicit_library_path: Option<PathBuf>,
24 pub explicit_library_dir: Option<PathBuf>,
26}
27
28pub struct BridgeLibrary {
30 library_path: PathBuf,
31 bindings: CbfBridge,
32}
33
34#[derive(Debug, thiserror::Error, Clone)]
35pub enum BridgeLoadError {
37 #[error("failed to resolve cbf bridge library path")]
39 PathNotFound,
40 #[error("failed to load cbf bridge library from {path}: {source}")]
42 LoadLibrary {
43 path: PathBuf,
45 #[source]
46 source: ArcLibloadingError,
48 },
49}
50
51#[derive(Debug, Clone)]
53pub struct ArcLibloadingError(std::sync::Arc<libloading::Error>);
54
55impl From<libloading::Error> for ArcLibloadingError {
56 fn from(value: libloading::Error) -> Self {
57 Self(std::sync::Arc::new(value))
58 }
59}
60
61impl std::fmt::Display for ArcLibloadingError {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 self.0.fmt(f)
64 }
65}
66
67impl std::error::Error for ArcLibloadingError {}
68
69impl BridgeLoadOptions {
70 pub fn with_explicit_library_path(mut self, path: impl Into<PathBuf>) -> Self {
72 self.explicit_library_path = Some(path.into());
73 self
74 }
75
76 pub fn with_explicit_library_dir(mut self, path: impl Into<PathBuf>) -> Self {
78 self.explicit_library_dir = Some(path.into());
79 self
80 }
81}
82
83impl BridgeLibrary {
84 pub fn load(options: &BridgeLoadOptions) -> Result<Self, BridgeLoadError> {
89 let library_path = resolve_bridge_library_path(options)?;
90 let bindings = unsafe { CbfBridge::new(&library_path) }.map_err(|source| {
91 BridgeLoadError::LoadLibrary {
92 path: library_path.clone(),
93 source: source.into(),
94 }
95 })?;
96
97 Ok(Self {
98 library_path,
99 bindings,
100 })
101 }
102
103 pub fn library_path(&self) -> &Path {
105 &self.library_path
106 }
107}
108
109impl std::fmt::Debug for BridgeLibrary {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct("BridgeApi")
112 .field("library_path", &self.library_path)
113 .finish_non_exhaustive()
114 }
115}
116
117impl Deref for BridgeLibrary {
118 type Target = CbfBridge;
119
120 fn deref(&self) -> &Self::Target {
121 &self.bindings
122 }
123}
124
125pub fn bridge() -> Result<&'static BridgeLibrary, BridgeLoadError> {
127 static BRIDGE: OnceLock<BridgeLibrary> = OnceLock::new();
128
129 if let Some(bridge) = BRIDGE.get() {
130 return Ok(bridge);
131 }
132
133 let loaded = BridgeLibrary::load(&BridgeLoadOptions::default())?;
134 BRIDGE.set(loaded).ok();
135 Ok(BRIDGE.get().expect("bridge api set"))
136}
137
138pub fn resolve_bridge_library_path(
156 options: &BridgeLoadOptions,
157) -> Result<PathBuf, BridgeLoadError> {
158 if let Some(path) = options
159 .explicit_library_path
160 .as_ref()
161 .filter(|path| path.is_file())
162 {
163 return Ok(path.clone());
164 }
165
166 if let Some(dir) = options.explicit_library_dir.as_ref() {
167 let path = dir.join(BRIDGE_LIB_FILE_NAME);
168 if path.is_file() {
169 return Ok(path);
170 }
171 }
172
173 if let Some(dir) = env::var_os("CBF_BRIDGE_LIB_DIR") {
174 let path = PathBuf::from(dir).join(BRIDGE_LIB_FILE_NAME);
175 if path.is_file() {
176 return Ok(path);
177 }
178 }
179
180 if let Some(path) = bridge_path_from_current_executable() {
181 return Ok(path);
182 }
183
184 Err(BridgeLoadError::PathNotFound)
185}
186
187fn bridge_path_from_current_executable() -> Option<PathBuf> {
188 let current_exe = env::current_exe().ok()?;
189 let exe_dir = current_exe.parent()?;
190
191 let sibling = exe_dir.join(BRIDGE_LIB_FILE_NAME);
192 if sibling.is_file() {
193 return Some(sibling);
194 }
195
196 #[cfg(target_os = "macos")]
197 {
198 let contents_dir = exe_dir.parent()?;
199 if contents_dir.file_name()?.to_str()? != "Contents" {
200 return None;
201 }
202
203 let frameworks = contents_dir.join("Frameworks").join(BRIDGE_LIB_FILE_NAME);
204 return frameworks.is_file().then_some(frameworks);
205 }
206
207 #[allow(unreachable_code)]
208 None
209}