samp_sdk/omp/timers.rs
1//! Bindings for the Open Multiplayer `ITimersComponent` interface.
2//!
3//! Open Multiplayer has no native `ProcessTick`-equivalent callback for
4//! components. To deliver the SDK's unified [`SampPlugin::on_tick`] on this
5//! server, the `samp` crate installs a repeating timer through this
6//! interface in `on_ready` and routes its timeout into the plugin.
7//!
8//! The interval is whatever the plugin requested via
9//! `samp::plugin::enable_tick_with(TickConfig::new().omp_interval(...))`,
10//! or 5 ms by default (from `enable_tick()`).
11//!
12//! [`SampPlugin::on_tick`]: ../../../samp/plugin/trait.SampPlugin.html#method.on_tick
13//!
14//! ## Primary `ITimersComponent` vtable
15//!
16//! The slot numbering differs per ABI, because Itanium emits two destructor
17//! slots (D1 + D0) where MSVC emits a single scalar deleting one, and because
18//! Itanium places the `getUID()` override (from `PROVIDE_UID`) in the primary
19//! vtable while MSVC keeps it in the secondary `IUIDProvider` vtable.
20//!
21//! **Itanium ABI** — confirmed by `nm`/vtable dump of the official `Timers.so`:
22//! `[0..3]` `IExtensible`, `[4..5]` destructors, `[6..16]` `IComponent`,
23//! **[17]** `getUID()`, **[18]** `create(handler, interval, repeating)`,
24//! `[19]` `create(handler, initial, interval, count)`, `[20]` `count()`.
25//!
26//! **MSVC ABI** — confirmed by RTTI + vtable dump of the official `Timers.dll`:
27//! `[0..3]` `IExtensible`, `[4]` destructor, `[5..15]` `IComponent`,
28//! `[16]` `create(handler, initial, interval, count)`,
29//! **[17]** `create(handler, interval, repeating)`, `[18]` `count()`.
30//!
31//! Note the reversal: MSVC emits an overload set in **reverse declaration
32//! order**, so the two `create` overloads swap places against Itanium. Verified
33//! by disassembly — slot [16] ends in `ret 0x18` (24 bytes of arguments, the
34//! four-argument overload) and slot [17] in `ret 0x10` (16 bytes, the one the
35//! SDK calls).
36//!
37//! ## `ITimer` vtable (slots starting from `IExtensible`)
38//!
39//! `ITimer` declares no destructor of its own, but inherits the virtual
40//! `~IExtensible()`, so the same D1/D0 shift applies.
41//!
42//! | Method | Itanium | MSVC |
43//! |------------------------|---------|------|
44//! | `IExtensible` (4) | [0..3] | [0..3] |
45//! | destructor | [4..5] | [4] |
46//! | `running() const` | [6] | [5] |
47//! | `remaining() const` | [7] | [6] |
48//! | `calls() const` | [8] | [7] |
49//! | `interval() const` | [9] | [8] |
50//! | `trigger()` | [10] | [9] |
51//! | `kill()` | [11] | [10] |
52//! | `handler() const` | [12] | [11] |
53//!
54//! ## `TimerTimeOutHandler` vtable (interface provided by the plugin)
55//!
56//! No virtual destructor in the header -> 2 slots only:
57//! - **[0]** `timeout(ITimer&)`
58//! - **[1]** `free(ITimer&)`
59
60use super::component_api::OmpComponentHandle;
61use super::server::{ServerComponent, query_component};
62use super::types::UID;
63use std::ptr::NonNull;
64
65/// UID of the Open Multiplayer `Timers` component.
66pub const TIMERS_COMPONENT_UID: UID = 0x2ad8_124c_5ea2_57a3;
67
68/// Slot of `create(handler, interval, repeating)` in the `ITimersComponent` vtable.
69///
70/// Itanium carries two destructor slots plus `getUID()` in the primary vtable;
71/// MSVC has one destructor and keeps `getUID()` in a secondary vtable.
72#[cfg(not(target_env = "msvc"))]
73const SLOT_CREATE_INTERVAL: usize = 18;
74
75#[cfg(target_env = "msvc")]
76const SLOT_CREATE_INTERVAL: usize = 17;
77
78/// Slot of `kill()` in the `ITimer` vtable (shifted by the extra Itanium
79/// destructor slot inherited from `IExtensible`).
80#[cfg(not(target_env = "msvc"))]
81const SLOT_TIMER_KILL: usize = 11;
82
83#[cfg(target_env = "msvc")]
84const SLOT_TIMER_KILL: usize = 10;
85
86/// Opaque pointer to the server's `ITimersComponent`.
87#[repr(C)]
88pub struct ITimersComponent {
89 _opaque: [u8; 0],
90}
91
92/// Opaque pointer to the server's `ITimer` — returned by `create_timer`.
93#[repr(C)]
94pub struct ITimer {
95 _opaque: [u8; 0],
96}
97
98/// `TimerTimeOutHandler` vtable — Itanium ABI.
99#[cfg(not(target_env = "msvc"))]
100#[repr(C)]
101pub struct TimerHandlerVTable {
102 pub timeout: unsafe extern "C" fn(*mut TimerTimeOutHandler, *mut ITimer),
103 pub free: unsafe extern "C" fn(*mut TimerTimeOutHandler, *mut ITimer),
104}
105
106/// `TimerTimeOutHandler` vtable — MSVC ABI (`this` in ECX).
107#[cfg(target_env = "msvc")]
108#[repr(C)]
109pub struct TimerHandlerVTable {
110 pub timeout: unsafe extern "thiscall" fn(*mut TimerTimeOutHandler, *mut ITimer),
111 pub free: unsafe extern "thiscall" fn(*mut TimerTimeOutHandler, *mut ITimer),
112}
113
114/// Object the server will invoke on each timer timeout.
115///
116/// `#[repr(C)]` layout: vtable pointer at offset 0 + the handler's own data.
117/// The server treats it as an opaque `TimerTimeOutHandler*` and only interacts
118/// via the vtable.
119#[repr(C)]
120pub struct TimerTimeOutHandler {
121 pub vtable: *const TimerHandlerVTable,
122}
123
124unsafe impl Send for TimerTimeOutHandler {}
125unsafe impl Sync for TimerTimeOutHandler {}
126
127/// Signature of `ITimersComponent::create(handler, interval, repeating)`.
128///
129/// `Milliseconds` is `std::chrono::milliseconds` in C++, a wrapper over `int64_t`.
130/// At the ABI it is passed as 8 bytes on the stack (or hidden in registers,
131/// depending on the compiler).
132#[cfg(not(target_env = "msvc"))]
133type CreateFn = unsafe extern "C" fn(
134 this: *mut ITimersComponent,
135 handler: *mut TimerTimeOutHandler,
136 interval_ms: i64,
137 repeating: bool,
138) -> *mut ITimer;
139
140#[cfg(target_env = "msvc")]
141type CreateFn = unsafe extern "thiscall" fn(
142 this: *mut ITimersComponent,
143 handler: *mut TimerTimeOutHandler,
144 interval_ms: i64,
145 repeating: bool,
146) -> *mut ITimer;
147
148#[cfg(not(target_env = "msvc"))]
149type KillFn = unsafe extern "C" fn(this: *mut ITimer);
150
151#[cfg(target_env = "msvc")]
152type KillFn = unsafe extern "thiscall" fn(this: *mut ITimer);
153
154/// Queries `ITimersComponent` in the server's component list.
155///
156/// # Safety
157/// `core` must point to a valid `ICore`. Internally uses `query_component`,
158/// which casts the `ServerComponent` from the list — follows its contract.
159pub unsafe fn query_timers_component(
160 components: *mut super::server::ServerComponentList,
161) -> *mut ITimersComponent {
162 if components.is_null() {
163 return std::ptr::null_mut();
164 }
165 let raw = unsafe { query_component(components, TIMERS_COMPONENT_UID) };
166 raw.cast::<ITimersComponent>()
167}
168
169/// Creates a repeating timer on the Open Multiplayer server.
170///
171/// Returns the server's `ITimer*` (non-owning — the server owns it). Use
172/// [`kill_timer`] at shutdown to stop it and free the server's resources.
173///
174/// # Safety
175/// - `timers` must be a valid `ITimersComponent` pointer (from `query_timers_component`)
176/// - `handler` must remain alive while the timer is active (allocate on the heap via `Box::into_raw`)
177pub unsafe fn create_repeating_timer(
178 timers: *mut ITimersComponent,
179 handler: *mut TimerTimeOutHandler,
180 interval_ms: i64,
181) -> *mut ITimer {
182 if handler.is_null() {
183 return std::ptr::null_mut();
184 }
185 let Some((_, slot)) = (unsafe {
186 super::vtable::secondary_call_target_ptr(timers.cast::<u8>(), 0, SLOT_CREATE_INTERVAL)
187 }) else {
188 return std::ptr::null_mut();
189 };
190 let create: CreateFn = unsafe { std::mem::transmute(slot) };
191 unsafe { create(timers, handler, interval_ms, true) }
192}
193
194// ---------------------------------------------------------------------------
195// TimersComponent — high-level typed wrapper
196// ---------------------------------------------------------------------------
197
198/// Typed wrapper for the Open Multiplayer server's `ITimersComponent`.
199///
200/// Obtained via `samp::plugin::omp_query::<TimersComponent>()`. Exposes
201/// `create_repeating` for timer creation and the generic `IComponent` methods
202/// (`name`, `version`).
203#[derive(Debug, Clone, Copy)]
204pub struct TimersComponent {
205 ptr: NonNull<ServerComponent>,
206}
207
208impl OmpComponentHandle for TimersComponent {
209 const UID: UID = TIMERS_COMPONENT_UID;
210
211 unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self {
212 Self { ptr }
213 }
214
215 fn as_raw(&self) -> NonNull<ServerComponent> {
216 self.ptr
217 }
218}
219
220impl TimersComponent {
221 /// Returns the component name.
222 #[must_use]
223 pub fn name(&self) -> Option<String> {
224 super::component_api::component_name(self)
225 }
226
227 /// Returns the component version.
228 #[must_use]
229 pub fn version(&self) -> Option<super::types::SemanticVersion> {
230 super::component_api::component_version(self)
231 }
232
233 /// Creates a repeating timer on the server.
234 ///
235 /// `handler` must be heap-allocated (e.g. `Box::into_raw`) and must be
236 /// dropped inside the `TimerHandlerVTable::free` callback.
237 ///
238 /// # Safety
239 /// `handler` must point to a live [`TimerTimeOutHandler`] while the timer
240 /// is active.
241 pub unsafe fn create_repeating(
242 &self,
243 handler: *mut TimerTimeOutHandler,
244 interval_ms: i64,
245 ) -> *mut ITimer {
246 unsafe {
247 create_repeating_timer(
248 self.ptr.as_ptr().cast::<ITimersComponent>(),
249 handler,
250 interval_ms,
251 )
252 }
253 }
254}
255
256/// Kills an active timer, stopping future fires.
257///
258/// After `kill`, the server calls `TimerTimeOutHandler::free(timer)` allowing
259/// the heap-allocated handler to be released. Without it, the handler leaks.
260///
261/// # Safety
262/// `timer` must be a valid pointer returned by `create_repeating_timer`.
263pub unsafe fn kill_timer(timer: *mut ITimer) {
264 let Some((_, slot)) = (unsafe {
265 super::vtable::secondary_call_target_ptr(timer.cast::<u8>(), 0, SLOT_TIMER_KILL)
266 }) else {
267 return;
268 };
269 let kill: KillFn = unsafe { std::mem::transmute(slot) };
270 unsafe { kill(timer) };
271}
272
273#[cfg(test)]
274mod tests {
275 //! Tests for the `timers` module.
276 //!
277 //! Cover: UID constant, `TimerTimeOutHandler` layout, defensive behavior
278 //! of `create_repeating_timer` and `kill_timer` against null or invalid
279 //! inputs.
280
281 use super::*;
282
283 /// Slots verified against the official binaries shipped with open.mp
284 /// 1.5.8.3079: vtable dump of `Timers.so` (Itanium) and of `Timers.dll`
285 /// via its RTTI (MSVC). Getting these wrong is silent: the server returns
286 /// a non-null pointer from the wrong virtual function and no timer runs.
287 #[test]
288 fn slots_match_the_official_binaries() {
289 #[cfg(not(target_env = "msvc"))]
290 {
291 assert_eq!(SLOT_CREATE_INTERVAL, 18, "Timers.so vtable [18] = create");
292 assert_eq!(SLOT_TIMER_KILL, 11, "Timer vtable [11] = kill");
293 }
294 #[cfg(target_env = "msvc")]
295 {
296 assert_eq!(
297 SLOT_CREATE_INTERVAL, 17,
298 "Timers.dll vtable [17] = create(handler, interval, repeating); \
299 MSVC reverses the overload set, [16] is the four-argument one"
300 );
301 assert_eq!(SLOT_TIMER_KILL, 10, "Timer vtable [10] = kill");
302 }
303 }
304
305 #[test]
306 fn timers_component_uid_is_known_value() {
307 // Value declared in `timers.hpp:44` of the Open Multiplayer SDK.
308 assert_eq!(TIMERS_COMPONENT_UID, 0x2ad8_124c_5ea2_57a3);
309 }
310
311 #[test]
312 fn timers_component_uid_via_trait() {
313 assert_eq!(
314 <TimersComponent as OmpComponentHandle>::UID,
315 TIMERS_COMPONENT_UID
316 );
317 }
318
319 #[test]
320 fn timer_handler_has_vtable_at_offset_zero() {
321 // The server reads the vtable at offset 0 of the handler — confirm layout.
322 assert_eq!(std::mem::offset_of!(TimerTimeOutHandler, vtable), 0);
323 }
324
325 #[test]
326 fn timer_handler_size_is_one_pointer() {
327 // No own data: only the vtable pointer.
328 assert_eq!(
329 std::mem::size_of::<TimerTimeOutHandler>(),
330 std::mem::size_of::<*const ()>()
331 );
332 }
333
334 #[test]
335 fn timer_handler_vtable_has_two_slots() {
336 // IUIDProvider does not declare a destructor -> 2 slots (timeout, free).
337 assert_eq!(
338 std::mem::size_of::<TimerHandlerVTable>(),
339 2 * std::mem::size_of::<*const ()>()
340 );
341 }
342
343 #[test]
344 fn create_repeating_timer_returns_null_when_handler_is_null() {
345 let timers = std::ptr::null_mut::<ITimersComponent>();
346 let ret = unsafe { create_repeating_timer(timers, std::ptr::null_mut(), 5) };
347 assert!(ret.is_null());
348 }
349
350 #[test]
351 fn create_repeating_timer_returns_null_when_component_is_null() {
352 // Dummy handler: since timers is null, it should not even try to deref the handler.
353 let fake_handler = std::ptr::dangling_mut::<TimerTimeOutHandler>();
354 let ret = unsafe { create_repeating_timer(std::ptr::null_mut(), fake_handler, 5) };
355 assert!(ret.is_null());
356 }
357
358 #[test]
359 fn kill_timer_is_noop_for_null_pointer() {
360 // Must not panic or segfault.
361 unsafe { kill_timer(std::ptr::null_mut()) };
362 }
363
364 #[test]
365 fn query_timers_component_returns_null_for_null_list() {
366 let ret = unsafe { query_timers_component(std::ptr::null_mut()) };
367 assert!(ret.is_null());
368 }
369}