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 super::vtable::{call_vtable_small_struct, slots};
19use std::ptr::NonNull;
20
21/// Trait implemented by typed wrappers for Open Multiplayer components.
22///
23/// Each implementation:
24/// - Provides the component's constant `UID` in [`UID`].
25/// - Constructs itself from a [`NonNull<ServerComponent>`] returned by
26///   [`samp_sdk::omp::server::query_component`].
27/// - Exposes the raw pointer via [`as_raw`].
28///
29/// [`as_raw`]: OmpComponentHandle::as_raw
30/// [`samp_sdk::omp::server::query_component`]: super::server::query_component
31pub trait OmpComponentHandle: Sized + Copy {
32    /// Component UID — known at compile time.
33    const UID: UID;
34
35    /// Builds the wrapper from the pointer returned by `query_component`.
36    ///
37    /// # Safety
38    /// `ptr` must have been obtained via `query_component(_, Self::UID)` and the
39    /// server must keep the component alive while the wrapper is used.
40    unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self;
41
42    /// Returns the raw component pointer.
43    fn as_raw(&self) -> NonNull<ServerComponent>;
44}
45
46/// A server interface reachable as a component, and the UID that finds it.
47///
48/// Implemented for the opaque interface handles (`IObjectsComponent`,
49/// `IVehiclesComponent`, ...). Tying the UID to the type is what [`Component`]
50/// builds on: the lookup and the cast cannot disagree.
51pub trait ComponentInterface {
52    /// The component's UID, as the server's headers declare it.
53    const UID: UID;
54
55    /// Where the `IComponent` subobject sits inside the interface.
56    ///
57    /// The server hands out an `IComponent*`. For almost every component that
58    /// is the start of the object, because `IComponent` is the first base; for
59    /// `INPCComponent` the pool comes first and the `IComponent` sits after its
60    /// vtable pointer, so the pointer has to move back before the interface's
61    /// own methods can be called through it. From clang's record layout, per
62    /// ABI.
63    const COMPONENT_OFFSET: isize = 0;
64}
65
66/// A server component, typed by the interface it implements.
67///
68/// Query it with `samp::plugin::omp_query::<Component<IObjectsComponent>>()`
69/// and pass [`Component::as_ptr`] to the functions that take the interface. The
70/// UID comes from the type, so there is no way to look one component up and
71/// use it as another — which a separate UID constant and cast allowed.
72pub struct Component<I: ComponentInterface> {
73    ptr: NonNull<ServerComponent>,
74    interface: std::marker::PhantomData<*mut I>,
75}
76
77impl<I: ComponentInterface> Component<I> {
78    /// The component as its interface, for the functions that take one.
79    #[must_use]
80    pub fn as_ptr(&self) -> *mut I {
81        // Back from the `IComponent` subobject to the start of the interface.
82        self.ptr
83            .as_ptr()
84            .cast::<u8>()
85            .wrapping_offset(-I::COMPONENT_OFFSET)
86            .cast::<I>()
87    }
88}
89
90// Written by hand: a derive would require `I: Clone`, and the interface types
91// are opaque handles that are never cloned themselves.
92impl<I: ComponentInterface> Clone for Component<I> {
93    fn clone(&self) -> Self {
94        *self
95    }
96}
97
98impl<I: ComponentInterface> Copy for Component<I> {}
99
100impl<I: ComponentInterface> OmpComponentHandle for Component<I> {
101    const UID: UID = I::UID;
102
103    unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self {
104        Self {
105            ptr,
106            interface: std::marker::PhantomData,
107        }
108    }
109
110    fn as_raw(&self) -> NonNull<ServerComponent> {
111        self.ptr
112    }
113}
114
115slots! {
116    /// Slot of `componentName()` in the `IComponent` vtable.
117    ///
118    /// Itanium emits two destructor slots (D1 + D0) where MSVC emits a single
119    /// scalar deleting one, which shifts every method after it by one. Both
120    /// numbers are confirmed against the official `Timers.so` / `Timers.dll`.
121    SLOT_COMPONENT_NAME: usize = 7, 6;
122}
123
124slots! {
125    /// Slot of `componentVersion()` in the `IComponent` vtable (same shift as
126    /// [`SLOT_COMPONENT_NAME`]).
127    SLOT_COMPONENT_VERSION: usize = 9, 8;
128}
129
130/// Reads the component name by calling `componentName()` ([`SLOT_COMPONENT_NAME`] of the `IComponent` vtable).
131///
132/// Returns a `String` with the UTF-8 name (copied — does not retain pointers from the component).
133/// `None` if the component or vtable are null, the slot is empty, the returned
134/// `StringView` is invalid, or the bytes are not valid UTF-8.
135pub fn component_name<T: OmpComponentHandle>(c: &T) -> Option<String> {
136    let raw = c.as_raw().as_ptr().cast::<u8>();
137    let view =
138        call_vtable_small_struct!(raw, 0, SLOT_COMPONENT_NAME, StringView, StringView::EMPTY)?;
139    unsafe { view.to_owned_string() }
140}
141
142/// Reads the component version by calling `componentVersion()` ([`SLOT_COMPONENT_VERSION`] of the `IComponent` vtable).
143///
144/// Official Open Multiplayer components return the server version (e.g. `1.5.8.3079`).
145/// `None` if the component or vtable are null or the slot is empty.
146pub fn component_version<T: OmpComponentHandle>(c: &T) -> Option<SemanticVersion> {
147    let raw = c.as_raw().as_ptr().cast::<u8>();
148    call_vtable_small_struct!(
149        raw,
150        0,
151        SLOT_COMPONENT_VERSION,
152        SemanticVersion,
153        SemanticVersion::new(0, 0, 0)
154    )
155}
156
157#[cfg(test)]
158mod tests {
159    //! Smoke tests for `component_name` and `component_version`.
160    //!
161    //! Sets up a fake `ServerComponent` with a mock vtable at the name
162    //! and version slots for the target ABI. Covers typed wrappers via a test type that implements
163    //! [`OmpComponentHandle`].
164
165    use super::*;
166    use crate::omp::vtable::MockTable;
167    use std::sync::Mutex;
168
169    static TEST_LOCK: Mutex<()> = Mutex::new(());
170
171    // Mock vtable: 16 slots (minimum size of IComponent MSVC).
172    // Only the name and version slots are populated.
173    static MOCK_VTABLE: std::sync::OnceLock<MockTable<16>> = std::sync::OnceLock::new();
174
175    fn mock_vtable() -> &'static [*const (); 16] {
176        &MOCK_VTABLE
177            .get_or_init(|| {
178                let mut v = [unused as *const (); 16];
179                v[SLOT_COMPONENT_NAME] = mock_name as *const ();
180                v[SLOT_COMPONENT_VERSION] = mock_version as *const ();
181                MockTable(v)
182            })
183            .0
184    }
185
186    // The mock functions MUST match the calling convention declared in
187    // `ComponentNameFn` / `ComponentVersionFn` (cfg-gated by ABI). Declaring
188    // them `extern "C"` on MSVC causes a STATUS_ACCESS_VIOLATION because the
189    // call site is built for `thiscall` (this in ECX, callee cleans the
190    // stack with `ret 4`) and reads `out` from the wrong stack slot.
191
192    #[cfg(not(target_env = "msvc"))]
193    unsafe extern "C" fn unused() {}
194    #[cfg(target_env = "msvc")]
195    unsafe extern "thiscall" fn unused() {}
196
197    static MOCK_NAME_BYTES: &[u8] = b"test-comp";
198
199    #[cfg(not(target_env = "msvc"))]
200    // Mirrors the real convention: Itanium returns the struct in registers.
201    unsafe extern "C" fn mock_name(_this: *mut ServerComponent) -> StringView {
202        StringView {
203            data: MOCK_NAME_BYTES.as_ptr(),
204            len: MOCK_NAME_BYTES.len(),
205        }
206    }
207
208    #[cfg(target_env = "msvc")]
209    unsafe extern "thiscall" fn mock_name(
210        _this: *mut ServerComponent,
211        out: *mut StringView,
212    ) -> *mut StringView {
213        unsafe {
214            *out = StringView {
215                data: MOCK_NAME_BYTES.as_ptr(),
216                len: MOCK_NAME_BYTES.len(),
217            };
218        }
219        out
220    }
221
222    #[cfg(not(target_env = "msvc"))]
223    unsafe extern "C" fn mock_version(_this: *mut ServerComponent) -> SemanticVersion {
224        SemanticVersion::new(2, 7, 3)
225    }
226
227    #[cfg(target_env = "msvc")]
228    unsafe extern "thiscall" fn mock_version(
229        _this: *mut ServerComponent,
230        out: *mut SemanticVersion,
231    ) -> *mut SemanticVersion {
232        unsafe {
233            *out = SemanticVersion::new(2, 7, 3);
234        }
235        out
236    }
237
238    /// Dummy type implementing `OmpComponentHandle` only for the test.
239    #[derive(Debug, Clone, Copy)]
240    struct DummyComponent {
241        ptr: NonNull<ServerComponent>,
242    }
243
244    impl OmpComponentHandle for DummyComponent {
245        const UID: UID = 0xDEAD_BEEF_CAFE_BABE;
246        unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self {
247            Self { ptr }
248        }
249        fn as_raw(&self) -> NonNull<ServerComponent> {
250            self.ptr
251        }
252    }
253
254    /// Builds a value simulating `ServerComponent`: vptr at offset 0.
255    /// The caller must bind to a local to get a stable address.
256    fn make_mock_component() -> *const *const () {
257        mock_vtable().as_ptr()
258    }
259
260    /// Slots verified against the official `Timers.so` (Itanium) and
261    /// `Timers.dll` (MSVC) vtables of open.mp 1.5.8.3079.
262    #[test]
263    fn component_slots_match_the_official_binaries() {
264        #[cfg(not(target_env = "msvc"))]
265        {
266            assert_eq!(SLOT_COMPONENT_NAME, 7);
267            assert_eq!(SLOT_COMPONENT_VERSION, 9);
268        }
269        #[cfg(target_env = "msvc")]
270        {
271            assert_eq!(SLOT_COMPONENT_NAME, 6);
272            assert_eq!(SLOT_COMPONENT_VERSION, 8);
273        }
274    }
275
276    #[test]
277    fn component_name_reads_slot_6_and_returns_string() {
278        let _g = TEST_LOCK.lock().unwrap();
279        let buf = make_mock_component();
280        let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
281        let nn = NonNull::new(raw).unwrap();
282        let comp = unsafe { DummyComponent::from_raw(nn) };
283
284        let name = component_name(&comp);
285        assert_eq!(name.as_deref(), Some("test-comp"));
286    }
287
288    #[test]
289    fn component_version_reads_slot_8_and_returns_semver() {
290        let _g = TEST_LOCK.lock().unwrap();
291        let buf = make_mock_component();
292        let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
293        let nn = NonNull::new(raw).unwrap();
294        let comp = unsafe { DummyComponent::from_raw(nn) };
295
296        let v = component_version(&comp).unwrap();
297        assert_eq!((v.major, v.minor, v.patch), (2, 7, 3));
298    }
299
300    #[test]
301    fn dummy_component_uid_is_consistent() {
302        assert_eq!(DummyComponent::UID, 0xDEAD_BEEF_CAFE_BABE);
303    }
304}