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`] 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, usize)> {
94    unsafe { super::vtable::secondary_call_target(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 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 usize`
271    /// is not const-evaluable. 10 slots = 8 of the `ILogger` header + 2 spare.
272    static MOCK_VTABLE: std::sync::OnceLock<[usize; 10]> = std::sync::OnceLock::new();
273
274    fn mock_vtable() -> &'static [usize; 10] {
275        MOCK_VTABLE.get_or_init(|| {
276            [
277                mock_print_ln as *const () as usize,    // [0] printLn
278                unused_slot as *const () as usize,      // [1] vprintLn
279                mock_log_ln as *const () as usize,      // [2] logLn
280                unused_slot as *const () as usize,      // [3] vlogLn
281                mock_print_ln_u8 as *const () as usize, // [4] printLnU8
282                unused_slot as *const () as usize,      // [5] vprintLnU8
283                mock_log_ln_u8 as *const () as usize,   // [6] logLnU8
284                unused_slot as *const () as usize,      // [7] vlogLnU8
285                0,
286                0,
287            ]
288        })
289    }
290
291    /// Builds a buffer simulating the `ICore` layout:
292    /// `[0..ILOGGER_OFFSET]` represent the `IExtensible` subobject (zeroed garbage);
293    /// `[ILOGGER_OFFSET..ILOGGER_OFFSET+4]` is the vptr to our mock vtable.
294    ///
295    /// Size 32x`usize` = 128 bytes on i686 (target); `usize` ensures natural
296    /// alignment for the `*mut usize` cast at the vptr slot.
297    fn make_mock_core() -> [usize; 32] {
298        let mut buf = [0usize; 32];
299        let vptr = mock_vtable().as_ptr() as usize;
300        // ILOGGER_OFFSET in bytes; on i686 each `usize` = 4 bytes.
301        let idx = usize::try_from(ILOGGER_OFFSET).expect("ILOGGER_OFFSET must be >= 0")
302            / std::mem::size_of::<usize>();
303        buf[idx] = vptr;
304        buf
305    }
306
307    #[test]
308    fn core_print_ln_calls_slot_0_at_logger_offset() {
309        let _g = TEST_LOCK.lock().unwrap();
310        reset_captures();
311        let mut core = make_mock_core();
312        let core_ptr = core.as_mut_ptr().cast::<ICore>();
313
314        let ok = unsafe { core_print_ln(core_ptr, "hello") };
315        assert!(ok, "core_print_ln must return true with a valid mock");
316
317        let c = last_capture();
318        assert_eq!(c.slot, Some(SLOT_PRINTLN));
319        assert_eq!(c.fmt.as_deref(), Some("%s"));
320        assert_eq!(c.message.as_deref(), Some("hello"));
321        assert_eq!(c.level, None, "printLn does not take a LogLevel");
322    }
323
324    #[test]
325    fn core_log_ln_calls_slot_2_with_level() {
326        let _g = TEST_LOCK.lock().unwrap();
327        reset_captures();
328        let mut core = make_mock_core();
329        let core_ptr = core.as_mut_ptr().cast::<ICore>();
330
331        let ok = unsafe { core_log_ln(core_ptr, LogLevel::Warning, "alert") };
332        assert!(ok);
333
334        let c = last_capture();
335        assert_eq!(c.slot, Some(SLOT_LOGLN));
336        assert_eq!(c.level, Some(LogLevel::Warning as c_int));
337        assert_eq!(c.fmt.as_deref(), Some("%s"));
338        assert_eq!(c.message.as_deref(), Some("alert"));
339    }
340
341    #[test]
342    fn core_print_ln_u8_calls_slot_4() {
343        let _g = TEST_LOCK.lock().unwrap();
344        reset_captures();
345        let mut core = make_mock_core();
346        let core_ptr = core.as_mut_ptr().cast::<ICore>();
347
348        let ok = unsafe { core_print_ln_u8(core_ptr, "hi") };
349        assert!(ok);
350
351        let c = last_capture();
352        assert_eq!(c.slot, Some(SLOT_PRINTLN_U8));
353        assert_eq!(c.message.as_deref(), Some("hi"));
354    }
355
356    #[test]
357    fn core_log_ln_u8_calls_slot_6_with_level() {
358        let _g = TEST_LOCK.lock().unwrap();
359        reset_captures();
360        let mut core = make_mock_core();
361        let core_ptr = core.as_mut_ptr().cast::<ICore>();
362
363        let ok = unsafe { core_log_ln_u8(core_ptr, LogLevel::Error, "critical failure") };
364        assert!(ok);
365
366        let c = last_capture();
367        assert_eq!(c.slot, Some(SLOT_LOGLN_U8));
368        assert_eq!(c.level, Some(LogLevel::Error as c_int));
369        assert_eq!(c.message.as_deref(), Some("critical failure"));
370    }
371
372    #[test]
373    fn all_log_fns_return_false_for_null_core() {
374        let _g = TEST_LOCK.lock().unwrap();
375        let nul = std::ptr::null_mut();
376        assert!(!unsafe { core_print_ln(nul, "x") });
377        assert!(!unsafe { core_log_ln(nul, LogLevel::Message, "x") });
378        assert!(!unsafe { core_print_ln_u8(nul, "x") });
379        assert!(!unsafe { core_log_ln_u8(nul, LogLevel::Message, "x") });
380    }
381
382    #[test]
383    fn log_fns_reject_message_with_interior_nul() {
384        let _g = TEST_LOCK.lock().unwrap();
385        let mut core = make_mock_core();
386        let core_ptr = core.as_mut_ptr().cast::<ICore>();
387        // CString::new fails on an interior NUL -> log_fn returns false silently
388        assert!(!unsafe { core_print_ln(core_ptr, "a\0b") });
389        assert!(!unsafe { core_log_ln(core_ptr, LogLevel::Message, "a\0b") });
390    }
391}