Skip to main content

samp_sdk/
amx.rs

1//! Safe API for a live AMX VM instance.
2//!
3//! Wraps the `*mut AMX` received from the server + the `amx_Exports` table.
4//! Each method here resolves the corresponding `amx_*` function on demand (via
5//! [`crate::exports`]) and invokes it with idiomatic Rust error handling.
6
7use crate::cell::{AmxCell, AmxPrimitive, AmxString, Buffer, Ref};
8use crate::consts::{AmxExecIdx, AmxFlags};
9use crate::error::{AmxError, AmxResult};
10// Intentional wildcard: brings in the 40+ marker types of the exported AMX
11// functions (`Register`, `Allot`, `Exec`, ...). Listing each one would be
12// noisy and fragile when a new function is added to the table.
13#[allow(clippy::wildcard_imports)]
14use crate::exports::*;
15use crate::raw::functions::AmxNative;
16use crate::raw::types::{AMX, AMX_HEADER, AMX_NATIVE_INFO};
17
18#[cfg(feature = "encoding")]
19use crate::encoding;
20
21use std::borrow::Cow;
22use std::ffi::CString;
23use std::ptr::NonNull;
24
25macro_rules! amx_try {
26    ($call:expr) => {
27        let result = $call;
28
29        if result > 0 {
30            return Err(result.into());
31        }
32    };
33}
34
35/// Reads a field of the `#[repr(C, packed)]` `AMX` via `read_unaligned` (taking
36/// a reference to a packed field is unsound). `None` when the pointer is null.
37macro_rules! read_reg {
38    ($self:ident . $field:ident) => {
39        NonNull::new($self.ptr)
40            .map(|amx| unsafe { std::ptr::addr_of!((*amx.as_ptr()).$field).read_unaligned() })
41    };
42}
43
44/// Wrapper over the raw `*mut AMX` and the exported function table.
45#[derive(Debug)]
46pub struct Amx {
47    ptr: *mut AMX,
48    fn_table: usize,
49}
50
51impl Amx {
52    /// Builds the wrapper.
53    ///
54    /// `ptr` is the pointer received in callbacks such as `AmxLoad`; `fn_table`
55    /// is the address resolved during plugin initialization (typically stored
56    /// in a global [`AtomicUsize`] read in `Load()` from
57    /// [`crate::consts::ServerData::AmxExports`]).
58    ///
59    /// [`AtomicUsize`]: std::sync::atomic::AtomicUsize
60    pub fn new(ptr: *mut AMX, fn_table: usize) -> Amx {
61        Amx { ptr, fn_table }
62    }
63
64    /// Registers plugin natives in the VM via `amx_Register`.
65    ///
66    /// Generally called in `AmxLoad` — the `#[native]` macro + `initialize_plugin!`
67    /// build the list automatically; only call manually from `raw` code.
68    ///
69    /// # Errors
70    /// Propagates any [`AmxError`] returned by `amx_Register` — typically
71    /// `AmxError::NotFound` if a listed native is not declared in the script,
72    /// or VM state errors if called outside the load cycle.
73    pub fn register(&self, natives: &[AMX_NATIVE_INFO]) -> AmxResult<()> {
74        let register = Register::from_table(self.fn_table);
75        // `usize` -> `i32`: the `amx_Register` ABI takes the count as `int`.
76        // Practical truncation would require >2 billion natives — impossible.
77        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
78        let len = natives.len() as i32;
79        let ptr = natives.as_ptr();
80
81        amx_try!(register(self.ptr, ptr, len));
82
83        Ok(())
84    }
85
86    pub(crate) fn allot<T: Sized + AmxPrimitive>(&self, cells: usize) -> AmxResult<Ref<'_, T>> {
87        if cells > i32::MAX as usize {
88            return Err(AmxError::Memory);
89        }
90
91        let allot = Allot::from_table(self.fn_table);
92
93        let mut amx_addr = 0;
94        let mut phys_addr = 0;
95
96        // `cells` was validated above as `<= i32::MAX`; cast is safe.
97        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
98        let cells_i32 = cells as i32;
99        amx_try!(allot(
100            self.ptr,
101            cells_i32,
102            &raw mut amx_addr,
103            &raw mut phys_addr
104        ));
105
106        if phys_addr == 0 {
107            return Err(AmxError::Memory);
108        }
109
110        unsafe { Ok(Ref::new(amx_addr, phys_addr as *mut T)) }
111    }
112
113    /// Executes the public function identified by `index` in the VM.
114    ///
115    /// Returns the Pawn return value (`i32`). Arguments must have been pushed
116    /// via [`push`] (in reverse order) and [`Allocator`] (for strings/arrays)
117    /// before this call.
118    ///
119    /// [`push`]: Amx::push
120    ///
121    /// # Errors
122    /// Propagates any [`AmxError`] from script execution — notably
123    /// `Exit`/`Assert` (Pawn aborted), `StackError`/`StackLow`/`HeapLow`
124    /// (stack or heap overflow), `Divide`, `Native` (a called native
125    /// returned an error) or `Index` if `index` does not match a valid function.
126    pub fn exec(&self, index: AmxExecIdx) -> AmxResult<i32> {
127        let exec = Exec::from_table(self.fn_table);
128        let mut retval = 0;
129
130        amx_try!(exec(self.ptr, &raw mut retval, index.into()));
131
132        Ok(retval)
133    }
134
135    /// Index of a native by name (resolved via `amx_FindNative`).
136    ///
137    /// # Errors
138    /// `AmxError::NotFound` if `name` contains an interior NUL byte or if the
139    /// native is not registered in the VM.
140    pub fn find_native(&self, name: &str) -> AmxResult<i32> {
141        let find_native = FindNative::from_table(self.fn_table);
142        let c_str = CString::new(name).map_err(|_| AmxError::NotFound)?;
143        let mut index = -1;
144
145        amx_try!(find_native(self.ptr, c_str.as_ptr(), &raw mut index));
146
147        Ok(index)
148    }
149
150    /// Calls a native registered by **another plugin** in the same AMX.
151    ///
152    /// SA-MP plugins inject their natives into every loaded AMX via
153    /// `amx_Register`, which writes a host function pointer into the
154    /// native's entry inside the `AMX_HEADER` natives table. This helper
155    /// resolves the name through `amx_FindNative`, reads that function
156    /// pointer back, builds the `params` block in the AMX convention
157    /// (first cell = `argc * sizeof(cell)`, then the arguments), and
158    /// invokes the native.
159    ///
160    /// Integer arguments are passed as their `i32` value. Floats are
161    /// passed bit-cast to `i32` (use [`f32::to_bits`] then
162    /// [`i32::from_ne_bytes`] on `to_ne_bytes`, or `f32::to_bits() as i32`).
163    /// String and array arguments are AMX cell addresses returned by
164    /// [`Allocator::allot_string`]/[`Allocator::allot_buffer`] — same
165    /// marshalling as for [`exec_public`](crate::exec_public).
166    ///
167    /// # Example
168    /// ```rust,ignore
169    /// // Calling Streamer_CreateDynamicObject from a Rust plugin
170    /// fn on_amx_load(&mut self, amx: &Amx) -> AmxResult<()> {
171    ///     let model_id: i32 = 1337;
172    ///     #[allow(clippy::cast_possible_wrap)]
173    ///     let x = 100.0_f32.to_bits() as i32;
174    ///     let y = 200.0_f32.to_bits() as i32;
175    ///     let z =  10.0_f32.to_bits() as i32;
176    ///     let object_id = amx.call_native(
177    ///         "Streamer_CreateDynamicObject",
178    ///         &[model_id, x, y, z, 0, 0, 0],
179    ///     )?;
180    ///     log::info!("created dynamic object id={object_id}");
181    ///     Ok(())
182    /// }
183    /// ```
184    ///
185    /// # Errors
186    /// - [`AmxError::NotFound`] if `name` contains an interior NUL byte,
187    ///   the native is not registered, or its address is still zero
188    ///   (registered name but no host pointer attached).
189    /// - [`AmxError::MemoryAccess`] if the AMX header cannot be read.
190    /// - [`AmxError::Index`] if the resolved index is out of range for
191    ///   the natives table reported by the AMX header.
192    /// - Any [`AmxError`] propagated from the called native via
193    ///   `amx.error` (re-raised by the caller through `amx_try!`).
194    pub fn call_native(&self, name: &str, params: &[i32]) -> AmxResult<i32> {
195        let index = self.find_native(name)?;
196        if index < 0 {
197            return Err(AmxError::NotFound);
198        }
199
200        let header_ptr = self.header().ok_or(AmxError::MemoryAccess)?;
201        // SAFETY: `header()` returned NonNull, and the AMX is alive for
202        // the duration of `&self`.
203        let (natives_off, libraries_off, defsize) = unsafe {
204            let h = header_ptr.as_ptr();
205            (
206                std::ptr::read_unaligned(&raw const (*h).natives),
207                std::ptr::read_unaligned(&raw const (*h).libraries),
208                std::ptr::read_unaligned(&raw const (*h).defsize),
209            )
210        };
211
212        if defsize <= 0 || libraries_off < natives_off {
213            return Err(AmxError::MemoryAccess);
214        }
215        let defsize_i32 = i32::from(defsize);
216        let table_bytes = libraries_off - natives_off;
217        let num_natives = table_bytes / defsize_i32;
218        if index >= num_natives {
219            return Err(AmxError::Index);
220        }
221
222        let amx_ptr = self.amx().ok_or(AmxError::MemoryAccess)?;
223        // SAFETY: `amx_ptr` is NonNull and points to the live AMX.
224        let base = unsafe { (*amx_ptr.as_ptr()).base };
225        if base.is_null() {
226            return Err(AmxError::MemoryAccess);
227        }
228
229        let entry_off = natives_off + index * defsize_i32;
230        // SAFETY: `entry_off` is within the natives table bounded by
231        // (libraries - natives), which the header advertises as part of
232        // the AMX-mapped region pointed to by `base`.
233        let entry_ptr = unsafe { base.offset(entry_off as isize) };
234
235        // First 4 bytes of each entry — both `AMX_FUNCSTUB` and
236        // `ANX_FUNCSTUBNT` start with `u32 address`, the host function
237        // pointer written by `amx_Register`.
238        let address = unsafe { std::ptr::read_unaligned(entry_ptr.cast::<u32>()) };
239        if address == 0 {
240            return Err(AmxError::NotFound);
241        }
242
243        // SAFETY: SA-MP / open.mp are 32-bit; the AMX cell width and host
244        // function pointer width are both 4 bytes. `address` came from
245        // `amx_Register`, which writes a valid `AmxNative` pointer.
246        let native: AmxNative = unsafe { std::mem::transmute(address as usize) };
247
248        // Build the params block: `[argc * sizeof(cell), arg0, arg1, ...]`.
249        // Bytes, not cells — matches the convention every AMX native
250        // implementation reads (`params[0] / sizeof(cell)` to recover argc).
251        let mut buf: Vec<i32> = Vec::with_capacity(params.len() + 1);
252        // `params.len()` bounded by `i32::MAX` in practice; the AMX
253        // would have failed long before reaching 2 billion args.
254        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
255        let argc_bytes = (params.len() as i32) * 4;
256        buf.push(argc_bytes);
257        buf.extend_from_slice(params);
258
259        let retval = native(self.ptr, buf.as_mut_ptr());
260        // Surface VM-side errors set by the native into `amx.error`.
261        // SAFETY: `amx_ptr` already validated above.
262        let err = unsafe { (*amx_ptr.as_ptr()).error };
263        if err > 0 {
264            return Err(err.into());
265        }
266        Ok(retval)
267    }
268
269    /// Index of a public function by name — pass the result to [`exec`].
270    ///
271    /// ```
272    /// use samp_sdk::amx::Amx;
273    /// use samp_sdk::error::AmxResult;
274    /// fn has_on_player_connect(amx: &Amx) -> AmxResult<bool> {
275    ///     let idx = amx.find_public("OnPlayerConnect")?;
276    ///     Ok(i32::from(idx) >= 0)
277    /// }
278    /// ```
279    ///
280    /// [`exec`]: Amx::exec
281    ///
282    /// # Errors
283    /// `AmxError::NotFound` if `name` contains an interior NUL byte or if the
284    /// public function is not declared in the Pawn script.
285    pub fn find_public(&self, name: &str) -> AmxResult<AmxExecIdx> {
286        let find_public = FindPublic::from_table(self.fn_table);
287        let c_str = CString::new(name).map_err(|_| AmxError::NotFound)?;
288        let mut index = -1;
289
290        amx_try!(find_public(self.ptr, c_str.as_ptr(), &raw mut index));
291
292        Ok(AmxExecIdx::from(index))
293    }
294
295    /// `Ref<T>` pointing to a public variable declared in the Pawn script.
296    ///
297    /// ```rust,no_run
298    /// # use samp_sdk::amx::Amx;
299    /// # use samp_sdk::error::AmxResult;
300    /// # fn check(amx: &Amx) -> AmxResult<()> {
301    /// let version = amx.find_pubvar::<f32>("my_plugin_version")?;
302    /// // outdated
303    /// if *version < 1.0 { }
304    /// # Ok(()) }
305    /// ```
306    ///
307    /// # Errors
308    /// `AmxError::NotFound` if `name` contains an interior NUL byte or if the
309    /// pubvar is not declared. `AmxError::MemoryAccess` if the address returned
310    /// by the VM is invalid.
311    pub fn find_pubvar<T: Sized + AmxPrimitive>(&self, name: &str) -> AmxResult<Ref<'_, T>> {
312        let find_pubvar = FindPubVar::from_table(self.fn_table);
313        let c_str = CString::new(name).map_err(|_| AmxError::NotFound)?;
314        let mut cell_ptr = 0;
315
316        amx_try!(find_pubvar(self.ptr, c_str.as_ptr(), &raw mut cell_ptr));
317
318        self.get_ref(cell_ptr)
319    }
320
321    /// Flags of the loaded `.amx`.
322    ///
323    /// # Errors
324    /// Propagates any [`AmxError`] returned by `amx_Flags` — in practice, it
325    /// only fails if the internal `AMX*` is corrupted or null.
326    pub fn flags(&self) -> AmxResult<AmxFlags> {
327        let flags = Flags::from_table(self.fn_table);
328        let mut value: u16 = 0;
329
330        amx_try!(flags(self.ptr, &raw mut value));
331
332        Ok(AmxFlags::from_bits_truncate(value))
333    }
334
335    /// Resolves an AMX cell (relative address) to a typed [`Ref<T>`].
336    ///
337    /// # Errors
338    /// `AmxError::MemoryAccess` if `address` does not correspond to a valid
339    /// cell in the Pawn script address space.
340    pub fn get_ref<T: Sized + AmxPrimitive>(&self, address: i32) -> AmxResult<Ref<'_, T>> {
341        let get_addr = GetAddr::from_table(self.fn_table);
342        let mut dest = 0;
343        let mut dest_addr = std::ptr::addr_of_mut!(dest);
344
345        amx_try!(get_addr(self.ptr, address, &raw mut dest_addr));
346
347        if dest_addr.is_null() {
348            return Err(AmxError::MemoryAccess);
349        }
350
351        unsafe { Ok(Ref::new(address, dest_addr.cast::<T>())) }
352    }
353
354    #[inline]
355    pub(crate) fn release(&self, address: i32) {
356        if let Some(mut amx) = self.amx() {
357            let amx = unsafe { amx.as_mut() };
358            if address >= 0 && amx.hea > address {
359                amx.hea = address;
360            }
361        }
362    }
363
364    /// Pushes an `AmxCell` value onto the VM stack. Use **in reverse order**
365    /// of the public function's arguments before calling [`exec`].
366    ///
367    /// [`exec`]: Amx::exec
368    ///
369    /// # Errors
370    /// Propagates any [`AmxError`] from `amx_Push` — typically
371    /// `AmxError::StackError`/`StackLow` if the stack is full.
372    pub fn push<'a, T: AmxCell<'a>>(&'a self, value: T) -> AmxResult<()> {
373        let push = Push::from_table(self.fn_table);
374
375        amx_try!(push(self.ptr, value.as_cell()));
376
377        Ok(())
378    }
379
380    /// Length in characters of an AMX string at address `value`.
381    ///
382    /// # Errors
383    /// `AmxError::MemoryAccess` if `value` does not point to valid memory in
384    /// the script space. Other [`AmxError`] are propagated from `amx_StrLen`.
385    pub fn strlen(&self, value: *const i32) -> AmxResult<usize> {
386        let strlen = StrLen::from_table(self.fn_table);
387        let mut len = 0;
388        amx_try!(strlen(value, &raw mut len));
389        // `len` returned by `amx_StrLen` is always >= 0 (a negative value
390        // would become an error via `amx_try!`).
391        #[allow(clippy::cast_sign_loss)]
392        Ok(len as usize)
393    }
394
395    /// Creates an [`Allocator`] bound to this `Amx`.
396    ///
397    /// All memory allocated via [`Allocator::allot`]/[`Allocator::allot_buffer`]/
398    /// [`Allocator::allot_string`] is released automatically when the
399    /// `Allocator` goes out of scope (`Drop`). Keep it alive while using the
400    /// returned references.
401    #[must_use]
402    pub fn allocator(&self) -> Allocator<'_> {
403        Allocator::new(self)
404    }
405
406    /// Raw pointer to the `AMX` (non-null) or `None` if constructed with null.
407    #[must_use]
408    pub fn amx(&self) -> Option<NonNull<AMX>> {
409        NonNull::new(self.ptr)
410    }
411
412    /// Raw pointer to the `AMX_HEADER` of the loaded `.amx`.
413    #[must_use]
414    pub fn header(&self) -> Option<NonNull<AMX_HEADER>> {
415        let amx = NonNull::new(self.ptr)?;
416        NonNull::new(unsafe { (*amx.as_ptr()).base.cast::<AMX_HEADER>() })
417    }
418
419    // ---- VM register accessors (all `None` when the pointer is null) ----
420
421    /// Current instruction pointer (`cip`) — a code-segment offset in a debug
422    /// hook. Read as `u32`.
423    #[must_use]
424    pub fn cip(&self) -> Option<u32> {
425        read_reg!(self.cip).map(i32::cast_unsigned)
426    }
427
428    /// Current frame pointer (`frm`); local/argument symbols are addressed
429    /// relative to it.
430    #[must_use]
431    pub fn frame(&self) -> Option<i32> {
432        read_reg!(self.frm)
433    }
434
435    /// Current stack pointer (`stk`).
436    #[must_use]
437    pub fn stack(&self) -> Option<i32> {
438        read_reg!(self.stk)
439    }
440
441    /// Current heap pointer (`hea`).
442    #[must_use]
443    pub fn heap(&self) -> Option<i32> {
444        read_reg!(self.hea)
445    }
446
447    /// Top of the stack (`stp`) — the upper bound of the data address space.
448    #[must_use]
449    pub fn stp(&self) -> Option<i32> {
450        read_reg!(self.stp)
451    }
452
453    /// Resolves a data-segment address to a raw pointer with the same bounds
454    /// checking as `amx_GetAddr`, without going through the exported function
455    /// table. Returns `None` when the address falls in the free region between
456    /// heap and stack, is negative, or is past the top of the stack.
457    ///
458    /// Unlike [`get_ref`](Self::get_ref), this works inside a debug hook, where
459    /// no native call context is available. It is the building block for
460    /// [`read_cell`](Self::read_cell)/[`write_cell`](Self::write_cell).
461    fn data_ptr(&self, addr: i32) -> Option<*mut u8> {
462        let amx = NonNull::new(self.ptr)?.as_ptr();
463        let base = unsafe { std::ptr::addr_of!((*amx).base).read_unaligned() };
464        if base.is_null() {
465            return None;
466        }
467        let data_field = unsafe { std::ptr::addr_of!((*amx).data).read_unaligned() };
468        let hea = unsafe { std::ptr::addr_of!((*amx).hea).read_unaligned() };
469        let stk = unsafe { std::ptr::addr_of!((*amx).stk).read_unaligned() };
470        let stp = unsafe { std::ptr::addr_of!((*amx).stp).read_unaligned() };
471
472        // `data` is `amx->data` when set, otherwise `amx->base + header->dat`.
473        let data = if data_field.is_null() {
474            let hdr = base.cast::<AMX_HEADER>();
475            let dat = unsafe { std::ptr::addr_of!((*hdr).dat).read_unaligned() };
476            unsafe { base.add(usize::try_from(dat).ok()?) }
477        } else {
478            data_field
479        };
480
481        // Same valid region as `amx_GetAddr`: reject the active heap/stack gap
482        // and anything outside `[0, stp)`.
483        if (addr >= hea && addr < stk) || addr < 0 || addr >= stp {
484            return None;
485        }
486        Some(unsafe { data.add(usize::try_from(addr).ok()?) })
487    }
488
489    /// Reads a 32-bit cell from the data segment at `addr`, validating bounds
490    /// like `amx_GetAddr`. Returns `None` if the address is inaccessible.
491    ///
492    /// Reads byte-wise (no alignment assumption). Usable from a debug hook.
493    #[must_use]
494    pub fn read_cell(&self, addr: i32) -> Option<i32> {
495        let ptr = self.data_ptr(addr)?;
496        let mut buf = [0u8; 4];
497        unsafe { std::ptr::copy_nonoverlapping(ptr, buf.as_mut_ptr(), 4) };
498        Some(i32::from_ne_bytes(buf))
499    }
500
501    /// Writes a 32-bit cell to the data segment at `addr`, validating bounds
502    /// like `amx_GetAddr`. Returns `false` if the address is inaccessible.
503    ///
504    /// Writes byte-wise (no alignment assumption). Usable from a debug hook to
505    /// edit a variable while the VM is paused.
506    pub fn write_cell(&self, addr: i32, value: i32) -> bool {
507        let Some(ptr) = self.data_ptr(addr) else {
508            return false;
509        };
510        let buf = value.to_ne_bytes();
511        unsafe { std::ptr::copy_nonoverlapping(buf.as_ptr(), ptr, 4) };
512        true
513    }
514
515    /// Installs a debug hook callback into this VM (`amx->debug = cb`), the
516    /// equivalent of `amx_SetDebugHook`. The VM then calls `cb` on every line,
517    /// provided the `.amx` was compiled with `-d2`/`-d3`.
518    ///
519    /// The callback runs on the VM thread and crosses the FFI boundary, so it
520    /// must never unwind (no panics).
521    pub fn install_debug_hook(&self, cb: crate::raw::functions::AmxDebug) {
522        if let Some(amx) = NonNull::new(self.ptr) {
523            unsafe { std::ptr::addr_of_mut!((*amx.as_ptr()).debug).write_unaligned(cb) };
524        }
525    }
526
527    /// Removes a previously installed debug hook, restoring `amx->debug` to a
528    /// no-op callback that returns `AMX_ERR_NONE`.
529    pub fn remove_debug_hook(&self) {
530        extern "C" fn noop(_amx: *mut AMX) -> i32 {
531            0
532        }
533        self.install_debug_hook(noop);
534    }
535}
536
537/// AMX heap allocator with automatic release (RAII).
538///
539/// Captures the value of `amx.hea` at creation time and restores it on `Drop`,
540/// freeing everything allocated by the `Allocator` in a single operation.
541/// Do not use multiple nested `Allocator`s — each one restores to a different
542/// heap point.
543pub struct Allocator<'amx> {
544    amx: &'amx Amx,
545    release_addr: i32,
546}
547
548impl<'amx> Allocator<'amx> {
549    pub(crate) fn new(amx: &'amx Amx) -> Allocator<'amx> {
550        let amx_ptr = amx
551            .amx()
552            .expect("Allocator::new() received Amx with null pointer")
553            .as_ptr();
554        let release_addr = unsafe { (*amx_ptr).hea };
555
556        Allocator { amx, release_addr }
557    }
558
559    /// Allocates a single cell on the heap and initializes it with `init_value`.
560    ///
561    /// # Errors
562    /// `AmxError::Memory` if the VM heap is exhausted.
563    pub fn allot<T: Sized + AmxPrimitive>(&self, init_value: T) -> AmxResult<Ref<'_, T>> {
564        let mut cell = self.amx.allot(1)?;
565        *cell = init_value;
566
567        Ok(cell)
568    }
569
570    /// Allocates `size` cells on the heap and returns a [`Buffer`] covering that region.
571    ///
572    /// # Errors
573    /// `AmxError::Memory` if the VM heap is exhausted or if `size` exceeds
574    /// `i32::MAX`.
575    pub fn allot_buffer(&self, size: usize) -> AmxResult<Buffer<'_>> {
576        let buffer = self.amx.allot(size)?;
577
578        Ok(Buffer::new(buffer, size))
579    }
580
581    /// Allocates space for `array.len()` cells and copies the content (`AmxCell::as_cell`).
582    ///
583    /// # Errors
584    /// `AmxError::Memory` if the VM heap is exhausted.
585    pub fn allot_array<T>(&self, array: &[T]) -> AmxResult<Buffer<'_>>
586    where
587        T: AmxCell<'amx> + AmxPrimitive,
588    {
589        let mut buffer = self.allot_buffer(array.len())?;
590
591        let slice = buffer.as_mut_slice();
592
593        for (idx, item) in array.iter().enumerate() {
594            slice[idx] = item.as_cell();
595        }
596
597        Ok(buffer)
598    }
599
600    /// Allocates space for a string and copies `string` (configured encoding),
601    /// adding the `0` terminator at the end.
602    ///
603    /// # Errors
604    /// `AmxError::Memory` if the VM heap is exhausted.
605    pub fn allot_string(&self, string: &str) -> AmxResult<AmxString<'_>> {
606        let bytes = Allocator::string_bytes(string);
607        let buffer = self.allot_buffer(bytes.len() + 1)?;
608
609        Ok(unsafe { AmxString::new(buffer, bytes.as_ref()) })
610    }
611
612    fn string_bytes(string: &str) -> Cow<'_, [u8]> {
613        #[cfg(feature = "encoding")]
614        return encoding::get().encode(string).0;
615
616        #[cfg(not(feature = "encoding"))]
617        return Cow::from(string.as_bytes());
618    }
619}
620
621impl Drop for Allocator<'_> {
622    fn drop(&mut self) {
623        // AMX::release never fails
624        self.amx.release(self.release_addr);
625    }
626}
627
628#[cfg(test)]
629mod vm_tests {
630    use super::Amx;
631    use crate::raw::types::AMX;
632    use std::mem::MaybeUninit;
633
634    /// Builds a synthetic `AMX` over `data` and runs `f` with an `Amx` wrapping
635    /// it. Only the fields the VM accessors read are initialized (`base`/`data`/
636    /// register fields); `data` non-null means `data_ptr` uses it directly,
637    /// without needing a real `AMX_HEADER`.
638    ///
639    /// Region layout: valid data is `[0, stp)` minus the active heap/stack gap
640    /// `[hea, stk)` — mirroring `amx_GetAddr`. Here `stp = data.len()`.
641    fn with_amx(data: &mut [u8], cip: i32, frm: i32, hea: i32, stk: i32, f: impl FnOnce(&Amx)) {
642        let stp = i32::try_from(data.len()).unwrap();
643        let mut raw = MaybeUninit::<AMX>::uninit();
644        let p = raw.as_mut_ptr();
645        unsafe {
646            let base = data.as_mut_ptr();
647            std::ptr::addr_of_mut!((*p).base).write_unaligned(base);
648            std::ptr::addr_of_mut!((*p).data).write_unaligned(base);
649            std::ptr::addr_of_mut!((*p).cip).write_unaligned(cip);
650            std::ptr::addr_of_mut!((*p).frm).write_unaligned(frm);
651            std::ptr::addr_of_mut!((*p).hea).write_unaligned(hea);
652            std::ptr::addr_of_mut!((*p).stk).write_unaligned(stk);
653            std::ptr::addr_of_mut!((*p).stp).write_unaligned(stp);
654        }
655        let amx = Amx::new(p, 0);
656        f(&amx);
657    }
658
659    #[test]
660    fn registers_read_back() {
661        let mut data = vec![0u8; 256];
662        with_amx(&mut data, 40, 100, 64, 192, |amx| {
663            assert_eq!(amx.cip(), Some(40));
664            assert_eq!(amx.frame(), Some(100));
665            assert_eq!(amx.heap(), Some(64));
666            assert_eq!(amx.stack(), Some(192));
667            assert_eq!(amx.stp(), Some(256));
668        });
669    }
670
671    #[test]
672    fn read_write_cell_roundtrip_and_bounds() {
673        let mut data = vec![0u8; 256];
674        // Seed a global at addr 0 (below the heap/stack gap [64,192)).
675        data[0..4].copy_from_slice(&7i32.to_ne_bytes());
676        with_amx(&mut data, 40, 100, 64, 192, |amx| {
677            // Valid below the gap.
678            assert_eq!(amx.read_cell(0), Some(7));
679            // Valid above the stack pointer (addr 200 in [192,256)).
680            assert!(amx.write_cell(200, 0x1234_5678));
681            assert_eq!(amx.read_cell(200), Some(0x1234_5678));
682            // Inside the active heap/stack gap: rejected like amx_GetAddr.
683            assert_eq!(amx.read_cell(100), None);
684            assert!(!amx.write_cell(100, 1));
685            // Negative and past the top of the stack: rejected.
686            assert_eq!(amx.read_cell(-4), None);
687            assert_eq!(amx.read_cell(256), None);
688            assert_eq!(amx.read_cell(260), None);
689        });
690    }
691
692    #[test]
693    fn null_amx_is_safe() {
694        let amx = Amx::new(std::ptr::null_mut(), 0);
695        assert_eq!(amx.cip(), None);
696        assert_eq!(amx.frame(), None);
697        assert_eq!(amx.stp(), None);
698        assert_eq!(amx.read_cell(0), None);
699        assert!(!amx.write_cell(0, 1));
700        // Installing/removing a hook on a null AMX must not crash.
701        amx.remove_debug_hook();
702    }
703}