#Correct way to deal with FFI function that takes owned value but not ownership
42 messages ยท Page 1 of 1 (latest)
what is this supposed to mean? Do you have a non-ffi mock-up?
Essentially the idea is to have T and a Borrowed<'_, T>.
An example function looks like:
fn safe_wrapper(array: &Array) {
let chunks: ManuallyDrop<Array> = unsafe {
(chunks as *const Array)
.cast::<ManuallyDrop<Array>>()
.read()
};
unsafe { my_ffi_func(chunks) }
}
I want to be able to provide a safer less ugly solution without wrapping every single type in some sort of borrowed version of the struct that is used in the FFI. For a few types this is fine but I don't want to expose and maintain a bunch of wrapper structs that hold a PhantomData for the lifetime. I can't think of a way to avoid solving this without unsafe or a bunch of boiler plate code (or macros).
Key part here is that T requires a specific alignment and size to pass it to the foreign function but doesn't actually take ownership of it. It only needs a byte copy.
I can't think of a way to do this without the mentioned problems.
My initial idea was to use something like
#[repr(C)]
struct BorrowedArray<'a> {arr: Array, phantom: PhantomData<&'a ()>}
Note that Array would be a byte copy of the value.
I want to avoid doing this for every type that is passed through FFI.
Didn't you say it takes an owned value? This looks like a borrowed value to me.
Did you mean it takes a bitcopy of the value? If it's trivially copiable, it sounds like you could just implement Copy and solve your problem on the Rust side?
Essentially the idea is to have
Tand aBorrowed<'_, T>
That should in fact just work:
struct Borrowed<'a, T>(T, PhantomData<&'a ()>);
it's a de-facto owned T with a lifetime. Not sure why you'd need the lifetime if T is owned, though
That solution is what I am trying out at the moment. It seams that it will introduce minimal boilerplate.
I can't implement Copy as the types require drop code and some are fairly large.
For example the Array struct above requires an allocation and its drop code to be called to avoid memory leaks, and so is many other types I need to do this with.
This seems to work fine but I am unsure if this could have unsoundness issues.
use std::{marker::PhantomData, mem::ManuallyDrop};
pub struct Borrowed<'a, T>(ManuallyDrop<T>, PhantomData<&'a T>);
impl<T> Borrowed<'_, T> {
pub(crate) fn new(value: &T) -> Self {
let val = unsafe { (value as *const T).cast::<ManuallyDrop<T>>().read() };
Self(val, PhantomData)
}
}
impl<T> AsRef<T> for Borrowed<'_, T> {
fn as_ref(&self) -> &T {
&self.0
}
}
I am increasingly more concerned as to what the heck you're doing
I guess Array wraps a pointer and the C takes this pointer?
The obvious thing to do here would be to access the pointer Array contains and pass that to C
Ok so here is an example struct I actually use.
#[repr(C)]
pub struct KVec<T> {
pub(super) len: usize,
pub(super) capacity: usize,
/// points to an allocation
pub(super) ptr: *mut T,
}
So I want to pass this value via FFI as is with its exact layout and size, not just its pointer or a pointer to it. A literal byte copy of it. Since ptr points to an allocation and requires Drop::drop to be called I cannot implement Copy for it without leaking memory.
This is fine to duplicate via ptr::read
Provided of course you only drop one of the copies
And don't keep the other one around afterwards
At which point I'm a bit confused as to what Borrowed is for
Can't you just read out of a reference?
That would also work but ideally I want to place the unsafe call to a specific point and just call array.into() in the call sites.
I can't say I follow. You expect users to carry around owned arrays, then turn them into borrowed arrays?
A safe wrapper for a C library ๐
why
what is the purpose of this
No no as in array: &Array
Yeah, having &Array does that
Just do the copy in the methods that call into C
Not sure I follow. The intention is the user provides a reference and I create a Borrowed<_> from it and pass it to the FFI call. From a users perspective they just pass a reference.
Thats what I am doing isn't it?
Creating a byte copy via Borrowed::new and passing Borrowed<'_, T> to the foreign function.
Yes, but the thing is, why bother with Borrowed
Just to avoid boiler plate unsafe code. I have nearly 100 foreign functions and about 40 of them will require this so I just want to hide this logic away.
I guess? Borrowed's lifetime is pointless though
You don't link it to the lifetime of value, so I can just write Borrowed::<'static, _>::new(value)
This is still sound since you also don't use that lifetime in the AsRef impl (not that you could). Provided you have no API that takes that lifetime seriously, you're fine
The actual unsoundness here is that you forgot #[repr(transparent)], but that's an easy fix
Your right. Fixed the lifetime and repr.
So the final state of Borrowed looks like this.
use std::{marker::PhantomData, mem::ManuallyDrop};
#[repr(transparent)]
pub struct Borrowed<'a, T>(ManuallyDrop<T>, PhantomData<&'a T>);
impl<'a, T> Borrowed<'a, T> {
pub(crate) fn new(value: &'a T) -> Self {
let val = unsafe { (value as *const T).cast::<ManuallyDrop<T>>().read() };
Self(val, PhantomData)
}
}
impl<T> AsRef<T> for Borrowed<'_, T> {
fn as_ref(&self) -> &T {
&self.0
}
}
I think this should be fully sound now.
This seems sound, yes