Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
raphaelcohn opened this issue Aug 1, 2021 · 1 comment
Open

Comments

@raphaelcohn
Copy link

raphaelcohn commented Aug 1, 2021

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
Copy link
Contributor

elmarco commented Aug 11, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants