a1ien/rusb

For interest: A way to eleminate the need for the Context Trait

raphaelcohn opened this issue · 1 comments

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)
	}
}

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..