commonlibsse_ng\skse\interfaces/
task.rs

1// C++ Original code
2// - ref: https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/include/SKSE/Interfaces.h
3// - ref: https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/src/SKSE/Interfaces.cpp
4// SPDX-FileCopyrightText: (C) 2018 Ryan-rsm-McKenzie
5// SPDX-License-Identifier: MIT
6//
7// SPDX-FileCopyrightText: (C) 2025 SARDONYX
8// SPDX-License-Identifier: Apache-2.0 OR MIT
9
10use crate::skse::impls::stab::SKSETaskInterface;
11use core::ffi::c_void;
12
13#[derive(Debug, Clone)]
14pub struct TaskInterface(&'static SKSETaskInterface);
15
16impl TaskInterface {
17    /// The version number of the task interface.
18    pub const VERSION: u32 = 2;
19
20    #[inline]
21    pub(crate) const fn new(interface: &'static SKSETaskInterface) -> Self {
22        Self(interface)
23    }
24
25    /// Returns the version number of the task interface.
26    #[inline]
27    pub const fn version(&self) -> u32 {
28        self.0.interfaceVersion
29    }
30
31    // NOTE: Omitted because I have not found a way to reproduce the ABI of std::function in Rust.
32
33    #[inline]
34    pub fn add_task_delegate(&self, f: Box<dyn FnOnce() + Send + 'static>) {
35        unsafe { (self.0.AddTask)(TaskDelegate::new_as_c_void(f)) }
36    }
37
38    #[inline]
39    pub fn add_ui_task_delegate(&self, f: Box<dyn FnOnce() + Send + 'static>) {
40        unsafe { (self.0.AddUiTask)((UIDelegateV1::new_as_c_void(f)).cast()) }
41    }
42}
43
44/// Same vtbl layout as TaskDelegate, so we can use it all the time.
45type UIDelegateV1 = TaskDelegate;
46
47/// Rust structure that exactly reproduces the memory layout of C++ virtual class
48///
49/// See [`C to C++ vtbl FFI`](https://godbolt.org/z/eTY4M7h7f)
50#[repr(C)] // <- This will mess up the memory layout if we don't guarantee the order of the fields.
51struct TaskDelegate {
52    /// When a C++ virtual function is impl, a pointer to the virtual function table
53    /// is added to the class situation. This reproduces it.
54    vtbl: &'static TaskDelegateVtbl,
55    /// This is not inherently present on the C++ side, but is necessary to run the Rust task.
56    ///
57    /// One-time call by `Option`
58    rust_fn: Option<Box<dyn FnOnce() + Send + 'static>>,
59}
60
61/// Virtual table for task delegate functions.
62#[repr(C)]
63#[derive(Debug)]
64struct TaskDelegateVtbl {
65    /// Executes the task.
66    ///
67    /// It is SKSE in C++ that calls this, not itself.
68    run: fn(&mut TaskDelegate),
69    /// Delete itself here because SKSE can call this to delete memory.
70    ///
71    /// In other words, if this is called twice or used after deletion (Use after free), it is an undefined operation.
72    dispose: fn(*mut TaskDelegate),
73}
74
75impl TaskDelegate {
76    /// Executes the task.
77    ///
78    /// It is SKSE in C++ that calls this, not itself.
79    fn run(&mut self) {
80        // One-time call by `Option`
81        if let Some(rust_fn) = self.rust_fn.take() {
82            rust_fn();
83        }
84    }
85
86    /// Delete itself here because SKSE can call this to delete memory.
87    ///
88    /// In other words, if this is called twice or used after deletion (Use after free), it is an undefined operation.
89    fn dispose(task: *mut Self) {
90        unsafe { drop(Box::from_raw(task)) };
91    }
92}
93
94impl TaskDelegate {
95    /// Creates a new Task delegate
96    fn new(f: Box<dyn FnOnce() + Send + 'static>) -> Box<Self> {
97        const TASK_VIRTUAL_FN_TABLE: TaskDelegateVtbl =
98            TaskDelegateVtbl { run: TaskDelegate::run, dispose: TaskDelegate::dispose };
99
100        Box::new(Self { rust_fn: Some(f), vtbl: &TASK_VIRTUAL_FN_TABLE })
101    }
102
103    /// Creates a new C++ task delegate
104    ///
105    /// This struct is not dropped unless dispose is called.
106    fn new_as_c_void(f: Box<dyn FnOnce() + Send + 'static>) -> *mut c_void {
107        Box::into_raw(Box::new(Self::new(f))).cast()
108    }
109}