Context: I'm exposing some Rust objects via FFI to other languages, in the form
of opaque pointers and functions that act on pointers to those objects. These
objects are freed by calling an FFI function (my_thing_free(thing_ptr)
) on
each of them.
While doing some performance optimization, I found an unnecessary clone in a hot
path, and I'm trying to remove it. In order to do this, I introduced a reference
from one type to another which didn't exist before, and I'm now considering
exactly what happens if client code uses those pointers incorrectly.
In particular, what if the "outer world" code calls the _free() functions in the
wrong order? I.e. what it first frees the referenced object, and then it frees
the object that contains the reference?
Concretely, this is the situation after I replaced the clone with a reference:
struct InternalDetails(String);
struct MyThing<'a> {
_details: &'a InternalDetails,
}
struct MyCollection {
details: InternalDetails,
}
impl MyCollection {
fn create_thing(&self) -> MyThing {
MyThing {
_details: &self.details,
}
}
}
These are exposed via various FFI functions, but let's only consider the _free()
functions here:
#[no_mangle]
extern "C" fn my_thing_free(thing: *mut MyThing) {
drop(unsafe { Box::from_raw(thing) })
}
#[no_mangle]
extern "C" fn my_collection_free(collection: *mut MyCollection) {
drop(unsafe { Box::from_raw(collection) })
}
This is some rust code trying to simulate what the incorrect outer world code
would do using the ffi functions:
fn main() {
let collection = Box::new(MyCollection {
details: InternalDetails("foo".to_string()),
});
// a pointer is created and handed off to the outer world
// via FFI
let collection_ptr = Box::into_raw(collection);
// the outer world uses other FFI functions to create a thing from the collection
let thing_ptr = {
let collection = unsafe { collection_ptr.as_ref().unwrap() };
Box::into_raw(Box::new(collection.create_thing()))
};
// C code does things with ptrs...
//
// Question is: what if the client code calls the two free functions
// in the wrong order? Is it UB?
my_collection_free(collection_ptr);
my_thing_free(thing_ptr);
}
If the outer world code calls my_collection_free() first and then my_thing_free(),
the Thing would contain an invalid rust reference, from the moment it is
re-interpreted as a rust value (unsafe { Box::from_raw(thing) }
) to the moment
when it's dropped.
At Behavior considered undefined - The Rust Reference I read
that it's UB to "produce" an invalid value, even in private fields and locals,
where "producing" means "a value is assigned to or read from a place, passed to
a function/primitive operation or returned from a function/primitive operation".
If I run the above code through MIRI
(Rust Playground)
it doesn't complain.
Am I right in my understanding that this is not UB, since dropping does not
match the rules for "producing" above?
Of course if I did anything that involves that dandling reference (for example
printing on drop) it would be UB, no doubts about that. But what about just
dropping the value?
Since I control the bindings in the other languages, I could enforce drop order
by keeping references in each object to the object from which it was created, if
there is a lifetime relationship on the rust side. But I would prefer to avoid
this extra (otherwise unused) reference, if this isn't hitting UB.
Any idea? Please do correct me if my reasoning is wrong.
Thanks!