Skip to main content

hrx/
sys.rs

1//! libhrx's C API, declared by hand from `libhrx/include/hrx_runtime.h`. No bindgen, so the whole
2//! surface this project uses is readable in one file.
3//!
4//! Two conventions from the header matter and are easy to get wrong:
5//!   * `hrx_status_t` is a pointer, and **NULL means success**. `hrx_status_is_ok` is a `static inline`
6//!     in the header, so it is not a linkable symbol — [`is_ok`] reimplements it.
7//!   * handles are opaque pointers; a null handle is never valid.
8use std::ffi::{c_char, c_int, c_void};
9
10pub type Status = *mut c_void;
11pub type Device = *mut c_void;
12pub type Stream = *mut c_void;
13pub type Buffer = *mut c_void;
14pub type Executable = *mut c_void;
15pub type Event = *mut c_void;
16pub type Semaphore = *mut c_void;
17#[repr(C)]
18#[derive(Clone, Copy, Debug, Default)]
19pub struct TimelinePoint {
20    pub semaphore: Semaphore,
21    pub value: u64,
22}
23pub const EVENT_FLAG_DISABLE_TIMING: u32 = 2;
24
25/// `hrx_status_is_ok`, which the header defines as `static inline` rather than exporting.
26#[inline]
27pub fn is_ok(status: Status) -> bool {
28    status.is_null()
29}
30
31pub const MEMORY_TYPE_DEVICE_LOCAL: u32 = 0x0000_0030;
32pub const BUFFER_USAGE_DEFAULT: u32 = 0x0000_0C03;
33
34#[repr(C)]
35#[derive(Clone, Copy, Debug, Default)]
36/// Immutable native export metadata, valid while its kernel is retained.
37pub struct ExportInfo {
38    /// Borrowed native export name; do not dereference after dropping the kernel.
39    pub name: *const c_char,
40    /// Native export flags.
41    pub flags: u32,
42    /// Required packed scalar byte length for binding dispatch.
43    pub constant_byte_length: u32,
44    /// Required number of buffer bindings.
45    pub binding_count: u32,
46    /// Native parameter count.
47    pub parameter_count: u32,
48    /// Compiled workgroup dimensions; zero denotes an unspecified dimension.
49    pub workgroup_size: [u32; 3],
50}
51
52#[repr(C)]
53#[derive(Clone, Copy, Debug)]
54pub struct DispatchConfig {
55    pub workgroup_count: [u32; 3],
56    pub workgroup_size: [u32; 3],
57    pub subgroup_size: u32,
58}
59
60#[repr(C)]
61#[derive(Clone, Copy, Debug)]
62pub struct BufferRef {
63    pub buffer: Buffer,
64    pub offset: usize,
65    pub length: usize,
66}
67
68pub type Allocator = *mut c_void;
69#[repr(C)]
70#[derive(Clone, Copy, Debug)]
71pub struct BufferParams {
72    pub memory_type: u32,
73    pub access: u16,
74    pub usage: u32,
75    pub queue_affinity: u64,
76}
77
78/// Symbols are resolved once. The libraries are deliberately never unloaded:
79/// native global devices and objects may still be held by another model cdylib.
80macro_rules! native_api {
81    ($(fn $name:ident($($arg:ident: $ty:ty),* $(,)?) -> $ret:ty;)*) => {
82        struct Api { $($name: unsafe extern "C" fn($($ty),*) -> $ret,)* }
83        impl Api {
84            unsafe fn resolve(library: &libloading::os::unix::Library) -> crate::Result<Self> {
85                Ok(Self {
86                    $($name: *unsafe { library.get(concat!(stringify!($name), "\0").as_bytes()) }
87                        .map_err(crate::Error::from)?,)*
88                })
89            }
90        }
91        $(
92            /// Raw native call.
93            /// # Safety
94            /// Initialize the runtime and satisfy the HRX C header contract.
95            #[allow(clippy::too_many_arguments)]
96            #[inline]
97            pub unsafe fn $name($($arg: $ty),*) -> $ret {
98                unsafe { (api().$name)($($arg),*) }
99            }
100        )*
101    };
102}
103native_api! {
104    fn hrx_executable_retain(executable: Executable) -> ();
105    fn hrx_event_create(device: Device, flags: u32, out_event: *mut Event) -> Status;
106    fn hrx_event_release(event: Event) -> ();
107    fn hrx_event_record(event: Event, stream: Stream) -> Status;
108    fn hrx_event_query(event: Event, complete: *mut bool) -> Status;
109    fn hrx_event_synchronize(event: Event) -> Status;
110    fn hrx_stream_wait_event(stream: Stream, event: Event) -> Status;
111    fn hrx_status_to_string(status: Status,
112    out_message: *mut *mut c_char,
113    out_length: *mut usize,) -> Status;
114    fn hrx_status_free_message(message: *mut c_char) -> ();
115    fn hrx_status_ignore(status: Status) -> ();
116    fn hrx_gpu_initialize(flags: u32) -> Status;
117    fn hrx_gpu_device_count(count: *mut c_int) -> Status;
118    fn hrx_gpu_device_get(index: c_int, device: *mut Device) -> Status;
119    fn hrx_stream_create(device: Device, flags: u32, out_stream: *mut Stream) -> Status;
120    fn hrx_stream_release(stream: Stream) -> ();
121    fn hrx_stream_synchronize(stream: Stream) -> Status;
122    fn hrx_stream_get_timeline_position(stream: Stream, position: *mut TimelinePoint) -> Status;
123    fn hrx_semaphore_wait(semaphore: Semaphore, value: u64, timeout_ns: u64) -> Status;
124    fn hrx_buffer_release(buffer: Buffer) -> ();
125    fn hrx_stream_fill_buffer(stream: Stream,
126    buffer: Buffer,
127    offset: usize,
128    size: usize,
129    pattern: *const c_void,
130    pattern_size: usize,) -> Status;
131    fn hrx_stream_copy_buffer(stream: Stream,
132    src: Buffer,
133    src_offset: usize,
134    dst: Buffer,
135    dst_offset: usize,
136    size: usize,) -> Status;
137    fn hrx_synchronous_h2d(device: Device,
138    host_src: *const c_void,
139    dst: Buffer,
140    dst_offset: usize,
141    size: usize,) -> Status;
142    fn hrx_synchronous_d2h(device: Device,
143    src: Buffer,
144    src_offset: usize,
145    host_dst: *mut c_void,
146    size: usize,) -> Status;
147    fn hrx_executable_load_data(device: Device,
148    data: *const c_void,
149    size: usize,
150    family: *const c_char,
151    key: *const c_char,
152    executable: *mut Executable,) -> Status;
153    fn hrx_executable_load_file(device: Device,
154    path: *const c_char,
155    target_family: *const c_char,
156    target_key: *const c_char,
157    out_executable: *mut Executable,) -> Status;
158    fn hrx_executable_release(executable: Executable) -> ();
159    fn hrx_executable_lookup_export_by_name(executable: Executable,
160    name: *const c_char,
161    out_ordinal: *mut u32,) -> Status;
162    fn hrx_executable_export_info(executable: Executable,
163    ordinal: u32,
164    out_info: *mut ExportInfo,) -> Status;
165    fn hrx_stream_dispatch(stream: Stream,
166    executable: Executable,
167    ordinal: u32,
168    config: *const DispatchConfig,
169    constants: *const c_void,
170    constants_size: usize,
171    bindings: *const BufferRef,
172    binding_count: usize,
173    flags: u32,) -> Status;
174    fn hrx_status_code(status: Status) -> c_int;
175    fn hrx_device_allocator(device: Device) -> Allocator;
176    fn hrx_allocator_allocate_buffer(allocator: Allocator,
177    params: BufferParams,
178    size: usize,
179    buffer: *mut Buffer,) -> Status;
180    fn hrx_allocator_import_buffer(allocator: Allocator,
181    params: BufferParams,
182    host_ptr: *mut c_void,
183    size: usize,
184    buffer: *mut Buffer,) -> Status;
185    fn hrx_buffer_get_device_ptr(buffer: Buffer, pointer: *mut *mut c_void) -> Status;
186    fn hrx_device_get_property(device: Device,
187    property: c_int,
188    value: *mut c_void,
189    size: usize,) -> Status;
190    fn hrx_stream_flush(stream: Stream) -> Status;
191    fn hrx_stream_query(stream: Stream, complete: *mut bool) -> Status;
192    fn hrx_graph_create(device: Device, flags: u32, graph: *mut Graph) -> Status;
193    fn hrx_graph_release(graph: Graph) -> ();
194    fn hrx_graph_add_kernel_node(graph: Graph,
195    deps: *const GraphNode,
196    count: usize,
197    attrs: *const GraphKernel,
198    node: *mut GraphNode,) -> Status;
199    fn hrx_graph_add_copy_buffer_node(graph: Graph,
200    deps: *const GraphNode,
201    count: usize,
202    attrs: *const GraphCopy,
203    node: *mut GraphNode,) -> Status;
204    fn hrx_graph_add_fill_buffer_node(graph: Graph,
205    deps: *const GraphNode,
206    count: usize,
207    attrs: *const GraphFill,
208    node: *mut GraphNode,) -> Status;
209    fn hrx_graph_add_empty_node(graph: Graph,
210    deps: *const GraphNode,
211    count: usize,
212    node: *mut GraphNode,) -> Status;
213    fn hrx_graph_add_dependencies(graph: Graph,
214    from: *const GraphNode,
215    to: *const GraphNode,
216    count: usize,) -> Status;
217    fn hrx_graph_instantiate(graph: Graph, flags: u32, exec: *mut GraphExec) -> Status;
218    fn hrx_graph_exec_release(exec: GraphExec) -> ();
219    fn hrx_graph_exec_launch(exec: GraphExec, stream: Stream) -> Status;
220}
221static DIRECTORY: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
222static API: std::sync::OnceLock<Api> = std::sync::OnceLock::new();
223pub(crate) fn load() -> crate::Result<()> {
224    static INIT: std::sync::Mutex<()> = std::sync::Mutex::new(());
225    if API.get().is_some() {
226        return Ok(());
227    }
228    let _guard = INIT
229        .lock()
230        .map_err(|_| crate::Error::Message("runtime loader poisoned".into()))?;
231    if API.get().is_some() {
232        return Ok(());
233    }
234    let directory = crate::bundle::resolve()?;
235    let path = std::fs::canonicalize(directory.join("libhrx.so"))?;
236    // Process-wide, independent of HOME / cache / which DSO contains this code.
237    // The file records the first library choice to refuse incompatible copies.
238    let mut lock = runtime_lock()?;
239    use std::io::{Read, Seek, Write};
240    let mut selected = String::new();
241    lock.file().read_to_string(&mut selected)?;
242    // PID reuse: only choices within this process lifetime are relevant.
243    let start = std::fs::read_to_string("/proc/self/stat")?;
244    let start = start
245        .rsplit_once(") ")
246        .ok_or_else(|| crate::Error::Message("invalid proc stat".into()))?
247        .1
248        .split_whitespace()
249        .nth(19)
250        .ok_or_else(|| crate::Error::Message("missing process start time".into()))?;
251    let identity = format!("{start} {}", path.display());
252    if selected.starts_with(&format!("{start} ")) && selected != identity {
253        return Err(crate::Error::Message(format!(
254            "another model already loaded {selected}; select one HRX_RUNTIME_DIR for the process"
255        )));
256    }
257    unsafe {
258        use libloading::os::unix::{Library, RTLD_LOCAL, RTLD_NOW};
259        // HSA comes from the same verified directory as libhrx; a different tree
260        // is selected by pointing HRX_RUNTIME_DIR at it, not by a second variable.
261        let provider = std::fs::canonicalize(directory.join("libhsa-runtime64.so.1"))?;
262        let hsa = Library::open(Some(&provider), RTLD_NOW | RTLD_LOCAL).map_err(|e| {
263            crate::Error::from(e).context(format!("loading {}", provider.display()))
264        })?;
265        // No library-controlled environment mutation, and no search-path changes.
266        let library = Library::open(Some(&path), RTLD_NOW | RTLD_LOCAL)
267            .map_err(|e| crate::Error::from(e).context(format!("loading {}", path.display())))?;
268        let result = Api::resolve(&library);
269        // Even a partial loader must not unload HSA/HRX behind a different DSO.
270        std::mem::forget(hsa);
271        std::mem::forget(library);
272        let api = result?;
273        lock.file().set_len(0)?;
274        lock.file().rewind()?;
275        lock.file().write_all(identity.as_bytes())?;
276        let _ = DIRECTORY.set(directory.clone());
277        API.set(api)
278            .map_err(|_| crate::Error::Message("runtime loader raced".into()))?;
279    }
280    Ok(())
281}
282static RUNTIME_LOCK_PATH: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
283extern "C" fn cleanup_runtime_lock() {
284    if let Some(path) = RUNTIME_LOCK_PATH.get() {
285        let _ = std::fs::remove_file(path);
286    }
287}
288
289fn private_runtime_directory(path: &std::path::Path) -> crate::Result<()> {
290    use std::os::unix::fs::{DirBuilderExt, MetadataExt};
291    match std::fs::DirBuilder::new().mode(0o700).create(path) {
292        Ok(()) => {}
293        Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
294        Err(e) => return Err(e.into()),
295    }
296    let metadata = std::fs::symlink_metadata(path)?;
297    if !metadata.is_dir()
298        || metadata.uid() != unsafe { libc::geteuid() }
299        || metadata.mode() & 0o077 != 0
300    {
301        return Err(crate::Error::Message(
302            "runtime lock directory must be owned by this user and private".into(),
303        ));
304    }
305    Ok(())
306}
307pub(crate) fn runtime_lock() -> crate::Result<crate::bundle::Lock> {
308    use std::os::unix::fs::MetadataExt;
309    if let Some(path) = RUNTIME_LOCK_PATH.get() {
310        return crate::bundle::Lock::acquire(path);
311    }
312    let directory = std::env::var_os("XDG_RUNTIME_DIR")
313        .map(std::path::PathBuf::from)
314        .filter(|p| p.is_absolute() && private_runtime_directory(p).is_ok())
315        .unwrap_or_else(|| {
316            std::path::PathBuf::from(format!("/tmp/hrx-{}", unsafe { libc::geteuid() }))
317        });
318    private_runtime_directory(&directory)?;
319    let path = directory.join(format!("hrx-{}.lock", std::process::id()));
320    let mut lock = crate::bundle::Lock::acquire(&path)?;
321    let metadata = lock.file().metadata()?;
322    if !metadata.is_file()
323        || metadata.uid() != unsafe { libc::geteuid() }
324        || metadata.mode() & 0o077 != 0
325    {
326        return Err(crate::Error::Message(
327            "runtime lock must be a private file owned by this user".into(),
328        ));
329    }
330    if RUNTIME_LOCK_PATH.set(path).is_ok() {
331        unsafe {
332            libc::atexit(cleanup_runtime_lock);
333        }
334    }
335    Ok(lock)
336}
337fn api() -> &'static Api {
338    API.get()
339        .expect("open a Device or Stream before raw HRX functions")
340}
341
342pub const STATUS_ALREADY_EXISTS: c_int = 6;
343pub const DEVICE_PROPERTY_ARCHITECTURE: c_int = 1;
344pub const MEMORY_TYPE_HOST_VISIBLE: u32 = 0x0000_0002;
345pub const MEMORY_TYPE_HOST_COHERENT: u32 = 0x0000_0004;
346pub const MEMORY_TYPE_HOST_LOCAL: u32 = 0x0000_0040 | MEMORY_TYPE_HOST_VISIBLE;
347pub const MEMORY_TYPE_DEVICE_VISIBLE: u32 = 0x0000_0010;
348pub const MEMORY_ACCESS_ALL: u16 = 7;
349pub const BUFFER_USAGE_MAPPING_SCOPED: u32 = 0x0100_0000;
350// The pinned native implementation ignores this field and uses executable metadata.
351pub const SUBGROUP_SIZE_FROM_EXECUTABLE: u32 = 0;
352
353pub type Graph = *mut c_void;
354pub type GraphNode = *mut c_void;
355pub type GraphExec = *mut c_void;
356#[repr(C)]
357pub struct GraphKernel {
358    pub executable: Executable,
359    pub ordinal: u32,
360    pub config: DispatchConfig,
361    pub constants: *const c_void,
362    pub constants_size: usize,
363    pub bindings: *const BufferRef,
364    pub binding_count: usize,
365    pub flags: u32,
366}
367#[repr(C)]
368pub struct GraphCopy {
369    pub src: BufferRef,
370    pub dst: BufferRef,
371}
372#[repr(C)]
373pub struct GraphFill {
374    pub dst: BufferRef,
375    pub pattern: u32,
376    pub pattern_size: usize,
377}
378
379/// Optional functions and their owning library have the same process lifetime.
380pub(crate) struct InteropApi {
381    _library: libloading::Library,
382    pub allocation_address: unsafe extern "C" fn(Buffer, *mut u64) -> Status,
383    pub export_dmabuf: unsafe extern "C" fn(Buffer, *mut i32, *mut u64) -> Status,
384}
385static INTEROP: std::sync::OnceLock<Result<InteropApi, std::sync::Arc<crate::Error>>> =
386    std::sync::OnceLock::new();
387
388pub(crate) fn interop() -> crate::Result<&'static InteropApi> {
389    load()?;
390    match INTEROP.get_or_init(|| resolve_interop().map_err(std::sync::Arc::new)) {
391        Ok(api) => Ok(api),
392        Err(error) => match error.as_ref() {
393            crate::Error::Unsupported(message) => Err(crate::Error::Unsupported(message.clone())),
394            _ => Err(crate::Error::Execution {
395                source: error.clone(),
396            }),
397        },
398    }
399}
400fn resolve_interop() -> crate::Result<InteropApi> {
401    let directory = DIRECTORY.get().expect("loaded runtime directory");
402    let library = unsafe { libloading::Library::new(directory.join("libhrx.so")) }?;
403    let abi = unsafe { library.get::<unsafe extern "C" fn() -> u32>(b"hrx_interop_abi_version\0") }
404        .map_err(|_| {
405            crate::Error::Unsupported(
406                "native bundle lacks shared-buffer ABI 1; rebuild with patches/loom/0008".into(),
407            )
408        })?;
409    if unsafe { abi() } != 1 {
410        return Err(crate::Error::Unsupported(
411            "incompatible shared-buffer ABI".into(),
412        ));
413    }
414    Ok(InteropApi {
415        allocation_address: *unsafe { library.get(b"hrx_buffer_allocation_address\0") }?,
416        export_dmabuf: *unsafe { library.get(b"hrx_buffer_export_dmabuf\0") }?,
417        _library: library,
418    })
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    #[test]
425    fn runtime_lock_directory_rejects_public_modes_and_symlinks() {
426        use std::os::unix::fs::{PermissionsExt, symlink};
427        let temporary = tempfile::tempdir().unwrap();
428        let directory = temporary.path().join("private");
429        private_runtime_directory(&directory).unwrap();
430        assert_eq!(
431            std::fs::metadata(&directory).unwrap().permissions().mode() & 0o777,
432            0o700
433        );
434        let link = temporary.path().join("link");
435        symlink(&directory, &link).unwrap();
436        assert!(private_runtime_directory(&link).is_err());
437        std::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o777)).unwrap();
438        assert!(private_runtime_directory(&directory).is_err());
439    }
440}