samp.directory
← Scripts y librerías

Fanorisky/pawn-x

Experiments and improvements to the Pawn compiler (open.mp / SA-MP): a compiler lab

Otro★ 2actualizado hace 15 díasSin licenciaPawn
Abrir en GitHub

Este repositorio no tiene archivo de licencia. Se puede leer, pero eso por sí solo no da permiso para reutilizarlo.

README.md

pawn-x

Native Pawn language features that replace YSI's script-side layer, for the open.mp / SA-MP Pawn compiler.

YSI implements varargs, iterators, and hooks with fragile script-side machinery (macros, #emit assembly, and runtime bytecode scanning/rewriting). pawn-x moves those into the compiler and a small companion plugin, so you get the same abilities (cheaper, deterministic, ~19× smaller output, and no bytecode surgery) as a standalone replacement (not a co-resident of YSI).

Two pillars, split by when the information exists:

  • Compiler, anything known at compile time: ___ varargs, the foreach keyword + compact-set natives, iterfunc generators, the hook keyword, and leaner switch codegen (a case a..b: range compiles to one bounds-check instead of one table record per value). Pure codegen, no new opcodes; the .amx runs on any AMX host.
  • Companion plugin (dynhook), the runtime-only piece: add / remove / replace hook handlers while the server runs, and transparently intercept built-in callbacks. Inline-hooks amx_Exec via subhook, no open.mp SDK, so it stays SA-MP compatible too.

Features vs YSI

pawn-xreplacesproven live
___ varargs forwardingy_vaexp 001, 005
foreach + set* natives (+ multi-dim)y_iterate / y_foreachexp 002/003/005
iterfunc generatorsy_iterate custom iteratorsexp 003
yield coroutine generators#define Iterator@N iteryield + yieldtests yield_*
async/await coroutinesy_async (abandoned sketch, never shipped)tests async_*
hook keyword (+ hook:N priority)y_hooks (compile-time)exp 004/005
hook native/function/stock + continue (call-site, incl. variadic ... targets)y_hooks real fn/native hookingtests chook_*
dynhook runtime hooks(YSI has no runtime equivalent)exp 006/007
compact switch codegen (range cases → bounds-check)(stock-Pawn table bloat)exp 013
inline closures + using inline/using public<sig> + Callback:y_inlinetests inline_*
hash()/ihash()/fnv1()/fnv1a() string hashing (compile-time + runtime, packed)y_stringhashtests stringhash_*
task/ptask Name[ms]() timers (compile-time auto-registration) + <timers> wrappersy_timerstests task_* / ptask_* / timers_lib

Most rows were run on a real omp-server and diffed against YSI. The yield, async/await, hook native/function/stock, and inline rows are proven by the yield_* / async_* / chook_* / inline_* compiler tests; async/await additionally ships a live open.mp host adapter (async_omp.inc, validated on open.mp 1.5.8). See experiments/*/RESULT.md.

Quickstart

#include <pawn-x>          // foreach + set* + iterfunc + hook + dynhook

new players[MAX_PLAYERS];

MyLog(const fmt[], ...)    { printf(fmt, ___); }          // varargs forwarding

hook OnPlayerConnect(playerid)                            // stack multiple hooks
{
    setadd(players, playerid);
    return HOOK_CONTINUE;
}

main()
{
    foreach (new id : players) MyLog("online: %d", id);   // native iteration
}

Do not include YSI's y_va / y_iterate / y_hooks in the same script. pawn-x reserves foreach and hook as keywords and the includes error out if YSI is detected. Coming from YSI? See docs/MIGRATION.md for the full API mapping.

Feature reference

Varargs (___). Forward a variadic tail into any function/native:

Log(const fmt[], ...)              { printf(fmt, ___); }        // forward all
After(a, b, const fmt[], ...)      { printf(fmt, ___); }        // after fixed args
Build(dst[], size, const f[], ...) { format(dst, size, f, ___); }
Fmt(const fmt[], ...)              { new s[96]; format(s, sizeof s, fmt, ___); return s; }

Iteration (foreach + compact-set natives). A plain array is a sorted set:

new s[64];
setadd(s, 7); setremove(s, 3); sethas(s, 7); setlen(s);
setfree(s); setrandom(s); setget(s, 0); setalloc(s);
foreach (new v : s) { }                 // ascending
foreach (new v : Reverse(s)) { }        // descending; safe to setremove(v) inside
new grid[MAX_VEH][CAP];                 // multi-dimensional = plain 2D array
foreach (new p : grid[vid]) { }

Generators (iterfunc). Lazy, zero-alloc; <iterators> ships Range/RangeStep/Powers/Fib:

iterfunc stock Count(cur, lo, hi) { if (cur==ITER_STOP) return lo<hi?lo:ITER_STOP; return cur+1<hi?cur+1:ITER_STOP; }
foreach (new i : Count(0, 10)) { }
iterfunc stock Fib(&acc, cur, lim) { ... }   // leading &ref = persistent state (Fibonacci etc.)

Coroutine generators (yield). Write the sequence straight-line; each yield return suspends and resumes on the next step (no #define Iterator@N iteryield needed, unlike YSI):

iterfunc Count(n) { for (new i = 0; i != n; ++i) yield return i; }   // scalar locals only
foreach (new v : Count(3)) { }               // v = 0, 1, 2; `return;` ends the sequence

Native async/await (#include <async>). Write sequential code over callback-style ops; the function suspends at each await and resumes when the op completes. Single-threaded (a coroutine transform, not parallelism), single-.amx. Scalar and array/string/multi-dim locals survive an await, they are lifted into the coroutine's own state block, natively, no plugin. YSI only ever sketched y_async. pawn-x is the first to actually implement it:

#include <async>

async GetScore(playerid)
{
    new base = 100;
    new tag[16] = "player";                   // array/string locals survive too
    new s = await AddScore(playerid, base);   // suspend; resumes with the result
    printf("%s: score=%d\n", tag, s + base);  // 'base' and 'tag' both intact
}

main()
{
    new t = Async_Start(GetScore, 7);         // starts; suspends at the await
    Async_Resume(t, AddScore(7, 100));        // a pump/host completion resumes it
}

await asyncFn(args) composes (the inner return resumes the awaiter). Completion is driven by Async_Resume(token, value): a synthetic pump in tests, the shipped async_omp.inc timer/callback adapter on a live host. Combinators are native: Async_All(n)/Async_Any(n) + await Async_Wait(g) fan several operations into one awaiter (task_all/task_any parity), driven by the Async_GateFeed seam. Faults are native too: Async_ResumeError/Async_GateFail report failure, observed via Async_Failed()/Async_Error() after the await; Async_Fail(err) + return auto-raises up a composed await asyncFn() chain (task_set_error parity). A real host adapter ships: async_omp.inc gives await Async_Ms(ms) on open.mp SetTimerEx, validated on a live open.mp 1.5.8 server (real timers resume coroutines off the tick loop, arrays/scalars survive, combinators + faults work, arena returns to baseline; see experiments/012-native-async/HOST-VALIDATION.md). Supported across an await: scalar and array/string/multi-dim locals; fixed-size array/string parameters (copied into the coroutine block, foo(buf[4])); a leaf await in a for/while/do loop; mid-expression await, leaf or composed (p + await F(), base + await Work()); a composed await asyncFn() inside a loop; a leaf await as an argument to a fixed-arity call (foo(await F(), p, q), any position); and multiple awaits in one statement (await A() + await B()). Multi-result delivery via Async_ResumeArr/Async_InboxArr (await_arr parity). Task management rounds out the PawnPlus parity: Async_Keep/Async_Result/Async_Release retain a completed task's return value for later reading (task_keep); Async_Cancel (cooperative, fault-notified via Async_Cancelled()) and Async_Kill (hard) tear a task down (task_delete); Async_Bind/Async_Detach fire a completion callback (task_bind); Async_Timeout/Async_TimeoutGate fault a task or a gate after a delay (task_set_error_ms); and an unobserved leaf fault now auto-raises to its awaiter instead of dropping silently. Still compile-rejected (error): unsized/multi-dim/&reference params (268), await inside a foreach (099), a leaf await inside a variadic call's argument list (099, no safe spill bound; hoist to a statement), a composed-then-leaf pair in one expression (099, reorder or split), and a suspend at arbitrary call-stack depth in a non-async helper. See docs/MIGRATION.md for the y_async mapping and the PawnPlus comparison.

Native inline closures (inline / using inline). A function defined inside another function that closes over the enclosing locals (read and write), passed to a receiver as a Callback: value and called indirectly. pawn-x's native, plugin-free y_inline:

ForEach(const arr[], size, Callback:cb) { for (new i; i < size; i++) cb(arr[i]); }
CountFives(const arr[], size) {
    new count = 0;
    inline IsFive(v) { if (v == 5) count++; }    // captures + mutates `count`
    ForEach(arr, size, using inline IsFive);
    return count;                                 // reflects the inline's writes
}

The closure captures via a static link (the enclosing frame), so mutations write straight back, no Callback_Restore. Scalars, arrays/strings, multiple independent closures, and inline return values are supported; inline const makes the captured locals read-only (compile error on write); using public Name<sig> passes a plain public as a Callback: too. This is the synchronous/visitor case async does not cover; deferred callbacks that outlive the frame use async/await. Capturing a by-&reference parameter is rejected (error 098). See tests inline_*.

Entity iterators. Ready-made connected-players / tracked-vehicle sets:

#include <players>                          // Player: auto-tracked via connect/disconnect hooks
foreach (new id : Player) { }
#include <vehicles>                          // Vehicle: tracked via Vehicle_Create/Vehicle_Destroy
new v = Vehicle_Create(model, x,y,z, a, c1,c2, respawn);
foreach (new id : Vehicle) { }               // (<actors> is the same pattern)

Hooks (hook). Many handlers per callback, source order, optional priority + default return:

hook OnFoo(a)     { ...; return HOOK_CONTINUE; }   // 1 run next / 0 = HOOK_CONTINUE_0
hook:100 OnFoo(a) { ...; return HOOK_STOP;     }   // higher priority first; -1 cancels, returns 0
hook default OnPlayerCommandText = 0;              // fall-through default (like YSI HOOK_RET)

Call-site hooks (hook native/function/stock + continue). Intercept every in-script call to a real native or pawn function/stock (single-.amx; fixed-arity or variadic ... targets):

hook function ComputeScore(p) { return continue(p) + 1; }  // continue = next hook, else the original
hook native random(range)     { return continue(range) % 8; }
hook function Sum(base, ...)  { return continue(base, ___); } // variadic: forward the tail with ___ (or bare continue())
// continue: 0×=replace, 1×=pass-through, N×=call original N times; args forwardable.
// In a variadic body numargs()/getarg(n)/setarg(n) use the user index; the hidden chain index is invisible.
// Forward references (a call before the hook) are redirected too.

Runtime hooks (dynhook, needs the companion plugin):

dynhook_intercept("OnPlayerDeath");                 // fire the chain automatically
dynhook_add("OnPlayerDeath", "MyHandler");          // add / remove / replace at runtime
dynhook_call("MyCustomEvent", "is", id, "hi");      // or dispatch a custom event

Compact switch codegen. Stock Pawn expands a case a..b: range into one (value, address) table record per value, so case 0..9999: alone becomes 10 000 records (a ~31 KB .amx). pawn-x coalesces each range into a single inline bounds-check and only sends the leftover discrete values through the OP_SWITCH table, so the same switch is 119 bytes (≈267× smaller). Pure codegen with existing opcodes, so the .amx still runs on any AMX host, and all existing semantics (default, duplicate/overlap error 040, enum exhaustiveness) are preserved:

new state = Classify(x);
switch (state)
{
    case 0..9999:      Big();       // one bounds-check, not 10 000 table records
    case -1000..-1:    Negative();  // signed ranges work
    case 40000:        Lone();      // discrete values still use the lean table
    default:           Other();
}

Nothing changes in how you write switch, only the emitted code shrinks. See experiments/013-switch-codegen/RESULT.md (measured + validated on a live open.mp 1.5.8 server).

Native string hashing (hash()). Compile-time string hashing, the native replacement for YSI y_stringhash. hash("literal") folds to a compile-time constant (usable anywhere a constant is, above all in switch/case); hash(expr) on a runtime string lowers to a plugin-free djb2 stock. Both use the same forward djb2 (h = h*33 + c, 32-bit wrap), so a folded case label and a runtime switch value agree:

#include <hash>
switch (hash(params))                     // runtime hash of a variable
{
    case hash("gun"):    GiveGun(playerid);      // folded to a constant
    case hash("health"): GiveHealth(playerid);
}

Unlike YSI's _H<> macro (restricted to a-z A-Z 0-9 _ and space, and paid for with expensive recursive preprocessing), the native fold accepts any byte and costs one pass; the fold itself needs no include. Full YSI parity: ihash() (case-insensitive djb2, for command dispatch), fnv1() / fnv1a() (32-bit FNV variants), and packed strings (both the folded literal and the runtime stock, via ispacked). hash (and the other three) stays usable as an ordinary identifier everywhere except call position. See tests stringhash_*.

Timers: native task + <timers> wrappers. The native task keyword is a repeating timer that auto-registers at compile time (no runtime public-table scan, the fragile core of YSI y_timers):

task Announce[60000]() { SendClientMessageToAll(-1, "still up"); }   // that's it

The body is hoisted to a hidden public and the compiler synthesises its SetTimer registration, chained onto OnGameModeInit/OnFilterScriptInit via the hook machinery. Alongside it, <timers> gives thin wrappers over the host natives for the on-demand cases: Timer_Repeat / Timer_Once (+ *Ex arg-forwarding variants built on ___ varargs) and Timer_Stop, with tagged Timer: handles. Deferred/stateful work (keep locals alive across the wait) belongs to async/await (await Async_Ms(ms)). ptask Name[ms](playerid) (needs #include <ptask>) is the per-player variant: it fires the body for every connected player each interval (a synthesised dispatcher loops the <players> set through a Callback: indirect call). Arbitrary timer arguments are not a task/ptask feature by design (an auto-registered timer has no value to pass); that is what Timer_RepeatEx/async are for. See tests task_* / ptask_* / timers_lib. See tests task_basic / task_native / timers_lib.

Build

# 1. compiler (pawncc / pawnruns)
cmake -S compiler/source/compiler -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-m32"
cmake --build build -j$(nproc)

# 2. dynhook plugin (runtime hooks): needs subhook (upstream Zeex/subhook is
#    gone; clone the Dasharo/subhook or tianocore/edk2-subhook mirror to deps/subhook)
gcc -m32 -fPIC -DSUBHOOK_STATIC -c deps/subhook/subhook.c -o /tmp/subhook.o
g++ -m32 -shared -fPIC -DLINUX -DSUBHOOK_STATIC -Icompiler/source/amx \
  -Icompiler/source/linux -Ideps/subhook \
  experiments/006-companion-plugin/dynhook.cpp /tmp/subhook.o -o dynhook.so

The set* operations are pure-Pawn stocks in <foreach>, so iteration needs no plugin. Compile a script: build/pawncc gm.pwn -icompiler/include -i<stdlib>. Run the test suite: tools/run-tests.sh -r build/pawnruns build. Deploy the .so plugins to the server's plugins/ and list them under pawn.legacy_plugins in config.json (open.mp) / plugins (SA-MP).

Layout

PathContents
compiler/Vendored Pawn compiler (modified) + compiler/include/ pawn-x includes
experiments/Each feature: spec, tests, and a RESULT.md with live proof
ysi-5/Vendored YSI 5 (reference for what is replaced)
deps/, openmp/Build deps and server package (gitignored)
docs/Coding standards, test baseline, design specs

See docs/CODING_STANDARDS.md before contributing.

License

Vendored compiler © ITB CompuPhase 1997-2006, community-modified (compiler/license.txt). subhook © Zeex (BSD). YSI is MPL 1.1, see CREDITS.md.

Esta página se genera con lo que GitHub publica sobre el repositorio. samp.directory no aloja el código. Informar de un problema con esta ficha

Fanorisky/pawn-x: Otro de SA-MP | samp.directory