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}