Skip to main content

micro_wakeword/
runtime.rs

1use std::fs;
2use std::io::{Read, Write};
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use sha2::{Digest, Sha256};
8use tflite_c::TfLiteLibrary;
9
10use crate::{Error, Result};
11
12/// Selects the TensorFlow Lite C runtime used for inference.
13#[derive(Clone, Debug, Default)]
14pub enum Runtime {
15    /// Check explicit environment overrides, then use the bundled runtime.
16    #[default]
17    Auto,
18    /// Load a specific shared library.
19    Path(PathBuf),
20    /// Search standard system locations supported by `tflite-c-rs`.
21    System,
22}
23
24impl Runtime {
25    /// Selects a TensorFlow Lite C shared library at an explicit path.
26    pub fn from_path(path: impl Into<PathBuf>) -> Self {
27        Self::Path(path.into())
28    }
29
30    pub(crate) fn load(&self) -> Result<Arc<TfLiteLibrary>> {
31        let explicit = match self {
32            Self::Path(path) => Some(path.clone()),
33            Self::Auto => std::env::var_os("MICRO_WAKEWORD_TFLITE_LIB")
34                .or_else(|| std::env::var_os("TFLITE_C_LIB"))
35                .map(PathBuf::from),
36            Self::System => None,
37        };
38        if let Some(path) = explicit {
39            return TfLiteLibrary::load_from_path(&path).map_err(Error::from);
40        }
41        match self {
42            Self::Auto => {
43                let path = bundled_runtime_path()?;
44                TfLiteLibrary::load_from_path(path).map_err(Error::from)
45            }
46            Self::System => TfLiteLibrary::load_default().map_err(Error::from),
47            Self::Path(_) => unreachable!(),
48        }
49    }
50
51    /// Returns the explicit library path for [`Runtime::Path`].
52    pub fn path(&self) -> Option<&Path> {
53        match self {
54            Self::Path(path) => Some(path),
55            Self::Auto | Self::System => None,
56        }
57    }
58}
59
60struct BundledRuntime {
61    bytes: &'static [u8],
62    sha256: &'static str,
63    version: &'static str,
64    target: &'static str,
65    file_name: &'static str,
66}
67
68#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
69fn bundled_runtime() -> BundledRuntime {
70    BundledRuntime {
71        bytes: include_bytes!("../runtime/windows-x86_64/tensorflowlite_c-2.17.1.dll"),
72        sha256: "882e6d8f9866ff84f23d4b964c145b7f0f0a8907fa830dcd8c499e7c46bf3365",
73        version: "2.17.1",
74        target: "x86_64-pc-windows",
75        file_name: "tensorflowlite_c.dll",
76    }
77}
78
79#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
80fn bundled_runtime() -> BundledRuntime {
81    BundledRuntime {
82        bytes: include_bytes!("../runtime/linux-x86_64/libtensorflowlite_c-2.17.1.so"),
83        sha256: "25465edb5cd7aadd00249d4d28f1d922ce5cc90195ad4403458111dd63493bae",
84        version: "2.17.1",
85        target: "x86_64-unknown-linux-gnu",
86        file_name: "libtensorflowlite_c.so",
87    }
88}
89
90#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
91fn bundled_runtime() -> BundledRuntime {
92    BundledRuntime {
93        bytes: include_bytes!("../runtime/linux-aarch64/libtensorflowlite_c-2.17.1.so"),
94        sha256: "12062437bfde367b1be592ebc4a9fe64b8453f90b3b7dc21fade002c38e1ac1a",
95        version: "2.17.1",
96        target: "aarch64-unknown-linux-gnu",
97        file_name: "libtensorflowlite_c.so",
98    }
99}
100
101#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
102fn bundled_runtime() -> BundledRuntime {
103    BundledRuntime {
104        bytes: include_bytes!("../runtime/macos-aarch64/libtensorflowlite_c-2.17.1.dylib"),
105        sha256: "e77597b3710e43f58f1c37a9c8979a82901ed9f3a11de71e95f2154a4c9ce6d7",
106        version: "2.17.1",
107        target: "aarch64-apple-darwin",
108        file_name: "libtensorflowlite_c.dylib",
109    }
110}
111
112#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
113fn bundled_runtime() -> BundledRuntime {
114    BundledRuntime {
115        bytes: include_bytes!("../runtime/macos-x86_64/libtensorflowlite_c-2.17.0.dylib"),
116        sha256: "6cf562771e6cb8d7856a86f859d004f0ef72861b6cafa88afbfad5d5f40261fc",
117        version: "2.17.0",
118        target: "x86_64-apple-darwin",
119        file_name: "libtensorflowlite_c.dylib",
120    }
121}
122
123#[cfg(any(
124    all(target_os = "windows", target_arch = "x86_64"),
125    all(
126        target_os = "linux",
127        any(target_arch = "x86_64", target_arch = "aarch64")
128    ),
129    all(
130        target_os = "macos",
131        any(target_arch = "x86_64", target_arch = "aarch64")
132    ),
133))]
134fn bundled_runtime_path() -> Result<PathBuf> {
135    let runtime = bundled_runtime();
136    let directory = runtime_cache_root()
137        .join("micro-wakeword")
138        .join(format!("runtime-{}-{}", runtime.version, runtime.target));
139    let destination = directory.join(runtime.file_name);
140    if has_expected_checksum(&destination, runtime.sha256)? {
141        return Ok(destination);
142    }
143
144    fs::create_dir_all(&directory).map_err(|source| Error::Io {
145        path: directory.clone(),
146        source,
147    })?;
148
149    static TEMPORARY_ID: AtomicU64 = AtomicU64::new(0);
150    let id = TEMPORARY_ID.fetch_add(1, Ordering::Relaxed);
151    let temporary = directory.join(format!(
152        ".{}.{}.{id}.tmp",
153        runtime.file_name,
154        std::process::id()
155    ));
156    let mut file = fs::OpenOptions::new()
157        .write(true)
158        .create_new(true)
159        .open(&temporary)
160        .map_err(|source| Error::Io {
161            path: temporary.clone(),
162            source,
163        })?;
164    if let Err(source) = file.write_all(runtime.bytes).and_then(|_| file.sync_all()) {
165        let _ = fs::remove_file(&temporary);
166        return Err(Error::Io {
167            path: temporary,
168            source,
169        });
170    }
171    drop(file);
172
173    if !has_expected_checksum(&temporary, runtime.sha256)? {
174        let _ = fs::remove_file(&temporary);
175        return Err(Error::InvalidConfig(
176            "embedded TensorFlow Lite runtime checksum mismatch".into(),
177        ));
178    }
179
180    // Another process may have completed extraction while this process wrote
181    // its temporary file. Reuse that verified file instead of replacing a
182    // library which may already be mapped by the other process.
183    if has_expected_checksum(&destination, runtime.sha256)? {
184        let _ = fs::remove_file(&temporary);
185        return Ok(destination);
186    }
187    if destination.exists() {
188        fs::remove_file(&destination).map_err(|source| Error::Io {
189            path: destination.clone(),
190            source,
191        })?;
192    }
193    if let Err(source) = fs::rename(&temporary, &destination) {
194        if has_expected_checksum(&destination, runtime.sha256)? {
195            let _ = fs::remove_file(&temporary);
196        } else {
197            let _ = fs::remove_file(&temporary);
198            return Err(Error::Io {
199                path: destination,
200                source,
201            });
202        }
203    }
204    Ok(destination)
205}
206
207#[cfg(any(
208    all(target_os = "windows", target_arch = "x86_64"),
209    all(
210        target_os = "linux",
211        any(target_arch = "x86_64", target_arch = "aarch64")
212    ),
213    all(
214        target_os = "macos",
215        any(target_arch = "x86_64", target_arch = "aarch64")
216    ),
217))]
218fn runtime_cache_root() -> PathBuf {
219    #[cfg(target_os = "windows")]
220    if let Some(path) = std::env::var_os("LOCALAPPDATA") {
221        return PathBuf::from(path);
222    }
223
224    #[cfg(target_os = "linux")]
225    if let Some(path) = std::env::var_os("XDG_CACHE_HOME") {
226        return PathBuf::from(path);
227    }
228
229    #[cfg(target_os = "macos")]
230    if let Some(path) = std::env::var_os("HOME") {
231        return PathBuf::from(path).join("Library").join("Caches");
232    }
233
234    #[cfg(target_os = "linux")]
235    if let Some(path) = std::env::var_os("HOME") {
236        return PathBuf::from(path).join(".cache");
237    }
238
239    std::env::temp_dir()
240}
241
242#[cfg(any(
243    all(target_os = "windows", target_arch = "x86_64"),
244    all(
245        target_os = "linux",
246        any(target_arch = "x86_64", target_arch = "aarch64")
247    ),
248    all(
249        target_os = "macos",
250        any(target_arch = "x86_64", target_arch = "aarch64")
251    ),
252))]
253fn has_expected_checksum(path: &Path, expected: &str) -> Result<bool> {
254    if !path.exists() {
255        return Ok(false);
256    }
257    Ok(file_sha256(path)? == expected)
258}
259
260#[cfg(any(
261    all(target_os = "windows", target_arch = "x86_64"),
262    all(
263        target_os = "linux",
264        any(target_arch = "x86_64", target_arch = "aarch64")
265    ),
266    all(
267        target_os = "macos",
268        any(target_arch = "x86_64", target_arch = "aarch64")
269    ),
270))]
271fn file_sha256(path: &Path) -> Result<String> {
272    let file = fs::File::open(path).map_err(|source| Error::Io {
273        path: path.to_owned(),
274        source,
275    })?;
276    let mut reader = std::io::BufReader::new(file);
277    let mut digest = Sha256::new();
278    let mut buffer = [0_u8; 64 * 1024];
279    loop {
280        let read = reader.read(&mut buffer).map_err(|source| Error::Io {
281            path: path.to_owned(),
282            source,
283        })?;
284        if read == 0 {
285            break;
286        }
287        digest.update(&buffer[..read]);
288    }
289    Ok(format!("{:x}", digest.finalize()))
290}
291
292#[cfg(not(any(
293    all(target_os = "windows", target_arch = "x86_64"),
294    all(
295        target_os = "linux",
296        any(target_arch = "x86_64", target_arch = "aarch64")
297    ),
298    all(
299        target_os = "macos",
300        any(target_arch = "x86_64", target_arch = "aarch64")
301    ),
302)))]
303fn bundled_runtime_path() -> Result<PathBuf> {
304    Err(Error::UnsupportedPlatform(
305        "no bundled TensorFlow Lite runtime for this target; use Runtime::Path or Runtime::System"
306            .into(),
307    ))
308}
309
310#[cfg(test)]
311mod tests {
312    #[cfg(any(
313        all(target_os = "windows", target_arch = "x86_64"),
314        all(
315            target_os = "linux",
316            any(target_arch = "x86_64", target_arch = "aarch64")
317        ),
318        all(
319            target_os = "macos",
320            any(target_arch = "x86_64", target_arch = "aarch64")
321        ),
322    ))]
323    #[test]
324    fn bundled_runtime_loads_all_required_symbols() {
325        let path = super::bundled_runtime_path().unwrap();
326        tflite_c::TfLiteLibrary::load_from_path(path).unwrap();
327    }
328}