onnx_runtime_ep_api/provider.rs
1//! The [`ExecutionProvider`] trait and its supporting types (§4.1).
2
3use std::ffi::c_void;
4use std::ptr::NonNull;
5
6use onnx_runtime_ir::{DataType, DeviceId, DeviceType, Graph, Node, NodeId, Shape, TensorLayout};
7
8use crate::epcontext::EpContext;
9use crate::error::{EpError, Result};
10use crate::kernel::{Kernel, KernelMatch};
11use crate::weight::ExecutionProviderCapabilities;
12
13/// Index of an EP within an [`crate::registry::EpRegistry`].
14#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
15pub struct EpId(pub u32);
16
17/// Opaque, namespaced configuration passed to [`ExecutionProvider::initialize`].
18#[derive(Clone, Debug, Default)]
19pub struct EpConfig {
20 /// Namespaced key/value options (e.g. `"cuda.arena_extend_strategy"`).
21 pub options: std::collections::HashMap<String, String>,
22}
23
24/// An owning handle to a single device allocation.
25///
26/// # Ownership & lifetime
27///
28/// A `DeviceBuffer` is the **sole owner** of the allocation it names. It is
29/// produced only by [`ExecutionProvider::allocate`] and released only by
30/// [`ExecutionProvider::deallocate`], which consumes it *by value*. The owning
31/// EP is both allocator and deallocator: the buffer records the [`DeviceId`]
32/// (hence which EP instance) that may free it, so a buffer must never be handed
33/// to a different EP. Ownership is unique — no two `DeviceBuffer`s ever alias
34/// the same allocation.
35///
36/// # No `Drop`
37///
38/// `DeviceBuffer` deliberately does **not** implement [`Drop`]. Freeing device
39/// memory generally needs the EP's context/stream (a CUDA context, an MLX
40/// queue, an allocator arena) that this bare handle does not carry, so a silent
41/// drop could not free correctly. Consequences:
42/// * Dropping a `DeviceBuffer` without passing it to `deallocate` **leaks** the
43/// allocation. It can never *double-free*, which is the memory-safety
44/// property we prioritize (plan §4.4).
45/// * The session layer owns the discipline of pairing every `allocate` with
46/// exactly one `deallocate`. Higher layers may wrap this handle in an
47/// RAII/`Arc` type that calls back into the EP; that policy lives above the
48/// EP contract, not here.
49///
50/// # Access
51///
52/// The base address is reachable only through [`DeviceBuffer::as_ptr`]
53/// (shared) and [`DeviceBuffer::as_mut_ptr`] (unique). Obtaining a pointer is
54/// safe; *dereferencing* it is `unsafe` and valid only on host-accessible
55/// devices ([`DeviceType::is_host_accessible`]) within the owning EP's context.
56///
57/// # Thread-safety
58///
59/// See the `Send`/`Sync` impls below for the exact invariant.
60#[derive(Debug)]
61pub struct DeviceBuffer {
62 device: DeviceId,
63 size: usize,
64 align: usize,
65 /// Non-null base address of the allocation. For CPU and MLX unified memory
66 /// this is a dereferenceable host pointer; for CUDA/ROCm it is an opaque
67 /// device address only meaningful inside the owning EP's context.
68 ptr: NonNull<c_void>,
69 /// Whether this handle *owns* the pointed-to allocation.
70 ///
71 /// [`BufferOwner::Owned`] (the default for [`DeviceBuffer::from_raw_parts`])
72 /// is the original contract: the owning EP must free it exactly once in
73 /// `deallocate`. Borrowed handles alias memory owned by *someone else*.
74 /// Read-only aliases come from [`DeviceBuffer::from_borrowed_parts`];
75 /// exclusive writable aliases come from
76 /// [`DeviceBuffer::from_borrowed_mut_parts`]. `deallocate` must **not** free
77 /// either kind.
78 owner: BufferOwner,
79}
80
81/// Whether a [`DeviceBuffer`] owns the allocation it names, or merely borrows
82/// (aliases) memory owned elsewhere.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84enum BufferOwner {
85 /// This handle is the sole owner; the owning EP frees it in `deallocate`.
86 Owned,
87 /// This handle aliases foreign memory (e.g. an mmap). `deallocate` must be
88 /// a no-op free; the real owner must outlive the buffer and every use of it.
89 Borrowed,
90 /// This handle has temporary exclusive write access to an allocation owned
91 /// elsewhere. Deallocation remains a no-op.
92 BorrowedMut,
93}
94
95impl DeviceBuffer {
96 /// Wrap a raw device allocation in an owning handle.
97 ///
98 /// # Safety
99 ///
100 /// The caller (the owning EP) must guarantee all of:
101 /// * `ptr` is non-null and points to the start of an allocation of at least
102 /// `size` bytes on `device`, aligned to at least `align` bytes.
103 /// * The allocation was produced by `device`'s EP and will be freed exactly
104 /// once, only by returning this handle to that EP's `deallocate` (or via
105 /// an equivalent raw free of the pointer obtained from
106 /// [`DeviceBuffer::into_raw`]).
107 /// * No other live `DeviceBuffer` aliases the same allocation.
108 ///
109 /// `align` must be a power of two (checked in debug builds).
110 pub unsafe fn from_raw_parts(
111 ptr: *mut c_void,
112 device: DeviceId,
113 size: usize,
114 align: usize,
115 ) -> Self {
116 debug_assert!(align.is_power_of_two(), "alignment must be a power of two");
117 Self {
118 device,
119 size,
120 align,
121 ptr: NonNull::new(ptr).expect("DeviceBuffer::from_raw_parts: null pointer"),
122 owner: BufferOwner::Owned,
123 }
124 }
125
126 /// Wrap **foreign, borrowed** memory in a non-owning `DeviceBuffer`.
127 ///
128 /// Unlike [`DeviceBuffer::from_raw_parts`], the returned handle does **not**
129 /// own the allocation: it aliases memory owned by someone else (for example
130 /// a `memmap2::Mmap` over an on-disk weight file). This lets an EP reference
131 /// initializer bytes zero-copy instead of allocating + copying them into
132 /// fresh RAM.
133 ///
134 /// [`is_borrowed`](DeviceBuffer::is_borrowed) returns `true`, and the owning
135 /// EP's `deallocate` must treat it as a **no-op free** (the guard checks
136 /// `is_borrowed()`). [`into_raw`](DeviceBuffer::into_raw) still yields the
137 /// raw pointer, but the caller must **not** free it.
138 ///
139 /// # Safety
140 ///
141 /// The caller must guarantee all of:
142 /// * `ptr` is non-null and points to the start of a readable region of at
143 /// least `size` bytes on `device`, aligned to at least `align` bytes.
144 /// * The memory is owned by another object (e.g. an mmap) that **outlives
145 /// this buffer and every use of it** (read via `as_ptr`). Nothing else may
146 /// free or unmap it while this handle or any alias derived from it lives.
147 /// * The buffer is treated as **read-only**: it is never written through
148 /// (`as_mut_ptr` must not be used to mutate borrowed memory) and is never
149 /// passed to an EP's `deallocate` expecting a free — `deallocate` skips
150 /// the free for borrowed buffers.
151 ///
152 /// `align` must be a power of two (checked in debug builds).
153 pub unsafe fn from_borrowed_parts(
154 ptr: *mut c_void,
155 device: DeviceId,
156 size: usize,
157 align: usize,
158 ) -> Self {
159 debug_assert!(align.is_power_of_two(), "alignment must be a power of two");
160 Self {
161 device,
162 size,
163 align,
164 ptr: NonNull::new(ptr).expect("DeviceBuffer::from_borrowed_parts: null pointer"),
165 owner: BufferOwner::Borrowed,
166 }
167 }
168
169 /// Wrap foreign memory in a non-owning, exclusively writable buffer handle.
170 ///
171 /// This is intended for persistent external output bindings: the real owner
172 /// retains the allocation while an executor temporarily writes through this
173 /// alias.
174 ///
175 /// # Safety
176 ///
177 /// The caller must guarantee all of:
178 /// * `ptr` names a non-null writable allocation of at least `size` bytes on
179 /// `device`, aligned to at least `align` bytes.
180 /// * The real owner outlives this handle and every operation using it.
181 /// * No other writer accesses the allocation while this handle is live.
182 /// * This handle is never used to free the allocation; `deallocate` treats
183 /// it as borrowed.
184 pub unsafe fn from_borrowed_mut_parts(
185 ptr: *mut c_void,
186 device: DeviceId,
187 size: usize,
188 align: usize,
189 ) -> Option<Self> {
190 debug_assert!(align.is_power_of_two(), "alignment must be a power of two");
191 Some(Self {
192 device,
193 size,
194 align,
195 ptr: NonNull::new(ptr)?,
196 owner: BufferOwner::BorrowedMut,
197 })
198 }
199
200 /// Whether this handle merely *borrows* (aliases) foreign memory rather than
201 /// owning it. A borrowed buffer must never be freed by `deallocate`.
202 pub fn is_borrowed(&self) -> bool {
203 matches!(self.owner, BufferOwner::Borrowed | BufferOwner::BorrowedMut)
204 }
205
206 /// The device this allocation lives on (and whose EP must free it).
207 pub fn device(&self) -> DeviceId {
208 self.device
209 }
210
211 /// Allocation size in bytes.
212 pub fn len(&self) -> usize {
213 self.size
214 }
215
216 /// Whether the allocation is zero-length.
217 pub fn is_empty(&self) -> bool {
218 self.size == 0
219 }
220
221 /// Alignment (bytes) the base pointer was allocated to.
222 pub fn alignment(&self) -> usize {
223 self.align
224 }
225
226 /// Shared base pointer. Safe to obtain; dereferencing is `unsafe` and only
227 /// sound on host-accessible devices within the owning EP's context.
228 pub fn as_ptr(&self) -> *const c_void {
229 self.ptr.as_ptr()
230 }
231
232 /// Unique mutable base pointer. Requires `&mut self` so the borrow checker
233 /// forbids two writers sharing one buffer — this is what makes the `Sync`
234 /// impl sound (a shared `&DeviceBuffer` can never hand out a writable
235 /// pointer through safe code).
236 pub fn as_mut_ptr(&mut self) -> *mut c_void {
237 self.ptr.as_ptr()
238 }
239
240 /// Consume the handle, returning the raw pointer *without* freeing it. For
241 /// an owned buffer the caller assumes the single-free obligation from
242 /// [`DeviceBuffer::from_raw_parts`]. For a **borrowed** buffer (see
243 /// [`DeviceBuffer::from_borrowed_parts`]) the pointer must **not** be freed;
244 /// check [`is_borrowed`](DeviceBuffer::is_borrowed) first if the caller
245 /// intends to free.
246 pub fn into_raw(self) -> *mut c_void {
247 self.ptr.as_ptr()
248 }
249}
250
251// SAFETY: `DeviceBuffer` is an owning *handle* — it stores only a base address
252// plus metadata and exposes no safe way to read or write the pointed-to memory
253// (all access goes through `as_ptr`/`as_mut_ptr`, which are safe to *call* but
254// `unsafe` to *use*). Moving the handle to another thread transfers ownership of
255// the address; this is sound for every allocator we target — host `malloc`,
256// CUDA device pointers, and MLX unified memory are all address-portable and not
257// thread-affine at the pointer level. Any data race on the *contents* is
258// prevented one layer up by `&`/`&mut` aliasing on `TensorView`/`TensorMut` and
259// by the scheduler, not by this type. If a future EP wires a genuinely
260// thread-affine allocator, it must wrap the handle in a non-`Send` owner rather
261// than weaken this invariant (plan §4.4 flags this for a dedicated review when
262// ep-cpu lands real memory).
263unsafe impl Send for DeviceBuffer {}
264// SAFETY: `&DeviceBuffer` grants no interior mutability — it can only produce a
265// `*const` via `as_ptr` (a plain address copy) and read `Copy` metadata, so
266// concurrent shared reads of the handle are race-free. Writing requires
267// `as_mut_ptr`, which needs `&mut self`; obtaining a writable pointer therefore
268// cannot happen through a shared reference in safe code. As with `Send`,
269// mutating the underlying memory is gated behind `unsafe` pointer use whose
270// synchronization is the caller's responsibility.
271unsafe impl Sync for DeviceBuffer {}
272
273/// A synchronization fence returned by async operations.
274#[derive(Debug, Default)]
275pub struct Fence {
276 pub id: u64,
277}
278
279/// Marker for an EP exported as an ORT-compatible C ABI plugin (Phase 2).
280#[derive(Debug, Default)]
281pub struct OrtPluginExport {
282 pub register_symbol: String,
283}
284
285/// The core EP interface. Every backend crate implements this (§4.1).
286pub trait ExecutionProvider: Send + Sync {
287 /// EP identifier (snake_case, e.g. `"cpu_ep"`, `"cuda_ep"`).
288 fn name(&self) -> &str;
289
290 fn device_type(&self) -> DeviceType;
291 fn device_id(&self) -> DeviceId;
292
293 /// Optional executor-to-EP capabilities. Stock EPs advertise none and
294 /// continue receiving resident [`crate::TensorView`] inputs.
295 fn capabilities(&self) -> ExecutionProviderCapabilities {
296 ExecutionProviderCapabilities::stock()
297 }
298
299 /// Initialize device resources / load libraries.
300 fn initialize(&mut self, config: &EpConfig) -> Result<()>;
301 /// Release device resources.
302 fn shutdown(&mut self) -> Result<()>;
303
304 /// Whether this EP can run `op` at the model's effective `opset` with the
305 /// given input shapes, dtypes, and layouts, and at what cost.
306 ///
307 /// Every [`KernelMatch::Unsupported`] result must carry an actionable reason:
308 /// state what the EP accepts and, where possible, how to fix the model or
309 /// registration rather than returning a bare decline.
310 fn supports_op(
311 &self,
312 op: &Node,
313 opset: u64,
314 shapes: &[Shape],
315 input_dtypes: &[DataType],
316 layouts: &[TensorLayout],
317 ) -> KernelMatch;
318
319 /// Get or create a kernel for `op` specialized to concrete `shapes`.
320 ///
321 /// `opset` is the effective operator-set version for `op`'s domain in the
322 /// owning graph. EPs use it to select opset-specialized kernels (e.g. the
323 /// opset-13 per-axis vs. the legacy opset-<13 2D-coercion `Softmax`).
324 fn get_kernel(&self, op: &Node, shapes: &[Vec<usize>], opset: u64) -> Result<Box<dyn Kernel>>;
325
326 /// Allocate device memory.
327 fn allocate(&self, size: usize, alignment: usize) -> Result<DeviceBuffer>;
328 /// Free device memory.
329 fn deallocate(&self, buffer: DeviceBuffer) -> Result<()>;
330
331 /// Synchronous copy (host↔device or device↔device).
332 fn copy(&self, src: &DeviceBuffer, dst: &mut DeviceBuffer, size: usize) -> Result<()>;
333 /// Asynchronous copy; returns a [`Fence`] to await.
334 fn copy_async(&self, src: &DeviceBuffer, dst: &mut DeviceBuffer, size: usize) -> Result<Fence>;
335
336 /// Whether this EP can select the first maximum f32 element on-device and
337 /// return the token id together with its capture-error status.
338 fn device_argmax_supported(&self) -> bool {
339 false
340 }
341
342 /// Launch an allocation-free device argmax over `elements` contiguous
343 /// `dtype` values (Float32 or Float16). `result` receives two native-endian
344 /// u32 values: token id, then the latching device capture-error bitmask.
345 fn device_argmax(
346 &self,
347 _logits: &DeviceBuffer,
348 _elements: usize,
349 _dtype: DataType,
350 _result: &mut DeviceBuffer,
351 ) -> Result<()> {
352 Err(EpError::KernelFailed(format!(
353 "{}: device argmax is not supported",
354 self.name()
355 )))
356 }
357
358 /// Begin recording the supplied, already-compiled kernel sequence into a
359 /// device graph. EPs without graph support reject the request.
360 fn begin_device_graph_capture(&self, _kernels: &[&dyn Kernel]) -> Result<()> {
361 Err(EpError::KernelFailed(format!(
362 "{}: device graph capture is not supported",
363 self.name()
364 )))
365 }
366
367 /// End device-graph capture and install the resulting executable.
368 fn end_device_graph_capture(&self) -> Result<()> {
369 Err(EpError::KernelFailed(format!(
370 "{}: device graph capture is not supported",
371 self.name()
372 )))
373 }
374
375 /// Abort an in-progress device-graph capture, returning the stream and
376 /// lifecycle to a clean idle state so a subsequent [`reset_device_graph`]
377 /// succeeds. Called on the error path of segmented capture when a node
378 /// fails mid-record: the capture must always be ended before reset, so the
379 /// stream is not left wedged in capture mode. EPs without device graphs have
380 /// nothing to abort.
381 ///
382 /// [`reset_device_graph`]: ExecutionProvider::reset_device_graph
383 fn abort_device_graph_capture(&self) -> Result<()> {
384 Ok(())
385 }
386
387 /// Replay the installed device graph.
388 ///
389 /// When the EP holds multiple captured **segments** (segmented capture), this
390 /// replays every installed segment in capture order. For the single-graph
391 /// fast path (one whole-subgraph capture) that is exactly the one graph.
392 fn replay_device_graph(&self) -> Result<()> {
393 Err(EpError::KernelFailed(format!(
394 "{}: device graph replay is not supported",
395 self.name()
396 )))
397 }
398
399 /// Replay one captured **segment** by its zero-based capture-order index.
400 ///
401 /// Segmented capture claims a whole subgraph even when only parts are
402 /// device-graph capturable: the executor captures each maximal capturable
403 /// run as its own segment and, at replay time, launches the segment graphs
404 /// in order while running the non-capturable seam nodes eagerly in between.
405 /// EPs without segmented graph support reject the request.
406 fn replay_device_graph_segment(&self, _index: usize) -> Result<()> {
407 Err(EpError::KernelFailed(format!(
408 "{}: segmented device graph replay is not supported",
409 self.name()
410 )))
411 }
412
413 /// Destroy any installed device graph before its referenced buffers move or
414 /// are released.
415 fn reset_device_graph(&self) -> Result<bool> {
416 Ok(false)
417 }
418
419 /// Read (without clearing) any latching device-side capture-safety error a
420 /// captured kernel recorded during graph replay, as a raw violation bitmask
421 /// (zero when none). EPs without device graphs report no error.
422 ///
423 /// The decode loop calls this at the per-step logits device→host sync so an
424 /// out-of-range bounds violation becomes a hard error before the produced
425 /// token is consumed, without adding a separate synchronization.
426 fn check_device_capture_error(&self) -> Result<u32> {
427 Ok(0)
428 }
429
430 /// Explicit device allocation/free counters, when the EP exposes them.
431 fn device_allocation_counts(&self) -> Option<(u64, u64)> {
432 None
433 }
434
435 /// Synchronously upload host bytes into a buffer owned by this EP.
436 fn copy_from_host(&self, src: &[u8], dst: &mut DeviceBuffer) -> Result<()> {
437 if !dst.device().is_host_accessible() {
438 return Err(EpError::KernelFailed(format!(
439 "{}: host upload is not implemented for device {:?}",
440 self.name(),
441 dst.device()
442 )));
443 }
444 if src.len() > dst.len() {
445 return Err(EpError::KernelFailed(format!(
446 "{}: host upload of {} bytes exceeds destination {} bytes",
447 self.name(),
448 src.len(),
449 dst.len()
450 )));
451 }
452 if src.is_empty() {
453 return Ok(());
454 }
455 // SAFETY: host accessibility is checked above, `dst` is uniquely
456 // borrowed, and its allocation is at least `src.len()` bytes.
457 unsafe {
458 std::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr().cast(), src.len());
459 }
460 Ok(())
461 }
462
463 /// Synchronously upload host bytes into a byte range of a buffer owned by
464 /// this EP.
465 fn copy_from_host_at(
466 &self,
467 src: &[u8],
468 dst: &mut DeviceBuffer,
469 byte_offset: usize,
470 ) -> Result<()> {
471 let end = byte_offset.checked_add(src.len()).ok_or_else(|| {
472 EpError::KernelFailed(format!("{}: host upload range overflows", self.name()))
473 })?;
474 if end > dst.len() {
475 return Err(EpError::KernelFailed(format!(
476 "{}: host upload range {byte_offset}..{end} exceeds destination {} bytes",
477 self.name(),
478 dst.len()
479 )));
480 }
481 if src.is_empty() {
482 return Ok(());
483 }
484 if !dst.device().is_host_accessible() {
485 return Err(EpError::KernelFailed(format!(
486 "{}: ranged host upload is not implemented for device {:?}",
487 self.name(),
488 dst.device()
489 )));
490 }
491 // SAFETY: host accessibility and bounds are checked above, and `dst` is
492 // uniquely borrowed for the duration of the copy.
493 unsafe {
494 std::ptr::copy_nonoverlapping(
495 src.as_ptr(),
496 dst.as_mut_ptr().cast::<u8>().add(byte_offset),
497 src.len(),
498 );
499 }
500 Ok(())
501 }
502
503 /// Synchronously download a buffer owned by this EP into host bytes.
504 fn copy_to_host(&self, src: &DeviceBuffer, dst: &mut [u8]) -> Result<()> {
505 if !src.device().is_host_accessible() {
506 return Err(EpError::KernelFailed(format!(
507 "{}: host download is not implemented for device {:?}",
508 self.name(),
509 src.device()
510 )));
511 }
512 if dst.len() > src.len() {
513 return Err(EpError::KernelFailed(format!(
514 "{}: host download of {} bytes exceeds source {} bytes",
515 self.name(),
516 dst.len(),
517 src.len()
518 )));
519 }
520 if dst.is_empty() {
521 return Ok(());
522 }
523 // SAFETY: host accessibility is checked above, `dst` is uniquely
524 // borrowed, and `src` contains at least `dst.len()` readable bytes.
525 unsafe {
526 std::ptr::copy_nonoverlapping(src.as_ptr().cast(), dst.as_mut_ptr(), dst.len());
527 }
528 Ok(())
529 }
530
531 /// Block until all pending work on this EP completes.
532 fn sync(&self) -> Result<()>;
533
534 /// Export this EP as an ORT C ABI plugin, if supported (Phase 2).
535 fn as_ort_plugin(&self) -> Option<OrtPluginExport> {
536 None
537 }
538
539 /// EP-specific optimization passes, run after the generic optimizer.
540 fn custom_passes(&self) -> Vec<Box<dyn onnx_runtime_optimizer::OptimizationPass>> {
541 Vec::new()
542 }
543
544 /// Nodes this EP claims unconditionally (bypassing cost-model placement).
545 fn claim_nodes(&self, graph: &Graph) -> Vec<NodeId> {
546 let _ = graph;
547 Vec::new()
548 }
549
550 /// The `EPContext` node `source` key(s) this EP accepts for compiled-context
551 /// dispatch (`docs/ORT2.md` §55.6). The keys come from the EP's own
552 /// config/data — **never** hardcoded in loader/session dispatch. An empty
553 /// list (the default) means the EP does not participate in `EPContext`
554 /// (e.g. the pure-Rust CPU EP has no compile step).
555 fn context_source_keys(&self) -> Vec<String> {
556 Vec::new()
557 }
558
559 /// Produce the runtime [`EpContext`] for this EP's freshly compiled subgraph
560 /// (the §55.4 dump path calls this). Default: unsupported — an EP with no
561 /// compile step returns [`EpError::UnsupportedContext`].
562 fn save_context(&self) -> Result<EpContext> {
563 Err(EpError::UnsupportedContext {
564 ep: self.name().to_string(),
565 })
566 }
567
568 /// Restore this EP from a runtime [`EpContext`], skipping convert+compile
569 /// (the §55.3 load path calls this). Default: unsupported — an EP that does
570 /// not consume `EPContext` returns [`EpError::UnsupportedContext`].
571 fn load_context(&self, ctx: &EpContext) -> Result<()> {
572 let _ = ctx;
573 Err(EpError::UnsupportedContext {
574 ep: self.name().to_string(),
575 })
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 fn _assert_send_sync<T: Send + Sync>() {}
584
585 /// Leak a boxed byte slice as a stand-in host allocation.
586 fn host_alloc(size: usize, align: usize) -> DeviceBuffer {
587 let boxed = vec![0u8; size].into_boxed_slice();
588 let ptr = Box::into_raw(boxed) as *mut c_void;
589 // SAFETY: `ptr` is a valid, unique, non-null allocation of `size` bytes
590 // on the host, aligned to the allocator's guarantee (>= 1); we treat the
591 // CPU EP as its owner and free it exactly once in `host_free`.
592 unsafe { DeviceBuffer::from_raw_parts(ptr, DeviceId::cpu(), size, align) }
593 }
594
595 fn host_free(buf: DeviceBuffer) {
596 let size = buf.len();
597 let ptr = buf.into_raw() as *mut u8;
598 // SAFETY: reconstruct the exact `Box<[u8]>` leaked in `host_alloc` so it
599 // is freed once. `into_raw` consumed the handle, so no alias remains.
600 unsafe {
601 drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, size)));
602 }
603 }
604
605 #[test]
606 fn device_buffer_is_send_sync() {
607 _assert_send_sync::<DeviceBuffer>();
608 }
609
610 #[test]
611 fn buffer_metadata_and_single_free() {
612 let mut buf = host_alloc(128, 64);
613 assert_eq!(buf.len(), 128);
614 assert!(!buf.is_empty());
615 assert_eq!(buf.alignment(), 64);
616 assert_eq!(buf.device(), DeviceId::cpu());
617 assert!(!buf.as_ptr().is_null());
618 assert!(!buf.as_mut_ptr().is_null());
619 // Single free path — a double free here would trip ASan/Miri.
620 host_free(buf);
621 }
622
623 #[test]
624 fn buffer_moves_across_thread() {
625 let buf = host_alloc(64, 16);
626 let base = buf.as_ptr() as usize;
627 let handle = std::thread::spawn(move || {
628 assert_eq!(buf.len(), 64);
629 assert_eq!(buf.as_ptr() as usize, base);
630 buf // hand ownership back so the main thread frees it once
631 });
632 let buf = handle.join().unwrap();
633 host_free(buf);
634 }
635
636 #[test]
637 fn owned_buffer_is_not_borrowed() {
638 let buf = host_alloc(32, 16);
639 assert!(
640 !buf.is_borrowed(),
641 "from_raw_parts must produce an owned buffer"
642 );
643 host_free(buf);
644 }
645
646 /// A borrowed buffer aliases memory owned by someone else (here a `Vec`):
647 /// it reports `is_borrowed()`, exposes the aliased pointer, and consuming it
648 /// via `into_raw` must NOT free the backing — the `Vec` stays valid.
649 #[test]
650 fn borrowed_buffer_aliases_without_owning() {
651 let mut backing = vec![7u8; 64];
652 let ptr = backing.as_mut_ptr() as *mut c_void;
653 // SAFETY: `ptr`/`len` name `backing`'s live allocation (aligned to 1);
654 // `backing` outlives the buffer and every use below, and we never write
655 // through the borrowed handle.
656 let buf = unsafe { DeviceBuffer::from_borrowed_parts(ptr, DeviceId::cpu(), 64, 1) };
657 assert!(buf.is_borrowed());
658 assert_eq!(buf.len(), 64);
659 assert_eq!(buf.as_ptr(), ptr as *const c_void);
660 // Consume without freeing: `into_raw` must never free a borrowed buffer.
661 let raw = buf.into_raw();
662 assert_eq!(raw, ptr);
663 // `backing` is still fully valid — a free would be a use-after-free here.
664 assert!(backing.iter().all(|&b| b == 7));
665 backing[0] = 9;
666 assert_eq!(backing[0], 9);
667 }
668
669 #[test]
670 fn borrowed_mut_buffer_writes_without_owning() {
671 let mut backing = vec![0u8; 8];
672 let ptr = backing.as_mut_ptr() as *mut c_void;
673 // SAFETY: `backing` exclusively owns this writable region and outlives
674 // the temporary alias.
675 let mut buffer =
676 unsafe { DeviceBuffer::from_borrowed_mut_parts(ptr, DeviceId::cpu(), 8, 1) }
677 .expect("non-null backing");
678 assert!(buffer.is_borrowed());
679 // SAFETY: the alias has exclusive access to all eight backing bytes.
680 unsafe {
681 std::ptr::copy_nonoverlapping([1u8, 2, 3].as_ptr(), buffer.as_mut_ptr().cast(), 3);
682 }
683 assert_eq!(buffer.into_raw(), ptr);
684 assert_eq!(&backing[..3], &[1, 2, 3]);
685 assert!(
686 unsafe {
687 DeviceBuffer::from_borrowed_mut_parts(std::ptr::null_mut(), DeviceId::cpu(), 0, 1)
688 }
689 .is_none()
690 );
691 }
692}