Skip to main content

samp_sdk/cell/
repr.rs

1//! Conversion between Rust types and AMX cells (raw i32).
2
3use crate::amx::Amx;
4use crate::error::{AmxError, AmxResult};
5
6/// Conversion to/from an AMX cell in the context of a live VM.
7///
8/// This is the centerpiece of natives: `T: AmxCell` arguments are parsed via
9/// [`from_raw`] from the parameter array, and return values serialized
10/// via [`as_cell`] to the output slot. Complex types (strings, buffers, refs)
11/// rely on `&Amx` to resolve relative addresses.
12///
13/// [`from_raw`]: AmxCell::from_raw
14/// [`as_cell`]: AmxCell::as_cell
15pub trait AmxCell<'amx>
16where
17    Self: Sized,
18{
19    /// Reconstructs the value from a raw AMX cell.
20    ///
21    /// # Errors
22    /// `AmxError::General` in the default implementation (use the concrete
23    /// impls); specific implementations may return `AmxError::MemoryAccess`
24    /// if the address is invalid, or other variants for decode failures.
25    fn from_raw(_amx: &'amx Amx, _cell: i32) -> AmxResult<Self>
26    where
27        Self: 'amx,
28    {
29        Err(AmxError::General)
30    }
31
32    fn as_cell(&self) -> i32;
33}
34
35/// Marker: the value fits in a single AMX cell (32 bits) and can be
36/// stored directly in the VM stack/heap without indirection.
37///
38/// Implemented for `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `usize`, `isize`,
39/// `f32` and `bool`.
40///
41/// # Safety
42/// Only implement for types that fit in 32 bits — the SDK assumes this when
43/// copying bytes from the cell. Larger types corrupt the VM memory.
44pub unsafe trait AmxPrimitive
45where
46    Self: Sized,
47{
48}
49
50/// Conversion between Rust and an AMX cell (`i32`) without needing `&Amx`.
51///
52/// Difference vs [`AmxCell`]: this trait operates on standalone values, ideal
53/// for bulk operations over [`Buffer`] (each element is an independent cell).
54///
55/// | Trait | When to use | Needs `&Amx`? |
56/// |-------|-------------|--------------------|
57/// | [`AmxCell`] | Argument of a `#[native]` | Yes (for complex types) |
58/// | `CellConvert` | Element of a [`Buffer`] | No |
59///
60/// Rarely needs to be imported/implemented directly — [`Buffer::get_as`] and
61/// [`Buffer::set_as`] already consume this trait.
62///
63/// # Example
64/// ```rust,no_run
65/// # use samp_sdk::cell::Buffer;
66/// fn scale_floats(buf: &mut Buffer, factor: f32) {
67///     for i in 0..buf.len() {
68///         if let Some(v) = buf.get_as::<f32>(i) {
69///             buf.set_as(i, v * factor);
70///         }
71///     }
72/// }
73/// ```
74///
75/// [`Buffer::get_as`]: crate::cell::Buffer::get_as
76/// [`Buffer::set_as`]: crate::cell::Buffer::set_as
77/// [`Buffer`]: crate::cell::Buffer
78pub trait CellConvert: Sized {
79    /// Decodes a raw `i32` into the Rust type.
80    fn from_cell(raw: i32) -> Self;
81
82    /// Encodes the value as a raw cell.
83    fn into_cell(self) -> i32;
84}
85
86impl<'a, T: AmxCell<'a>> AmxCell<'a> for &'a T {
87    fn as_cell(&self) -> i32 {
88        (**self).as_cell()
89    }
90}
91
92impl<'a, T: AmxCell<'a>> AmxCell<'a> for &'a mut T {
93    fn as_cell(&self) -> i32 {
94        (**self).as_cell()
95    }
96}
97
98// The `as` cast is intentional: the AMX VM uses `i32` cells for every
99// primitive type, so truncation/sign-extension/lossless casts follow the
100// Pawn ABI (e.g. a Pawn `byte` lives in an i32 cell and truncates to u8 on read).
101// `try_into` would add error handling in a hot path with no semantic gain.
102macro_rules! impl_for_primitive {
103    ($type:ty) => {
104        #[allow(
105            clippy::cast_possible_truncation,
106            clippy::cast_possible_wrap,
107            clippy::cast_sign_loss,
108            clippy::cast_lossless
109        )]
110        impl AmxCell<'_> for $type {
111            fn from_raw(_amx: &Amx, cell: i32) -> AmxResult<Self> {
112                Ok(cell as Self)
113            }
114
115            fn as_cell(&self) -> i32 {
116                *self as i32
117            }
118        }
119
120        #[allow(
121            clippy::cast_possible_truncation,
122            clippy::cast_possible_wrap,
123            clippy::cast_sign_loss,
124            clippy::cast_lossless
125        )]
126        impl CellConvert for $type {
127            #[inline]
128            fn from_cell(raw: i32) -> Self {
129                raw as Self
130            }
131
132            #[inline]
133            fn into_cell(self) -> i32 {
134                self as i32
135            }
136        }
137
138        unsafe impl AmxPrimitive for $type {}
139    };
140}
141
142impl_for_primitive!(i8);
143impl_for_primitive!(u8);
144impl_for_primitive!(i16);
145impl_for_primitive!(u16);
146impl_for_primitive!(i32);
147impl_for_primitive!(u32);
148impl_for_primitive!(usize);
149impl_for_primitive!(isize);
150
151// `cell as u32`: bit-for-bit reinterpretation of the i32 coming from the AMX
152// VM, required by `f32::from_bits`. Sign-loss here is the goal (i32 and u32
153// share the same bit pattern for the same cell content).
154impl AmxCell<'_> for f32 {
155    fn from_raw(_amx: &Amx, cell: i32) -> AmxResult<f32> {
156        #[allow(clippy::cast_sign_loss)]
157        let bits = cell as u32;
158        Ok(f32::from_bits(bits))
159    }
160
161    fn as_cell(&self) -> i32 {
162        f32::to_bits(*self).cast_signed()
163    }
164}
165
166impl CellConvert for f32 {
167    #[inline]
168    fn from_cell(raw: i32) -> Self {
169        #[allow(clippy::cast_sign_loss)]
170        let bits = raw as u32;
171        f32::from_bits(bits)
172    }
173
174    #[inline]
175    fn into_cell(self) -> i32 {
176        f32::to_bits(self).cast_signed()
177    }
178}
179
180impl AmxCell<'_> for bool {
181    fn from_raw(_amx: &Amx, cell: i32) -> AmxResult<bool> {
182        // Explicit comparison instead of `transmute` or `as bool`: any non-zero
183        // value counts as `true`, mirroring the `if (val)` behavior in C.
184        Ok(cell != 0)
185    }
186
187    fn as_cell(&self) -> i32 {
188        i32::from(*self)
189    }
190}
191
192impl CellConvert for bool {
193    #[inline]
194    fn from_cell(raw: i32) -> Self {
195        raw != 0
196    }
197
198    #[inline]
199    fn into_cell(self) -> i32 {
200        i32::from(self)
201    }
202}
203
204unsafe impl AmxPrimitive for f32 {}
205unsafe impl AmxPrimitive for bool {}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn i32_as_cell_identity() {
213        for v in [0, 1, -1, 42, i32::MAX, i32::MIN] {
214            assert_eq!(v.as_cell(), v);
215        }
216    }
217
218    #[test]
219    fn f32_as_cell_preserves_bits() {
220        for v in [0.0f32, 1.0, -1.0, 42.5, f32::MAX, f32::MIN, f32::EPSILON] {
221            let cell = v.as_cell();
222            let recovered = f32::from_bits(cell.cast_unsigned());
223            assert_eq!(
224                v.to_bits(),
225                recovered.to_bits(),
226                "f32 {v} did not preserve bits"
227            );
228        }
229    }
230
231    #[test]
232    fn bool_as_cell() {
233        assert_eq!(true.as_cell(), 1);
234        assert_eq!(false.as_cell(), 0);
235    }
236
237    #[test]
238    fn u8_as_cell() {
239        assert_eq!(0u8.as_cell(), 0);
240        assert_eq!(255u8.as_cell(), 255);
241    }
242
243    #[test]
244    fn i8_as_cell() {
245        assert_eq!(0i8.as_cell(), 0);
246        assert_eq!((-1i8).as_cell(), -1);
247        assert_eq!(127i8.as_cell(), 127);
248    }
249
250    #[test]
251    fn u16_as_cell() {
252        assert_eq!(0u16.as_cell(), 0);
253        assert_eq!(65535u16.as_cell(), 65535);
254    }
255
256    #[test]
257    fn i16_as_cell() {
258        assert_eq!(0i16.as_cell(), 0);
259        assert_eq!((-1i16).as_cell(), -1);
260    }
261
262    #[test]
263    fn ref_delegates_to_inner() {
264        let val = 42i32;
265        let r = &val;
266        assert_eq!(r.as_cell(), 42);
267    }
268
269    #[test]
270    fn mut_ref_delegates_to_inner() {
271        let mut val = 42i32;
272        let r = &mut val;
273        assert_eq!(r.as_cell(), 42);
274    }
275}