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}