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 (same on both ABIs — `[6]`).
46const SLOT_COMPONENT_NAME: usize = 6;
47
48/// Slot of `componentVersion()` in the `IComponent` vtable (same on both ABIs — `[8]`).
49const SLOT_COMPONENT_VERSION: usize = 8;
50
51/// Signature of `componentName()` — returns `StringView` via hidden pointer.
52#[cfg(not(target_env = "msvc"))]
53type ComponentNameFn =
54    unsafe extern "C" fn(*mut ServerComponent, *mut StringView) -> *mut StringView;
55
56#[cfg(target_env = "msvc")]
57type ComponentNameFn =
58    unsafe extern "thiscall" fn(*mut ServerComponent, *mut StringView) -> *mut StringView;
59
60/// Signature of `componentVersion()` — returns `SemanticVersion` via hidden pointer.
61#[cfg(not(target_env = "msvc"))]
62type ComponentVersionFn =
63    unsafe extern "C" fn(*mut ServerComponent, *mut SemanticVersion) -> *mut SemanticVersion;
64
65#[cfg(target_env = "msvc")]
66type ComponentVersionFn =
67    unsafe extern "thiscall" fn(*mut ServerComponent, *mut SemanticVersion) -> *mut SemanticVersion;
68
69/// Reads the component name by calling `componentName()` (slot [6] of the `IComponent` vtable).
70///
71/// Returns a `String` with the UTF-8 name (copied — does not retain pointers from the component).
72/// `None` if the component or vtable are null, the slot is empty, the returned
73/// `StringView` is invalid, or the bytes are not valid UTF-8.
74pub fn component_name<T: OmpComponentHandle>(c: &T) -> Option<String> {
75    let raw = c.as_raw().as_ptr();
76    // The primary vtable (IComponent) is at offset 0 of the object.
77    let (_, slot) =
78        unsafe { super::vtable::secondary_call_target(raw.cast::<u8>(), 0, SLOT_COMPONENT_NAME)? };
79    let f: ComponentNameFn = unsafe { std::mem::transmute(slot) };
80    let mut sv = StringView {
81        data: std::ptr::null(),
82        len: 0,
83    };
84    unsafe { f(raw, &raw mut sv) };
85    if sv.data.is_null() || sv.len == 0 {
86        return None;
87    }
88    let bytes = unsafe { std::slice::from_raw_parts(sv.data, sv.len) };
89    std::str::from_utf8(bytes).ok().map(String::from)
90}
91
92/// Reads the component version by calling `componentVersion()` (slot [8] of the `IComponent` vtable).
93///
94/// Official Open Multiplayer components return the server version (e.g. `1.5.8.3079`).
95/// `None` if the component or vtable are null or the slot is empty.
96pub fn component_version<T: OmpComponentHandle>(c: &T) -> Option<SemanticVersion> {
97    let raw = c.as_raw().as_ptr();
98    let (_, slot) = unsafe {
99        super::vtable::secondary_call_target(raw.cast::<u8>(), 0, SLOT_COMPONENT_VERSION)?
100    };
101    let f: ComponentVersionFn = unsafe { std::mem::transmute(slot) };
102    let mut version = SemanticVersion::new(0, 0, 0);
103    unsafe { f(raw, &raw mut version) };
104    Some(version)
105}
106
107#[cfg(test)]
108mod tests {
109    //! Smoke tests for `component_name` and `component_version`.
110    //!
111    //! Sets up a fake `ServerComponent` with a mock vtable at slots [6] (name)
112    //! and [8] (version). Covers typed wrappers via a test type that implements
113    //! [`OmpComponentHandle`].
114
115    use super::*;
116    use std::sync::Mutex;
117
118    static TEST_LOCK: Mutex<()> = Mutex::new(());
119
120    // Mock vtable: 16 slots (minimum size of IComponent MSVC).
121    // Only slots [6] (name) and [8] (version) are populated.
122    static MOCK_VTABLE: std::sync::OnceLock<[usize; 16]> = std::sync::OnceLock::new();
123
124    fn mock_vtable() -> &'static [usize; 16] {
125        MOCK_VTABLE.get_or_init(|| {
126            let mut v = [unused as *const () as usize; 16];
127            v[SLOT_COMPONENT_NAME] = mock_name as *const () as usize;
128            v[SLOT_COMPONENT_VERSION] = mock_version as *const () as usize;
129            v
130        })
131    }
132
133    // The mock functions MUST match the calling convention declared in
134    // `ComponentNameFn` / `ComponentVersionFn` (cfg-gated by ABI). Declaring
135    // them `extern "C"` on MSVC causes a STATUS_ACCESS_VIOLATION because the
136    // call site is built for `thiscall` (this in ECX, callee cleans the
137    // stack with `ret 4`) and reads `out` from the wrong stack slot.
138
139    #[cfg(not(target_env = "msvc"))]
140    unsafe extern "C" fn unused() {}
141    #[cfg(target_env = "msvc")]
142    unsafe extern "thiscall" fn unused() {}
143
144    static MOCK_NAME_BYTES: &[u8] = b"test-comp";
145
146    #[cfg(not(target_env = "msvc"))]
147    unsafe extern "C" fn mock_name(
148        _this: *mut ServerComponent,
149        out: *mut StringView,
150    ) -> *mut StringView {
151        unsafe {
152            *out = StringView {
153                data: MOCK_NAME_BYTES.as_ptr(),
154                len: MOCK_NAME_BYTES.len(),
155            };
156        }
157        out
158    }
159
160    #[cfg(target_env = "msvc")]
161    unsafe extern "thiscall" fn mock_name(
162        _this: *mut ServerComponent,
163        out: *mut StringView,
164    ) -> *mut StringView {
165        unsafe {
166            *out = StringView {
167                data: MOCK_NAME_BYTES.as_ptr(),
168                len: MOCK_NAME_BYTES.len(),
169            };
170        }
171        out
172    }
173
174    #[cfg(not(target_env = "msvc"))]
175    unsafe extern "C" fn mock_version(
176        _this: *mut ServerComponent,
177        out: *mut SemanticVersion,
178    ) -> *mut SemanticVersion {
179        unsafe {
180            *out = SemanticVersion::new(2, 7, 3);
181        }
182        out
183    }
184
185    #[cfg(target_env = "msvc")]
186    unsafe extern "thiscall" fn mock_version(
187        _this: *mut ServerComponent,
188        out: *mut SemanticVersion,
189    ) -> *mut SemanticVersion {
190        unsafe {
191            *out = SemanticVersion::new(2, 7, 3);
192        }
193        out
194    }
195
196    /// Dummy type implementing `OmpComponentHandle` only for the test.
197    #[derive(Debug, Clone, Copy)]
198    struct DummyComponent {
199        ptr: NonNull<ServerComponent>,
200    }
201
202    impl OmpComponentHandle for DummyComponent {
203        const UID: UID = 0xDEAD_BEEF_CAFE_BABE;
204        unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self {
205            Self { ptr }
206        }
207        fn as_raw(&self) -> NonNull<ServerComponent> {
208            self.ptr
209        }
210    }
211
212    /// Builds a value simulating `ServerComponent`: vptr at offset 0.
213    /// The caller must bind to a local to get a stable address.
214    fn make_mock_component() -> usize {
215        mock_vtable().as_ptr() as usize
216    }
217
218    #[test]
219    fn component_name_reads_slot_6_and_returns_string() {
220        let _g = TEST_LOCK.lock().unwrap();
221        let buf = make_mock_component();
222        let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
223        let nn = NonNull::new(raw).unwrap();
224        let comp = unsafe { DummyComponent::from_raw(nn) };
225
226        let name = component_name(&comp);
227        assert_eq!(name.as_deref(), Some("test-comp"));
228    }
229
230    #[test]
231    fn component_version_reads_slot_8_and_returns_semver() {
232        let _g = TEST_LOCK.lock().unwrap();
233        let buf = make_mock_component();
234        let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
235        let nn = NonNull::new(raw).unwrap();
236        let comp = unsafe { DummyComponent::from_raw(nn) };
237
238        let v = component_version(&comp).unwrap();
239        assert_eq!((v.major, v.minor, v.patch), (2, 7, 3));
240    }
241
242    #[test]
243    fn dummy_component_uid_is_consistent() {
244        assert_eq!(DummyComponent::UID, 0xDEAD_BEEF_CAFE_BABE);
245    }
246}