For interest: A way to eleminate the need for the Context Trait
raphaelcohn opened this issue · 1 comments
raphaelcohn commented
This code is quite involved. In essence, it uses a modified Arc that behaves differently on drop when dealing with a libusb default (null) context.
It makes extensive use of nightly Rust (allocator related APIs mostly) so won't be suitable for projects that are averse to that.
I thought you might find it useful for the future. It's not battle-tested - more of a suggested approach.
/// A libusb context.
#[derive(Debug)]
#[repr(transparent)]
pub struct Context(NonNull<ContextInner>);
unsafe impl Send for Context
{
}
unsafe impl Sync for Context
{
}
impl Drop for Context
{
#[inline(always)]
fn drop(&mut self)
{
let (previous_reference_count, wraps_default_libusb_context) = self.inner().decrement();
let previous_reference_count = previous_reference_count.get();
if wraps_default_libusb_context
{
if previous_reference_count == (ContextInner::MinimumReferenceCount + 1)
{
self.uninitialize();
}
else if previous_reference_count == ContextInner::MinimumReferenceCount
{
self.free();
}
}
else
{
if unlikely!(previous_reference_count == ContextInner::MinimumReferenceCount)
{
self.uninitialize();
self.free();
}
}
}
}
impl Clone for Context
{
#[inline(always)]
fn clone(&self) -> Self
{
self.fallible_clone().expect("Could not reinitialize (but this should not be possible as the default context always reinitializes on first use once the reference count has fallen to 1)")
}
}
impl Context
{
/// The default context.
#[inline(always)]
pub fn default() -> Result<Self, ContextInitializationError>
{
static Cell: SyncOnceCell<Context> = SyncOnceCell::new();
let reference = Cell.get_or_try_init(|| Self::wrap_libusb_context(null_mut()))?;
reference.fallible_clone()
}
/// A specialized context.
#[inline(always)]
pub fn new() -> Result<Self, ContextInitializationError>
{
let mut libusb_context = MaybeUninit::uninit();
ContextInner::initialize(libusb_context.as_mut_ptr())?;
let libusb_context = unsafe { libusb_context.assume_init() };
match Self::wrap_libusb_context(libusb_context)
{
Ok(this) => Ok(this),
Err(error) =>
{
unsafe { libusb_exit(libusb_context) }
Err(ContextInitializationError::CouldNotAllocateMemoryInRust(error))
}
}
}
#[inline(always)]
pub(crate) fn as_ptr(&self) -> *mut libusb_context
{
self.inner().libusb_context
}
#[inline(always)]
fn fallible_clone(&self) -> Result<Self, ContextInitializationError>
{
let (previous_reference_count, wraps_default_libusb_context) = self.inner().increment();
if wraps_default_libusb_context
{
if unlikely!(previous_reference_count == ContextInner::MinimumReferenceCount)
{
ContextInner::reinitialize()?
}
}
Ok(Self(self.0))
}
/// `libusb_context` will NOT have been initialized if it is the default (null) context.
/// `libusb_context` will have been initialized if it is the default (null) context.
fn wrap_libusb_context(libusb_context: *mut libusb_context) -> Result<Self, AllocError>
{
let slice = Global.allocate(Self::layout())?;
let inner: NonNull<ContextInner> = slice.as_non_null_ptr().cast();
unsafe
{
inner.as_ptr().write
(
ContextInner
{
libusb_context,
reference_count: AtomicUsize::new(ContextInner::MinimumReferenceCount)
}
)
};
Ok(Self(inner))
}
#[inline(always)]
fn uninitialize(&self)
{
self.inner().uninitialize();
}
#[inline(always)]
fn free(&self)
{
unsafe { Global.deallocate(self.0.cast(), Self::layout()) }
}
#[inline(always)]
fn inner<'a>(&self) -> &'a ContextInner
{
unsafe { & * (self.0.as_ptr()) }
}
#[inline(always)]
const fn layout() -> Layout
{
Layout::new::<ContextInner>()
}
}
/// Designed so that when the default libusb context is held in a static variable, such as a SyncLazyCell, libusb_exit() is still called even though a static reference is held.
///
/// Also designed so that if a static reference is then later used after being the only held reference, the libusb default context is re-initialized.
#[derive(Debug)]
struct ContextInner
{
libusb_context: *mut libusb_context,
reference_count: AtomicUsize,
}
impl ContextInner
{
const MinimumReferenceCount: usize = 1;
const NoReferenceCount: usize = Self::MinimumReferenceCount - 1;
const ReferenceChange: usize = 1;
#[inline(always)]
fn decrement(&self) -> (NonZeroUsize, bool)
{
debug_assert_ne!(self.current_reference_count(), Self::NoReferenceCount);
let previous_reference_count = self.reference_count.fetch_sub(Self::ReferenceChange, SeqCst);
(new_non_zero_usize(previous_reference_count), self.is_default_libusb_context())
}
#[inline(always)]
fn increment(&self) -> (usize, bool)
{
debug_assert_ne!(self.current_reference_count(), Self::NoReferenceCount);
let previous_reference_count = self.reference_count.fetch_add(Self::ReferenceChange, SeqCst);
(previous_reference_count, self.is_default_libusb_context())
}
#[inline(always)]
fn reinitialize() -> Result<(), ContextInitializationError>
{
Self::initialize(null_mut())
}
#[inline(always)]
fn uninitialize(&self)
{
debug_assert_eq!(self.current_reference_count(), Self::NoReferenceCount);
unsafe { libusb_exit(self.libusb_context) }
}
#[inline(always)]
fn initialize(libusb_context_pointer: *mut *mut libusb_context) -> Result<(), ContextInitializationError>
{
use ContextInitializationError::*;
let result = unsafe { libusb_init(libusb_context_pointer) };
if likely!(result == 0)
{
Ok(())
}
else if likely!(result < 0)
{
let error = match result
{
LIBUSB_ERROR_IO => InputOutputError,
LIBUSB_ERROR_INVALID_PARAM => unreachable!("Windows and Linux have a 4096 byte transfer limit (including setup byte)"),
LIBUSB_ERROR_ACCESS => AccessDenied,
LIBUSB_ERROR_NO_DEVICE => NoDevice,
LIBUSB_ERROR_NOT_FOUND => RequestedResourceNotFound,
LIBUSB_ERROR_BUSY => unreachable!("Should not have been called from an event handling context"),
LIBUSB_ERROR_TIMEOUT => TimedOut,
LIBUSB_ERROR_OVERFLOW => BufferOverflow,
LIBUSB_ERROR_PIPE => Pipe,
LIBUSB_ERROR_INTERRUPTED => unreachable!("Does not invoke handle_events()"),
LIBUSB_ERROR_NO_MEM => OutOfMemoryInLibusb,
LIBUSB_ERROR_NOT_SUPPORTED => NotSupported,
-98 ..= -13 => panic!("Newly defined error code {}", result),
LIBUSB_ERROR_OTHER => Other,
_ => unreachable!("LIBUSB_ERROR out of range: {}", result)
};
Err(error)
}
else
{
unreachable!("Positive result {} from libusb_init()")
}
}
#[inline(always)]
const fn is_default_libusb_context(&self) -> bool
{
self.libusb_context.is_null()
}
#[inline(always)]
fn current_reference_count(&self) -> usize
{
self.reference_count.load(SeqCst)
}
}
/// A context initialization error.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[allow(missing_docs)]
pub enum ContextInitializationError
{
CouldNotAllocateMemoryInRust(AllocError),
InputOutputError,
AccessDenied,
NoDevice,
RequestedResourceNotFound,
TimedOut,
BufferOverflow,
Pipe,
OutOfMemoryInLibusb,
NotSupported,
Other,
}
impl Display for ContextInitializationError
{
#[inline(always)]
fn fmt(&self, f: &mut Formatter) -> fmt::Result
{
Debug::fmt(self, f)
}
}
impl error::Error for ContextInitializationError
{
#[inline(always)]
fn source(&self) -> Option<&(dyn error::Error + 'static)>
{
use ContextInitializationError::*;
match self
{
CouldNotAllocateMemoryInRust(cause) => Some(cause),
_ => None,
}
}
}
impl From<AllocError> for ContextInitializationError
{
#[inline(always)]
fn from(cause: AllocError) -> Self
{
ContextInitializationError::CouldNotAllocateMemoryInRust(cause)
}
}
elmarco commented
So you would still implement drop? Each global call would effectively open/free the context, then. Perhaps that's ok.
But if we simply want to keep the context open (as it does currently per my understanding), why not simply have a fn global() -> UsbContext
with a static holding the ref? I wonder..