use tokio::task;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let num = Arc::new(5u32);
let cloned = Arc::clone(&num);
let r = &*num;
let r: &'static u32 = unsafe { std::mem::transmute(r) };
task::spawn(async move {
println!("{num}");
println!("{r}");
});
println!("{cloned}");
}
Is this code always sound? My thought process is that since both the referenced value and the static ref is moved into the same scope, the static ref is always valid in the case if the moved Arc pointer was the only one alive. Since only the Arc pointer gets moved, and not the pointed to value, the reference doesn't get invalidated. Is my reasoning correct or no? I checked with Miri and it didn't detect any UB, but of course it doesn't mean there aren't any undetected UB.
you are basically correct yes, num protects r from being invalidated until num goes out of scope.
your code is equivalent to
use tokio::task;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let num = Arc::new(5u32);
let cloned = Arc::clone(&num);
task::spawn(async move {
let r = &*num;
println!("{num}");
println!("{r}");
});
println!("{cloned}");
}
in every meaningful way (yes the future is smaller in my case but it doesn't matter).
therefore it is sound, because this code is fully safe.
this is the case because Arc never asserts uniqueness and would not work with Box for example.
this is of course not true in general, and you probably should avoid doing this if you can
my example is bad here. imagine that i need to use r before moving it into a task, and that r is expensive to create (not a regular reference). the goal is to not need to create two r
my example is bad here. imagine that i need to use r before moving it into a task, and that r is expensive to create (not a regular reference). the goal is to not need to create two r
the exact code would be ideal for communication.
i can imagine potential solutions, but they are likely to not fit depending on the exact needs
pub struct Group {
pub id: Id,
pub all_apps: Arc<[App]>,
pub apps_i: Vec<usize>,
unblocker_tx: Mutex<Option<Unblocker>>,
is_locked: AtomicBool,
}
impl Group {
pub(super) async fn block_with_timer(
self: Arc<Self>,
timer: Timer,
lock_when_blocked: LockWhenBlocked,
cbi: CommonBlockInfo,
) {
let now = Local::now();
let diff = timer.end_date_time() - now;
if diff.num_seconds() > 0 {
if let Some(more_lock_config) = *lock_when_blocked {
self.lock(more_lock_config, &cbi.blocker);
}
let apps = self.apps();
cbi.blocker.block_vec(&apps).await;
let diff_std = diff
.to_std()
.expect("if statement gurantees positive duration");
println!("{diff_std:?}");
// REASON FOR UNSAFE: `task::spawn` requires captured values to be `'static`, therefore `&'static` is required
// SAFETY: `apps` holds references into the `Arc<[App]>` owned by `self`.
// `self` is an `Arc`, so moving it does not move the underlying data.
// Both `apps` and `self` are moved into the spawned task, sharing the same scope,
// so `self` is guaranteed to outlive `apps`. `App` is `'static`, so no internal
// references exist that could be invalidated. Transmute is sound.
let apps: Vec<&'static App> = unsafe { std::mem::transmute(apps) };
task::spawn(async move {
tokio::select! {
res = cbi.unblock_rx => {
res.unwrap();
},
_ = time::sleep(diff_std) => {
println!("Block Timer completed")
}
}
if let Some(more_lock_config) = *lock_when_blocked {
self.unlock(more_lock_config, &cbi.blocker);
}
cbi.blocker.unblock_vec(&apps).await;
self.clear_unblocker();
});
} else {
self.clear_unblocker();
}
}
pub fn apps(&self) -> Vec<&App> {
self.apps_i.iter().map(|i| &self.all_apps[*i]).collect()
}
}
There are some irrelevant parts but you should be able to see the importants bits. Tell me if i missed something.
Now I did just realise that i can just inline the Group::apps, and change the map closure to suit my needs to avoid this issue. I also am aware that a Vec<&T> isnt really that expensive. But I am just foolishly curious to see if my reasoning is correct.
when you say "moving things in the spawn" do you mean the code (inlining the Group::apps function body and changing the map closure) or the values when the code runs? i presume you mean the former?
this code is not ideal because i need the code before the spawned task in my code snippet to be completed before i return the function. this is for other reasons.
For anyone curious, the tentative future plan for how to make this less sketchy is to go through Tracking issue for unsafe binder types · Issue #130516 · rust-lang/rust · GitHub -- which are still massively unsafe, but hopefully at least communicate things better than &'static which is super easy to misuse without even realizing it.