I have some code in a complex application which roughly looks like this (all main thread only)
let callback: RefCell<Box<dyn Fn()>> = ...;
// RcBlock here is an obj-c callback block.
// this is not the core of the issue (but may be part of it)
// as it works fine when not using the `callback` var.
let other_fn = RcBlock(|| {
callback.borrow()();
})
// this is just to symbolise that we are no longer the owner of this
// `other_fn` and that the os will call the callback from the main thread
// note that `callback` is never dropped
dispatch(other_fn);
This works on all targets I tested, except on iOS where I got a EXC_BAD_ACCESS with the stack trace leading me to boxed.rs:
#[stable(feature = "boxed_closure_impls", since = "1.35.0")]
impl<Args: Tuple, F: Fn<Args> + ?Sized, A: Allocator> Fn<Args> for Box<F, A> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output {
<F as Fn<Args>>::call(self, args)
}
}
which I presume is the internal call implementation of a boxed closure. It is really odd to me why this happens, but maybe someone can shed some light onto this.
Edit: I found that it is refcell related, but I also find it hard to believe that it's a lifetime issue.
Are you casting the closure to void* at any point?
Box<dyn …> and closures that capture any state are not compatible with C pointers. They are 2×usize fat pointers, and need to be passed through FFI as Box<Box<dyn …>> or similar.
I think this is mainly because IOS is strict about memory access, try wrapping that RefCell in an Rc and clone it before moving it into the RcBlock,
And check if the issue persists.
I'm curious about this too
I found the issue! For some reason only in the iOS build (maybe due to some optimisation settings), the function pointer inside the box gets moved to a new address even though it should not (because that would break rust's own rules afaik). As @x0rw mentioned I've solved this by using Rc, clone and move (i guess a Pin would also work). It is still interesting why the pointer gets moved though.
How does this even work. The closure would borrow from callback, but the closure gets sent to another thread and called aftercallback is already dropped if I understand this correctly. What is the function signature of dispatch? Does it bound other_fn to be 'static?
fn main(){
let callback:RefCell<Box<dyn Fn()>> = ...;
let launch_signal = |...|{
let other_fn = RcBlock(|| {
callback.borrow()();
})
//other_fn now leaves Rust and is owned by obj-c code
//this is safe because RcBlock is a proper wrapper
//somewhere some obj-c object during the lifetime of the
//application can call this hook.
//This will always run on the main thread (or be dispatched
//to the main thread)
hook_with_system(other_fn)
// At this point the box fn is still valid, but as soon as the hook
// hook is called the program crashes.
//so more or less my problem is: why is the pointer in the
//box moved? It shouldn't be possible right? because there
}
//This entire loop does not exists in rust but is ran
//on the main thread in obj-c
loop {
//code from other threads that need to be synchronized
//this is not how its actually done (just a model)
dispatch_queue.iter().for_each(|f|f.call());
for event in events {
match event {
Event::ApplicationLaunch => launch_signal()
}
}
do_application_code()
}
}
I hope this clears things up. It is a bit hard to explain because of all the obj-c intermingling.