Skip to main content

cbf_chrome_sys/
bridge.rs

1//! Runtime-loaded bridge access for `cbf_bridge`.
2
3use 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/// Options for overriding how the bridge library is located at runtime.
20#[derive(Debug, Clone, Default)]
21pub struct BridgeLoadOptions {
22    /// An explicit full path to the bridge library file.
23    pub explicit_library_path: Option<PathBuf>,
24    /// An explicit directory that contains the bridge library file.
25    pub explicit_library_dir: Option<PathBuf>,
26}
27
28/// A process-wide loaded bridge API wrapper.
29pub struct BridgeLibrary {
30    library_path: PathBuf,
31    bindings: CbfBridge,
32}
33
34#[derive(Debug, thiserror::Error, Clone)]
35/// Errors that can occur while locating or opening the bridge library.
36pub enum BridgeLoadError {
37    /// No supported runtime search location contained the bridge library.
38    #[error("failed to resolve cbf bridge library path")]
39    PathNotFound,
40    /// Loading the discovered bridge library file or one of its required symbols failed.
41    #[error("failed to load cbf bridge library from {path}: {source}")]
42    LoadLibrary {
43        /// The path of the library that failed to load.
44        path: PathBuf,
45        #[source]
46        /// The underlying dynamic loader error.
47        source: ArcLibloadingError,
48    },
49}
50
51/// A cloneable wrapper around `libloading::Error`.
52#[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    /// Return options that load the bridge from an explicit file path.
71    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    /// Return options that load the bridge from an explicit directory.
77    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    /// Load the bridge API using the provided search options.
85    ///
86    /// The library file path is resolved by [`resolve_bridge_library_path`].
87    /// See that function for the runtime search order and fallback behavior.
88    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    /// Return the resolved filesystem path of the loaded bridge library.
104    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
125/// Return the process-wide bridge API instance, loading it on first use.
126pub 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
138/// Resolve the bridge library path from explicit options and known runtime locations.
139///
140/// Path resolution proceeds in this order:
141///
142/// 1. If [`BridgeLoadOptions::explicit_library_path`] is set and points to an
143///    existing file, return it as-is.
144/// 2. If [`BridgeLoadOptions::explicit_library_dir`] is set, append the
145///    platform-specific library file name and return that path when the file
146///    exists.
147/// 3. If the `CBF_BRIDGE_LIB_DIR` environment variable is set, append the same
148///    platform-specific file name and return that path when the file exists.
149/// 4. Fall back to locations derived from the current executable:
150///    - a sibling file next to the executable on all platforms
151///    - `Contents/Frameworks/<bridge-lib>` inside a macOS app bundle
152///
153/// The first existing file wins. If none of these locations contain the bridge
154/// library, this function returns [`BridgeLoadError::PathNotFound`].
155pub 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}