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//! [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}