Skip to main content

samp_sdk/omp/
component_api.rs

1//! High-level API for Open Multiplayer server components.
2//!
3//! The raw pointer returned by `server::query_component` is opaque — it only
4//! lets you check presence, with no way to interact with the component's
5//! vtable. This module defines the [`OmpComponentHandle`] trait that specific
6//! types (`PawnComponent`, `TimersComponent`, etc.) implement to provide:
7//!
8//! - The component's known `UID` (associated constant)
9//! - Safe construction from the raw pointer
10//! - Access to shared methods (`componentName`, `componentVersion`) via the
11//!   generic utility functions in this module
12//!
13//! Plugins implementing their own external component declare the trait with
14//! the UID generated by the SDK.
15
16use super::server::ServerComponent;
17use super::types::{SemanticVersion, StringView, UID};
18use std::ptr::NonNull;
19
20/// Trait implemented by typed wrappers for Open Multiplayer components.
21///
22/// Each implementation:
23/// - Provides the component's constant `UID` in [`UID`].
24/// - Constructs itself from a [`NonNull<ServerComponent>`] returned by
25///   [`samp_sdk::omp::server::query_component`].
26/// - Exposes the raw pointer via [`as_raw`].
27///
28/// [`as_raw`]: OmpComponentHandle::as_raw
29/// [`samp_sdk::omp::server::query_component`]: super::server::query_component
30pub trait OmpComponentHandle: Sized + Copy {
31    /// Component UID — known at compile time.
32    const UID: UID;
33
34    /// Builds the wrapper from the pointer returned by `query_component`.
35    ///
36    /// # Safety
37    /// `ptr` must have been obtained via `query_component(_, Self::UID)` and the
38    /// server must keep the component alive while the wrapper is used.
39    unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self;
40
41    /// Returns the raw component pointer.
42    fn as_raw(&self) -> NonNull<ServerComponent>;
43}
44
45/// Slot of `componentName()` in the `IComponent` vtable.
46///
47/// Itanium emits two destructor slots (D1 + D0) where MSVC emits a single
48/// scalar deleting one, which shifts every method after it by one. Both
49/// numbers are confirmed against the official `Timers.so` / `Timers.dll`.
50#[cfg(not(target_env = "msvc"))]
51const SLOT_COMPONENT_NAME: usize = 7;
52
53#[cfg(target_env = "msvc")]
54const SLOT_COMPONENT_NAME: usize = 6;
55
56/// Slot of `componentVersion()` in the `IComponent` vtable (same shift as
57/// [`SLOT_COMPONENT_NAME`]).
58#[cfg(not(target_env = "msvc"))]
59const SLOT_COMPONENT_VERSION: usize = 9;
60
61#[cfg(target_env = "msvc")]
62const SLOT_COMPONENT_VERSION: usize = 8;
63
64/// Signature of `componentName()`. The Itanium ABI returns the 8-byte
65/// `StringView` in registers; MSVC returns it through a hidden pointer.
66#[cfg(not(target_env = "msvc"))]
67type ComponentNameFn = unsafe extern "C" fn(*mut ServerComponent) -> StringView;
68
69#[cfg(target_env = "msvc")]
70type ComponentNameFn =
71    unsafe extern "thiscall" fn(*mut ServerComponent, *mut StringView) -> *mut StringView;
72
73/// Signature of `componentVersion()`. Same split as `componentName`: registers
74/// under Itanium, hidden pointer under MSVC.
75#[cfg(not(target_env = "msvc"))]
76type ComponentVersionFn = unsafe extern "C" fn(*mut ServerComponent) -> SemanticVersion;
77
78#[cfg(target_env = "msvc")]
79type ComponentVersionFn =
80    unsafe extern "thiscall" fn(*mut ServerComponent, *mut SemanticVersion) -> *mut SemanticVersion;
81
82/// Reads the component name by calling `componentName()` ([`SLOT_COMPONENT_NAME`] of the `IComponent` vtable).
83///
84/// Returns a `String` with the UTF-8 name (copied — does not retain pointers from the component).
85/// `None` if the component or vtable are null, the slot is empty, the returned
86/// `StringView` is invalid, or the bytes are not valid UTF-8.
87pub fn component_name<T: OmpComponentHandle>(c: &T) -> Option<String> {
88    let raw = c.as_raw().as_ptr();
89    // The primary vtable (IComponent) is at offset 0 of the object.
90    let (_, slot) = unsafe {
91        super::vtable::secondary_call_target_ptr(raw.cast::<u8>(), 0, SLOT_COMPONENT_NAME)?
92    };
93    let f: ComponentNameFn = unsafe { std::mem::transmute(slot) };
94
95    // The Itanium ABI hands back a struct this small in EAX:EDX; MSVC writes it
96    // through a hidden pointer the caller supplies. Getting this backwards
97    // reads whatever the registers happened to hold — or crashes the server, as
98    // it did when the same mistake was made for `IPlayer::getName`.
99    #[cfg(not(target_env = "msvc"))]
100    let sv = unsafe { f(raw) };
101
102    #[cfg(target_env = "msvc")]
103    let sv = {
104        let mut sv = StringView {
105            data: std::ptr::null(),
106            len: 0,
107        };
108        unsafe { f(raw, &raw mut sv) };
109        sv
110    };
111    if sv.data.is_null() || sv.len == 0 {
112        return None;
113    }
114    let bytes = unsafe { std::slice::from_raw_parts(sv.data, sv.len) };
115    std::str::from_utf8(bytes).ok().map(String::from)
116}
117
118/// Reads the component version by calling `componentVersion()` ([`SLOT_COMPONENT_VERSION`] of the `IComponent` vtable).
119///
120/// Official Open Multiplayer components return the server version (e.g. `1.5.8.3079`).
121/// `None` if the component or vtable are null or the slot is empty.
122pub fn component_version<T: OmpComponentHandle>(c: &T) -> Option<SemanticVersion> {
123    let raw = c.as_raw().as_ptr();
124    let (_, slot) = unsafe {
125        super::vtable::secondary_call_target_ptr(raw.cast::<u8>(), 0, SLOT_COMPONENT_VERSION)?
126    };
127    let f: ComponentVersionFn = unsafe { std::mem::transmute(slot) };
128
129    #[cfg(not(target_env = "msvc"))]
130    let version = unsafe { f(raw) };
131
132    #[cfg(target_env = "msvc")]
133    let version = {
134        let mut version = SemanticVersion::new(0, 0, 0);
135        unsafe { f(raw, &raw mut version) };
136        version
137    };
138    Some(version)
139}
140
141#[cfg(test)]
142mod tests {
143    //! Smoke tests for `component_name` and `component_version`.
144    //!
145    //! Sets up a fake `ServerComponent` with a mock vtable at the name
146    //! and version slots for the target ABI. Covers typed wrappers via a test type that implements
147    //! [`OmpComponentHandle`].
148
149    use super::*;
150    use crate::omp::vtable::MockTable;
151    use std::sync::Mutex;
152
153    static TEST_LOCK: Mutex<()> = Mutex::new(());
154
155    // Mock vtable: 16 slots (minimum size of IComponent MSVC).
156    // Only the name and version slots are populated.
157    static MOCK_VTABLE: std::sync::OnceLock<MockTable<16>> = std::sync::OnceLock::new();
158
159    fn mock_vtable() -> &'static [*const (); 16] {
160        &MOCK_VTABLE
161            .get_or_init(|| {
162                let mut v = [unused as *const (); 16];
163                v[SLOT_COMPONENT_NAME] = mock_name as *const ();
164                v[SLOT_COMPONENT_VERSION] = mock_version as *const ();
165                MockTable(v)
166            })
167            .0
168    }
169
170    // The mock functions MUST match the calling convention declared in
171    // `ComponentNameFn` / `ComponentVersionFn` (cfg-gated by ABI). Declaring
172    // them `extern "C"` on MSVC causes a STATUS_ACCESS_VIOLATION because the
173    // call site is built for `thiscall` (this in ECX, callee cleans the
174    // stack with `ret 4`) and reads `out` from the wrong stack slot.
175
176    #[cfg(not(target_env = "msvc"))]
177    unsafe extern "C" fn unused() {}
178    #[cfg(target_env = "msvc")]
179    unsafe extern "thiscall" fn unused() {}
180
181    static MOCK_NAME_BYTES: &[u8] = b"test-comp";
182
183    #[cfg(not(target_env = "msvc"))]
184    // Mirrors the real convention: Itanium returns the struct in registers.
185    unsafe extern "C" fn mock_name(_this: *mut ServerComponent) -> StringView {
186        StringView {
187            data: MOCK_NAME_BYTES.as_ptr(),
188            len: MOCK_NAME_BYTES.len(),
189        }
190    }
191
192    #[cfg(target_env = "msvc")]
193    unsafe extern "thiscall" fn mock_name(
194        _this: *mut ServerComponent,
195        out: *mut StringView,
196    ) -> *mut StringView {
197        unsafe {
198            *out = StringView {
199                data: MOCK_NAME_BYTES.as_ptr(),
200                len: MOCK_NAME_BYTES.len(),
201            };
202        }
203        out
204    }
205
206    #[cfg(not(target_env = "msvc"))]
207    unsafe extern "C" fn mock_version(_this: *mut ServerComponent) -> SemanticVersion {
208        SemanticVersion::new(2, 7, 3)
209    }
210
211    #[cfg(target_env = "msvc")]
212    unsafe extern "thiscall" fn mock_version(
213        _this: *mut ServerComponent,
214        out: *mut SemanticVersion,
215    ) -> *mut SemanticVersion {
216        unsafe {
217            *out = SemanticVersion::new(2, 7, 3);
218        }
219        out
220    }
221
222    /// Dummy type implementing `OmpComponentHandle` only for the test.
223    #[derive(Debug, Clone, Copy)]
224    struct DummyComponent {
225        ptr: NonNull<ServerComponent>,
226    }
227
228    impl OmpComponentHandle for DummyComponent {
229        const UID: UID = 0xDEAD_BEEF_CAFE_BABE;
230        unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self {
231            Self { ptr }
232        }
233        fn as_raw(&self) -> NonNull<ServerComponent> {
234            self.ptr
235        }
236    }
237
238    /// Builds a value simulating `ServerComponent`: vptr at offset 0.
239    /// The caller must bind to a local to get a stable address.
240    fn make_mock_component() -> *const *const () {
241        mock_vtable().as_ptr()
242    }
243
244    /// Slots verified against the official `Timers.so` (Itanium) and
245    /// `Timers.dll` (MSVC) vtables of open.mp 1.5.8.3079.
246    #[test]
247    fn component_slots_match_the_official_binaries() {
248        #[cfg(not(target_env = "msvc"))]
249        {
250            assert_eq!(SLOT_COMPONENT_NAME, 7);
251            assert_eq!(SLOT_COMPONENT_VERSION, 9);
252        }
253        #[cfg(target_env = "msvc")]
254        {
255            assert_eq!(SLOT_COMPONENT_NAME, 6);
256            assert_eq!(SLOT_COMPONENT_VERSION, 8);
257        }
258    }
259
260    #[test]
261    fn component_name_reads_slot_6_and_returns_string() {
262        let _g = TEST_LOCK.lock().unwrap();
263        let buf = make_mock_component();
264        let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
265        let nn = NonNull::new(raw).unwrap();
266        let comp = unsafe { DummyComponent::from_raw(nn) };
267
268        let name = component_name(&comp);
269        assert_eq!(name.as_deref(), Some("test-comp"));
270    }
271
272    #[test]
273    fn component_version_reads_slot_8_and_returns_semver() {
274        let _g = TEST_LOCK.lock().unwrap();
275        let buf = make_mock_component();
276        let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
277        let nn = NonNull::new(raw).unwrap();
278        let comp = unsafe { DummyComponent::from_raw(nn) };
279
280        let v = component_version(&comp).unwrap();
281        assert_eq!((v.major, v.minor, v.patch), (2, 7, 3));
282    }
283
284    #[test]
285    fn dummy_component_uid_is_consistent() {
286        assert_eq!(DummyComponent::UID, 0xDEAD_BEEF_CAFE_BABE);
287    }
288}