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}