Skip to main content

swiftui_native/
asyncfn.rs

1//! Swift `async` closures, supplied from Rust.
2//!
3//! `refreshable(action: @Sendable () async -> ())` cannot take an ordinary
4//! function pointer. An async closure's first word is not the function — it is
5//! an **async function pointer**, a two-field record the compiler emits:
6//!
7//! | offset | contents |
8//! |--------|----------|
9//! | `+0`   | 32-bit relative pointer to the async function |
10//! | `+4`   | the context size the function expects |
11//!
12//! The function itself runs under the async convention: its context arrives in
13//! **x22**, and it returns by tail-calling the continuation stored at `+8` in
14//! that context. An empty `{ }` compiles to exactly two instructions —
15//! `ldr x0, [x22, #8]` then `br x0` — which is the shape the thunks below keep,
16//! with the Rust callback run in between.
17//!
18//! The work happens synchronously and the continuation is called straight away:
19//! this is an async closure that never suspends, which is a real one, not a
20//! stub. Suspension would mean allocating a child context and driving the task
21//! system, and nothing here needs that.
22//!
23//! The thunks below are known to *run*: building `.task` on top of this and
24//! rendering it does reach the Rust callback. What is parity-*verified* is the
25//! value `refreshable` builds, not the closure running — a refresh action only
26//! fires on a pull, which the suite does not perform.
27//!
28//! Like [`crate::keypath`], the record is a `static mut`: its one field is a
29//! 32-bit offset to code in this image, which the heap is too far from.
30
31use core::ffi::c_void;
32
33/// The record an async closure's first word points at.
34#[repr(C, align(4))]
35pub struct FunctionPointer {
36    function: i32,
37    context_size: u32,
38}
39
40impl FunctionPointer {
41    /// An empty record, to be filled by [`prepare`].
42    pub const fn new() -> Self {
43        FunctionPointer { function: 0, context_size: 0 }
44    }
45}
46
47impl Default for FunctionPointer {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53/// The context size to declare.
54///
55/// Thirty-two is what the compiler's partial-apply forwarder asks for — an empty
56/// closure's own record says sixteen, but the forwarder is what a call site
57/// actually passes, and asking for more than is used is free.
58const EMPTY_CONTEXT: u32 = 32;
59
60/// Point `record` at `thunk` and arm the thunk to call `action`.
61///
62/// Returns the two words of the closure: the record, and a null context —
63/// exactly what the compiler passes for a closure that captures nothing.
64///
65/// # Safety
66/// `record` must be a `static mut` in this image, and `slot` one of the three
67/// below. Arming a slot twice is allowed and the later action wins — two
68/// `.task` modifiers on two different views share a slot, and both run the
69/// second one's action, which is what the `fn()` version did too.
70pub unsafe fn prepare(
71    record: &'static mut FunctionPointer,
72    slot: usize,
73    action: impl Fn() + Send + Sync + 'static,
74) -> [u64; 2] {
75    unsafe {
76        let (code, target, shim) = thunks(slot);
77        // The thunk `blr`s straight to the target, so the target has to be
78        // executable code taking no arguments — which a boxed closure is not.
79        // Each slot therefore owns a shim that calls through the box beside it.
80        // The action has to be `Send + Sync` because this one is genuinely
81        // async: Swift may resume the task on a cooperative thread, not the
82        // main one, so unlike every other callback here it is not main-thread
83        // by construction.
84        // Last arming wins, as it did when this was a bare `fn` pointer; the
85        // previous action is leaked rather than dropped, because SwiftUI may
86        // already be running it.
87        ACTIONS[slot].store(
88            Box::into_raw(Box::new(Box::new(action) as Box<dyn Fn() + Send + Sync>)),
89            Ordering::Release,
90        );
91        *target = shim;
92        let at = &record.function as *const i32 as *const u8;
93        let delta = code as isize - at as isize;
94        record.function = i32::try_from(delta).expect("async function pointer out of range");
95        record.context_size = EMPTY_CONTEXT;
96        [record as *const FunctionPointer as u64, 0]
97    }
98}
99
100// x22 holds the async context and `[x22, #8]` is the continuation to tail-call
101// on return — which is the whole of what an empty `{ }` compiles to.
102//
103// The prologue is the one the compiler emits for an async function that calls
104// out and then returns. Bit 60 of x29 marks the frame as async so the unwinder
105// walks the context chain rather than the stack; x22 is saved because the
106// continuation is read from it after the call, and x30 because `blr` overwrites
107// it. Leaving any of that out lets the closure run to completion and then kills
108// the process on the way back — which `refreshable` never notices, because a
109// refresh action only fires on a pull, and `task` notices immediately.
110macro_rules! async_thunks {
111    ($($name:ident, $sym:literal, $target:ident;)*) => {$(
112        crate::thunk::runtime_thunk! {
113            /// An `async` closure body that runs a Rust callback and resumes.
114            fn $name = $sym via $target {
115                prologue: [
116                    "orr x29, x29, #0x1000000000000000",
117                    "sub sp, sp, #0x20",
118                    "stp x29, x30, [sp, #0x10]",
119                    "str x22, [sp, #0x8]",
120                    "add x29, sp, #0x10",
121                ],
122                branch: "blr",
123                epilogue: [
124                    "ldr x22, [sp, #0x8]",
125                    "ldp x29, x30, [sp, #0x10]",
126                    "and x29, x29, #0xefffffffffffffff",
127                    "add sp, sp, #0x20",
128                    "ldr x0, [x22, #8]",
129                    "br x0",
130                ],
131            }
132        }
133    )*};
134}
135
136async_thunks! {
137    async0, "rswift_async0", RSWIFT_ASYNC0;
138    async1, "rswift_async1", RSWIFT_ASYNC1;
139    async2, "rswift_async2", RSWIFT_ASYNC2;
140}
141
142/// Where each slot's action lives.
143///
144/// A slot, not a single static, because a thunk carries no context of its own:
145/// an async closure is a *record* plus a context word, and the record only
146/// holds a 32-bit offset to code. The one way back to the right closure is to
147/// have branched from the right thunk, so there are as many simultaneous
148/// actions as there are thunks — three, one per call site: `refreshable`,
149/// `task`, and `task_id`. Slot 2 used to fall through to slot 1's thunk, so a
150/// view carrying both a `.task` and a `.task(id:)` ran whichever was applied
151/// second, twice.
152///
153/// **Within a slot, the last arming wins.** Two `.task` modifiers on two
154/// different views share slot 1, and both will run the second one's action.
155/// This is what the `fn()` version did as well — it stored the bare pointer in
156/// the same one slot — so nothing regressed, but it is a real ceiling, and the
157/// place to lift it is the context word: a per-closure Swift heap object,
158/// passed as the second word instead of the null below, would let one thunk
159/// serve every call. That needs the context to survive SwiftUI's retain and
160/// release of it, which a Rust box does not.
161static ACTIONS: [AtomicPtr<Box<dyn Fn() + Send + Sync>>; SLOTS] =
162    [const { AtomicPtr::new(core::ptr::null_mut()) }; SLOTS];
163
164use core::sync::atomic::{AtomicPtr, Ordering};
165
166const SLOTS: usize = 3;
167
168/// One shim per slot, since each has to name its own index.
169macro_rules! shims {
170    ($($name:ident = $i:expr;)*) => {$(
171        unsafe extern "C" fn $name() {
172            let f = ACTIONS[$i].load(Ordering::Acquire);
173            if !f.is_null() {
174                unsafe { (**f)() };
175            }
176        }
177    )*};
178}
179
180shims! { call0 = 0; call1 = 1; call2 = 2; }
181
182/// The async thunks, by slot — the code to branch from, the slot to write, and
183/// the shim to write into it.
184fn thunks(slot: usize) -> (*const c_void, *mut *const c_void, *const c_void) {
185    match slot {
186        0 => (async0 as *const c_void, &raw mut RSWIFT_ASYNC0, call0 as *const c_void),
187        1 => (async1 as *const c_void, &raw mut RSWIFT_ASYNC1, call1 as *const c_void),
188        _ => (async2 as *const c_void, &raw mut RSWIFT_ASYNC2, call2 as *const c_void),
189    }
190}
191
192// ── A task's entry, which *does* get a context ──
193//
194// The three slots above exist because a SwiftUI `@ViewBuilder` async closure is
195// handed `(record, context)` and SwiftUI passes back the context it was given —
196// which for a Rust closure is nothing it can own, so the closure has to live in
197// a slot the thunk names by its own address. That is a real ceiling: three
198// simultaneous actions, last arming wins within a slot.
199//
200// `swift_task_create` is not like that. Its fifth argument *is* the closure
201// context, and the entry receives it in **x20** — read off the disassembly of
202// `Task.detached { }`, whose entry starts `ldr x9, [x20, #0x10]` before x20
203// could have been set by anything else. So a task carries its own closure and
204// there is no slot and no ceiling: as many tasks as you like, at once.
205//
206// The frame is the same as above; the one addition is moving x20 into the first
207// argument register before the call, which nothing in the prologue disturbs.
208crate::thunk::runtime_thunk! {
209    /// A `Task`'s entry: run the closure the context names, then resume.
210    fn task_entry = "rswift_task_entry" via RSWIFT_TASK_ENTRY {
211        prologue: [
212            "orr x29, x29, #0x1000000000000000",
213            "sub sp, sp, #0x20",
214            "stp x29, x30, [sp, #0x10]",
215            "str x22, [sp, #0x8]",
216            "add x29, sp, #0x10",
217            "mov x0, x20",
218        ],
219        branch: "blr",
220        epilogue: [
221            "ldr x22, [sp, #0x8]",
222            "ldp x29, x30, [sp, #0x10]",
223            "and x29, x29, #0xefffffffffffffff",
224            "add sp, sp, #0x20",
225            "ldr x0, [x22, #8]",
226            "br x0",
227        ],
228    }
229}
230
231/// How a task's completion is reported back to whoever holds its handle.
232///
233/// A Swift `Task`'s result is delivered by awaiting it, and Rust has nothing to
234/// await from — so the entry signals this instead, and
235/// [`crate::runtime::Task::wait`] blocks on it. Shared by `Arc` between the
236/// handle and the context, so a handle dropped early frees it and a task that
237/// outlives its handle still has somewhere to write.
238pub(crate) struct TaskState {
239    done: std::sync::Mutex<bool>,
240    woken: std::sync::Condvar,
241}
242
243impl TaskState {
244    pub(crate) fn new() -> Self {
245        Self { done: std::sync::Mutex::new(false), woken: std::sync::Condvar::new() }
246    }
247
248    /// Has the task's closure returned?
249    pub(crate) fn is_finished(&self) -> bool {
250        *self.done.lock().expect("task state")
251    }
252
253    /// Block until it has, or until `limit` elapses. `true` if it finished.
254    pub(crate) fn wait(&self, limit: Option<std::time::Duration>) -> bool {
255        let mut done = self.done.lock().expect("task state");
256        match limit {
257            None => {
258                while !*done {
259                    done = self.woken.wait(done).expect("task state");
260                }
261                true
262            }
263            Some(limit) => {
264                let deadline = std::time::Instant::now() + limit;
265                while !*done {
266                    let left = deadline.saturating_duration_since(std::time::Instant::now());
267                    if left.is_zero() {
268                        return false;
269                    }
270                    let (next, timeout) = self
271                        .woken
272                        .wait_timeout(done, left)
273                        .expect("task state");
274                    done = next;
275                    if timeout.timed_out() && !*done {
276                        return false;
277                    }
278                }
279                true
280            }
281        }
282    }
283
284    fn finish(&self) {
285        *self.done.lock().expect("task state") = true;
286        self.woken.notify_all();
287    }
288}
289
290/// What a task's context carries.
291///
292/// Laid out like [`crate::abi::ClosureCtx`] and for the same reason: the runtime
293/// treats it as a `HeapObject`, so it needs an `isa` and a refcount, and the
294/// refcount is the immortal pattern so the retains and releases around it are
295/// no-ops. A Rust `Box` would be freed by the first release.
296#[repr(C)]
297struct TaskCtx {
298    isa: *const c_void,
299    refcount: u64,
300    /// The closure, owned. Null once the entry has taken it.
301    body: *mut Box<dyn FnOnce() + Send>,
302    /// The completion signal, as a raw `Arc`. Null once the entry has used it.
303    state: *const TaskState,
304}
305
306/// Contexts whose task has finished, ready to carry another.
307///
308/// Not freed, and that is the point: the runtime releases the closure context
309/// after the task completes, and with the immortal refcount that release is a
310/// no-op *that still reads the header*. So the allocation has to outlive the
311/// task — but only the header does, and everything after it can be reused. The
312/// alternative was a fresh thirty-two bytes leaked per task forever; this bounds
313/// it at the high-water mark of tasks in flight.
314fn retired() -> &'static std::sync::Mutex<Vec<usize>> {
315    static R: std::sync::OnceLock<std::sync::Mutex<Vec<usize>>> = std::sync::OnceLock::new();
316    R.get_or_init(|| std::sync::Mutex::new(Vec::new()))
317}
318
319/// Branch target of [`task_entry`]: run the closure the context names.
320///
321/// # Safety
322/// `ctx` must be a `TaskCtx` from [`task_entry_point`].
323#[unsafe(no_mangle)]
324unsafe extern "C" fn rswift_task_body(ctx: *mut c_void) {
325    let c = unsafe { &mut *(ctx as *mut TaskCtx) };
326
327    let body = core::mem::replace(&mut c.body, core::ptr::null_mut());
328    if !body.is_null() {
329        let body = unsafe { Box::from_raw(body) };
330        // A Rust panic must not unwind into the Swift async frame this was
331        // called from: there is no landing pad there, and what happens instead
332        // is unspecified. Caught and reported, and the task still completes —
333        // which is what a Swift task does when its body throws.
334        if std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)).is_err() {
335            eprintln!("rswift: a detached task's closure panicked; the task completed anyway");
336        }
337    }
338
339    let state = core::mem::replace(&mut c.state, core::ptr::null());
340    if !state.is_null() {
341        // Reclaiming the `Arc` the context was holding, and signalling before
342        // dropping it: a waiter is holding the other reference.
343        let state = unsafe { std::sync::Arc::from_raw(state) };
344        state.finish();
345    }
346
347    retired().lock().expect("retired contexts").push(ctx as usize);
348}
349
350/// The async function pointer and context for a task running `action`.
351///
352/// `swift_task_create` wants the two-field *record*, not the function, and the
353/// context separately.
354///
355/// # Safety
356/// The record is a `static mut` in this image, shared by every task; only its
357/// two fields are written, and they are the same values every time.
358pub(crate) unsafe fn task_entry_point(
359    action: Box<dyn FnOnce() + Send>,
360    state: &std::sync::Arc<TaskState>,
361) -> (*const c_void, *mut c_void) {
362    static mut RECORD: FunctionPointer = FunctionPointer::new();
363    static ONCE: std::sync::Once = std::sync::Once::new();
364    unsafe {
365        ONCE.call_once(|| {
366            RSWIFT_TASK_ENTRY = rswift_task_body as *const c_void;
367            let record = crate::static_mut(&raw mut RECORD);
368            let at = &record.function as *const i32 as *const u8;
369            let delta = task_entry as *const u8 as isize - at as isize;
370            record.function =
371                i32::try_from(delta).expect("async function pointer out of range");
372            record.context_size = EMPTY_CONTEXT;
373        });
374
375        let body = Box::into_raw(Box::new(action));
376        let state = std::sync::Arc::into_raw(std::sync::Arc::clone(state));
377
378        let reused = retired().lock().expect("retired contexts").pop();
379        let ctx = match reused {
380            Some(addr) => addr as *mut TaskCtx,
381            None => Box::leak(Box::new(TaskCtx {
382                isa: crate::abi::immortal_isa(),
383                refcount: crate::abi::IMMORTAL_REFCOUNT,
384                body: core::ptr::null_mut(),
385                state: core::ptr::null(),
386            })) as *mut TaskCtx,
387        };
388        // Only the payload is written; the header a reused context already has
389        // is the one the runtime may still be releasing against.
390        (*ctx).body = body;
391        (*ctx).state = state;
392
393        (&raw const RECORD as *const c_void, ctx as *mut c_void)
394    }
395}