Skip to main content

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