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