Skip to main content

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