rust-samp-sdk 3.0.0

Low-level FFI bindings for the SA-MP AMX virtual machine and open.mp native component ABI. Used internally by `rust-samp`; depend on it directly only if you need raw access without the higher-level macros and lifecycle.
Documentation
//! High-level API for Open Multiplayer server components.
//!
//! The raw pointer returned by `server::query_component` is opaque — it only
//! lets you check presence, with no way to interact with the component's
//! vtable. This module defines the [`OmpComponentHandle`] trait that specific
//! types (`PawnComponent`, `TimersComponent`, etc.) implement to provide:
//!
//! - The component's known `UID` (associated constant)
//! - Safe construction from the raw pointer
//! - Access to shared methods (`componentName`, `componentVersion`) via the
//!   generic utility functions in this module
//!
//! Plugins implementing their own external component declare the trait with
//! the UID generated by the SDK.

use super::server::ServerComponent;
use super::types::{SemanticVersion, StringView, UID};
use std::ptr::NonNull;

/// Trait implemented by typed wrappers for Open Multiplayer components.
///
/// Each implementation:
/// - Provides the component's constant `UID` in [`UID`].
/// - Constructs itself from a [`NonNull<ServerComponent>`] returned by
///   [`samp_sdk::omp::server::query_component`].
/// - Exposes the raw pointer via [`as_raw`].
///
/// [`as_raw`]: OmpComponentHandle::as_raw
/// [`samp_sdk::omp::server::query_component`]: super::server::query_component
pub trait OmpComponentHandle: Sized + Copy {
    /// Component UID — known at compile time.
    const UID: UID;

    /// Builds the wrapper from the pointer returned by `query_component`.
    ///
    /// # Safety
    /// `ptr` must have been obtained via `query_component(_, Self::UID)` and the
    /// server must keep the component alive while the wrapper is used.
    unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self;

    /// Returns the raw component pointer.
    fn as_raw(&self) -> NonNull<ServerComponent>;
}

/// Slot of `componentName()` in the `IComponent` vtable (same on both ABIs — `[6]`).
const SLOT_COMPONENT_NAME: usize = 6;

/// Slot of `componentVersion()` in the `IComponent` vtable (same on both ABIs — `[8]`).
const SLOT_COMPONENT_VERSION: usize = 8;

/// Signature of `componentName()` — returns `StringView` via hidden pointer.
#[cfg(not(target_env = "msvc"))]
type ComponentNameFn =
    unsafe extern "C" fn(*mut ServerComponent, *mut StringView) -> *mut StringView;

#[cfg(target_env = "msvc")]
type ComponentNameFn =
    unsafe extern "thiscall" fn(*mut ServerComponent, *mut StringView) -> *mut StringView;

/// Signature of `componentVersion()` — returns `SemanticVersion` via hidden pointer.
#[cfg(not(target_env = "msvc"))]
type ComponentVersionFn =
    unsafe extern "C" fn(*mut ServerComponent, *mut SemanticVersion) -> *mut SemanticVersion;

#[cfg(target_env = "msvc")]
type ComponentVersionFn =
    unsafe extern "thiscall" fn(*mut ServerComponent, *mut SemanticVersion) -> *mut SemanticVersion;

/// Reads the component name by calling `componentName()` (slot [6] of the `IComponent` vtable).
///
/// Returns a `String` with the UTF-8 name (copied — does not retain pointers from the component).
/// `None` if the component or vtable are null, the slot is empty, the returned
/// `StringView` is invalid, or the bytes are not valid UTF-8.
pub fn component_name<T: OmpComponentHandle>(c: &T) -> Option<String> {
    let raw = c.as_raw().as_ptr();
    // The primary vtable (IComponent) is at offset 0 of the object.
    let (_, slot) =
        unsafe { super::vtable::secondary_call_target(raw.cast::<u8>(), 0, SLOT_COMPONENT_NAME)? };
    let f: ComponentNameFn = unsafe { std::mem::transmute(slot) };
    let mut sv = StringView {
        data: std::ptr::null(),
        len: 0,
    };
    unsafe { f(raw, &raw mut sv) };
    if sv.data.is_null() || sv.len == 0 {
        return None;
    }
    let bytes = unsafe { std::slice::from_raw_parts(sv.data, sv.len) };
    std::str::from_utf8(bytes).ok().map(String::from)
}

/// Reads the component version by calling `componentVersion()` (slot [8] of the `IComponent` vtable).
///
/// Official Open Multiplayer components return the server version (e.g. `1.5.8.3079`).
/// `None` if the component or vtable are null or the slot is empty.
pub fn component_version<T: OmpComponentHandle>(c: &T) -> Option<SemanticVersion> {
    let raw = c.as_raw().as_ptr();
    let (_, slot) = unsafe {
        super::vtable::secondary_call_target(raw.cast::<u8>(), 0, SLOT_COMPONENT_VERSION)?
    };
    let f: ComponentVersionFn = unsafe { std::mem::transmute(slot) };
    let mut version = SemanticVersion::new(0, 0, 0);
    unsafe { f(raw, &raw mut version) };
    Some(version)
}

#[cfg(test)]
mod tests {
    //! Smoke tests for `component_name` and `component_version`.
    //!
    //! Sets up a fake `ServerComponent` with a mock vtable at slots [6] (name)
    //! and [8] (version). Covers typed wrappers via a test type that implements
    //! [`OmpComponentHandle`].

    use super::*;
    use std::sync::Mutex;

    static TEST_LOCK: Mutex<()> = Mutex::new(());

    // Mock vtable: 16 slots (minimum size of IComponent MSVC).
    // Only slots [6] (name) and [8] (version) are populated.
    static MOCK_VTABLE: std::sync::OnceLock<[usize; 16]> = std::sync::OnceLock::new();

    fn mock_vtable() -> &'static [usize; 16] {
        MOCK_VTABLE.get_or_init(|| {
            let mut v = [unused as *const () as usize; 16];
            v[SLOT_COMPONENT_NAME] = mock_name as *const () as usize;
            v[SLOT_COMPONENT_VERSION] = mock_version as *const () as usize;
            v
        })
    }

    // The mock functions MUST match the calling convention declared in
    // `ComponentNameFn` / `ComponentVersionFn` (cfg-gated by ABI). Declaring
    // them `extern "C"` on MSVC causes a STATUS_ACCESS_VIOLATION because the
    // call site is built for `thiscall` (this in ECX, callee cleans the
    // stack with `ret 4`) and reads `out` from the wrong stack slot.

    #[cfg(not(target_env = "msvc"))]
    unsafe extern "C" fn unused() {}
    #[cfg(target_env = "msvc")]
    unsafe extern "thiscall" fn unused() {}

    static MOCK_NAME_BYTES: &[u8] = b"test-comp";

    #[cfg(not(target_env = "msvc"))]
    unsafe extern "C" fn mock_name(
        _this: *mut ServerComponent,
        out: *mut StringView,
    ) -> *mut StringView {
        unsafe {
            *out = StringView {
                data: MOCK_NAME_BYTES.as_ptr(),
                len: MOCK_NAME_BYTES.len(),
            };
        }
        out
    }

    #[cfg(target_env = "msvc")]
    unsafe extern "thiscall" fn mock_name(
        _this: *mut ServerComponent,
        out: *mut StringView,
    ) -> *mut StringView {
        unsafe {
            *out = StringView {
                data: MOCK_NAME_BYTES.as_ptr(),
                len: MOCK_NAME_BYTES.len(),
            };
        }
        out
    }

    #[cfg(not(target_env = "msvc"))]
    unsafe extern "C" fn mock_version(
        _this: *mut ServerComponent,
        out: *mut SemanticVersion,
    ) -> *mut SemanticVersion {
        unsafe {
            *out = SemanticVersion::new(2, 7, 3);
        }
        out
    }

    #[cfg(target_env = "msvc")]
    unsafe extern "thiscall" fn mock_version(
        _this: *mut ServerComponent,
        out: *mut SemanticVersion,
    ) -> *mut SemanticVersion {
        unsafe {
            *out = SemanticVersion::new(2, 7, 3);
        }
        out
    }

    /// Dummy type implementing `OmpComponentHandle` only for the test.
    #[derive(Debug, Clone, Copy)]
    struct DummyComponent {
        ptr: NonNull<ServerComponent>,
    }

    impl OmpComponentHandle for DummyComponent {
        const UID: UID = 0xDEAD_BEEF_CAFE_BABE;
        unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self {
            Self { ptr }
        }
        fn as_raw(&self) -> NonNull<ServerComponent> {
            self.ptr
        }
    }

    /// Builds a value simulating `ServerComponent`: vptr at offset 0.
    /// The caller must bind to a local to get a stable address.
    fn make_mock_component() -> usize {
        mock_vtable().as_ptr() as usize
    }

    #[test]
    fn component_name_reads_slot_6_and_returns_string() {
        let _g = TEST_LOCK.lock().unwrap();
        let buf = make_mock_component();
        let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
        let nn = NonNull::new(raw).unwrap();
        let comp = unsafe { DummyComponent::from_raw(nn) };

        let name = component_name(&comp);
        assert_eq!(name.as_deref(), Some("test-comp"));
    }

    #[test]
    fn component_version_reads_slot_8_and_returns_semver() {
        let _g = TEST_LOCK.lock().unwrap();
        let buf = make_mock_component();
        let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
        let nn = NonNull::new(raw).unwrap();
        let comp = unsafe { DummyComponent::from_raw(nn) };

        let v = component_version(&comp).unwrap();
        assert_eq!((v.major, v.minor, v.patch), (2, 7, 3));
    }

    #[test]
    fn dummy_component_uid_is_consistent() {
        assert_eq!(DummyComponent::UID, 0xDEAD_BEEF_CAFE_BABE);
    }
}