v3.4.0
Feature release: #[event] — write Pawn callback handlers (observers, or
handlers that cancel the callback) directly in Rust, the missing half for
building gamemodes rather than only plugins. Ships alongside
Amx::exec_public_scope for output-array callbacks and a round of buffer/stack
hardening across the FFI boundary. Event delivery and the hardening were verified
end-to-end on a live SA-MP server.
Added
#[event]— Pawn callback handlers. Observe gamemode callbacks
(OnPlayerConnect,OnPlayerSpawn, …) directly in Rust, the missing half for
writing gamemodes rather than only plugins. Mark a method
#[event(name = "OnPlayerConnect")]and register it via the new
initialize_plugin!(events: [...])list; arguments are marshalled exactly like
#[native]. Under the hood the SDK detours the VM'samx_Exec(viaretour)
and dispatches each public into the matching handlers before the gamemode's own
public runs. The detour is installed lazily — plugins with no events never
touchamx_Exec.- Handlers are observers by default (return
AmxResult<T>/T, value
ignored, the public runs). A handler that returnsEventReturncan instead
cancel the callback (EventReturn::Suppress(value)skips the gamemode's
public and returnsvalue;EventReturn::suppress(v)encodes a typed
f32/bool/int forFloat:/bool:callbacks). Dispatch is O(1) per public
(keyed by(amx, index)), reentrancy-guarded (a handler re-entering the same
public runs it directly instead of recursing), and de-duplicated per AMX. #[event(name = "…", raw)]hands the handler theArgscursor
(fn(&mut self, amx: &Amx, args: &mut Args) -> EventReturn) for variadic or
protocol-specific callbacks, mirroring#[native(raw)].- Verified end-to-end on a live SA-MP server (arg order for int/multi-arg/
string, observer vs suppression, reentrancy, panic isolation). The same
detour drives native open.mp, but that path has not been validated on a
live open.mp server yet. - The detour is x86/x86_64 only (the arches SA-MP/open.mp run on); the
retourdependency and dispatch code are scoped accordingly, so the aarch64
check job still builds with events as a no-op. retouris pinned to=0.4.0-alpha.4— the only release line that compiles
on the stable channel (0.3.x needs nightly). Revisit when a stable0.4ships.- Inspired by an API proposal in the upstream
samp-rsproject
(PR #29, issue #3). The macro
surface follows that proposal; the implementation — including theamx_Exec
detour and stack marshalling the proposal left untested — was written and
validated here from scratch. No code from that proposal is used, and it is
not a contribution to this repository.
- Handlers are observers by default (return
Amx::exec_public_scope— calls a public inside a managedAllocator
scope, the escape hatch for callbacks with output arrays (which the
input-onlyexec_public!macro cannot express): allocate buffers, push args,
exec, and read outputs back before the scope frees them. Validated on a live
SA-MP server.
Tests
- Property/fuzz tests for the marshalling boundary (dependency-free,
deterministic):AmxStringdecoding is total (no panic/overrun) for random
cells, corrupted lengths and non-UTF8 bytes;Buffer::get_as/set_asstay in
bounds for any index;into_sized_bufferlength is exactly
min(requested, segment, 1 MiB);Args::countis never negative/absurd.
Hardened
UnsizedBuffer::into_sized_bufferclamps the requested size to the VM data
region[0, stp). A native that passes asizelarger than the real Pawn
array (e.g. a corrupted or attacker-influenced length) can no longer produce a
slice that reads or writes past the AMX allocation and segfaults; the size is
bounded to the script's own memory (and the existing 1 MiB ceiling). It still
cannot detect a size that overruns the array but stays inside the segment —
always pass the realsizeof(arr). Verified on a live SA-MP server.AmxString::to_bytesno longer indexes an empty backing buffer, returning
an empty string instead of panicking on a corrupted length.Allocatornow rewinds the VM stack as well as the heap on drop. If a
pushsequence insideexec_public!fails part-way (VM stack exhausted), the
already-pushed cells are restored so the stack stays balanced instead of
drifting. On the normal balanced path it is a no-op — verified with 1000
exec_public!calls on a live SA-MP server (all succeeded, stack healthy).Allocator::newno longer panics on a null VM pointer (only reachable from
tests); it captures(0, 0)and everyallot*then fails gracefully via
amx_Allot.
Security
- RUSTSEC-2026-0204 (
crossbeam-epochinvalid pointer dereference in the
fmt::Pointerimpl forAtomic/Shared) — updated to 0.9.20. The advisory
reached the SDK only through a dev-dependency (criterion→rayon→
crossbeam-deque), so no shipped plugin was affected, but it was failing the
cargo auditCI step onmaster. - CVE-2026-61632 / GHSA-9xwg-3r6f-jcx2 (
pymdown-extensionsb64 path
traversal) — bumped10.21.3→11.0.1indocs/requirements.txt. This is a
docs-build-only dependency (MkDocs) over the project's own trusted
markdown, shipped in no crate; the fix just clears the Dependabot alert.
Changed
- CI: benchmarks no longer run on GitHub. The
benchjob (and itschanges
gate) and thebench-release.ymlworkflow were removed — benches were noisy on
shared runners and added little signal on every push/PR/release. They are now
dev-local only: runscripts/bench.sh(criterion on i686, extra args
forwarded tocargo bench). The CI-only reporting scripts
(extract-bench/render-bench-entry/build-bench-comment/append-bench-history)
were dropped with it. - Docs: added a "Not affiliated" disclaimer to the READMEs (root + published
crates) and the docs home, making explicit that this is an independent fork
with no affiliation to SA-MP, open.mp, or the upstreamsamp-rsproject. - CI: the cross-only (aarch64) job now runs
clippy -D warningsinstead of a
barecargo check, so warnings that surface only on that arch — e.g.
dead_codefrom target-gated code — fail the build like on every other target. - Dependency maintenance since v3.3.1. Notable library bumps:
syn2 → 3
(samp-codegen),time0.3.53 → 0.3.54 (samp),quote→ 1.0.47,
proc-macro2→ 1.0.107,memcache0.19 → 0.20 (advancedexample), and
sentry0.48.5 → 0.49.0 (sink-demoexample — itsClientOptionsbecame
#[non_exhaustive], now built viaClientOptions::default()). Routine GitHub
Actions bumps as well (codeql-action/upload-sarif,dorny/paths-filter,
actions/checkout,actions/setup-python,ossf/scorecard-action,
softprops/action-gh-release,marocchino/sticky-pull-request-comment). None
of these touch the shipped public API.
Crate versions
rust-samp(libsamp): 3.2.0 → 3.3.0 (new#[event]/EventReturnsurface;
requiresrust-samp-sdk3.3.0).rust-samp-sdk(libsamp_sdk): 3.2.1 → 3.3.0 (newAmx::exec_public_scope;
buffer/stack hardening).rust-samp-codegen(libsamp_codegen): 1.3.0 → 1.4.0 (new#[event]macro,
includingrawmode).
Full Changelog: v3.3.1...v3.4.0