[Tutorial] State-based hooks (aka pre-hooks)

Started by SA:MP, May 05, 2023, 05:48 AM

Previous topic - Next topic

0 Members and 4 Guests are viewing this topic.

SA:MP

[Tutorial] State-based hooks (aka pre-hooks)

This is a hook method I came up with long ago (before hook method 7, i.e. modern ALS). It uses states to determine if a hook exists or not.  This was previously documented, but that documentation is both deleted and obsolete - the technique is the same, but the implementation has evolved a little bit.




PHP Code:




#include <a_samp>


/*

First define several states for "hooked" and "unhooked".  These functions are

purely to define states, they are never used.  The states only need to be

defined once per mode, and both fixes.inc and YSI already define them.  The

function name is unimportant, but the state specifiers must be exactly as below:

*/


static stock Library_IncludeStates() <_ALS _ALS_x0_ALS _ALS_x1_ALS _ALS_x2_ALS _ALS_x3>

{

}


static 
stock Library_IncludeStates() <_ALS _ALS_go>

{

}


/*

Here is our first hooked function, it is always a good idea to hook a ScriptInit

function so you can set "_ALS_" state, but you can also do that in any other

callback, this is just merely a small optimisation.

*/

public OnGameModeInit()

{

    
/*

    Set the `_ALS_` state to `_ALS_go`.  The basic idea is that this state is

    ALWAYS `_ALS_go`, if the next callback in a callback chain exists it will

    have an implementation defined for the `_ALS_go` state; if there is no next

    callback, only the fallback will exist.  This should be done before the

    first chain call - it only NEEDs to be done once but multiple times is fine,

    and multiple libraries can (and do) do this.

    */

    
state _ALS _ALS_go;


    
/*

    Now call the next callback in the chain.  With the normal ALS method this

    requires a pre-processor `#if` directive to protect against the case where

    the next item in the chain doesn't exist.  With this version, the next

    chain callback always exists in some form even if the user didn't define it.

    */

    
return Library_OnGameModeInit();

}


/*

Forward the next callback in the chain.

*/

forward Library_OnGameModeInit();


/*

Normal ALS redefinition checks.

*/

#if defined _ALS_OnGameModeInit

    #undef OnGameModeInit

#else

    #define _ALS_OnGameModeInit

#endif


/*

Now this is where it starts getting interesting.  Because we unconditionally

call `Library_OnGameModeInit`, the code MUST include this function, but because

we are hooking it, we don't know if it will or not - so we create it!  But if we

create it, then what's in the next library (or mode) in the chain?  The answer

is they are BOTH the next function in the chain thanks to the magic of states.


The first line here defines `OnGameModeInit` when `_ALS` is set to two of the

other values - this is actually NEVER the case, but is important as it tells the

compiler which automata our `Library_OnGameModeInit` function is controlled by

(i.e. `_ALS`).  The second line is the "fallback" function - if `_ALS` is in a

state for which there is no specific implementation, this one will get called

instead and just instantly return (maybe you can now see the trick).  If `_ALS`

is set to `_ALS_go` and there is no other function, then the compiler will

identify this fallback function as the correct one to call (or rather the

runtime will).  If the hooked function DOES exist, then that more specialised

version will be called instead.


If we didn't have the first function to define states, then the fallback

wouldn't compile.

*/

public Library_OnGameModeInit() <_ALS _ALS_x0_ALS _ALS_x1> { return 1; }

public 
Library_OnGameModeInit() <> { return 1; }


/*

Because we are now using states, we need a slightly more complex redefinition of

the next callback's function definition - we need to transparently add the

`_ALS : _ALS_go` state to it, so that's exactly what this line does.


Remember that `_ALS` only needs to be defined once in a mode, and `Library_` is

just the prefix I've used in this example - it needs to be unique.

*/

#define OnGameModeInit(%0) Library_OnGameModeInit(%0) <_ALS : _ALS_go> 







PHP Code:




/*

This is just another `OnGameModeInit` that will be correctly called if it

exists.  It shouldn't be part of your library (i.e. this is user code).

*/


public OnGameModeInit()

{

    
printf("Hello");

    return 
1;








y_hooks are simpler to write, and faster to run.  Normal ALS hooks are slightly faster, but lack two properties.  Because this version uses states the next item in the chain always exists, and the entry point to the chain function is in a known format and location in the AMX.  These properties together allow us to detect and intercept these calls at run-time.  And so introduces "pre-hooks".  The standard call order for hooks is as follows:

  1. y_hooks, in include order.

  2. ALS hooks, in include order.

  3. User code.


However, there are some libraries (notably fixes.inc) which must be called before y_hooks hooks, but can't rely on y_hooks to use `hook` and thus come first.  For these, we have pre-hooks, defined by these state hooks, and natively supported by y_hooks (which leads to the interesting property that including y_hooks actually optimises these functions.  The new order thus becomes:

  1. Pre-hooks.

  2. y_hooks, in include order.

  3. ALS hooks, in include order.

  4. User code.


Pre-hooks are called in include order, but y_hooks can not determine which is the last one, so we need to explicitly inform it of the order:




PHP Code:




CHAIN_ORDER(Library);

#undef CHAIN_ORDER

#define CHAIN_ORDER CHAIN_NEXT(Library) 






This assumes you have YSI (and thus y_prehook, part of YSI_Core).  Otherwise, check the file for the macro definitions:



https://github.com/pawn-lang/YSI-Inc.../y_prehook.inc



It also defines macros to simplify the forwarding, and add custom return values:




PHP Code:




/*

Normal ALS definitions.

*/

#if defined _ALS_OnGameModeInit

    #undef OnGameModeInit

#else

    #define _ALS_OnGameModeInit

#endif


/*

All that is required for forwarding.  The `= 1` is to define the default return

value, so in `OnPlayerCommandText` this would be `= 0`.

*/

CHAIN_FORWARD:Library_OnGameModeInit() = 1;


/*

Now the redefinition.

*/

#define OnGameModeInit(%0) CHAIN_PUBLIC:Library_OnGameModeInit(%0) 






Again, check y_prehook for more information.  These macros are also mostly in fixes.inc, but currently misnamed.

Source: [Tutorial] State-based hooks (aka pre-hooks)