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