Skip to main content

iris_chat_core/
fips_ble_ffi.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5use flume::Sender;
6
7use crate::{CoreMsg, FfiApp, FipsBleCommand, FipsBleEvent};
8
9#[derive(uniffi::Object)]
10pub struct FfiFipsBle {
11    runtime: tokio::runtime::Runtime,
12    adapter: Arc<fips_core::transport::ble::host::HostBleAdapter>,
13    core_tx: Sender<CoreMsg>,
14    closed: AtomicBool,
15    ownership: crate::core::HostBleOwnership,
16}
17
18#[derive(Debug, Clone, uniffi::Error)]
19pub enum FipsBleBridgeError {
20    Initialization(String),
21    Teardown(String),
22}
23
24impl std::fmt::Display for FipsBleBridgeError {
25    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Self::Initialization(message) | Self::Teardown(message) => formatter.write_str(message),
28        }
29    }
30}
31
32impl std::error::Error for FipsBleBridgeError {}
33
34#[uniffi::export]
35impl FfiFipsBle {
36    #[uniffi::constructor]
37    pub fn new(app: Arc<FfiApp>) -> Result<Arc<Self>, FipsBleBridgeError> {
38        Self::connect(app.background_tx.clone(), Duration::from_secs(2))
39    }
40
41    pub fn next_command(&self, timeout_ms: u64) -> Option<FipsBleCommand> {
42        let timeout = Duration::from_millis(timeout_ms.clamp(1, 60_000));
43        self.runtime.block_on(async {
44            tokio::time::timeout(timeout, self.adapter.next_command())
45                .await
46                .ok()
47                .flatten()
48                .map(Into::into)
49        })
50    }
51
52    pub fn emit(&self, event: FipsBleEvent) -> bool {
53        self.runtime
54            .block_on(self.adapter.emit(event.into()))
55            .is_ok()
56    }
57
58    pub fn detach(&self) -> Result<(), FipsBleBridgeError> {
59        self.detach_with_timeout(Duration::from_secs(2))
60    }
61
62    /// Called off the UI thread. Completion is a storage barrier when ending.
63    pub fn background_activity(&self, generation: u64, active: bool) -> bool {
64        let (reply_tx, reply_rx) = flume::bounded(1);
65        self.core_tx
66            .send(CoreMsg::HostBleActivity {
67                ownership: self.ownership.clone(),
68                generation,
69                active,
70                reply_tx,
71            })
72            .is_ok()
73            && reply_rx.recv().unwrap_or(false)
74    }
75}
76
77impl FfiFipsBle {
78    pub(crate) fn connect(
79        core_tx: Sender<CoreMsg>,
80        timeout: Duration,
81    ) -> Result<Arc<Self>, FipsBleBridgeError> {
82        let runtime = tokio::runtime::Builder::new_multi_thread()
83            .worker_threads(1)
84            .enable_time()
85            .build()
86            .map_err(|error| {
87                FipsBleBridgeError::Initialization(format!(
88                    "Could not create the FIPS BLE runtime: {error}"
89                ))
90            })?;
91        let local_token = format!(
92            "iris-{}",
93            SystemTime::now()
94                .duration_since(UNIX_EPOCH)
95                .unwrap_or_default()
96                .as_nanos()
97        );
98        let (io, adapter) = runtime
99            .block_on(async {
100                fips_core::transport::ble::host::HostBleIo::channel("mobile", local_token, 256)
101            })
102            .map_err(|error| {
103                FipsBleBridgeError::Initialization(format!(
104                    "Could not create the FIPS BLE bridge: {error}"
105                ))
106            })?;
107        // The iOS shell owns bounded OS wake tasks. Negotiate this with the
108        // attachment: another synchronous core request here would deadlock
109        // against bind waiting for the native Listen command pump to start.
110        let attachment =
111            crate::core::HostBleAttachment::new(io).with_background(cfg!(target_os = "ios"));
112        let ownership = attachment.ownership();
113        // Construct the owner before queueing attachment. Any error below drops
114        // it, cancelling this exact attachment even if the core replies late.
115        let bridge = Arc::new(Self {
116            runtime,
117            adapter: Arc::new(adapter),
118            core_tx,
119            closed: AtomicBool::new(false),
120            ownership,
121        });
122        let (reply_tx, reply_rx) = flume::bounded(1);
123        bridge
124            .core_tx
125            .send(CoreMsg::AttachHostBle {
126                attachment,
127                reply_tx,
128            })
129            .map_err(|error| {
130                FipsBleBridgeError::Initialization(format!(
131                    "Could not request the FIPS BLE bridge attachment: {error}"
132                ))
133            })?;
134        reply_rx
135            .recv_timeout(timeout)
136            .map_err(|error| {
137                FipsBleBridgeError::Initialization(format!(
138                    "The Iris core did not attach the FIPS BLE bridge: {error}"
139                ))
140            })?
141            .map_err(FipsBleBridgeError::Initialization)?;
142        Ok(bridge)
143    }
144
145    pub(crate) fn detach_with_timeout(&self, timeout: Duration) -> Result<(), FipsBleBridgeError> {
146        if self.closed.load(Ordering::Acquire) {
147            return Ok(());
148        }
149        self.ownership.cancel();
150        let (reply_tx, reply_rx) = flume::bounded(1);
151        self.core_tx
152            .send(CoreMsg::DetachHostBle {
153                ownership: self.ownership.clone(),
154                reply_tx,
155            })
156            .map_err(|error| {
157                FipsBleBridgeError::Teardown(format!(
158                    "Could not request FIPS BLE teardown: {error}"
159                ))
160            })?;
161        reply_rx.recv_timeout(timeout).map_err(|error| {
162            FipsBleBridgeError::Teardown(format!(
163                "The Iris core has not acknowledged FIPS BLE teardown: {error}"
164            ))
165        })?;
166        self.closed.store(true, Ordering::Release);
167        Ok(())
168    }
169}
170
171impl Drop for FfiFipsBle {
172    fn drop(&mut self) {
173        if !self.closed.load(Ordering::Acquire) {
174            self.ownership.cancel();
175            let (reply_tx, _) = flume::bounded(1);
176            let _ = self.core_tx.send(CoreMsg::DetachHostBle {
177                ownership: self.ownership.clone(),
178                reply_tx,
179            });
180        }
181    }
182}
183
184impl From<fips_core::transport::ble::host::HostBleCommand> for FipsBleCommand {
185    fn from(command: fips_core::transport::ble::host::HostBleCommand) -> Self {
186        use fips_core::transport::ble::host::HostBleCommand as Host;
187        match command {
188            Host::Listen {
189                request_id,
190                preferred_psm,
191            } => Self::Listen {
192                request_id,
193                preferred_psm,
194            },
195            Host::StopListening => Self::StopListening,
196            Host::StartAdvertising {
197                request_id,
198                bootstrap,
199            } => Self::StartAdvertising {
200                request_id,
201                bootstrap,
202            },
203            Host::StopAdvertising { request_id } => Self::StopAdvertising { request_id },
204            Host::StartScanning { request_id } => Self::StartScanning { request_id },
205            Host::StopScanning => Self::StopScanning,
206            Host::Connect {
207                request_id,
208                peer_token,
209                psm,
210            } => Self::Connect {
211                request_id,
212                peer_token,
213                psm,
214            },
215            Host::Write {
216                request_id,
217                connection_id,
218                bytes,
219            } => Self::Write {
220                request_id,
221                connection_id,
222                bytes,
223            },
224            Host::Close { connection_id } => Self::Close { connection_id },
225        }
226    }
227}
228
229impl From<FipsBleEvent> for fips_core::transport::ble::host::HostBleEvent {
230    fn from(event: FipsBleEvent) -> Self {
231        use fips_core::transport::ble::host::HostBleEvent as Host;
232        match event {
233            FipsBleEvent::Listening { request_id, psm } => Host::Listening { request_id, psm },
234            FipsBleEvent::AdvertisingStarted { request_id } => {
235                Host::AdvertisingStarted { request_id }
236            }
237            FipsBleEvent::AdvertisingStopped { request_id } => {
238                Host::AdvertisingStopped { request_id }
239            }
240            FipsBleEvent::ScanningStarted { request_id } => Host::ScanningStarted { request_id },
241            FipsBleEvent::PeerDiscovered {
242                peer_token,
243                bootstrap,
244            } => Host::PeerDiscovered {
245                peer_token,
246                bootstrap,
247            },
248            FipsBleEvent::Connected {
249                request_id,
250                connection_id,
251                peer_token,
252                send_segment_mtu,
253                receive_segment_mtu,
254            } => Host::Connected {
255                request_id,
256                connection_id,
257                peer_token,
258                send_segment_mtu,
259                receive_segment_mtu,
260            },
261            FipsBleEvent::IncomingConnection {
262                connection_id,
263                peer_token,
264                send_segment_mtu,
265                receive_segment_mtu,
266            } => Host::IncomingConnection {
267                connection_id,
268                peer_token,
269                send_segment_mtu,
270                receive_segment_mtu,
271            },
272            FipsBleEvent::BytesReceived {
273                connection_id,
274                bytes,
275            } => Host::BytesReceived {
276                connection_id,
277                bytes,
278            },
279            FipsBleEvent::WriteCompleted { request_id } => Host::WriteCompleted { request_id },
280            FipsBleEvent::Disconnected {
281                connection_id,
282                reason,
283            } => Host::Disconnected {
284                connection_id,
285                reason,
286            },
287            FipsBleEvent::Failed {
288                request_id,
289                message,
290            } => Host::Failed {
291                request_id,
292                message,
293            },
294        }
295    }
296}