1use 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#[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)]
36pub struct ExportInfo {
38 pub name: *const c_char,
40 pub flags: u32,
42 pub constant_byte_length: u32,
44 pub binding_count: u32,
46 pub parameter_count: u32,
48 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
78macro_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 #[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 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 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 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 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 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;
350pub 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
379pub(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}