use std::ffi::{c_int, c_void, CString};
use std::sync::Arc;
use crate::cudarc_shim::{ctx, device, module, pool, primary_ctx, stream};
use crate::error::*;
use crate::init;
#[derive(Clone, Copy, Debug)]
pub struct LaunchConfig {
pub grid_dim: (u32, u32, u32),
pub block_dim: (u32, u32, u32),
pub shared_mem_bytes: u32,
}
pub trait ForeignOwner: Send + Sync + 'static {}
impl<T: Send + Sync + 'static> ForeignOwner for T {}
#[derive(Clone, Default)]
pub struct KeepAlive(Option<Arc<dyn ForeignOwner>>);
impl KeepAlive {
pub fn none() -> Self {
Self(None)
}
pub fn owner(owner: Arc<dyn ForeignOwner>) -> Self {
Self(Some(owner))
}
}
impl std::fmt::Debug for KeepAlive {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(if self.0.is_some() {
"KeepAlive(owner)"
} else {
"KeepAlive(none)"
})
}
}
impl PartialEq for KeepAlive {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl Eq for KeepAlive {}
#[derive(Debug)]
pub struct Device {
pub(crate) cu_device: cuda_bindings::CUdevice,
pub(crate) cu_ctx: cuda_bindings::CUcontext,
pub(crate) ordinal: usize,
owned: bool,
_keep_alive: KeepAlive,
}
unsafe impl Send for Device {}
unsafe impl Sync for Device {}
impl Drop for Device {
fn drop(&mut self) {
if !self.owned {
return;
}
let _guard = teardown_lock();
stream_pool()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.remove(&self.ordinal);
let _ = self.bind_to_thread();
let ctx = std::mem::replace(&mut self.cu_ctx, std::ptr::null_mut());
if !ctx.is_null() {
let _ = unsafe { primary_ctx::release(self.cu_device) };
}
}
}
impl PartialEq for Device {
fn eq(&self, other: &Self) -> bool {
self.cu_device == other.cu_device
&& self.cu_ctx == other.cu_ctx
&& self.ordinal == other.ordinal
}
}
impl Eq for Device {}
fn ordinal_to_c_int(ordinal: usize) -> Result<c_int, DriverError> {
c_int::try_from(ordinal)
.map_err(|_| DriverError(cuda_bindings::cudaError_enum_CUDA_ERROR_INVALID_DEVICE))
}
impl Device {
pub fn new(ordinal: usize) -> Result<Arc<Self>, DriverError> {
let cu_ordinal = ordinal_to_c_int(ordinal)?;
unsafe { init(0)? };
let cu_device = device::get(cu_ordinal)?;
let cu_ctx = unsafe { primary_ctx::retain(cu_device) }?;
let device = Arc::new(Device {
cu_device,
cu_ctx,
ordinal,
owned: true,
_keep_alive: KeepAlive::none(),
});
device.bind_to_thread()?;
Ok(device)
}
pub unsafe fn borrow_raw(cu_ctx: *mut c_void, cu_device: c_int, ordinal: usize) -> Arc<Self> {
Arc::new(Device {
cu_device: cu_device as cuda_bindings::CUdevice,
cu_ctx: cu_ctx as cuda_bindings::CUcontext,
ordinal,
owned: false,
_keep_alive: KeepAlive::none(),
})
}
pub unsafe fn borrow_with_owner(
cu_ctx: *mut c_void,
cu_device: c_int,
ordinal: usize,
owner: Arc<dyn ForeignOwner>,
) -> Arc<Self> {
Arc::new(Device {
cu_device: cu_device as cuda_bindings::CUdevice,
cu_ctx: cu_ctx as cuda_bindings::CUcontext,
ordinal,
owned: false,
_keep_alive: KeepAlive::owner(owner),
})
}
pub fn device_count() -> Result<i32, DriverError> {
unsafe { init(0)? };
device::get_count()
}
pub fn raw_device(ordinal: usize) -> Result<cuda_bindings::CUdevice, DriverError> {
let cu_ordinal = ordinal_to_c_int(ordinal)?;
unsafe { init(0)? };
device::get(cu_ordinal)
}
pub fn ordinal(&self) -> usize {
self.ordinal
}
pub fn name(&self) -> Result<String, DriverError> {
device::get_name(self.cu_device)
}
pub fn cu_device(&self) -> cuda_bindings::CUdevice {
self.cu_device
}
pub fn cu_ctx(&self) -> cuda_bindings::CUcontext {
self.cu_ctx
}
pub fn bind_to_thread(&self) -> Result<(), DriverError> {
if match ctx::get_current()? {
Some(curr_ctx) => curr_ctx != self.cu_ctx,
None => true,
} {
unsafe { ctx::set_current(self.cu_ctx) }?;
}
Ok(())
}
pub unsafe fn synchronize(&self) -> Result<(), DriverError> {
ctx::synchronize()
}
pub fn new_stream(self: &Arc<Self>) -> Result<Arc<Stream>, DriverError> {
self.bind_to_thread()?;
let pooled = if self.owned {
stream_pool()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get_mut(&self.ordinal)
.and_then(Vec::pop)
} else {
None
};
let cu_stream = match pooled {
Some(handle) => handle as cuda_bindings::CUstream,
None => stream::create(stream::StreamKind::NonBlocking)?,
};
Ok(Arc::new(Stream {
cu_stream,
device: self.clone(),
owned: true,
_keep_alive: KeepAlive::none(),
}))
}
pub fn load_module_from_ptx_src(
self: &Arc<Self>,
ptx_src: &str,
) -> Result<Arc<Module>, DriverError> {
self.bind_to_thread()?;
let cu_module = {
let c_src = CString::new(ptx_src).unwrap();
unsafe { module::load_data(c_src.as_ptr() as *const _) }
}?;
Ok(Arc::new(Module {
cu_module,
device: self.clone(),
owned: true,
}))
}
pub fn load_module_from_file(
self: &Arc<Self>,
filename: &str,
) -> Result<Arc<Module>, DriverError> {
self.bind_to_thread()?;
let cu_module = { module::load(filename) }?;
Ok(Arc::new(Module {
cu_module,
device: self.clone(),
owned: true,
}))
}
pub unsafe fn load_module_from_bytes(
self: &Arc<Self>,
image: &[u8],
) -> Result<Arc<Module>, DriverError> {
if !image.starts_with(b"\x7fELF") {
return Err(DriverError(
cuda_bindings::cudaError_enum_CUDA_ERROR_INVALID_IMAGE,
));
}
self.bind_to_thread()?;
let cu_module = unsafe { module::load_data(image.as_ptr().cast()) }?;
Ok(Arc::new(Module {
cu_module,
device: self.clone(),
owned: true,
}))
}
pub fn new_mem_pool(self: &Arc<Self>) -> Result<Arc<MemPool>, DriverError> {
self.bind_to_thread()?;
let mut props: cuda_bindings::CUmemPoolProps = unsafe { std::mem::zeroed() };
props.allocType = cuda_bindings::CUmemAllocationType_enum_CU_MEM_ALLOCATION_TYPE_PINNED;
props.handleTypes = cuda_bindings::CUmemAllocationHandleType_enum_CU_MEM_HANDLE_TYPE_NONE;
props.location.type_ = cuda_bindings::CUmemLocationType_enum_CU_MEM_LOCATION_TYPE_DEVICE;
cuda_bindings::set_mem_location_id(&mut props.location, self.ordinal as c_int);
let cu_pool = unsafe { pool::create(&props) }?;
Ok(Arc::new(MemPool {
cu_pool,
device: self.clone(),
owned: true,
}))
}
pub fn default_mem_pool(self: &Arc<Self>) -> Result<Arc<MemPool>, DriverError> {
self.bind_to_thread()?;
let cu_pool = unsafe { pool::get_default(self.cu_device) }?;
Ok(Arc::new(MemPool {
cu_pool,
device: self.clone(),
owned: false,
}))
}
}
#[derive(Debug)]
pub struct MemPool {
pub(crate) cu_pool: cuda_bindings::CUmemoryPool,
pub(crate) device: Arc<Device>,
owned: bool,
}
unsafe impl Send for MemPool {}
unsafe impl Sync for MemPool {}
impl Drop for MemPool {
fn drop(&mut self) {
if !self.owned {
return;
}
let _ = self.device.bind_to_thread();
let _ = unsafe { pool::destroy(self.cu_pool) };
}
}
impl MemPool {
pub fn cu_pool(&self) -> cuda_bindings::CUmemoryPool {
self.cu_pool
}
pub fn device(&self) -> &Arc<Device> {
&self.device
}
pub fn set_release_threshold(&self, threshold: u64) -> Result<(), DriverError> {
self.device.bind_to_thread()?;
unsafe { pool::set_release_threshold(self.cu_pool, threshold) }
}
pub fn mem_stats(&self) -> Result<PoolMemStats, DriverError> {
self.device.bind_to_thread()?;
unsafe {
Ok(PoolMemStats {
used_current: pool::get_attribute_u64(
self.cu_pool,
cuda_bindings::CUmemPool_attribute_enum_CU_MEMPOOL_ATTR_USED_MEM_CURRENT,
)?,
used_high: pool::get_attribute_u64(
self.cu_pool,
cuda_bindings::CUmemPool_attribute_enum_CU_MEMPOOL_ATTR_USED_MEM_HIGH,
)?,
reserved_current: pool::get_attribute_u64(
self.cu_pool,
cuda_bindings::CUmemPool_attribute_enum_CU_MEMPOOL_ATTR_RESERVED_MEM_CURRENT,
)?,
reserved_high: pool::get_attribute_u64(
self.cu_pool,
cuda_bindings::CUmemPool_attribute_enum_CU_MEMPOOL_ATTR_RESERVED_MEM_HIGH,
)?,
})
}
}
pub fn reset_used_high(&self) -> Result<(), DriverError> {
self.device.bind_to_thread()?;
unsafe {
pool::reset_high_watermark(
self.cu_pool,
cuda_bindings::CUmemPool_attribute_enum_CU_MEMPOOL_ATTR_USED_MEM_HIGH,
)
}
}
pub fn reset_reserved_high(&self) -> Result<(), DriverError> {
self.device.bind_to_thread()?;
unsafe {
pool::reset_high_watermark(
self.cu_pool,
cuda_bindings::CUmemPool_attribute_enum_CU_MEMPOOL_ATTR_RESERVED_MEM_HIGH,
)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PoolMemStats {
pub used_current: u64,
pub used_high: u64,
pub reserved_current: u64,
pub reserved_high: u64,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Stream {
pub(crate) cu_stream: cuda_bindings::CUstream,
pub(crate) device: Arc<Device>,
owned: bool,
_keep_alive: KeepAlive,
}
fn stream_pool() -> &'static std::sync::Mutex<std::collections::HashMap<usize, Vec<usize>>> {
static POOL: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashMap<usize, Vec<usize>>>,
> = std::sync::OnceLock::new();
POOL.get_or_init(Default::default)
}
pub(crate) fn teardown_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
LOCK.get_or_init(|| std::sync::Mutex::new(()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
unsafe impl Send for Stream {}
unsafe impl Sync for Stream {}
pub struct Event {
cu_event: cuda_bindings::CUevent,
device: Arc<Device>,
}
unsafe impl Send for Event {}
unsafe impl Sync for Event {}
impl Event {
pub fn record(&self, stream: &Arc<Stream>) -> Result<(), DriverError> {
if stream.device().ordinal() != self.device.ordinal() {
return Err(DriverError(
cuda_bindings::cudaError_enum_CUDA_ERROR_INVALID_VALUE,
));
}
unsafe { crate::cudarc_shim::event::record(self.cu_event, stream.cu_stream()) }
}
pub fn synchronize(&self) -> Result<(), DriverError> {
unsafe { crate::cudarc_shim::event::synchronize(self.cu_event) }
}
pub fn query(&self) -> Result<bool, DriverError> {
match unsafe { crate::cudarc_shim::event::query(self.cu_event) } {
Ok(()) => Ok(true),
Err(DriverError(cuda_bindings::cudaError_enum_CUDA_ERROR_NOT_READY)) => Ok(false),
Err(e) => Err(e),
}
}
pub fn elapsed_time(&self, end: &Event) -> Result<f32, DriverError> {
unsafe { crate::cudarc_shim::event::elapsed(self.cu_event, end.cu_event) }
}
}
impl Drop for Event {
fn drop(&mut self) {
let _ = unsafe { crate::cudarc_shim::event::destroy(self.cu_event) };
}
}
impl Device {
pub fn new_event(self: &Arc<Self>) -> Result<Event, DriverError> {
self.bind_to_thread()?;
let cu_event =
crate::cudarc_shim::event::create(cuda_bindings::CUevent_flags_enum_CU_EVENT_DEFAULT)?;
Ok(Event {
cu_event,
device: self.clone(),
})
}
pub fn l2_cache_size_bytes(&self) -> Result<usize, DriverError> {
let mut value: core::ffi::c_int = 0;
unsafe {
cuda_bindings::cuDeviceGetAttribute(
&mut value,
cuda_bindings::CUdevice_attribute_enum_CU_DEVICE_ATTRIBUTE_L2_CACHE_SIZE,
self.cu_device,
)
.result()?;
}
Ok(value.max(0) as usize)
}
}
impl Drop for Stream {
fn drop(&mut self) {
if !self.owned || self.cu_stream.is_null() {
return;
}
let _ = self.device.bind_to_thread();
if self.device.owned {
let _ = unsafe { stream::synchronize(self.cu_stream) };
stream_pool()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.entry(self.device.ordinal)
.or_default()
.push(self.cu_stream as usize);
} else {
let _guard = teardown_lock();
let _ = unsafe { stream::synchronize(self.cu_stream) };
let _ = unsafe { stream::destroy(self.cu_stream) };
}
}
}
impl Stream {
pub unsafe fn borrow_raw(cu_stream: *mut c_void, device: &Arc<Device>) -> Arc<Self> {
Arc::new(Stream {
cu_stream: cu_stream as cuda_bindings::CUstream,
device: device.clone(),
owned: false,
_keep_alive: KeepAlive::none(),
})
}
pub unsafe fn borrow_with_owner(
cu_stream: *mut c_void,
device: &Arc<Device>,
owner: Arc<dyn ForeignOwner>,
) -> Arc<Self> {
Arc::new(Stream {
cu_stream: cu_stream as cuda_bindings::CUstream,
device: device.clone(),
owned: false,
_keep_alive: KeepAlive::owner(owner),
})
}
pub fn cu_stream(&self) -> cuda_bindings::CUstream {
self.cu_stream
}
pub fn device(&self) -> &Arc<Device> {
&self.device
}
pub unsafe fn synchronize(&self) -> Result<(), DriverError> {
stream::synchronize(self.cu_stream)
}
pub unsafe fn query(&self) -> Result<bool, DriverError> {
stream::query(self.cu_stream)
}
pub fn wait_event(&self, event: &Event) -> Result<(), DriverError> {
if event.device.ordinal() != self.device.ordinal() {
return Err(DriverError(
cuda_bindings::cudaError_enum_CUDA_ERROR_INVALID_VALUE,
));
}
unsafe {
stream::wait_event(
self.cu_stream,
event.cu_event,
cuda_bindings::CUevent_wait_flags_enum_CU_EVENT_WAIT_DEFAULT,
)
}
}
pub unsafe fn launch_host_function<F: FnOnce() + Send + 'static>(
&self,
host_func: F,
) -> Result<(), DriverError> {
Self::enqueue_boxed(host_func, |func, arg| unsafe {
stream::launch_host_function(self.cu_stream, func, arg)
})
}
pub unsafe fn launch_host_function_with_sync_mode<F: FnOnce() + Send + 'static>(
&self,
host_func: F,
sync_mode: ::core::ffi::c_uint,
) -> Result<(), DriverError> {
Self::enqueue_boxed(host_func, |func, arg| unsafe {
stream::launch_host_function_v2(self.cu_stream, func, arg, sync_mode)
})
}
fn enqueue_boxed<F: FnOnce() + Send + 'static>(
host_func: F,
enqueue: impl FnOnce(unsafe extern "C" fn(*mut c_void), *mut c_void) -> Result<(), DriverError>,
) -> Result<(), DriverError> {
let user_data = Box::into_raw(Box::new(host_func)).cast::<c_void>();
let result = enqueue(Self::callback_wrapper::<F>, user_data);
if result.is_err() {
drop(unsafe { Box::from_raw(user_data.cast::<F>()) });
}
result
}
unsafe extern "C" fn callback_wrapper<F: FnOnce() + Send + 'static>(callback: *mut c_void) {
let _ = std::panic::catch_unwind(|| {
let callback: Box<F> = unsafe { Box::from_raw(callback.cast::<F>()) };
callback();
});
}
pub unsafe fn begin_capture(
&self,
mode: cuda_bindings::CUstreamCaptureMode,
) -> Result<(), DriverError> {
stream::begin_capture(self.cu_stream, mode)
}
pub unsafe fn end_capture(&self) -> Result<cuda_bindings::CUgraph, DriverError> {
stream::end_capture(self.cu_stream)
}
}
#[derive(Debug)]
pub struct Module {
pub(crate) cu_module: cuda_bindings::CUmodule,
pub(crate) device: Arc<Device>,
owned: bool,
}
unsafe impl Send for Module {}
unsafe impl Sync for Module {}
impl Drop for Module {
fn drop(&mut self) {
if !self.owned {
return;
}
let _guard = teardown_lock();
let _ = self.device.bind_to_thread();
let _ = unsafe { module::unload(self.cu_module) };
}
}
impl Module {
pub unsafe fn borrow_raw(cu_module: *mut c_void, device: &Arc<Device>) -> Arc<Self> {
Arc::new(Module {
cu_module: cu_module as cuda_bindings::CUmodule,
device: device.clone(),
owned: false,
})
}
pub fn cu_module(&self) -> cuda_bindings::CUmodule {
self.cu_module
}
pub fn load_function(self: &Arc<Self>, fn_name: &str) -> Result<Function, DriverError> {
let cu_function = unsafe { module::get_function(self.cu_module, fn_name) }?;
Ok(Function {
cu_function,
module: self.clone(),
})
}
}
#[derive(Debug, Clone)]
pub struct Function {
pub(crate) cu_function: cuda_bindings::CUfunction,
#[allow(unused)]
pub(crate) module: Arc<Module>,
}
unsafe impl Send for Function {}
unsafe impl Sync for Function {}
impl Function {
pub unsafe fn borrow_raw(cu_function: *mut c_void, module: &Arc<Module>) -> Function {
Function {
cu_function: cu_function as cuda_bindings::CUfunction,
module: module.clone(),
}
}
pub unsafe fn cu_function(&self) -> cuda_bindings::CUfunction {
self.cu_function
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct Probe {
calls: Arc<AtomicUsize>,
drops: Arc<AtomicUsize>,
}
impl Probe {
fn new() -> (Self, Arc<AtomicUsize>, Arc<AtomicUsize>) {
let calls = Arc::new(AtomicUsize::new(0));
let drops = Arc::new(AtomicUsize::new(0));
let probe = Probe {
calls: calls.clone(),
drops: drops.clone(),
};
(probe, calls, drops)
}
}
impl Drop for Probe {
fn drop(&mut self) {
self.drops.fetch_add(1, Ordering::SeqCst);
}
}
#[test]
fn refused_host_function_launch_reclaims_the_boxed_closure() {
let (probe, calls, drops) = Probe::new();
let refused = DriverError(cuda_bindings::cudaError_enum_CUDA_ERROR_INVALID_HANDLE);
let result = Stream::enqueue_boxed(
move || {
probe.calls.fetch_add(1, Ordering::SeqCst);
},
|_trampoline, _user_data| Err(refused),
);
assert_eq!(result, Err(refused), "the driver's error must surface");
assert_eq!(
calls.load(Ordering::SeqCst),
0,
"a refused launch never runs"
);
assert_eq!(
drops.load(Ordering::SeqCst),
1,
"the closure and its captures must be dropped exactly once"
);
}
#[test]
fn accepted_host_function_launch_hands_the_box_to_the_trampoline() {
let (probe, calls, drops) = Probe::new();
let result = Stream::enqueue_boxed(
move || {
probe.calls.fetch_add(1, Ordering::SeqCst);
},
|trampoline, user_data| {
unsafe { trampoline(user_data) };
Ok(())
},
);
assert_eq!(result, Ok(()));
assert_eq!(calls.load(Ordering::SeqCst), 1, "the callback runs once");
assert_eq!(
drops.load(Ordering::SeqCst),
1,
"the trampoline is the sole reclaimer on success: no leak, no double free"
);
}
fn has_gpu() -> bool {
Device::device_count().map(|n| n > 0).unwrap_or(false)
}
fn pool_tests_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn pooled_handles(ordinal: usize) -> Vec<usize> {
stream_pool()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get(&ordinal)
.cloned()
.unwrap_or_default()
}
fn borrow(owner: &Arc<Device>) -> Arc<Device> {
unsafe {
Device::borrow_raw(
owner.cu_ctx().cast(),
owner.cu_device() as c_int,
owner.ordinal(),
)
}
}
#[test]
fn ordinals_beyond_c_int_are_rejected_not_truncated() {
let invalid = DriverError(cuda_bindings::cudaError_enum_CUDA_ERROR_INVALID_DEVICE);
assert_eq!(Device::new(usize::MAX).err(), Some(invalid));
assert_eq!(Device::raw_device(usize::MAX).err(), Some(invalid));
let wraps_to_zero = (c_int::MAX as usize) + 1 + (c_int::MAX as usize) + 1;
assert_eq!(Device::new(wraps_to_zero).err(), Some(invalid));
assert_eq!(ordinal_to_c_int(0), Ok(0));
assert_eq!(ordinal_to_c_int(c_int::MAX as usize), Ok(c_int::MAX));
}
#[test]
fn owned_device_streams_are_parked_and_reused() {
if !has_gpu() {
return;
}
let _serialized = pool_tests_lock();
let owner = Device::new(0).unwrap();
let first = owner.new_stream().unwrap();
let handle = first.cu_stream() as usize;
drop(first);
assert!(
pooled_handles(0).contains(&handle),
"an owned device's stream is parked, not destroyed"
);
let second = owner.new_stream().unwrap();
assert_eq!(second.cu_stream() as usize, handle, "and handed out again");
}
#[test]
fn borrowed_device_streams_never_enter_the_pool() {
if !has_gpu() {
return;
}
let _serialized = pool_tests_lock();
let owner = Device::new(0).unwrap();
let parked = {
let seed = owner.new_stream().unwrap();
seed.cu_stream() as usize
};
assert!(pooled_handles(0).contains(&parked));
let borrowed = borrow(&owner);
let stream = borrowed.new_stream().unwrap();
let handle = stream.cu_stream() as usize;
assert_ne!(
handle, parked,
"a borrowed device must not pop a pooled handle: the pool's handles \
belong to contexts this crate retained, not to the borrowed one"
);
assert!(
pooled_handles(0).contains(&parked),
"and leaves the pool as it was"
);
unsafe { stream.synchronize() }.unwrap();
drop(stream);
assert!(
!pooled_handles(0).contains(&handle),
"a borrowed device's stream must be destroyed on drop, never parked \
where a later owned Device for this ordinal could pop it"
);
assert!(pooled_handles(0).contains(&parked));
}
#[test]
fn panicking_host_function_is_caught_and_still_reclaimed() {
let (probe, calls, drops) = Probe::new();
let result = Stream::enqueue_boxed(
move || {
probe.calls.fetch_add(1, Ordering::SeqCst);
panic!("callback panic must not cross the C ABI");
},
|trampoline, user_data| {
unsafe { trampoline(user_data) };
Ok(())
},
);
assert_eq!(result, Ok(()));
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_eq!(drops.load(Ordering::SeqCst), 1);
}
}