smol-rs/async-task

Unclear behaviour of casting ptr of RawTask to ptr of Header

Closed this issue · 1 comments

mnpw commented

Hi, I'm going through this repo in order to understand async runtimes better. I'm not able to understand a part of the codebase:

Task's field ptr is is a pointer to RawTask. But in Task's methods, I see it being casted as a pointer to Header.

impl Task {
    fn poll_task(&mut self, cx: &mut Context<'_>) -> Poll<Option<T>> {
            let ptr = self.ptr.as_ptr();
            let header = ptr as *const Header<M>;
            ...
    }
    ...
}

From what I understand, this can happen since ptr to Header is the first field in RawTask:

/// Raw pointers to the fields inside a task.
pub(crate) struct RawTask<F, T, S, M> {
    /// The task header.
    pub(crate) header: *const Header<M>,
    ...
}

According to this post, shouldn't this be only possible with #[repr(C)] on RawTask?

The raw task pointer isn't a pointer to RawTask, it's a pointer to an anonymous structure whose first field is a Header.

Specifically, the structure is laid out manually, like so:

async-task/src/raw.rs

Lines 105 to 133 in 918ec72

/// Computes the memory layout for a task.
#[inline]
const fn eval_task_layout() -> Option<TaskLayout> {
// Compute the layouts for `Header`, `S`, `F`, and `T`.
let layout_header = Layout::new::<Header<M>>();
let layout_s = Layout::new::<S>();
let layout_f = Layout::new::<F>();
let layout_r = Layout::new::<Result<T, Panic>>();
// Compute the layout for `union { F, T }`.
let size_union = max(layout_f.size(), layout_r.size());
let align_union = max(layout_f.align(), layout_r.align());
let layout_union = Layout::from_size_align(size_union, align_union);
// Compute the layout for `Header` followed `S` and `union { F, T }`.
let layout = layout_header;
let (layout, offset_s) = leap!(layout.extend(layout_s));
let (layout, offset_union) = leap!(layout.extend(layout_union));
let offset_f = offset_union;
let offset_r = offset_union;
Some(TaskLayout {
layout: unsafe { layout.into_std() },
offset_s,
offset_f,
offset_r,
})
}
}

This roughly translates to this equivalent Rust structure:

#[repr(C)]
struct Anonymous<F, T, S, M> {
    header: Header<M>,
    result_future_union: union {
        future: F,
        result: T,
    },
    schedule: S,
}

This is the structure that the pointer actually points to. Since the header comes first in this structure, we can cast the pointer to the raw task into a Header pointer without any issues.

The purpose of the RawTask structure is to provide pointers to the fields of the above anonymous structure. Since we can't actually name the anonymous structure in our code (as it is constructed manually on the heap), we have to refer to its field by pointers. Hence, the pointers to the header, the future, et al. Casting a pointer to RawTask to a Header pointer wouldn't actually work, since the header pointer is a pointer to a Header, not an actual Header.