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}