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