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}