rust-samp-sdk 3.6.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 super::vtable::{call_vtable_small_struct, slots};
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>;
}

/// A server interface reachable as a component, and the UID that finds it.
///
/// Implemented for the opaque interface handles (`IObjectsComponent`,
/// `IVehiclesComponent`, ...). Tying the UID to the type is what [`Component`]
/// builds on: the lookup and the cast cannot disagree.
pub trait ComponentInterface {
    /// The component's UID, as the server's headers declare it.
    const UID: UID;

    /// Where the `IComponent` subobject sits inside the interface.
    ///
    /// The server hands out an `IComponent*`. For almost every component that
    /// is the start of the object, because `IComponent` is the first base; for
    /// `INPCComponent` the pool comes first and the `IComponent` sits after its
    /// vtable pointer, so the pointer has to move back before the interface's
    /// own methods can be called through it. From clang's record layout, per
    /// ABI.
    const COMPONENT_OFFSET: isize = 0;
}

/// A server component, typed by the interface it implements.
///
/// Query it with `samp::plugin::omp_query::<Component<IObjectsComponent>>()`
/// and pass [`Component::as_ptr`] to the functions that take the interface. The
/// UID comes from the type, so there is no way to look one component up and
/// use it as another — which a separate UID constant and cast allowed.
pub struct Component<I: ComponentInterface> {
    ptr: NonNull<ServerComponent>,
    interface: std::marker::PhantomData<*mut I>,
}

impl<I: ComponentInterface> Component<I> {
    /// The component as its interface, for the functions that take one.
    #[must_use]
    pub fn as_ptr(&self) -> *mut I {
        // Back from the `IComponent` subobject to the start of the interface.
        self.ptr
            .as_ptr()
            .cast::<u8>()
            .wrapping_offset(-I::COMPONENT_OFFSET)
            .cast::<I>()
    }
}

// Written by hand: a derive would require `I: Clone`, and the interface types
// are opaque handles that are never cloned themselves.
impl<I: ComponentInterface> Clone for Component<I> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<I: ComponentInterface> Copy for Component<I> {}

impl<I: ComponentInterface> OmpComponentHandle for Component<I> {
    const UID: UID = I::UID;

    unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self {
        Self {
            ptr,
            interface: std::marker::PhantomData,
        }
    }

    fn as_raw(&self) -> NonNull<ServerComponent> {
        self.ptr
    }
}

slots! {
    /// Slot of `componentName()` in the `IComponent` vtable.
    ///
    /// Itanium emits two destructor slots (D1 + D0) where MSVC emits a single
    /// scalar deleting one, which shifts every method after it by one. Both
    /// numbers are confirmed against the official `Timers.so` / `Timers.dll`.
    SLOT_COMPONENT_NAME: usize = 7, 6;
}

slots! {
    /// Slot of `componentVersion()` in the `IComponent` vtable (same shift as
    /// [`SLOT_COMPONENT_NAME`]).
    SLOT_COMPONENT_VERSION: usize = 9, 8;
}

/// Reads the component name by calling `componentName()` ([`SLOT_COMPONENT_NAME`] 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().cast::<u8>();
    let view =
        call_vtable_small_struct!(raw, 0, SLOT_COMPONENT_NAME, StringView, StringView::EMPTY)?;
    unsafe { view.to_owned_string() }
}

/// Reads the component version by calling `componentVersion()` ([`SLOT_COMPONENT_VERSION`] 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().cast::<u8>();
    call_vtable_small_struct!(
        raw,
        0,
        SLOT_COMPONENT_VERSION,
        SemanticVersion,
        SemanticVersion::new(0, 0, 0)
    )
}

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

    use super::*;
    use crate::omp::vtable::MockTable;
    use std::sync::Mutex;

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

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

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

    // 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"))]
    // Mirrors the real convention: Itanium returns the struct in registers.
    unsafe extern "C" fn mock_name(_this: *mut ServerComponent) -> StringView {
        StringView {
            data: MOCK_NAME_BYTES.as_ptr(),
            len: MOCK_NAME_BYTES.len(),
        }
    }

    #[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) -> SemanticVersion {
        SemanticVersion::new(2, 7, 3)
    }

    #[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() -> *const *const () {
        mock_vtable().as_ptr()
    }

    /// Slots verified against the official `Timers.so` (Itanium) and
    /// `Timers.dll` (MSVC) vtables of open.mp 1.5.8.3079.
    #[test]
    fn component_slots_match_the_official_binaries() {
        #[cfg(not(target_env = "msvc"))]
        {
            assert_eq!(SLOT_COMPONENT_NAME, 7);
            assert_eq!(SLOT_COMPONENT_VERSION, 9);
        }
        #[cfg(target_env = "msvc")]
        {
            assert_eq!(SLOT_COMPONENT_NAME, 6);
            assert_eq!(SLOT_COMPONENT_VERSION, 8);
        }
    }

    #[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);
    }
}