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}