Skip to main content

samp_sdk/omp/
core.rs

1//! Bindings for the `ILogger` interface of `ICore` (Open Multiplayer).
2//!
3//! `ICore` inherits from `IExtensible` and `ILogger` (`ICore : public IExtensible, public ILogger`).
4//! As multiple inheritance, `ILogger` is a secondary base class with its own vtable,
5//! located after the `IExtensible` subobject.
6//!
7//! ## Offsets (both confirmed via disasm of `Console.dll` / `Console.so`)
8//!
9//! - **MSVC i686** (`Console.dll`):
10//!   `lea edx, [core+0x38]; mov ecx, [edx]; call [ecx+8]` -> `ILogger` at offset **56**
11//! - **Linux GCC i686 / Itanium** (`Console.so`):
12//!   `add edi, 0x28; mov ebx, [edi]; call [ebx+8]` -> `ILogger` at offset **40**
13//!
14//! In both, `slot[2]` is `logLn` — matches the order declared in `core.hpp:151-184`.
15//!
16//! ## `ILogger` vtable (order defined in `core.hpp:151-184`)
17//!
18//! ```text
19//! [0] printLn(fmt, ...)         — print without level
20//! [1] vprintLn(fmt, va_list)
21//! [2] logLn(level, fmt, ...)    — print with LogLevel
22//! [3] vlogLn(level, fmt, va_list)
23//! [4] printLnU8(fmt, ...)       — UTF-8 variants
24//! [5] vprintLnU8(fmt, va_list)
25//! [6] logLnU8(level, fmt, ...)
26//! [7] vlogLnU8(level, fmt, va_list)
27//! ```
28//!
29//! ## Calling convention
30//!
31//! Variadic virtual methods on x86 use **`__cdecl`** on both MSVC and Itanium
32//! (thiscall does not support varargs). `this` is the **first arg pushed on the stack**;
33//! the caller is responsible for cleaning the stack.
34//!
35//! Since stable Rust does not support `extern "C"` variadic (the `c_variadic`
36//! feature is nightly), we declare the functions with a fixed arity of 1 arg and
37//! use the format `"%s"`: the caller formats the message in Rust (`format!`) and
38//! passes the resulting `CString` as the single variadic argument. The ABI is
39//! identical to that of the C variadic function — `printf("%s", msg)` is
40//! equivalent to `printf(msg)` for the calling convention.
41
42use super::component::ICore;
43use super::vtable::slots;
44use std::ffi::CString;
45use std::os::raw::{c_char, c_int};
46
47slots! {
48    /// Offset of the `ILogger` subobject inside `ICore`.
49    ILOGGER_OFFSET: isize = 40, 56;
50}
51
52/// Slot of the `printLn(fmt, ...)` function in the `ILogger` vtable.
53const SLOT_PRINTLN: usize = 0;
54
55/// Slot of the `logLn(level, fmt, ...)` function in the `ILogger` vtable.
56const SLOT_LOGLN: usize = 2;
57
58/// Slot of the `printLnU8(fmt, ...)` function in the `ILogger` vtable.
59const SLOT_PRINTLN_U8: usize = 4;
60
61/// Slot of the `logLnU8(level, fmt, ...)` function in the `ILogger` vtable.
62const SLOT_LOGLN_U8: usize = 6;
63
64/// Open Multiplayer log level (corresponds to `LogLevel` in `core.hpp`).
65#[repr(C)]
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum LogLevel {
68    Debug = 0,
69    Message = 1,
70    Warning = 2,
71    Error = 3,
72}
73
74/// Type of the `printLn(this, fmt, arg)` function.
75///
76/// Declared with fixed arity instead of variadic — uses `fmt = "%s"` and a
77/// single `arg` (already-formatted message). ABI-compatible with the original
78/// variadic function.
79type PrintLnFn = unsafe extern "C" fn(this: *mut u8, fmt: *const c_char, arg: *const c_char);
80
81/// Type of the `logLn(this, level, fmt, arg)` function.
82type LogLnFn =
83    unsafe extern "C" fn(this: *mut u8, level: c_int, fmt: *const c_char, arg: *const c_char);
84
85/// Loads a slot from the `ILogger` vtable given the `ICore` pointer.
86///
87/// Thin wrapper over [`vtable::secondary_call_target_ptr`] with the offset
88/// pre-resolved for the `ILogger` subobject.
89///
90/// # Safety
91/// `core` must point to a valid `ICore` (alive, with the secondary vtable initialized).
92unsafe fn logger_slot(core: *mut ICore, slot: usize) -> Option<(*mut u8, *const ())> {
93    unsafe { super::vtable::secondary_call_target_ptr(core.cast::<u8>(), ILOGGER_OFFSET, slot) }
94}
95
96/// `ICore::printLn(message)` — writes a line to the server log.
97///
98/// The message is passed as a `%s` arg, avoiding interpretation of `%` in the content.
99/// Returns `false` if `core` is null or the vtable is corrupted (nothing is printed).
100///
101/// # Safety
102/// `core` must point to a valid `ICore` received in `on_load`.
103pub unsafe fn core_print_ln(core: *mut ICore, message: &str) -> bool {
104    let Some((this, slot)) = (unsafe { logger_slot(core, SLOT_PRINTLN) }) else {
105        return false;
106    };
107    let Ok(msg) = CString::new(message) else {
108        return false;
109    };
110    let fmt = c"%s";
111    let f: PrintLnFn = unsafe { std::mem::transmute(slot) };
112    unsafe { f(this, fmt.as_ptr(), msg.as_ptr()) };
113    true
114}
115
116/// `ICore::logLn(level, message)` — writes a line with a log level.
117///
118/// The Open Multiplayer server prepends `[Info]`/`[Warning]`/`[Error]`/`[Debug]` and a
119/// timestamp to the message, exactly as it does for its own logs.
120///
121/// # Safety
122/// `core` must point to a valid `ICore` received in `on_load`.
123pub unsafe fn core_log_ln(core: *mut ICore, level: LogLevel, message: &str) -> bool {
124    let Some((this, slot)) = (unsafe { logger_slot(core, SLOT_LOGLN) }) else {
125        return false;
126    };
127    let Ok(msg) = CString::new(message) else {
128        return false;
129    };
130    let fmt = c"%s";
131    let f: LogLnFn = unsafe { std::mem::transmute(slot) };
132    unsafe { f(this, level as c_int, fmt.as_ptr(), msg.as_ptr()) };
133    true
134}
135
136/// `ICore::printLnU8(message)` — UTF-8 variant of `printLn`.
137///
138/// Uses the Open Multiplayer server's UTF-8 pipeline, which preserves accented characters
139/// regardless of the console locale (important on Windows, where the default code
140/// page can corrupt non-ASCII bytes if passed through the regular `printLn`).
141///
142/// # Safety
143/// `core` must point to a valid `ICore` received in `on_load`.
144pub unsafe fn core_print_ln_u8(core: *mut ICore, message: &str) -> bool {
145    let Some((this, slot)) = (unsafe { logger_slot(core, SLOT_PRINTLN_U8) }) else {
146        return false;
147    };
148    let Ok(msg) = CString::new(message) else {
149        return false;
150    };
151    let fmt = c"%s";
152    let f: PrintLnFn = unsafe { std::mem::transmute(slot) };
153    unsafe { f(this, fmt.as_ptr(), msg.as_ptr()) };
154    true
155}
156
157/// `ICore::logLnU8(level, message)` — UTF-8 variant of `logLn`.
158///
159/// Combines the server's UTF-8 pipeline with a log level. Recommended as the
160/// default for any message that may contain accented characters or non-ASCII
161/// symbols.
162///
163/// # Safety
164/// `core` must point to a valid `ICore` received in `on_load`.
165pub unsafe fn core_log_ln_u8(core: *mut ICore, level: LogLevel, message: &str) -> bool {
166    let Some((this, slot)) = (unsafe { logger_slot(core, SLOT_LOGLN_U8) }) else {
167        return false;
168    };
169    let Ok(msg) = CString::new(message) else {
170        return false;
171    };
172    let fmt = c"%s";
173    let f: LogLnFn = unsafe { std::mem::transmute(slot) };
174    unsafe { f(this, level as c_int, fmt.as_ptr(), msg.as_ptr()) };
175    true
176}
177
178#[cfg(test)]
179mod tests {
180    //! Smoke tests for the 4 log functions of `ICore`.
181    //!
182    //! Each test sets up a fake `ICore` in a buffer and installs a mock vtable
183    //! that captures `(slot, level, fmt, message)`. It validates that each
184    //! `core_*_ln*` calls the correct slot of the `ILogger` secondary vtable at
185    //! the correct offset.
186    //!
187    //! Runs serially via `TEST_LOCK` because the captured state is global.
188
189    use super::*;
190    use crate::omp::vtable::MockTable;
191    use std::ffi::CStr;
192    use std::sync::Mutex;
193
194    static TEST_LOCK: Mutex<()> = Mutex::new(());
195
196    #[derive(Default, Clone)]
197    struct Captured {
198        slot: Option<usize>,
199        level: Option<c_int>,
200        fmt: Option<String>,
201        message: Option<String>,
202    }
203
204    static CAPTURED: Mutex<Option<Captured>> = Mutex::new(None);
205
206    fn reset_captures() {
207        *CAPTURED.lock().unwrap() = Some(Captured::default());
208    }
209
210    fn last_capture() -> Captured {
211        CAPTURED.lock().unwrap().clone().unwrap_or_default()
212    }
213
214    fn cstr_to_string(ptr: *const c_char) -> Option<String> {
215        if ptr.is_null() {
216            return None;
217        }
218        unsafe { CStr::from_ptr(ptr) }
219            .to_str()
220            .ok()
221            .map(String::from)
222    }
223
224    unsafe extern "C" fn mock_print_ln(_this: *mut u8, fmt: *const c_char, arg: *const c_char) {
225        let mut guard = CAPTURED.lock().unwrap();
226        let c = guard.as_mut().unwrap();
227        c.slot = Some(SLOT_PRINTLN);
228        c.fmt = cstr_to_string(fmt);
229        c.message = cstr_to_string(arg);
230    }
231
232    unsafe extern "C" fn mock_log_ln(
233        _this: *mut u8,
234        level: c_int,
235        fmt: *const c_char,
236        arg: *const c_char,
237    ) {
238        let mut guard = CAPTURED.lock().unwrap();
239        let c = guard.as_mut().unwrap();
240        c.slot = Some(SLOT_LOGLN);
241        c.level = Some(level);
242        c.fmt = cstr_to_string(fmt);
243        c.message = cstr_to_string(arg);
244    }
245
246    unsafe extern "C" fn mock_print_ln_u8(_this: *mut u8, fmt: *const c_char, arg: *const c_char) {
247        let mut guard = CAPTURED.lock().unwrap();
248        let c = guard.as_mut().unwrap();
249        c.slot = Some(SLOT_PRINTLN_U8);
250        c.fmt = cstr_to_string(fmt);
251        c.message = cstr_to_string(arg);
252    }
253
254    unsafe extern "C" fn mock_log_ln_u8(
255        _this: *mut u8,
256        level: c_int,
257        fmt: *const c_char,
258        arg: *const c_char,
259    ) {
260        let mut guard = CAPTURED.lock().unwrap();
261        let c = guard.as_mut().unwrap();
262        c.slot = Some(SLOT_LOGLN_U8);
263        c.level = Some(level);
264        c.fmt = cstr_to_string(fmt);
265        c.message = cstr_to_string(arg);
266    }
267
268    unsafe extern "C" fn unused_slot() {}
269
270    /// Mock vtable — initialized at runtime via `OnceLock` because `fn as *const ()`
271    /// is not const-evaluable. 10 slots = 8 of the `ILogger` header + 2 spare.
272    static MOCK_VTABLE: std::sync::OnceLock<MockTable<10>> = std::sync::OnceLock::new();
273
274    fn mock_vtable() -> &'static [*const (); 10] {
275        &MOCK_VTABLE
276            .get_or_init(|| {
277                MockTable([
278                    mock_print_ln as *const (),    // [0] printLn
279                    unused_slot as *const (),      // [1] vprintLn
280                    mock_log_ln as *const (),      // [2] logLn
281                    unused_slot as *const (),      // [3] vlogLn
282                    mock_print_ln_u8 as *const (), // [4] printLnU8
283                    unused_slot as *const (),      // [5] vprintLnU8
284                    mock_log_ln_u8 as *const (),   // [6] logLnU8
285                    unused_slot as *const (),      // [7] vlogLnU8
286                    std::ptr::null(),
287                    std::ptr::null(),
288                ])
289            })
290            .0
291    }
292
293    /// Builds a buffer simulating the `ICore` layout:
294    /// `[0..ILOGGER_OFFSET]` represent the `IExtensible` subobject (zeroed garbage);
295    /// `[ILOGGER_OFFSET..ILOGGER_OFFSET+4]` is the vptr to our mock vtable.
296    ///
297    /// Size 32 pointers = 128 bytes on i686 (target); pointer elements give
298    /// natural alignment at the vptr slot and keep the vptr's provenance.
299    fn make_mock_core() -> [*const (); 32] {
300        let mut buf = [std::ptr::null::<()>(); 32];
301        let vptr = mock_vtable().as_ptr().cast::<()>();
302        // ILOGGER_OFFSET in bytes; on i686 each pointer = 4 bytes.
303        let idx = usize::try_from(ILOGGER_OFFSET).expect("ILOGGER_OFFSET must be >= 0")
304            / std::mem::size_of::<*const ()>();
305        buf[idx] = vptr;
306        buf
307    }
308
309    #[test]
310    fn core_print_ln_calls_slot_0_at_logger_offset() {
311        let _g = TEST_LOCK.lock().unwrap();
312        reset_captures();
313        let mut core = make_mock_core();
314        let core_ptr = core.as_mut_ptr().cast::<ICore>();
315
316        let ok = unsafe { core_print_ln(core_ptr, "hello") };
317        assert!(ok, "core_print_ln must return true with a valid mock");
318
319        let c = last_capture();
320        assert_eq!(c.slot, Some(SLOT_PRINTLN));
321        assert_eq!(c.fmt.as_deref(), Some("%s"));
322        assert_eq!(c.message.as_deref(), Some("hello"));
323        assert_eq!(c.level, None, "printLn does not take a LogLevel");
324    }
325
326    #[test]
327    fn core_log_ln_calls_slot_2_with_level() {
328        let _g = TEST_LOCK.lock().unwrap();
329        reset_captures();
330        let mut core = make_mock_core();
331        let core_ptr = core.as_mut_ptr().cast::<ICore>();
332
333        let ok = unsafe { core_log_ln(core_ptr, LogLevel::Warning, "alert") };
334        assert!(ok);
335
336        let c = last_capture();
337        assert_eq!(c.slot, Some(SLOT_LOGLN));
338        assert_eq!(c.level, Some(LogLevel::Warning as c_int));
339        assert_eq!(c.fmt.as_deref(), Some("%s"));
340        assert_eq!(c.message.as_deref(), Some("alert"));
341    }
342
343    #[test]
344    fn core_print_ln_u8_calls_slot_4() {
345        let _g = TEST_LOCK.lock().unwrap();
346        reset_captures();
347        let mut core = make_mock_core();
348        let core_ptr = core.as_mut_ptr().cast::<ICore>();
349
350        let ok = unsafe { core_print_ln_u8(core_ptr, "hi") };
351        assert!(ok);
352
353        let c = last_capture();
354        assert_eq!(c.slot, Some(SLOT_PRINTLN_U8));
355        assert_eq!(c.message.as_deref(), Some("hi"));
356    }
357
358    #[test]
359    fn core_log_ln_u8_calls_slot_6_with_level() {
360        let _g = TEST_LOCK.lock().unwrap();
361        reset_captures();
362        let mut core = make_mock_core();
363        let core_ptr = core.as_mut_ptr().cast::<ICore>();
364
365        let ok = unsafe { core_log_ln_u8(core_ptr, LogLevel::Error, "critical failure") };
366        assert!(ok);
367
368        let c = last_capture();
369        assert_eq!(c.slot, Some(SLOT_LOGLN_U8));
370        assert_eq!(c.level, Some(LogLevel::Error as c_int));
371        assert_eq!(c.message.as_deref(), Some("critical failure"));
372    }
373
374    #[test]
375    fn all_log_fns_return_false_for_null_core() {
376        let _g = TEST_LOCK.lock().unwrap();
377        let nul = std::ptr::null_mut();
378        assert!(!unsafe { core_print_ln(nul, "x") });
379        assert!(!unsafe { core_log_ln(nul, LogLevel::Message, "x") });
380        assert!(!unsafe { core_print_ln_u8(nul, "x") });
381        assert!(!unsafe { core_log_ln_u8(nul, LogLevel::Message, "x") });
382    }
383
384    #[test]
385    fn log_fns_reject_message_with_interior_nul() {
386        let _g = TEST_LOCK.lock().unwrap();
387        let mut core = make_mock_core();
388        let core_ptr = core.as_mut_ptr().cast::<ICore>();
389        // CString::new fails on an interior NUL -> log_fn returns false silently
390        assert!(!unsafe { core_print_ln(core_ptr, "a\0b") });
391        assert!(!unsafe { core_log_ln(core_ptr, LogLevel::Message, "a\0b") });
392    }
393}