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