So, you have multiple options, depending on how you want to model it.
Ownership-based
The simplest approach is to let the C stuff take ownership of your thing: that is, you'd be operating with a Box<T>
, for instance, which would be Box::into_raw()
ed before giving the resulting raw pointer to C.
Then, you'd return some newtype wrapping this raw pointer which could expose a back_to_box
kind of functionality, by virtue of first unregistering the pointer w.r.t. the C API (e.g., through some ngSpice_Destroy
function), before calling Box::from_raw()
back on the wrapped pointer.
extern "C" {
fn register(_: *mut c_void);
fn unregister(_: *mut c_void);
}
pub
struct Registered<T> /* = */ (
*mut T,
);
impl<T> From<Box<T>> for Registered<T> {
fn from(b: T)
-> Registered<T>
{
let ptr = Box::into_raw(b);
unsafe { register(ptr.cast()); }
Self(ptr)
}
}
impl<T> Registered<T> {
pub
fn into_inner(self)
-> Box<T>
{
unsafe {
unregister(self.ptr.cast());
Box::from_raw(self.ptr)
}
}
}
- Note: the
Box
here, in and of itself, is not paramout; any owning pointer, even shared ones if you only lend &
access to the inner value, would be fine. So Arc<T>
and/or Weak<T>
would be a convenient way for you to keep access to the T
state while it is being used by the C library.
Borrowing/lifetime-based by using a scoped API
The other approach is to use the previous ownership-based method but for some unrelated-to-pointers data structure, that shall, symbolically/conceptually, represent the FFI state. I'll call it &mut CLibrary.
Its drop glue would unregister all the registered pointers, somehow:
extern "C" { fn unregister_all(...); }
Then, you'd be able to use the inherent 'c_library
as input to a method, at which point you'd be guaranteed that &'c_library T
would be a legitimate non-dangling pointer to use:
pub
struct CLibrary<'c_library> {
/* whatever necessary state to interact with your C library */,
_invariant: PhantomData<*mut &'c_library ()>,
}
impl Drop for CLibrary<'_> {
fn drop(&mut self)
{
unsafe {
unregister_all(self....);
}
}
}
impl<'c_library> CLibrary<'c_library> {
pub
fn register(
&mut self,
value: &'c_library T,
)
{
unsafe { register(<*const _>::cast(value)); }
}
}
Now, this seems fine: 'c_library
must span beyond the point of last usage of the : CLibrary
owned instance, therefore spanning beyond its drop glue, i.e., the point where the registered pointers are unregistered, and we know that our &'c_library
reference and thus pointer won't dangle for that duration/lifetime.
Except... "beyond last usage [...] therefore spanning beyond its drop glue" is WRONG
Indeed, mem::forget(c_library_instance);
is a thing safe code owning the instance could write. That would result in a last usage of the c_library_instance
which would skip the drop glue, and thus skip the necessary-for-soundness unregistering of the pointers.
The solution to this is to never lend ownership of your CLibrary
instance to user code. Which can be done, but only through a scoped / callback-based API;
impl CLibrary<'_> {
pub
fn with_new<R>(scope: impl FnOnce(_: &'_ mut CLibrary<'_>) -> R)
-> R
{
/* only this local function, which we control, owns the `CLibrary` instance, and we know we don't `forget(it)` */
let it = CLibrary { ... };
scope(&mut CLibrary)
}
}
Using Pin
guarantees
Now, a scoped API can be quite cumbersome to use; and we only did that because of the need for something to unregister stuff before it is dropped, without room for forget
shenanigans.
And it turns out that this is very related to what Pin
wants to achieve.
the exact semantics of Pin
are very and incredibly subtle
so ask for code review in this forum if going down this route.
The exact property that we can have, with Pin
, some Pointee<T>
type, and some Ptr<Pointee<T>>
such as &'_ Pointee<T>
, is that:
- unless
Pointee<T> : Unpin
/ if Pointee<T> : !Unpin
;
- and if
Pointee<T>
has drop
glue (e.g., Pointee<T> : Drop
);
- and if we get our hands onto some
Pin<&'does_not_matter Pointee<T>>
"witness" instance,
then, and only then, we know that:
- either the
Pointee
is never moved or deallocated, i.e., the 'does_not_matter
was kind of 'static
;
- or the
Pointee
's aforementioned drop glue is to be run before the value is moved or deallocated.
Moreover, if Pointee
happens to hold a value of type T
inside it, then a fortiori, that value will also not have moved, provided we never expose an API/accessor to that value.
From all this, we can thus write:
use ::core::marker::Unpin as PinSensitive;
type Pointer<'lt, Pointee> = &'lt mut Pointee;
pub
struct Unregisterer<T> {
value: T,
_pin_sensitive: PinSensitive,
}
impl<T> From<T> for Unregisterer<T> { ... }
impl<T> Unregisterer<T> {
pub
fn register(
self: Pin<Pointer<'_, Self>>,
)
{
unsafe { register(ptr::addr_of!(self.value).cast()); }
}
}
impl<T> Dropf or Unregisterer<T> {
fn drop(&mut self)
{
// TODO: ensure this function is ok to run for an unregistered thing
// or add a boolean to check for registration
unsafe { unregister(ptr::addr_of!(self.value)); }
}
}