transcribe_cpp/backend.rs
1//! Backend module loading and compute-device discovery.
2//!
3//! In a static (or plain `shared`) build the compiled-in backends are already
4//! registered and [`init_backends`] is a harmless no-op. In a `dynamic-backends`
5//! build the compute backends are loadable modules; a host points the library at
6//! the provider directory ONCE, before the first model load — use
7//! [`init_backends_default`] when the modules sit next to libtranscribe, or
8//! [`init_backends`] with an explicit bundled provider directory. See the C
9//! header's backend-module section for the degradation contract.
10
11use std::ffi::CString;
12use std::path::Path;
13
14use transcribe_cpp_sys as sys;
15
16use crate::error::{check, Result};
17use crate::result::owned_str;
18use crate::types::Backend;
19
20/// One registered compute device.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Device {
23 /// ggml device name, e.g. "Metal".
24 pub name: String,
25 /// Human-readable description, e.g. "Apple M4 Max".
26 pub description: String,
27 /// Classified kind: "cpu", "accel", "metal", "vulkan", "cuda", "sycl",
28 /// "gpu", or "unknown".
29 pub kind: String,
30}
31
32/// Load backend modules from `dir` (dynamic builds) and register their
33/// devices. A no-op in static builds. Call once, before the first model load.
34///
35/// Errors with [`crate::Error::Backend`] if, after loading, the process has no
36/// registered compute device (a dynamic build pointed at a directory with no
37/// usable modules). This call is idempotent per directory and NOT retryable in
38/// the same process.
39pub fn init_backends(dir: impl AsRef<Path>) -> Result<()> {
40 let dir = dir.as_ref();
41 // Pass the path bytes through faithfully (Unix) / reject non-UTF-8 (Windows),
42 // matching model loading — never lossily mangle a path with to_string_lossy.
43 let c_dir = CString::new(crate::model::path_bytes(dir)?)?;
44 let status = unsafe { sys::transcribe_init_backends(c_dir.as_ptr()) };
45 check(status, "init_backends")
46}
47
48/// Load the backend modules the way the build expects, with no caller-supplied
49/// path.
50///
51/// - **Compiled-in builds** (the default static build, or a plain `shared`
52/// build): a no-op returning `Ok(())` — the backends are already registered.
53/// - **`dynamic-backends` builds**: resolves the directory containing the
54/// loaded libtranscribe and scans only that directory. Ship the backend
55/// modules next to libtranscribe for this helper to find them.
56///
57/// If your app uses a different layout, call [`init_backends`] with that
58/// resolved module directory instead. Like [`init_backends`], this is
59/// idempotent and must run once before the first model load.
60pub fn init_backends_default() -> Result<()> {
61 let status = unsafe { sys::transcribe_init_backends_default() };
62 check(status, "init_backends_default")
63}
64
65/// The number of compute devices currently registered.
66pub fn device_count() -> usize {
67 let n = unsafe { sys::transcribe_backend_device_count() };
68 n.max(0) as usize
69}
70
71/// Every registered compute device.
72pub fn devices() -> Vec<Device> {
73 let mut out = Vec::with_capacity(device_count());
74 for i in 0..device_count() as i32 {
75 let mut raw: sys::transcribe_backend_device = unsafe { std::mem::zeroed() };
76 unsafe { sys::transcribe_backend_device_init(&mut raw) };
77 let status = unsafe { sys::transcribe_get_backend_device(i, &mut raw) };
78 if status == sys::transcribe_status::TRANSCRIBE_OK {
79 out.push(Device {
80 name: owned_str(raw.name),
81 description: owned_str(raw.description),
82 kind: owned_str(raw.kind),
83 });
84 }
85 }
86 out
87}
88
89/// Whether a backend request can be satisfied by some registered device. This
90/// is the probe to turn `Backend::Vulkan` on a machine without Vulkan into a
91/// clear error instead of a failed model load.
92pub fn backend_available(backend: Backend) -> bool {
93 unsafe { sys::transcribe_backend_available(backend.to_raw()) }
94}