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    /// Calls a public inside a managed [`Allocator`] scope — the escape hatch for
136    /// callbacks with **output arrays**, which the input-only [`exec_public!`]
137    /// macro cannot express.
138    ///
139    /// Resolves `name` to its public index and opens an [`Allocator`], then hands
140    /// both to `body`. Inside, allocate input/output buffers, [`push`] the
141    /// arguments (in reverse order), call [`exec`], and read any output buffers
142    /// back — all before the scope closes and frees the heap. The scope also
143    /// rewinds the VM stack, so a mid-sequence `push` failure cannot unbalance it.
144    ///
145    /// [`exec_public!`]: crate::exec_public
146    /// [`push`]: Amx::push
147    /// [`exec`]: Amx::exec
148    ///
149    /// # Errors
150    /// `AmxError::NotFound` if the public does not exist; otherwise whatever
151    /// `body` returns (typically propagated from `push`/`exec`).
152    ///
153    /// # Example
154    /// ```rust,no_run
155    /// # use samp_sdk::amx::Amx;
156    /// # use samp_sdk::error::AmxResult;
157    /// # fn demo(amx: &Amx) -> AmxResult<Vec<i32>> {
158    /// // Pawn: forward FillSquares(out[], size);
159    /// let squares = amx.exec_public_scope("FillSquares", |alloc, idx| {
160    ///     let buf = alloc.allot_buffer(8)?; // output array
161    ///     amx.push(8)?;                      // size   (pushed first = last arg)
162    ///     amx.push(&buf)?;                   // out[]  (pushed last  = first arg)
163    ///     amx.exec(idx)?;
164    ///     Ok(buf.as_slice().to_vec())        // read the array back before it frees
165    /// })?;
166    /// # Ok(squares)
167    /// # }
168    /// ```
169    pub fn exec_public_scope<F, R>(&self, name: &str, body: F) -> AmxResult<R>
170    where
171        F: FnOnce(&Allocator<'_>, AmxExecIdx) -> AmxResult<R>,
172    {
173        let index = self.find_public(name)?;
174        let allocator = self.allocator();
175        body(&allocator, index)
176    }
177
178    /// Index of a native by name (resolved via `amx_FindNative`).
179    ///
180    /// # Errors
181    /// `AmxError::NotFound` if `name` contains an interior NUL byte or if the
182    /// native is not registered in the VM.
183    pub fn find_native(&self, name: &str) -> AmxResult<i32> {
184        let find_native = FindNative::from_table(self.fn_table);
185        let c_str = CString::new(name).map_err(|_| AmxError::NotFound)?;
186        let mut index = -1;
187
188        amx_try!(find_native(self.ptr, c_str.as_ptr(), &raw mut index));
189
190        Ok(index)
191    }
192
193    /// Calls a native registered by **another plugin** in the same AMX.
194    ///
195    /// SA-MP plugins inject their natives into every loaded AMX via
196    /// `amx_Register`, which writes a host function pointer into the
197    /// native's entry inside the `AMX_HEADER` natives table. This helper
198    /// resolves the name through `amx_FindNative`, reads that function
199    /// pointer back, builds the `params` block in the AMX convention
200    /// (first cell = `argc * sizeof(cell)`, then the arguments), and
201    /// invokes the native.
202    ///
203    /// Integer arguments are passed as their `i32` value. Floats are
204    /// passed bit-cast to `i32` (use [`f32::to_bits`] then
205    /// [`i32::from_ne_bytes`] on `to_ne_bytes`, or `f32::to_bits() as i32`).
206    /// String and array arguments are AMX cell addresses returned by
207    /// [`Allocator::allot_string`]/[`Allocator::allot_buffer`] — same
208    /// marshalling as for [`exec_public`](crate::exec_public).
209    ///
210    /// # Example
211    /// ```rust,ignore
212    /// // Calling Streamer_CreateDynamicObject from a Rust plugin
213    /// fn on_amx_load(&mut self, amx: &Amx) -> AmxResult<()> {
214    ///     let model_id: i32 = 1337;
215    ///     #[allow(clippy::cast_possible_wrap)]
216    ///     let x = 100.0_f32.to_bits() as i32;
217    ///     let y = 200.0_f32.to_bits() as i32;
218    ///     let z =  10.0_f32.to_bits() as i32;
219    ///     let object_id = amx.call_native(
220    ///         "Streamer_CreateDynamicObject",
221    ///         &[model_id, x, y, z, 0, 0, 0],
222    ///     )?;
223    ///     log::info!("created dynamic object id={object_id}");
224    ///     Ok(())
225    /// }
226    /// ```
227    ///
228    /// # Errors
229    /// - [`AmxError::NotFound`] if `name` contains an interior NUL byte,
230    ///   the native is not registered, or its address is still zero
231    ///   (registered name but no host pointer attached).
232    /// - [`AmxError::MemoryAccess`] if the AMX header cannot be read.
233    /// - [`AmxError::Index`] if the resolved index is out of range for
234    ///   the natives table reported by the AMX header.
235    /// - Any [`AmxError`] propagated from the called native via
236    ///   `amx.error` (re-raised by the caller through `amx_try!`).
237    pub fn call_native(&self, name: &str, params: &[i32]) -> AmxResult<i32> {
238        let index = self.find_native(name)?;
239        if index < 0 {
240            return Err(AmxError::NotFound);
241        }
242
243        let header_ptr = self.header().ok_or(AmxError::MemoryAccess)?;
244        // SAFETY: `header()` returned NonNull, and the AMX is alive for
245        // the duration of `&self`.
246        let (natives_off, libraries_off, defsize) = unsafe {
247            let h = header_ptr.as_ptr();
248            (
249                std::ptr::read_unaligned(&raw const (*h).natives),
250                std::ptr::read_unaligned(&raw const (*h).libraries),
251                std::ptr::read_unaligned(&raw const (*h).defsize),
252            )
253        };
254
255        if defsize <= 0 || libraries_off < natives_off {
256            return Err(AmxError::MemoryAccess);
257        }
258        let defsize_i32 = i32::from(defsize);
259        let table_bytes = libraries_off - natives_off;
260        let num_natives = table_bytes / defsize_i32;
261        if index >= num_natives {
262            return Err(AmxError::Index);
263        }
264
265        let amx_ptr = self.amx().ok_or(AmxError::MemoryAccess)?;
266        // SAFETY: `amx_ptr` is NonNull and points to the live AMX.
267        let base = unsafe { (*amx_ptr.as_ptr()).base };
268        if base.is_null() {
269            return Err(AmxError::MemoryAccess);
270        }
271
272        let entry_off = natives_off + index * defsize_i32;
273        // SAFETY: `entry_off` is within the natives table bounded by
274        // (libraries - natives), which the header advertises as part of
275        // the AMX-mapped region pointed to by `base`.
276        let entry_ptr = unsafe { base.offset(entry_off as isize) };
277
278        // First 4 bytes of each entry — both `AMX_FUNCSTUB` and
279        // `ANX_FUNCSTUBNT` start with `u32 address`, the host function
280        // pointer written by `amx_Register`.
281        let address = unsafe { std::ptr::read_unaligned(entry_ptr.cast::<u32>()) };
282        if address == 0 {
283            return Err(AmxError::NotFound);
284        }
285
286        // SAFETY: SA-MP / open.mp are 32-bit; the AMX cell width and host
287        // function pointer width are both 4 bytes. `address` came from
288        // `amx_Register`, which writes a valid `AmxNative` pointer.
289        let native: AmxNative = unsafe { std::mem::transmute(address as usize) };
290
291        // Build the params block: `[argc * sizeof(cell), arg0, arg1, ...]`.
292        // Bytes, not cells — matches the convention every AMX native
293        // implementation reads (`params[0] / sizeof(cell)` to recover argc).
294        let mut buf: Vec<i32> = Vec::with_capacity(params.len() + 1);
295        // `params.len()` bounded by `i32::MAX` in practice; the AMX
296        // would have failed long before reaching 2 billion args.
297        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
298        let argc_bytes = (params.len() as i32) * 4;
299        buf.push(argc_bytes);
300        buf.extend_from_slice(params);
301
302        let retval = native(self.ptr, buf.as_mut_ptr());
303        // Surface VM-side errors set by the native into `amx.error`.
304        // SAFETY: `amx_ptr` already validated above.
305        let err = unsafe { (*amx_ptr.as_ptr()).error };
306        if err > 0 {
307            return Err(err.into());
308        }
309        Ok(retval)
310    }
311
312    /// Index of a public function by name — pass the result to [`exec`].
313    ///
314    /// ```
315    /// use samp_sdk::amx::Amx;
316    /// use samp_sdk::error::AmxResult;
317    /// fn has_on_player_connect(amx: &Amx) -> AmxResult<bool> {
318    ///     let idx = amx.find_public("OnPlayerConnect")?;
319    ///     Ok(i32::from(idx) >= 0)
320    /// }
321    /// ```
322    ///
323    /// [`exec`]: Amx::exec
324    ///
325    /// # Errors
326    /// `AmxError::NotFound` if `name` contains an interior NUL byte or if the
327    /// public function is not declared in the Pawn script.
328    pub fn find_public(&self, name: &str) -> AmxResult<AmxExecIdx> {
329        let find_public = FindPublic::from_table(self.fn_table);
330        let c_str = CString::new(name).map_err(|_| AmxError::NotFound)?;
331        let mut index = -1;
332
333        amx_try!(find_public(self.ptr, c_str.as_ptr(), &raw mut index));
334
335        Ok(AmxExecIdx::from(index))
336    }
337
338    /// `Ref<T>` pointing to a public variable declared in the Pawn script.
339    ///
340    /// ```rust,no_run
341    /// # use samp_sdk::amx::Amx;
342    /// # use samp_sdk::error::AmxResult;
343    /// # fn check(amx: &Amx) -> AmxResult<()> {
344    /// let version = amx.find_pubvar::<f32>("my_plugin_version")?;
345    /// // outdated
346    /// if *version < 1.0 { }
347    /// # Ok(()) }
348    /// ```
349    ///
350    /// # Errors
351    /// `AmxError::NotFound` if `name` contains an interior NUL byte or if the
352    /// pubvar is not declared. `AmxError::MemoryAccess` if the address returned
353    /// by the VM is invalid.
354    pub fn find_pubvar<T: Sized + AmxPrimitive>(&self, name: &str) -> AmxResult<Ref<'_, T>> {
355        let find_pubvar = FindPubVar::from_table(self.fn_table);
356        let c_str = CString::new(name).map_err(|_| AmxError::NotFound)?;
357        let mut cell_ptr = 0;
358
359        amx_try!(find_pubvar(self.ptr, c_str.as_ptr(), &raw mut cell_ptr));
360
361        self.get_ref(cell_ptr)
362    }
363
364    /// Flags of the loaded `.amx`.
365    ///
366    /// # Errors
367    /// Propagates any [`AmxError`] returned by `amx_Flags` — in practice, it
368    /// only fails if the internal `AMX*` is corrupted or null.
369    pub fn flags(&self) -> AmxResult<AmxFlags> {
370        let flags = Flags::from_table(self.fn_table);
371        let mut value: u16 = 0;
372
373        amx_try!(flags(self.ptr, &raw mut value));
374
375        Ok(AmxFlags::from_bits_truncate(value))
376    }
377
378    /// Returns the VM's opcode dispatch table (`amx_opcodelist`): `count` raw
379    /// label addresses, one per opcode, in opcode order.
380    ///
381    /// On a server built with computed-goto threading (GCC/Clang, the SA-MP and
382    /// open.mp builds), the loader rewrites each opcode in the code segment to the
383    /// *address* of its handler label, so a byte read with [`read_code`] yields a
384    /// pointer, not the opcode number. Inverting this table (address → opcode)
385    /// lets a debugger recover the real opcode at `cip`. The table is fetched the
386    /// way the loader itself does it — set the `BROWSE` flag and call `amx_Exec`
387    /// with index `0`, which returns `&amx_opcodelist` instead of running code.
388    ///
389    /// `count` is the number of opcodes the caller expects (`OP_NUM_OPCODES`);
390    /// the SDK does not hardcode the VM's opcode count. Returns `None` only when
391    /// the table cannot be obtained (null VM/table).
392    ///
393    /// The `AMX_FLAG_RELOC` header bit is intentionally **not** consulted: it is
394    /// set by the loader in the file header and may not yet be visible at
395    /// `AmxLoad` time, even though the dispatch table is already available. A
396    /// non-computed-goto VM would return a table whose addresses simply never
397    /// match a real opcode, so inverting it is harmless (the consumer finds no
398    /// match and treats the code value as a raw opcode).
399    ///
400    /// [`read_code`]: Self::read_code
401    pub fn opcode_table(&self, count: usize) -> Option<Vec<usize>> {
402        let amx = NonNull::new(self.ptr)?.as_ptr();
403
404        // Toggle the BROWSE flag so `amx_Exec(.., 0)` returns the label table
405        // instead of executing. Restore the previous flags afterwards.
406        let saved = unsafe { std::ptr::addr_of!((*amx).flags).read_unaligned() };
407        unsafe {
408            std::ptr::addr_of_mut!((*amx).flags)
409                .write_unaligned(saved | i32::from(AmxFlags::BROWSE.bits()));
410        }
411        let exec = Exec::from_table(self.fn_table);
412        // `retval` receives `(cell)amx_opcodelist` — a pointer to the table. On the
413        // 32-bit SA-MP/open.mp VMs `cell` and `void*` are both 32-bit (the VM
414        // asserts `sizeof(cell)==sizeof(void*)`), so it round-trips through i32.
415        let mut retval: i32 = 0;
416        let _ = exec(self.ptr, &raw mut retval, 0);
417        unsafe {
418            std::ptr::addr_of_mut!((*amx).flags).write_unaligned(saved);
419        }
420
421        let table = usize::try_from(retval.cast_unsigned()).ok()? as *const usize;
422        if table.is_null() {
423            return None;
424        }
425        // Read `count` pointer-sized entries from the table.
426        let mut out = Vec::with_capacity(count);
427        for i in 0..count {
428            out.push(unsafe { table.add(i).read_unaligned() });
429        }
430        Some(out)
431    }
432
433    /// Resolves an AMX cell (relative address) to a typed [`Ref<T>`].
434    ///
435    /// # Errors
436    /// `AmxError::MemoryAccess` if `address` does not correspond to a valid
437    /// cell in the Pawn script address space.
438    pub fn get_ref<T: Sized + AmxPrimitive>(&self, address: i32) -> AmxResult<Ref<'_, T>> {
439        let get_addr = GetAddr::from_table(self.fn_table);
440        let mut dest = 0;
441        let mut dest_addr = std::ptr::addr_of_mut!(dest);
442
443        amx_try!(get_addr(self.ptr, address, &raw mut dest_addr));
444
445        if dest_addr.is_null() {
446            return Err(AmxError::MemoryAccess);
447        }
448
449        unsafe { Ok(Ref::new(address, dest_addr.cast::<T>())) }
450    }
451
452    /// Rewinds the VM heap and stack to the values captured when an
453    /// [`Allocator`] scope opened, freeing everything it allocated **and**
454    /// pushed in one shot.
455    ///
456    /// - `hea`: restores the heap top (frees `allot*` buffers).
457    /// - `stk`: restores the stack top. The stack grows downward, so this only
458    ///   rewinds when the current `stk` sits *below* the captured value (i.e.
459    ///   something was pushed and not yet consumed) — the corrective path for a
460    ///   `push` sequence that failed part-way through `exec_public!`. A balanced
461    ///   `exec` leaves `stk` back at the captured value, making this a no-op.
462    #[inline]
463    pub(crate) fn release_scope(&self, hea: i32, stk: i32) {
464        if let Some(mut amx) = self.amx() {
465            let amx = unsafe { amx.as_mut() };
466            if hea >= 0 && amx.hea > hea {
467                amx.hea = hea;
468            }
469            if stk >= 0 && stk <= amx.stp && amx.stk < stk {
470                amx.stk = stk;
471            }
472        }
473    }
474
475    /// Pushes an `AmxCell` value onto the VM stack. Use **in reverse order**
476    /// of the public function's arguments before calling [`exec`].
477    ///
478    /// [`exec`]: Amx::exec
479    ///
480    /// # Errors
481    /// Propagates any [`AmxError`] from `amx_Push` — typically
482    /// `AmxError::StackError`/`StackLow` if the stack is full.
483    pub fn push<'a, T: AmxCell<'a>>(&'a self, value: T) -> AmxResult<()> {
484        let push = Push::from_table(self.fn_table);
485
486        amx_try!(push(self.ptr, value.as_cell()));
487
488        Ok(())
489    }
490
491    /// Length in characters of an AMX string at address `value`.
492    ///
493    /// # Errors
494    /// `AmxError::MemoryAccess` if `value` does not point to valid memory in
495    /// the script space. Other [`AmxError`] are propagated from `amx_StrLen`.
496    pub fn strlen(&self, value: *const i32) -> AmxResult<usize> {
497        let strlen = StrLen::from_table(self.fn_table);
498        let mut len = 0;
499        amx_try!(strlen(value, &raw mut len));
500        // `len` returned by `amx_StrLen` is always >= 0 (a negative value
501        // would become an error via `amx_try!`).
502        #[allow(clippy::cast_sign_loss)]
503        Ok(len as usize)
504    }
505
506    /// Creates an [`Allocator`] bound to this `Amx`.
507    ///
508    /// All memory allocated via [`Allocator::allot`]/[`Allocator::allot_buffer`]/
509    /// [`Allocator::allot_string`] is released automatically when the
510    /// `Allocator` goes out of scope (`Drop`). Keep it alive while using the
511    /// returned references.
512    #[must_use]
513    pub fn allocator(&self) -> Allocator<'_> {
514        Allocator::new(self)
515    }
516
517    /// Raw pointer to the `AMX` (non-null) or `None` if constructed with null.
518    #[must_use]
519    pub fn amx(&self) -> Option<NonNull<AMX>> {
520        NonNull::new(self.ptr)
521    }
522
523    /// Raw pointer to the `AMX_HEADER` of the loaded `.amx`.
524    #[must_use]
525    pub fn header(&self) -> Option<NonNull<AMX_HEADER>> {
526        let amx = NonNull::new(self.ptr)?;
527        NonNull::new(unsafe { (*amx.as_ptr()).base.cast::<AMX_HEADER>() })
528    }
529
530    // ---- VM register accessors (all `None` when the pointer is null) ----
531
532    /// Current instruction pointer (`cip`) — a code-segment offset in a debug
533    /// hook. Read as `u32`.
534    #[must_use]
535    pub fn cip(&self) -> Option<u32> {
536        read_reg!(self.cip).map(i32::cast_unsigned)
537    }
538
539    /// Current frame pointer (`frm`); local/argument symbols are addressed
540    /// relative to it.
541    #[must_use]
542    pub fn frame(&self) -> Option<i32> {
543        read_reg!(self.frm)
544    }
545
546    /// Current stack pointer (`stk`).
547    #[must_use]
548    pub fn stack(&self) -> Option<i32> {
549        read_reg!(self.stk)
550    }
551
552    /// Current heap pointer (`hea`).
553    #[must_use]
554    pub fn heap(&self) -> Option<i32> {
555        read_reg!(self.hea)
556    }
557
558    /// Top of the stack (`stp`) — the upper bound of the data address space.
559    #[must_use]
560    pub fn stp(&self) -> Option<i32> {
561        read_reg!(self.stp)
562    }
563
564    /// Primary register (`pri`) — the VM's main accumulator. In a debug hook it
565    /// holds the operand the next instruction will act on; e.g. for `OP_BOUNDS`
566    /// it is the index being range-checked.
567    #[must_use]
568    pub fn pri(&self) -> Option<i32> {
569        read_reg!(self.pri)
570    }
571
572    /// Alternate register (`alt`) — the VM's secondary accumulator. For the
573    /// division opcodes (`OP_DIV`/`OP_SDIV`) it holds the divisor, so reading it
574    /// in a debug hook lets a debugger detect a divide-by-zero before it aborts.
575    #[must_use]
576    pub fn alt(&self) -> Option<i32> {
577        read_reg!(self.alt)
578    }
579
580    /// Reads a 32-bit cell from the **code** segment at `offset` (a code-segment
581    /// offset, like `cip`). Returns `None` when the VM pointer is null or the
582    /// offset is outside the code segment `[0, header.dat - header.cod)`.
583    ///
584    /// The code segment is read-only and laid out as `base + header.cod`; this is
585    /// the counterpart of [`read_cell`](Self::read_cell) for instructions. A
586    /// debugger uses it to decode the opcode at `cip` inside a debug hook (e.g. to
587    /// catch a runtime error before the VM aborts). Reads byte-wise (no alignment
588    /// assumption), since the `AMX_HEADER` is packed.
589    #[must_use]
590    pub fn read_code(&self, offset: u32) -> Option<i32> {
591        let amx = NonNull::new(self.ptr)?.as_ptr();
592        let base = unsafe { std::ptr::addr_of!((*amx).base).read_unaligned() };
593        if base.is_null() {
594            return None;
595        }
596        let hdr = base.cast::<AMX_HEADER>();
597        let cod = unsafe { std::ptr::addr_of!((*hdr).cod).read_unaligned() };
598        let dat = unsafe { std::ptr::addr_of!((*hdr).dat).read_unaligned() };
599        // Code segment spans `[cod, dat)`; the offset is relative to `cod`.
600        let size = u32::try_from(dat - cod).ok()?;
601        if offset >= size {
602            return None;
603        }
604        let cod = usize::try_from(cod).ok()?;
605        let off = usize::try_from(offset).ok()?;
606        let ptr = unsafe { base.add(cod + off) };
607        let mut buf = [0u8; 4];
608        unsafe { std::ptr::copy_nonoverlapping(ptr, buf.as_mut_ptr(), 4) };
609        Some(i32::from_ne_bytes(buf))
610    }
611
612    /// Resolves a data-segment address to a raw pointer with the same bounds
613    /// checking as `amx_GetAddr`, without going through the exported function
614    /// table. Returns `None` when the address falls in the free region between
615    /// heap and stack, is negative, or is past the top of the stack.
616    ///
617    /// Unlike [`get_ref`](Self::get_ref), this works inside a debug hook, where
618    /// no native call context is available. It is the building block for
619    /// [`read_cell`](Self::read_cell)/[`write_cell`](Self::write_cell).
620    fn data_ptr(&self, addr: i32) -> Option<*mut u8> {
621        let amx = NonNull::new(self.ptr)?.as_ptr();
622        let base = unsafe { std::ptr::addr_of!((*amx).base).read_unaligned() };
623        if base.is_null() {
624            return None;
625        }
626        let data_field = unsafe { std::ptr::addr_of!((*amx).data).read_unaligned() };
627        let hea = unsafe { std::ptr::addr_of!((*amx).hea).read_unaligned() };
628        let stk = unsafe { std::ptr::addr_of!((*amx).stk).read_unaligned() };
629        let stp = unsafe { std::ptr::addr_of!((*amx).stp).read_unaligned() };
630
631        // `data` is `amx->data` when set, otherwise `amx->base + header->dat`.
632        let data = if data_field.is_null() {
633            let hdr = base.cast::<AMX_HEADER>();
634            let dat = unsafe { std::ptr::addr_of!((*hdr).dat).read_unaligned() };
635            unsafe { base.add(usize::try_from(dat).ok()?) }
636        } else {
637            data_field
638        };
639
640        // Same valid region as `amx_GetAddr`: reject the active heap/stack gap
641        // and anything outside `[0, stp)`.
642        if (addr >= hea && addr < stk) || addr < 0 || addr >= stp {
643            return None;
644        }
645        Some(unsafe { data.add(usize::try_from(addr).ok()?) })
646    }
647
648    /// Reads a 32-bit cell from the data segment at `addr`, validating bounds
649    /// like `amx_GetAddr`. Returns `None` if the address is inaccessible.
650    ///
651    /// Reads byte-wise (no alignment assumption). Usable from a debug hook.
652    #[must_use]
653    pub fn read_cell(&self, addr: i32) -> Option<i32> {
654        let ptr = self.data_ptr(addr)?;
655        let mut buf = [0u8; 4];
656        unsafe { std::ptr::copy_nonoverlapping(ptr, buf.as_mut_ptr(), 4) };
657        Some(i32::from_ne_bytes(buf))
658    }
659
660    /// Writes a 32-bit cell to the data segment at `addr`, validating bounds
661    /// like `amx_GetAddr`. Returns `false` if the address is inaccessible.
662    ///
663    /// Writes byte-wise (no alignment assumption). Usable from a debug hook to
664    /// edit a variable while the VM is paused.
665    pub fn write_cell(&self, addr: i32, value: i32) -> bool {
666        let Some(ptr) = self.data_ptr(addr) else {
667            return false;
668        };
669        let buf = value.to_ne_bytes();
670        unsafe { std::ptr::copy_nonoverlapping(buf.as_ptr(), ptr, 4) };
671        true
672    }
673
674    /// Installs a debug hook callback into this VM (`amx->debug = cb`), the
675    /// equivalent of `amx_SetDebugHook`. The VM then calls `cb` on every line,
676    /// provided the `.amx` was compiled with `-d2`/`-d3`.
677    ///
678    /// The callback runs on the VM thread and crosses the FFI boundary, so it
679    /// must never unwind (no panics).
680    pub fn install_debug_hook(&self, cb: crate::raw::functions::AmxDebug) {
681        if let Some(amx) = NonNull::new(self.ptr) {
682            unsafe { std::ptr::addr_of_mut!((*amx.as_ptr()).debug).write_unaligned(cb) };
683        }
684    }
685
686    /// Removes a previously installed debug hook, restoring `amx->debug` to a
687    /// no-op callback that returns `AMX_ERR_NONE`.
688    pub fn remove_debug_hook(&self) {
689        extern "C" fn noop(_amx: *mut AMX) -> i32 {
690            0
691        }
692        self.install_debug_hook(noop);
693    }
694}
695
696/// AMX heap allocator with automatic release (RAII).
697///
698/// Captures the value of `amx.hea` at creation time and restores it on `Drop`,
699/// freeing everything allocated by the `Allocator` in a single operation.
700/// Do not use multiple nested `Allocator`s — each one restores to a different
701/// heap point.
702pub struct Allocator<'amx> {
703    amx: &'amx Amx,
704    release_hea: i32,
705    release_stk: i32,
706}
707
708impl<'amx> Allocator<'amx> {
709    pub(crate) fn new(amx: &'amx Amx) -> Allocator<'amx> {
710        // Capture the heap and stack tops to restore on drop. A null VM (only
711        // reachable from tests) yields `(0, 0)` and every `allot*` then fails
712        // gracefully via `amx_Allot` — no panic at construction.
713        let (release_hea, release_stk) = amx.amx().map_or((0, 0), |ptr| {
714            let ptr = ptr.as_ptr();
715            unsafe { ((*ptr).hea, (*ptr).stk) }
716        });
717
718        Allocator {
719            amx,
720            release_hea,
721            release_stk,
722        }
723    }
724
725    /// Allocates a single cell on the heap and initializes it with `init_value`.
726    ///
727    /// # Errors
728    /// `AmxError::Memory` if the VM heap is exhausted.
729    pub fn allot<T: Sized + AmxPrimitive>(&self, init_value: T) -> AmxResult<Ref<'_, T>> {
730        let mut cell = self.amx.allot(1)?;
731        *cell = init_value;
732
733        Ok(cell)
734    }
735
736    /// Allocates `size` cells on the heap and returns a [`Buffer`] covering that region.
737    ///
738    /// # Errors
739    /// `AmxError::Memory` if the VM heap is exhausted or if `size` exceeds
740    /// `i32::MAX`.
741    pub fn allot_buffer(&self, size: usize) -> AmxResult<Buffer<'_>> {
742        let buffer = self.amx.allot(size)?;
743
744        Ok(Buffer::new(buffer, size))
745    }
746
747    /// Allocates space for `array.len()` cells and copies the content (`AmxCell::as_cell`).
748    ///
749    /// # Errors
750    /// `AmxError::Memory` if the VM heap is exhausted.
751    pub fn allot_array<T>(&self, array: &[T]) -> AmxResult<Buffer<'_>>
752    where
753        T: AmxCell<'amx> + AmxPrimitive,
754    {
755        let mut buffer = self.allot_buffer(array.len())?;
756
757        let slice = buffer.as_mut_slice();
758
759        for (idx, item) in array.iter().enumerate() {
760            slice[idx] = item.as_cell();
761        }
762
763        Ok(buffer)
764    }
765
766    /// Allocates space for a string and copies `string` (configured encoding),
767    /// adding the `0` terminator at the end.
768    ///
769    /// # Errors
770    /// `AmxError::Memory` if the VM heap is exhausted.
771    pub fn allot_string(&self, string: &str) -> AmxResult<AmxString<'_>> {
772        let bytes = Allocator::string_bytes(string);
773        let buffer = self.allot_buffer(bytes.len() + 1)?;
774
775        Ok(unsafe { AmxString::new(buffer, bytes.as_ref()) })
776    }
777
778    fn string_bytes(string: &str) -> Cow<'_, [u8]> {
779        #[cfg(feature = "encoding")]
780        return encoding::get().encode(string).0;
781
782        #[cfg(not(feature = "encoding"))]
783        return Cow::from(string.as_bytes());
784    }
785}
786
787impl Drop for Allocator<'_> {
788    fn drop(&mut self) {
789        // Rewinds heap + stack to the captured scope. Never fails; on a balanced
790        // `exec_public!` the stack rewind is a no-op, on a failed push sequence
791        // it restores the leftover cells so the VM stack stays balanced.
792        self.amx.release_scope(self.release_hea, self.release_stk);
793    }
794}
795
796#[cfg(test)]
797mod vm_tests {
798    use super::Amx;
799    use crate::raw::types::{AMX, AMX_HEADER};
800    use std::mem::MaybeUninit;
801
802    /// Builds a synthetic `AMX` over `data` and runs `f` with an `Amx` wrapping
803    /// it. Only the fields the VM accessors read are initialized (`base`/`data`/
804    /// register fields); `data` non-null means `data_ptr` uses it directly,
805    /// without needing a real `AMX_HEADER`.
806    ///
807    /// Region layout: valid data is `[0, stp)` minus the active heap/stack gap
808    /// `[hea, stk)` — mirroring `amx_GetAddr`. Here `stp = data.len()`.
809    fn with_amx(data: &mut [u8], cip: i32, frm: i32, hea: i32, stk: i32, f: impl FnOnce(&Amx)) {
810        let stp = i32::try_from(data.len()).unwrap();
811        let mut raw = MaybeUninit::<AMX>::uninit();
812        let p = raw.as_mut_ptr();
813        unsafe {
814            let base = data.as_mut_ptr();
815            std::ptr::addr_of_mut!((*p).base).write_unaligned(base);
816            std::ptr::addr_of_mut!((*p).data).write_unaligned(base);
817            std::ptr::addr_of_mut!((*p).cip).write_unaligned(cip);
818            std::ptr::addr_of_mut!((*p).frm).write_unaligned(frm);
819            std::ptr::addr_of_mut!((*p).hea).write_unaligned(hea);
820            std::ptr::addr_of_mut!((*p).stk).write_unaligned(stk);
821            std::ptr::addr_of_mut!((*p).stp).write_unaligned(stp);
822            // pri/alt seeded deterministically so the register test can read them.
823            std::ptr::addr_of_mut!((*p).pri).write_unaligned(11);
824            std::ptr::addr_of_mut!((*p).alt).write_unaligned(0);
825        }
826        let amx = Amx::new(p, 0);
827        f(&amx);
828    }
829
830    #[test]
831    fn registers_read_back() {
832        let mut data = vec![0u8; 256];
833        with_amx(&mut data, 40, 100, 64, 192, |amx| {
834            assert_eq!(amx.cip(), Some(40));
835            assert_eq!(amx.frame(), Some(100));
836            assert_eq!(amx.heap(), Some(64));
837            assert_eq!(amx.stack(), Some(192));
838            assert_eq!(amx.stp(), Some(256));
839            // pri/alt as seeded by `with_amx` (alt = 0 models a divide-by-zero).
840            assert_eq!(amx.pri(), Some(11));
841            assert_eq!(amx.alt(), Some(0));
842        });
843    }
844
845    #[test]
846    fn read_code_reads_instructions_and_bounds() {
847        // Build a minimal blob: AMX_HEADER followed by the code segment. Only the
848        // `cod`/`dat` fields matter here — `cod` marks where the code starts and
849        // `dat` its end (the data segment would follow). The `base` pointer is the
850        // blob itself, mirroring how the loader lays the `.amx` out in memory.
851        let hdr_size = std::mem::size_of::<AMX_HEADER>();
852        let cod = i32::try_from(hdr_size).unwrap();
853        // Two 4-byte cells of code: 0xAABBCCDD then 0x00000011.
854        let mut blob = vec![0u8; hdr_size + 8];
855        blob[hdr_size..hdr_size + 4].copy_from_slice(&0xAABB_CCDDu32.to_ne_bytes());
856        blob[hdr_size + 4..hdr_size + 8].copy_from_slice(&0x11i32.to_ne_bytes());
857        let dat = i32::try_from(hdr_size + 8).unwrap();
858
859        let mut raw = MaybeUninit::<AMX>::uninit();
860        let p = raw.as_mut_ptr();
861        unsafe {
862            let base = blob.as_mut_ptr();
863            std::ptr::addr_of_mut!((*p).base).write_unaligned(base);
864            let hdr = base.cast::<AMX_HEADER>();
865            std::ptr::addr_of_mut!((*hdr).cod).write_unaligned(cod);
866            std::ptr::addr_of_mut!((*hdr).dat).write_unaligned(dat);
867        }
868        let amx = Amx::new(p, 0);
869        // Offset 0 and 4 are the two seeded cells.
870        assert_eq!(amx.read_code(0), Some(0xAABB_CCDDu32.cast_signed()));
871        assert_eq!(amx.read_code(4), Some(0x11));
872        // Past the end of the code segment (size = 8): rejected.
873        assert_eq!(amx.read_code(8), None);
874        assert_eq!(amx.read_code(100), None);
875    }
876
877    #[test]
878    fn read_write_cell_roundtrip_and_bounds() {
879        let mut data = vec![0u8; 256];
880        // Seed a global at addr 0 (below the heap/stack gap [64,192)).
881        data[0..4].copy_from_slice(&7i32.to_ne_bytes());
882        with_amx(&mut data, 40, 100, 64, 192, |amx| {
883            // Valid below the gap.
884            assert_eq!(amx.read_cell(0), Some(7));
885            // Valid above the stack pointer (addr 200 in [192,256)).
886            assert!(amx.write_cell(200, 0x1234_5678));
887            assert_eq!(amx.read_cell(200), Some(0x1234_5678));
888            // Inside the active heap/stack gap: rejected like amx_GetAddr.
889            assert_eq!(amx.read_cell(100), None);
890            assert!(!amx.write_cell(100, 1));
891            // Negative and past the top of the stack: rejected.
892            assert_eq!(amx.read_cell(-4), None);
893            assert_eq!(amx.read_cell(256), None);
894            assert_eq!(amx.read_cell(260), None);
895        });
896    }
897
898    #[test]
899    fn null_amx_is_safe() {
900        let amx = Amx::new(std::ptr::null_mut(), 0);
901        assert_eq!(amx.cip(), None);
902        assert_eq!(amx.frame(), None);
903        assert_eq!(amx.stp(), None);
904        assert_eq!(amx.pri(), None);
905        assert_eq!(amx.alt(), None);
906        assert_eq!(amx.read_cell(0), None);
907        assert_eq!(amx.read_code(0), None);
908        assert_eq!(amx.opcode_table(256), None);
909        assert!(!amx.write_cell(0, 1));
910        // Installing/removing a hook on a null AMX must not crash.
911        amx.remove_debug_hook();
912    }
913}