samp_sdk/omp/component.rs
1//! `IComponent` interface from the Open Multiplayer SDK in pure Rust.
2//!
3//! The memory layout and vtable indices were derived from the public
4//! specification of the Open Multiplayer SDK (<https://github.com/openmultiplayer/open.mp-sdk>).
5//! No SDK code was copied — only signatures and vtable layouts were used
6//! as reference for this pure Rust reimplementation.
7//!
8//! ## Platform support
9//!
10//! | Target | SA-MP | native Open Multiplayer |
11//! |-----------------------------|-------|----------------|
12//! | `i686-unknown-linux-gnu` | yes | yes (Itanium ABI) |
13//! | `i686-pc-windows-msvc` | yes | yes (MSVC ABI) |
14//! | `i686-pc-windows-gnu` | yes | no (incompatible ABI) |
15//!
16//! ## Multiple inheritance and vtables
17//!
18//! `IComponent : public IExtensible, public IUIDProvider` results in two
19//! vtable pointers in the object. The offsets differ between Itanium (Linux GCC)
20//! and MSVC because the `FlatHashMap` (`robin_hood::unordered_flat_map`) has a
21//! different size on the two platforms — confirmed via disasm of `omp-server.exe`.
22//!
23//! ```text
24//! Offset Field (Linux / GCC i686)
25//! ------ -----
26//! 0 vtable* (primary: IExtensible + IComponent)
27//! 4..39 _misc_ext (robin_hood::unordered_flat_map, 36 bytes)
28//! 40 uid_vtable* (secondary: IUIDProvider)
29//! 44 uid (u64 — plugin's own field)
30//! 52 plugin_ptr (*mut () — plugin's own field)
31//!
32//! Offset Field (Windows / MSVC i686)
33//! ------ -----
34//! 0 vtable* (primary: IExtensible + IComponent)
35//! 4..55 _misc_ext (52 bytes: padding + robin_hood + trailing padding)
36//! 56 uid_vtable* (secondary: IUIDProvider) — offset hardcoded by the server
37//! 60 _uid_pad (4 bytes to align uid (u64) to 8 bytes)
38//! 64 uid (u64 — plugin's own field)
39//! 72 plugin_ptr (*mut () — plugin's own field)
40//! ```
41//!
42//! ## Primary vtable
43//!
44//! **Itanium ABI** — two destructor slots (D1 complete + D0 deleting):
45//!
46//! ```text
47//! [0] getExtension
48//! [1] addExtension
49//! [2] removeExtension(ext*)
50//! [3] removeExtension(uid)
51//! [4] ~destructor D1 (complete object)
52//! [5] ~destructor D0 (deleting)
53//! [6] supportedVersion
54//! [7] componentName
55//! [8] componentType
56//! [9] componentVersion
57//! [10] onLoad
58//! [11] onInit
59//! [12] onReady
60//! [13] onFree
61//! [14] provideConfiguration
62//! [15] free
63//! [16] reset
64//! [17] getUID (the `IUIDProvider` override also lands in the primary vtable)
65//! ```
66//!
67//! **MSVC ABI** — single destructor (scalar deleting) between `IExtensible` and `IComponent`:
68//!
69//! ```text
70//! [0] getExtension
71//! [1] addExtension
72//! [2] removeExtension(uid) <- MSVC emits an overload set in REVERSE
73//! [3] removeExtension(ext*) declaration order
74//! [4] ~destructor (single scalar deleting — MSVC does not emit D0)
75//! [5] supportedVersion
76//! [6] componentName
77//! [7] componentType
78//! [8] componentVersion
79//! [9] onLoad
80//! [10] onInit
81//! [11] onReady
82//! [12] onFree
83//! [13] provideConfiguration
84//! [14] free
85//! [15] reset
86//! ```
87//!
88//! The slots were confirmed by runtime + disasm of `omp-server.exe`
89//! (`componentVersion` calls at `[edx+0x20]` = slot 8; `componentName` at
90//! `[eax+0x18]` = slot 6).
91//!
92//! ## Secondary vtable — `IUIDProvider`
93//!
94//! **Itanium ABI** — a single slot, exactly like MSVC. `IUIDProvider` declares
95//! no virtual destructor, so its secondary vtable holds only the `getUID`
96//! thunk (confirmed in the official `Timers.so` and `Pawn.so`: the secondary
97//! vtable at offset-to-top `-40` has one entry).
98//!
99//! ```text
100//! [0] getUID
101//! ```
102//!
103//! **MSVC ABI** — only `getUID` (`IUIDProvider` does not declare a virtual destructor):
104//!
105//! ```text
106//! [0] getUID
107//! ```
108
109#[allow(unused_imports)]
110use super::types::{ComponentType, SemanticVersion, StringView, UID};
111use super::vtable::{opaque, slots};
112
113// ---------------------------------------------------------------------------
114// Opaque types — pointers to server interfaces we do not implement
115// ---------------------------------------------------------------------------
116
117opaque! {
118 /// `ICore*` — opaque pointer to the Open Multiplayer server core.
119 /// Received in `on_load`; use only for caching or future queries.
120 pub ICore;
121 /// `IComponentList*` — list of loaded components.
122 /// Received in `on_init`; use to query other components.
123 pub IComponentList;
124 /// `ILogger*` — server logging interface.
125 pub ILogger;
126 /// `IEarlyConfig*` — configuration during initialization.
127 pub IEarlyConfig;
128}
129
130// ---------------------------------------------------------------------------
131// Primary vtable: IExtensible + IComponent — Itanium ABI
132// ---------------------------------------------------------------------------
133//
134// **Why duplicate the entire vtable (Itanium vs MSVC) instead of using type
135// aliases?** Function signatures differ **substantially**, not just in
136// calling convention:
137//
138// - **MSVC** returns `StringView` (8 bytes) and `SemanticVersion` (6 bytes)
139// via hidden pointer in `[ESP+4]`. The functions become `extern "thiscall"
140// fn()` (no parameters, naked asm) so Rust emits `ret` without
141// `ret 4`. Itanium, by contrast, returns these types by value with the
142// full signature `fn(*const OmpComponent) -> StringView`.
143// - **MSVC** collapses the two destructor slots (D1+D0) into a single
144// scalar deleting.
145//
146// A type alias only covers ABI; here the function shape itself changes.
147// Keeping the two definitions explicit is clearer than trying to abstract.
148
149/// Primary `IComponent` vtable for the Itanium ABI (Linux).
150///
151/// Calling convention: `extern "C"` (cdecl).
152/// Two destructor slots: D1 (complete) and D0 (deleting).
153#[cfg(not(target_env = "msvc"))]
154#[repr(C)]
155pub struct IComponentVTable {
156 // --- IExtensible [0-3] ---
157 pub get_extension: unsafe extern "C" fn(*mut OmpComponent, uid: UID) -> *mut (),
158 pub add_extension:
159 unsafe extern "C" fn(*mut OmpComponent, ext: *mut (), auto_delete: bool) -> bool,
160 pub remove_extension_ptr: unsafe extern "C" fn(*mut OmpComponent, ext: *mut ()) -> bool,
161 pub remove_extension_uid: unsafe extern "C" fn(*mut OmpComponent, uid: UID) -> bool,
162 /// D1 — complete object destructor (Itanium ABI).
163 pub destructor: unsafe extern "C" fn(*mut OmpComponent),
164 /// D0 — deleting destructor (Itanium ABI requires two slots).
165 pub destructor_deleting: unsafe extern "C" fn(*mut OmpComponent),
166 // --- IComponent [6-16] ---
167 pub supported_version: unsafe extern "C" fn(*const OmpComponent) -> i32,
168 pub component_name: unsafe extern "C" fn(*const OmpComponent) -> StringView,
169 pub component_type: unsafe extern "C" fn(*const OmpComponent) -> ComponentType,
170 pub component_version: unsafe extern "C" fn(*const OmpComponent) -> SemanticVersion,
171 pub on_load: unsafe extern "C" fn(*mut OmpComponent, *mut ICore),
172 pub on_init: unsafe extern "C" fn(*mut OmpComponent, *mut IComponentList),
173 pub on_ready: unsafe extern "C" fn(*mut OmpComponent),
174 pub on_free: unsafe extern "C" fn(*mut OmpComponent, *mut OmpComponent),
175 pub provide_configuration:
176 unsafe extern "C" fn(*mut OmpComponent, *mut ILogger, *mut IEarlyConfig, bool),
177 pub free: unsafe extern "C" fn(*mut OmpComponent),
178 pub reset: unsafe extern "C" fn(*mut OmpComponent),
179 // --- IUIDProvider override [17] ---
180 /// Itanium places the `getUID()` override in the primary vtable as well as
181 /// in the secondary one (confirmed in the official `Timers.so` / `Pawn.so`,
182 /// where it sits at slot [17]). MSVC keeps it only in the secondary vtable.
183 pub get_uid: unsafe extern "C" fn(*const OmpComponent) -> UID,
184}
185
186// ---------------------------------------------------------------------------
187// Primary vtable: IExtensible + IComponent — MSVC ABI
188// ---------------------------------------------------------------------------
189
190/// Primary `IComponent` vtable for the MSVC ABI (Windows).
191///
192/// Calling convention: `extern "thiscall"` (`this` in ECX).
193///
194/// MSVC i686 with single inheritance generates **a single** destructor slot (scalar deleting).
195/// The destructor sits at the position where `~IExtensible()` was declared (after the other
196/// IExtensible virtuals):
197/// [0] getExtension, [1] addExtension, [2] removeExtension(UID),
198/// [3] removeExtension(ptr), [4] ~IExtensible (scalar deleting)
199/// (MSVC reverses overload sets — see the field comments below)
200/// IComponent adds:
201/// [5] supportedVersion, [6] componentName, [7] componentType,
202/// [8] componentVersion, [9] onLoad, [10] onInit, [11] onReady,
203/// [12] onFree, [13] provideConfiguration, [14] free, [15] reset
204#[cfg(target_env = "msvc")]
205#[repr(C)]
206pub struct IComponentVTable {
207 // --- IExtensible [0-4] ---
208 pub get_extension: unsafe extern "thiscall" fn(*mut OmpComponent, uid: UID) -> *mut (),
209 pub add_extension:
210 unsafe extern "thiscall" fn(*mut OmpComponent, ext: *mut (), auto_delete: bool) -> bool,
211 // MSVC emits an overload set in REVERSE declaration order, so
212 // `removeExtension(UID)` comes before `removeExtension(IExtension*)`.
213 // Confirmed by disassembly of the official `Timers.dll`: slot [2] ends in
214 // `ret 8` (the 8-byte UID) and slot [3] in `ret 4` (the pointer). Getting
215 // this backwards corrupts the stack, because under `thiscall` the callee
216 // pops the arguments.
217 pub remove_extension_uid: unsafe extern "thiscall" fn(*mut OmpComponent, uid: UID) -> bool,
218 pub remove_extension_ptr: unsafe extern "thiscall" fn(*mut OmpComponent, ext: *mut ()) -> bool,
219 // Functions with no stack args besides this: this in ECX, no explicit parameter.
220 // This prevents the compiler from emitting `ret 4` which would corrupt the stack.
221 pub destructor: unsafe extern "thiscall" fn(),
222 pub supported_version: unsafe extern "thiscall" fn() -> i32,
223 // Naked functions: return via eax:edx, return type () in the Rust signature.
224 pub component_name: unsafe extern "thiscall" fn(),
225 pub component_type: unsafe extern "thiscall" fn() -> i32,
226 pub component_version: unsafe extern "thiscall" fn(),
227 // Functions with additional stack args: this in ECX + args on the stack (ret N correct).
228 pub on_load: unsafe extern "thiscall" fn(*mut OmpComponent, *mut ICore),
229 pub on_init: unsafe extern "thiscall" fn(*mut OmpComponent, *mut IComponentList),
230 pub on_ready: unsafe extern "thiscall" fn(),
231 pub on_free: unsafe extern "thiscall" fn(*mut OmpComponent, *mut OmpComponent),
232 pub provide_configuration:
233 unsafe extern "thiscall" fn(*mut OmpComponent, *mut ILogger, *mut IEarlyConfig, bool),
234 pub free: unsafe extern "thiscall" fn(),
235 pub reset: unsafe extern "thiscall" fn(),
236}
237
238// ---------------------------------------------------------------------------
239// Secondary vtable: IUIDProvider — Itanium ABI
240// ---------------------------------------------------------------------------
241//
242// Genuinely different layouts: Itanium has 3 slots (D1, D0, getUID);
243// MSVC has 1 slot (only getUID — no virtual destructor). Kept duplicated
244// because the static initializers in `crates/samp-codegen/src/plugin.rs` are also
245// cfg-gated with different field names; unifying would require changing
246// both ends and would lose the clarity of the `pub destructor_*` fields on Itanium.
247
248/// Secondary `IUIDProvider` vtable for the Itanium ABI (Linux).
249///
250/// Two destructor thunk slots before `getUID`.
251#[cfg(not(target_env = "msvc"))]
252#[repr(C)]
253pub struct IUIDProviderVTable {
254 /// Slot [0]: `getUID()` — `this` points to the `IUIDProvider` subobject
255 /// (offset 40 on Linux).
256 pub get_uid: unsafe extern "C" fn(*const u8) -> UID,
257}
258
259// ---------------------------------------------------------------------------
260// Secondary vtable: IUIDProvider — MSVC ABI
261// ---------------------------------------------------------------------------
262
263/// Secondary `IUIDProvider` vtable for the MSVC ABI (Windows).
264///
265/// `IUIDProvider` declares ONLY `virtual UID getUID() = 0;` — no virtual destructor.
266/// Confirmed by server disasm: `add ecx, 0x38; mov eax, [esi+0x38]; call [eax]`
267/// (adjusts `this` by +56, loads secondary vtable, calls slot [0]).
268#[cfg(target_env = "msvc")]
269#[repr(C)]
270pub struct IUIDProviderVTable {
271 /// Slot [0]: `getUID()` — `this` points to the IUIDProvider subobject (offset 56 on MSVC).
272 pub get_uid: unsafe extern "thiscall" fn(*const u8) -> UID,
273}
274
275// ---------------------------------------------------------------------------
276// Object compatible with IComponent* — per-platform layout
277// ---------------------------------------------------------------------------
278
279slots! {
280 /// Bytes of `IExtensible`'s `miscExtensions` map. The server expects the
281 /// `IUIDProvider` vptr right after `IExtensible`: offset 40 on Itanium,
282 /// 56 on MSVC (4-byte vptr plus this), confirmed by disassembling the server.
283 MISC_EXT_SIZE: usize = 36, 52;
284}
285
286/// Rust object with a layout compatible with Open Multiplayer's `IComponent*`.
287///
288/// The layout differs between Linux (GCC i686) and Windows MSVC i686 because
289/// `FlatHashMap` (robin_hood::unordered_flat_map) has a different sizeof on
290/// each platform. The offset of `uid_vtable` (IUIDProvider subobject) is
291/// hardcoded by the server and was confirmed via disasm:
292/// - Linux/GCC i686: `uid_vtable` at offset **40**.
293/// - MSVC i686: `uid_vtable` at offset **56** (server emits `add ecx, 0x38`
294/// when calling `getUID()` on `IComponent*`).
295///
296/// Layout on i686 Linux (GCC / Itanium ABI):
297/// ```text
298/// offset 0: vtable* (primary IExtensible/IComponent)
299/// offset 4: _misc_ext[36] (robin_hood::unordered_flat_map, zero-init = empty)
300/// offset 40: uid_vtable* (secondary IUIDProvider)
301/// offset 44: uid (UID = u64)
302/// offset 52: plugin_ptr (*mut ())
303/// ```
304///
305/// Layout on i686 Windows (MSVC ABI):
306/// ```text
307/// offset 0: vtable* (primary IExtensible/IComponent)
308/// offset 4: _misc_ext[52] (padding + robin_hood + trailing pad, zero-init)
309/// offset 56: uid_vtable* (secondary IUIDProvider)
310/// offset 60: _uid_pad[4] (padding to align uid (u64) to 8 bytes)
311/// offset 64: uid (UID = u64)
312/// offset 72: plugin_ptr (*mut ())
313/// ```
314#[repr(C)]
315pub struct OmpComponent {
316 vtable: *const IComponentVTable,
317 _misc_ext: [u8; MISC_EXT_SIZE],
318 uid_vtable: *const IUIDProviderVTable,
319 #[cfg(target_env = "msvc")]
320 _uid_pad: u32,
321 /// Unique UID for this component.
322 pub uid: UID,
323 /// Pointer to the Rust plugin (`SampPlugin`).
324 pub plugin_ptr: *mut (),
325}
326
327// SAFETY: OmpComponent is sent to the server as an opaque pointer.
328// The server is single-threaded across component lifecycle calls.
329unsafe impl Send for OmpComponent {}
330unsafe impl Sync for OmpComponent {}
331
332/// Compile-time check of the `OmpComponent` layout on i686 Linux (Itanium ABI).
333///
334/// On GCC i686, `uint64_t` is aligned to 4 bytes — without `_pad`, the `robin_hood` map
335/// starts at offset 4 and `uid_vtable` (the `IUIDProvider` subobject) lands at offset 40.
336#[cfg(all(target_arch = "x86", target_os = "linux"))]
337const _: () = {
338 assert!(
339 std::mem::offset_of!(OmpComponent, uid_vtable) == 40,
340 "OmpComponent: invalid offset. On GCC i686, uint64_t is aligned to 4 bytes — uid_vtable must be at offset 40."
341 );
342 assert!(
343 std::mem::size_of::<OmpComponent>() == 56,
344 "OmpComponent: invalid size for the Itanium ABI. Use --target i686-unknown-linux-gnu to compile with native Open Multiplayer support."
345 );
346};
347
348/// Compile-time check of the `OmpComponent` layout on i686 Windows MSVC.
349///
350/// The Open Multiplayer server expects the IUIDProvider vptr at offset 56 (confirmed by disasm of
351/// `omp-server.exe`: `add ecx, 0x38` when calling `getUID()` via `IComponent*`).
352/// Total IExtensible = vptr(4) + miscExtensions+padding(52) = 56 bytes.
353#[cfg(all(target_arch = "x86", target_env = "msvc"))]
354const _: () = {
355 assert!(
356 std::mem::offset_of!(OmpComponent, uid_vtable) == 56,
357 "OmpComponent MSVC: uid_vtable must be at offset 56 (IUIDProvider after IExtensible=56 bytes)."
358 );
359};
360
361impl OmpComponent {
362 /// Creates a new `OmpComponent` with a layout compatible with the platform ABI.
363 #[must_use]
364 pub fn new(
365 vtable: *const IComponentVTable,
366 uid_vtable: *const IUIDProviderVTable,
367 uid: UID,
368 ) -> Self {
369 Self {
370 vtable,
371 _misc_ext: [0u8; MISC_EXT_SIZE],
372 uid_vtable,
373 #[cfg(target_env = "msvc")]
374 _uid_pad: 0,
375 uid,
376 plugin_ptr: std::ptr::null_mut(),
377 }
378 }
379}
380
381// ---------------------------------------------------------------------------
382// Default implementations of primary vtable functions — Itanium ABI
383// ---------------------------------------------------------------------------
384
385/// # Safety
386/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
387#[cfg(not(target_env = "msvc"))]
388pub unsafe extern "C" fn ext_get_extension(_this: *mut OmpComponent, _uid: UID) -> *mut () {
389 std::ptr::null_mut()
390}
391
392/// # Safety
393/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
394#[cfg(not(target_env = "msvc"))]
395pub unsafe extern "C" fn ext_add_extension(
396 _this: *mut OmpComponent,
397 _ext: *mut (),
398 _auto_delete: bool,
399) -> bool {
400 false
401}
402
403/// # Safety
404/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
405#[cfg(not(target_env = "msvc"))]
406pub unsafe extern "C" fn ext_remove_extension_ptr(_this: *mut OmpComponent, _ext: *mut ()) -> bool {
407 false
408}
409
410/// # Safety
411/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
412#[cfg(not(target_env = "msvc"))]
413pub unsafe extern "C" fn ext_remove_extension_uid(_this: *mut OmpComponent, _uid: UID) -> bool {
414 false
415}
416
417/// D1 complete object destructor — no-op: cleanup is done via `free()`.
418///
419/// # Safety
420/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
421#[cfg(not(target_env = "msvc"))]
422pub unsafe extern "C" fn ext_destructor(_this: *mut OmpComponent) {}
423
424/// D0 deleting destructor — no-op: the server must not call `delete` on the component.
425///
426/// # Safety
427/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
428#[cfg(not(target_env = "msvc"))]
429pub unsafe extern "C" fn ext_destructor_deleting(_this: *mut OmpComponent) {}
430
431/// # Safety
432/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
433#[cfg(not(target_env = "msvc"))]
434#[must_use]
435pub unsafe extern "C" fn comp_supported_version(_this: *const OmpComponent) -> i32 {
436 1
437}
438
439/// # Safety
440/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
441#[cfg(not(target_env = "msvc"))]
442#[must_use]
443pub unsafe extern "C" fn comp_component_type(_this: *const OmpComponent) -> ComponentType {
444 ComponentType::Other
445}
446
447/// # Safety
448/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
449#[cfg(not(target_env = "msvc"))]
450pub unsafe extern "C" fn comp_on_init(_this: *mut OmpComponent, _components: *mut IComponentList) {}
451
452/// # Safety
453/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
454#[cfg(not(target_env = "msvc"))]
455pub unsafe extern "C" fn comp_on_ready(_this: *mut OmpComponent) {}
456
457/// # Safety
458/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
459#[cfg(not(target_env = "msvc"))]
460pub unsafe extern "C" fn comp_on_free(_this: *mut OmpComponent, _component: *mut OmpComponent) {}
461
462/// # Safety
463/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
464#[cfg(not(target_env = "msvc"))]
465pub unsafe extern "C" fn comp_provide_configuration(
466 _this: *mut OmpComponent,
467 _logger: *mut ILogger,
468 _config: *mut IEarlyConfig,
469 _defaults: bool,
470) {
471}
472
473// ---------------------------------------------------------------------------
474// Default implementations of primary vtable functions — MSVC ABI
475// ---------------------------------------------------------------------------
476
477/// # Safety
478/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
479#[cfg(target_env = "msvc")]
480pub unsafe extern "thiscall" fn ext_get_extension(_this: *mut OmpComponent, _uid: UID) -> *mut () {
481 std::ptr::null_mut()
482}
483
484/// # Safety
485/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
486#[cfg(target_env = "msvc")]
487pub unsafe extern "thiscall" fn ext_add_extension(
488 _this: *mut OmpComponent,
489 _ext: *mut (),
490 _auto_delete: bool,
491) -> bool {
492 false
493}
494
495/// # Safety
496/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
497#[cfg(target_env = "msvc")]
498pub unsafe extern "thiscall" fn ext_remove_extension_ptr(
499 _this: *mut OmpComponent,
500 _ext: *mut (),
501) -> bool {
502 false
503}
504
505/// # Safety
506/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
507#[cfg(target_env = "msvc")]
508pub unsafe extern "thiscall" fn ext_remove_extension_uid(
509 _this: *mut OmpComponent,
510 _uid: UID,
511) -> bool {
512 false
513}
514
515/// Scalar deleting destructor — no-op: cleanup is done via `free()`.
516/// No explicit parameter: this in ECX, no args on the stack (avoids `ret 4`).
517///
518/// # Safety
519/// Called by the Open Multiplayer server via vtable.
520#[cfg(target_env = "msvc")]
521pub unsafe extern "thiscall" fn ext_destructor() {}
522
523/// # Safety
524/// Called by the Open Multiplayer server via vtable; this in ECX (ignored), no args on the stack.
525#[cfg(target_env = "msvc")]
526pub unsafe extern "thiscall" fn comp_supported_version() -> i32 {
527 1
528}
529
530/// # Safety
531/// Called by the Open Multiplayer server via vtable; this in ECX (ignored), no args on the stack.
532#[cfg(target_env = "msvc")]
533pub unsafe extern "thiscall" fn comp_component_type() -> i32 {
534 0
535}
536
537/// # Safety
538/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
539#[cfg(target_env = "msvc")]
540pub unsafe extern "thiscall" fn comp_on_init(
541 _this: *mut OmpComponent,
542 _components: *mut IComponentList,
543) {
544}
545
546/// # Safety
547/// Called by the Open Multiplayer server via vtable; this in ECX (ignored), no args on the stack.
548#[cfg(target_env = "msvc")]
549pub unsafe extern "thiscall" fn comp_on_ready() {}
550
551/// # Safety
552/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
553#[cfg(target_env = "msvc")]
554pub unsafe extern "thiscall" fn comp_on_free(
555 _this: *mut OmpComponent,
556 _component: *mut OmpComponent,
557) {
558}
559
560/// # Safety
561/// Called by the Open Multiplayer server via vtable; `_this` must be a valid pointer to `OmpComponent`.
562#[cfg(target_env = "msvc")]
563pub unsafe extern "thiscall" fn comp_provide_configuration(
564 _this: *mut OmpComponent,
565 _logger: *mut ILogger,
566 _config: *mut IEarlyConfig,
567 _defaults: bool,
568) {
569}
570
571// ---------------------------------------------------------------------------
572// Default implementations of the secondary vtable (IUIDProvider) — Itanium ABI
573// ---------------------------------------------------------------------------
574
575/// `getUID()` via the primary vtable (Itanium ABI, slot [17]).
576///
577/// Here `this` already points to the start of the object — no thunk
578/// adjustment, unlike [`uid_get_uid`].
579///
580/// # Safety
581/// `this` must be a valid pointer to an `OmpComponent`.
582#[cfg(not(target_env = "msvc"))]
583#[must_use]
584pub unsafe extern "C" fn comp_get_uid(this: *const OmpComponent) -> UID {
585 unsafe { (*this).uid }
586}
587
588/// `getUID()` via the secondary `IUIDProvider` vtable (Itanium ABI).
589///
590/// `this` points to the `IUIDProvider` subobject (offset 44). We subtract
591/// `offsetof(OmpComponent, uid_vtable)` to recover the pointer to the object.
592///
593/// # Safety
594/// `this` must be a valid pointer to the `IUIDProvider` subobject of an `OmpComponent`.
595#[cfg(not(target_env = "msvc"))]
596#[must_use]
597pub unsafe extern "C" fn uid_get_uid(this: *const u8) -> UID {
598 let offset = std::mem::offset_of!(OmpComponent, uid_vtable);
599 // FFI: `OmpComponent` is allocated via `Box::new` (alignment >= 8 bytes on
600 // i686); subtracting `offsetof(uid_vtable)` recovers the start of the object.
601 #[allow(clippy::cast_ptr_alignment)]
602 let comp_ptr = this.wrapping_sub(offset).cast::<OmpComponent>();
603 unsafe { (*comp_ptr).uid }
604}
605
606// ---------------------------------------------------------------------------
607// Implementation of the secondary vtable (IUIDProvider) — MSVC ABI
608// ---------------------------------------------------------------------------
609
610/// `getUID()` via the secondary IUIDProvider vtable (MSVC ABI).
611///
612/// `this` points to the IUIDProvider subobject at offset 56 of `OmpComponent`.
613/// We subtract `offsetof(OmpComponent, uid_vtable)` to recover the pointer to the object.
614///
615/// # Safety
616/// `this` must be a valid pointer to the `IUIDProvider` subobject of an `OmpComponent`.
617#[cfg(target_env = "msvc")]
618pub unsafe extern "thiscall" fn uid_get_uid(this: *const u8) -> UID {
619 let offset = std::mem::offset_of!(OmpComponent, uid_vtable);
620 let comp_ptr = this.wrapping_sub(offset).cast::<OmpComponent>();
621 unsafe { (*comp_ptr).uid }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 #[cfg(not(target_env = "msvc"))]
628 use crate::omp::types::SemanticVersion;
629
630 // Helper functions to assemble vtables in tests.
631 // The calling convention varies per ABI: "C" on Itanium (Linux), "thiscall" on MSVC.
632 // On MSVC, methods with no stack args are declared `fn()` (this lives in ECX);
633 // declaring an explicit `_this` would make Rust emit `ret 4` and corrupt the stack.
634 #[cfg(not(target_env = "msvc"))]
635 unsafe extern "C" fn test_name(_: *const OmpComponent) -> StringView {
636 StringView::from_static("test\0")
637 }
638 #[cfg(target_env = "msvc")]
639 unsafe extern "thiscall" fn test_name() {}
640
641 #[cfg(not(target_env = "msvc"))]
642 unsafe extern "C" fn test_version(_: *const OmpComponent) -> SemanticVersion {
643 SemanticVersion::new(1, 0, 0)
644 }
645 #[cfg(target_env = "msvc")]
646 unsafe extern "thiscall" fn test_version() {}
647
648 #[cfg(not(target_env = "msvc"))]
649 unsafe extern "C" fn test_on_load(_: *mut OmpComponent, _: *mut ICore) {}
650 #[cfg(target_env = "msvc")]
651 unsafe extern "thiscall" fn test_on_load(_: *mut OmpComponent, _: *mut ICore) {}
652
653 #[cfg(not(target_env = "msvc"))]
654 unsafe extern "C" fn test_on_init(_: *mut OmpComponent, _: *mut IComponentList) {}
655 #[cfg(target_env = "msvc")]
656 unsafe extern "thiscall" fn test_on_init(_: *mut OmpComponent, _: *mut IComponentList) {}
657
658 #[cfg(not(target_env = "msvc"))]
659 unsafe extern "C" fn test_on_ready(_: *mut OmpComponent) {}
660 #[cfg(target_env = "msvc")]
661 unsafe extern "thiscall" fn test_on_ready() {}
662
663 #[cfg(not(target_env = "msvc"))]
664 unsafe extern "C" fn test_on_free(_: *mut OmpComponent, _: *mut OmpComponent) {}
665 #[cfg(target_env = "msvc")]
666 unsafe extern "thiscall" fn test_on_free(_: *mut OmpComponent, _: *mut OmpComponent) {}
667
668 #[cfg(not(target_env = "msvc"))]
669 unsafe extern "C" fn test_provide_cfg(
670 _: *mut OmpComponent,
671 _: *mut ILogger,
672 _: *mut IEarlyConfig,
673 _: bool,
674 ) {
675 }
676 #[cfg(target_env = "msvc")]
677 unsafe extern "thiscall" fn test_provide_cfg(
678 _: *mut OmpComponent,
679 _: *mut ILogger,
680 _: *mut IEarlyConfig,
681 _: bool,
682 ) {
683 }
684
685 #[cfg(not(target_env = "msvc"))]
686 unsafe extern "C" fn test_free(_: *mut OmpComponent) {}
687 #[cfg(target_env = "msvc")]
688 unsafe extern "thiscall" fn test_free() {}
689
690 #[cfg(not(target_env = "msvc"))]
691 unsafe extern "C" fn test_reset(_: *mut OmpComponent) {}
692 #[cfg(target_env = "msvc")]
693 unsafe extern "thiscall" fn test_reset() {}
694
695 #[cfg(not(target_env = "msvc"))]
696 fn make_vtable() -> IComponentVTable {
697 IComponentVTable {
698 get_extension: ext_get_extension,
699 add_extension: ext_add_extension,
700 remove_extension_ptr: ext_remove_extension_ptr,
701 remove_extension_uid: ext_remove_extension_uid,
702 destructor: ext_destructor,
703 destructor_deleting: ext_destructor_deleting,
704 supported_version: comp_supported_version,
705 component_name: test_name,
706 component_type: comp_component_type,
707 component_version: test_version,
708 on_load: test_on_load,
709 on_init: test_on_init,
710 on_ready: test_on_ready,
711 on_free: test_on_free,
712 provide_configuration: test_provide_cfg,
713 free: test_free,
714 reset: test_reset,
715 get_uid: comp_get_uid,
716 }
717 }
718
719 #[cfg(target_env = "msvc")]
720 fn make_vtable() -> IComponentVTable {
721 IComponentVTable {
722 get_extension: ext_get_extension,
723 add_extension: ext_add_extension,
724 remove_extension_ptr: ext_remove_extension_ptr,
725 remove_extension_uid: ext_remove_extension_uid,
726 destructor: ext_destructor,
727 supported_version: comp_supported_version,
728 component_name: test_name,
729 component_type: comp_component_type,
730 component_version: test_version,
731 on_load: test_on_load,
732 on_init: test_on_init,
733 on_ready: test_on_ready,
734 on_free: test_on_free,
735 provide_configuration: test_provide_cfg,
736 free: test_free,
737 reset: test_reset,
738 }
739 }
740
741 #[cfg(not(target_env = "msvc"))]
742 fn make_uid_vtable() -> IUIDProviderVTable {
743 IUIDProviderVTable {
744 get_uid: uid_get_uid,
745 }
746 }
747
748 #[cfg(target_env = "msvc")]
749 fn make_uid_vtable() -> IUIDProviderVTable {
750 IUIDProviderVTable {
751 get_uid: uid_get_uid,
752 }
753 }
754
755 // --- Layout ---
756
757 #[test]
758 #[cfg(all(target_arch = "x86", target_os = "linux"))]
759 fn omp_component_layout_i686_linux() {
760 // GCC i686: uint64_t aligned to 4 bytes -> no _pad, uid_vtable at offset 40
761 assert_eq!(std::mem::offset_of!(OmpComponent, uid_vtable), 40);
762 assert_eq!(std::mem::size_of::<OmpComponent>(), 56);
763 }
764
765 #[test]
766 #[cfg(all(target_arch = "x86", target_env = "msvc"))]
767 fn omp_component_layout_i686_msvc() {
768 // Open Multiplayer server expects the IUIDProvider vptr at offset 56 (confirmed by
769 // disasm: `add ecx, 0x38` when calling getUID via IComponent*).
770 assert_eq!(std::mem::offset_of!(OmpComponent, uid_vtable), 56);
771 }
772
773 // --- OmpComponent::new ---
774
775 #[test]
776 fn omp_component_new_stores_uid() {
777 let vt = make_vtable();
778 let uvt = make_uid_vtable();
779 let comp = OmpComponent::new(&raw const vt, &raw const uvt, 0xDEAD_BEEF_CAFE_BABE);
780 assert_eq!(comp.uid, 0xDEAD_BEEF_CAFE_BABE);
781 }
782
783 #[test]
784 fn omp_component_plugin_ptr_null_on_new() {
785 let vt = make_vtable();
786 let uvt = make_uid_vtable();
787 let comp = OmpComponent::new(&raw const vt, &raw const uvt, 0);
788 assert!(comp.plugin_ptr.is_null());
789 }
790
791 // --- uid_get_uid ---
792
793 #[test]
794 fn uid_get_uid_recovers_from_subobject_pointer() {
795 let vt = make_vtable();
796 let uvt = make_uid_vtable();
797 let comp = OmpComponent::new(&raw const vt, &raw const uvt, 0xCAFE_BABE_u64);
798 // Derive from the whole object, as the server does: a pointer taken from the
799 // field alone only carries provenance for that field.
800 let uid_ptr = (&raw const comp)
801 .cast::<u8>()
802 .wrapping_add(std::mem::offset_of!(OmpComponent, uid_vtable));
803 let recovered = unsafe { uid_get_uid(uid_ptr) };
804 assert_eq!(recovered, 0xCAFE_BABE_u64);
805 }
806
807 // --- Default vtable functions ---
808
809 #[test]
810 fn ext_get_extension_returns_null() {
811 let vt = make_vtable();
812 let uvt = make_uid_vtable();
813 let mut comp = OmpComponent::new(&raw const vt, &raw const uvt, 0);
814 let result = unsafe { ext_get_extension(&raw mut comp, 0) };
815 assert!(result.is_null());
816 }
817
818 #[test]
819 fn ext_add_extension_returns_false() {
820 let vt = make_vtable();
821 let uvt = make_uid_vtable();
822 let mut comp = OmpComponent::new(&raw const vt, &raw const uvt, 0);
823 let result = unsafe { ext_add_extension(&raw mut comp, std::ptr::null_mut(), false) };
824 assert!(!result);
825 }
826
827 #[test]
828 #[cfg(not(target_env = "msvc"))]
829 fn comp_supported_version_is_one() {
830 let vt = make_vtable();
831 let uvt = make_uid_vtable();
832 let comp = OmpComponent::new(&raw const vt, &raw const uvt, 0);
833 assert_eq!(unsafe { comp_supported_version(&raw const comp) }, 1);
834 }
835
836 /// On MSVC `comp_supported_version` takes `this` in `ECX` with no
837 /// stack args (`fn()`); the Rust call site cannot pass `_this`.
838 #[test]
839 #[cfg(target_env = "msvc")]
840 fn comp_supported_version_is_one() {
841 assert_eq!(unsafe { comp_supported_version() }, 1);
842 }
843
844 #[test]
845 #[cfg(not(target_env = "msvc"))]
846 fn comp_component_type_is_other() {
847 let vt = make_vtable();
848 let uvt = make_uid_vtable();
849 let comp = OmpComponent::new(&raw const vt, &raw const uvt, 0);
850 assert_eq!(
851 unsafe { comp_component_type(&raw const comp) },
852 ComponentType::Other
853 );
854 }
855
856 /// On MSVC `comp_component_type` returns `i32` (the discriminant of
857 /// `ComponentType::Other`) and takes no stack args.
858 #[test]
859 #[cfg(target_env = "msvc")]
860 fn comp_component_type_is_other() {
861 assert_eq!(
862 unsafe { comp_component_type() },
863 ComponentType::Other as i32
864 );
865 }
866}