[Tutorial] PAWN Pre-Processor - Pre-processor alternatives - Part 5/7
#1

Contents

Part 1 - Covers an introduction to the pre-processor and covers some important things when writing function-like macros.
Part 2 - Explains exactly what the compiler searches for and looks at some common macro uses.
Part 3 - Describes the other directives available (beside "#define") and looks at definitions with no replacement value.
Part 4 - How to use strings with the pre-processor.
Part 5 - Alternatives to the pre-processor, multiple symbols and recursion.
Part 6 - Case Study (y_remote).
Part 7 - Macro issues and spaces.

Additional

Utilising tagof - By g_aSlice.
Future-proof string concatenation
String bug
Macros that do multiple things
Advanced "tag" macros

Not Using The Pre-Processor

This is basically just a list of alternatives to the pre-processor, which can be used in certain circumstances.
  • const
"const" is like "new" - it declares a variable, the difference being that the variable is "constant" - you can't change it. This may sound useless, but it's very similar to pre-processor definitions:

Code:
#define MY_NUM (27)

main()
{
    printf("%d", MY_NUM);
}
Is functionaly equivalent to:

Code:
const MY_NUM = 27;

main()
{
    printf("%d", MY_NUM);
}
There are important differences however. Firstly definitions can be anything - including code, constants can only be anything you can assign to a variable. Using constants allows you to use the compiler to find mistakes:

Code:
#define MY_NUM if (a)

main()
{
    printf("%d", MY_NUM);
}
Verses:

Code:
const MY_NUM = if (a);

main()
{
    printf("%d", MY_NUM);
}
The first version will give an error where the symbol is used, because that's where the code is inserted. People looking at the line may get a syntax error on the line, despite the fact that the line itself looks fine. The second version will get a syntax error at the definition of "MY_NUM" - which is a far more useful error for debugging.

const variables are also normal variables, so you can use them as arrays. The following code will not work:

Code:
#define MY_ARRAY {0, 1, 2, 3}

main()
{
    printf("%d", MY_ARRAY[2]);
}
This equivalent code will work - and here using "const" instead of "new" for the array is better for compilation optimisations:

Code:
const MY_ARRAY[] = {0, 1, 2, 3};

main()
{
    printf("%d", MY_ARRAY[2]);
}
This is vrey useful in functions with lookup tables instead of using "new", which will redelare the array every time the function is called:

The final advantage of "const" over defines is that they are scoped like regular variables - you can have local and global const variables, unlike defines which are available any time in code below where they are defined. "const" is done by the normal compiler, so knows about braces and scoping, the pre-processor is by definition done before (pre-) the main processor, thus has no knowledge of functions.
  • sizeof
This is similar to const in that it is generally compiled to a single number, rather than a function call or variable lookup. "sizeof" returns the size of an array, and as in PAWN all arrays must be defined at compile time, "sizeof" is also a constant at compile time. Using this in combination with compiler determined array sizes makes it easy to quickly change data:

Code:
new
    array[] = {1, 2, 3, 4};

printf("Elements: %d", sizeof (array));
In the code above, the "[]" syntax after the array declaration means that the compiler determines the size of the array, rather than having you set it explicitly. This is done by counting the number of elements (here 4) and setting it to that value. If you want more than that, you will need to set the size yourself, but "sizeof" can still be used in that case.

Because this is not part of the pre-processor, the following code (see part 4) will not work:

Code:
new
    array[] = {1, 2, 3, 4};

printf("Elements: " #sizeof (array));
In fact it will give an error as it will compile as:

Code:
new
    array[] = {1, 2, 3, 4};

printf("Elements: sizeof (array"));
With TWO trailing close brackets, and the text "sizeof" actually printed instead of the size.

Maths

Some of this has already been covered, but this is just some more useful information

The compiler will execute as much maths as it can at compile time, saving run time execution. This only applies to constant maths, for example:

Code:
printf("%d", 4 * 5);
Will be compiled as:

Code:
printf("%d", 20);
Saving run time execution. This can save you effort trying to work things out manually. For example the following macros can be used to simplify writing timer delays:

Code:
#define MS_TO_MS(%0) (%0)                  // Milliseconds to milliseconds
#define S_TO_MS(%0)  (MS_TO_MS(%0) * 1000) // Seconds to milliseconds
#define M_TO_MS(%0)  (S_TO_MS(%0)  * 60)   // Minutes to milliseconds
#define H_TO_MS(%0)  (M_TO_MS(%0)  * 60)   // Hours to milliseconds
Then doing:

Code:
SetTimer("MyFunc", M_TO_MS(5), 1);
Will cause your timer to be called once every 5 minutes, without you ever needing to know that that is 300,000 milliseconds. You may wonder why I included a macro to convert milliseconds to milliseconds, this is just what's called a "helper macro" - it serves no real purpose but makes code nicer and more consistent. If you decided to change from 5 minutes to 5 milliseconds then converting your code would be much simpler.

An alternate macro you may like to consider is:

Code:
#define ms
#define secs *1000 ms
#define mins *60 secs
This way you can do:

Code:
SetTimer("MyFunc", 5 mins, 1);
SetTimer("MyFunc", 7 secs, 1);
SetTimer("MyFunc", 28 ms, 1);
This is an example of "syntactic sugar" - making your code look more like real writing, or making it look easier to read, by introducing new macros which look like normal code. You just have to be careful of operator order in this code, but multiply has a very high precedence, so that shouldn't really be a problem.

Multiple Symbols

Because the pre-processor and the main compiler are separate, you can actually have multiple symbols with the same name. One defined by the pre-processor, one defined by the processor:

Code:
MyFunc()
{
    return 11;
}

#define MyFunc() 10

printf("%d", MyFunc());
That code will print the number 10, not the number 11, but will also not give an error about "MyFunc" already being defined. Note that the other way round will not work:

Code:
#define MyFunc() 10

MyFunc()
{
    return 11;
}

printf("%d", MyFunc());
That will try to define a function called "10", because the "MyFunc" in the function name will be replaced by the pre-processor. In the first version the function declaration is BEFORE the pre-processor definition, so is not replaced - this is a VERY important issue to note - definitions ONLY replace text that comes AFTER they are defined.
  • Match Failure
The pre-processor will only replace something if it correctly pattern matches it. This means if you do:

Code:
#define MOO(%0,%1) %0+%1

printf("%d", MOO(4, 5));
printf("%d", MOO(6));
You will get:

Code:
printf("%d", 4+ 5);
printf("%d", MOO(6));
The "MOO" pattern is looking for: "MOO" followed by "(", followed by anything, followed by ",", followed by anything again, followed by ")". The first use had this, the two "anythings" being "4" and " 5" (note the included space). The second use did not have this - the comma was not found. Note that the following is also valid:

Code:
printf("%d", MOO(4, 5, 6));
In this case the first "anything" ("%0") is "4" and the second ("%1") is " 5, 6". The first parameter is anything up to the first comma, the second is anything up to the first close bracket after the first comma - this includes the second comma.

Be warned, this IS valid code:

Code:
printf("%d %d", MOO(6), 7);
And will generate:

Code:
printf("%d %d", 6)+ 7;
The examples above prove that the given patterns are very important, and mean that you can in some cases access both the define and the symbol when you have two versions of a symbol - if the pattern doesn't match it will use the main symbol:

Code:
stock MOO(a)
{
    return 7 + a;
}

#define MOO(%0,%1) %0+%1

printf("%d", MOO(4, 5));
printf("%d", MOO(6));
The output of that code will be:

Code:
9
13
The first line coming from the pre-processor match generating "4+ 5", the second coming from the pre-processor failing to match the full pattern and instead using the function called "MOO", passing "6".
  • Trailing Commas
One pattern I use a lot, designed to detect and remove trailing commas, is:

Code:
#define _:%0,) _:%0)
First it should be noted that "anything" includes "nothing" - if, in the examples above, we had done:

Code:
#define MOO(%0,%1) %0+%1

printf("%d", MOO(,));
That would have correctly matched to give:

Code:
printf("%d", +);
Clearly generating invalid code, but the important thing is that what came before the first comma was nothing, so "%0" was empty.

Imagine the following code:

Code:
#define MY_FUNC<%0>(%1) my_func_%0(param, %1)

MY_FUNC<hello>(playerid)
{
    printf("%d %d", param, playerid);
}
This will generate:

Code:
my_func_hello(param, playerid)
{
    printf("%d %d", param, playerid);
}
All well and good, but if we don't want any parameters:

Code:
#define MY_FUNC<%0>(%1) my_func_%0(param, %1)

MY_FUNC<hello>()
{
    printf("%d", param);
}
We will end up with a trailing comma:

Code:
my_func_hello(param, )
{
    printf("%d", param);
}
This is not a valid function definition and will give an error - we need to somehow only put the comma in when it's needed, i.e. when there are other parameters. Or, to put it another way, we need to remove it when there are no other parameters:

Code:
#define _:%0,) _:%0)

#define MY_FUNC<%0>(%1) my_func_%0(_:param, %1)

MY_FUNC<first>(playerid)
{
    printf("%d %d", param, playerid);
}

MY_FUNC<second>()
{
    printf("%d", param);
}
This will generate:

Code:
#define _:%0,) _:%0)

my_func_first(_:param, playerid)
{
    printf("%d %d", param, playerid);
}

my_func_second(_:param, )
{
    printf("%d", param);
}
This time is a little different. "_:" is the tag for normal variables (like "Float:", but you don't need to use it most of the time - however if you do it doesn't matter). For "my_func_first", "param" is declared using the "_:" tag. For "my_func_second", "param" is again declared using the "_:" tag, but this time it ALSO matches the definition called "_"; the pattern of which was "_:", followed by something, followed by a comma, followed by a close bracket (the spacing between the comma and close bracket is not important as they are operators, not text). This results in the match doing a replace to get:

Code:
my_func_first(_:param, playerid)
{
    printf("%d %d", param, playerid);
}

my_func_second(_:param)
{
    printf("%d", param);
}
We have sucessfully removed the trailing comma in the case which needed it! Note that the tag is very important here - you could define a version for all other tags too if you liked, I've just never needed one as all the variables I needed to use it on were normal tagged variables.

There is an equivalent definition I use for string generation:

Code:
#define x,) x)

#define CALL_FUNC:%0[%1](%2) CallLocalFunction(#%0,#%1#x,%2)

CALL_FUNC:OnPlayerConnect[i](playerid);
CALL_FUNC:OnGameModeInit[]();
This will generate:

Code:
#define x,) x)

CallLocalFunction(#OnPlayerConnect,#i#x,playerid);
CallLocalFunction(#OnGameModeInit,##x,);
The "x" define will then kick in:

Code:
CallLocalFunction(#OnPlayerConnect,#i#x,playerid);
CallLocalFunction(#OnGameModeInit,##x);
That will finally compile as:

Code:
CallLocalFunction("OnPlayerConnect","ix",playerid);
CallLocalFunction("OnGameModeInit","x");
"x" is not a valid variable type in "CallLocalFunction" (it means "hex" in "sscanf", but not here), so both bits of code will pass the correct number of parameters - invalid specifiers are ignored. The "##x" above means: convert nothing to a string and append it to a string of "x" - resulting in an invalid specifier type which is ignored.

Now although "x" is a common variable letter (used, for example, for co-ordinates), this define will not mess up most code unless that code is wrong in the first place as it only matches an x followed by a trailing comma. This is unlikely to happen, and even if it does, this define will actually FIX your code - as will the "_" define above.
  • ALS
This technique is what the Advanced Library System (ALS) is based on. Because you can have multiple symbols with the same name, you can change the names of things:

Code:
public OnPlayerConnect(playerid)
{
}

#define OnPlayerConnect Me_OnPlayerConnect

public OnPlayerConnect(playerid)
{
}
In the code above the function "OnPlayerConnect" appears twice - if this were normal code the compiler would give an error because the symbol is already defined, but this is not normal code! Instead, the pre-processor renames the second function definition (the only one which comes AFTER the definition):

Code:
public OnPlayerConnect(playerid)
{
}

public Me_OnPlayerConnect(playerid)
{
}
Now we only have one version of each function. Using this technique, libraries can hook (in this example) "OnPlayerConnect", with the callback in the main gamemode being renamed to "Me_OnPlayerConnect", and leaving the user of the library none the wiser. ALS actually goes a bit further than this. In all the following snippets, the second "OnPlayerConnect" is ommitted, only the code which appears in the library is given:
  • First you need to actually call the function in the main gamemode manually, as it is no longer "OnPlayerConnect", and, because it's public, forward it:

    Code:
    public OnPlayerConnect(playerid)
    {
        Me_OnPlayerConnect(playerid);
    }
    
    forward Me_OnPlayerConnect(playerid);
    
    #define OnPlayerConnect Me_OnPlayerConnect
  • Secondly, the user might actually delete their OnPlayerConnect (as they are prefectly entitled to do), because they do not need it. In this case the above code will give an error that "Me_OnPlayerConnect" is not defined, so we need to only call it if it exists:

    Code:
    public OnPlayerConnect(playerid)
    {
        CallLocalFunction("Me_OnPlayerConnect", "i", playerid);
    }
    
    forward Me_OnPlayerConnect(playerid);
    
    #define OnPlayerConnect Me_OnPlayerConnect
  • What happens if multiple libraries hook OnPlayerConnect? You will end up with the following code:

    Code:
    public OnPlayerConnect(playerid)
    {
        CallLocalFunction("Me_OnPlayerConnect", "i", playerid);
    }
    
    forward Me_OnPlayerConnect(playerid);
    
    #define OnPlayerConnect Me_OnPlayerConnect
    Code:
    public OnPlayerConnect(playerid)
    {
        CallLocalFunction("You_OnPlayerConnect", "i", playerid);
    }
    
    forward You_OnPlayerConnect(playerid);
    
    #define OnPlayerConnect You_OnPlayerConnect
    That is valid right up till the last line. The second OnPlayerConnect is renamed "Me_OnPlayerConnect" and called, this in turn then calls the third hook, called "You_OnPlayerConnect". However the pre-processor definition called "OnPlayerConnect" already exists (you can have two, not three), so the last line gives an error. We need to first undefine the symbol:

    Code:
    public OnPlayerConnect(playerid)
    {
        CallLocalFunction("Me_OnPlayerConnect", "i", playerid);
    }
    
    forward Me_OnPlayerConnect(playerid);
    
    #undef OnPlayerConnect
    #define OnPlayerConnect Me_OnPlayerConnect
  • The code above undefines OnPlayerConnect on the first libaray as well - there is no way for a library to know if it is the first or second library included, so it doesn't know if it needs to undefine the symbol or not. However, because for the first library, the PRE-PROCESSOR SYMBOL called "OnPlayerConnect" has not yet been defined, that code will attempt to undefine the FUNCTION called "OnPlayerConnect". This can't be done so will give an error. To fix this we use the following code which ONLY undefines "OnPlayerConnect" if another library exists. If there isn't another library (yet) we mark that there is another library which uses OnPlayerConnect:

    Code:
    public OnPlayerConnect(playerid)
    {
        CallLocalFunction("Me_OnPlayerConnect", "i", playerid);
    }
    
    forward Me_OnPlayerConnect(playerid);
    
    #if defined _ALS_OnPlayerConnect
        #undef OnPlayerConnect
    #else
        #define _ALS_OnPlayerConnect
    #endif
    #define OnPlayerConnect Me_OnPlayerConnect
You can use the above code in as many libraries as you like. Just make sure that you ALWAYS use the prefix "_ALS_" to define that callbacks are used - if you don't your code won't work with other libraries that do. You also need to ensure that the prefix you use instead of "Me_" in the code above is unique.

Note that "_ALS_OnVehicleDamageStatusUpdate", "_ALS_OnPlayerEnterRaceCheckpoint" and "_ALS_OnPlayerLeaveRaceCheckpoint" are too long to be valid symbol names (max 31), so I use (and suggest you use for compatibility): "_ALS_OnVehicleDamageStatusUpd", "_ALS_OnPlayerEnterRaceCP" and "_ALS_OnPlayerEnterRaceCP".

Recursion

Provided you have an end point, macros can call themselves (note that the term "call" here is a bit of a misnomer). The following code will simply hang the compiler:

Code:
#define A 7+A

printf("%d", A);
That will generate:

Code:
#define A 7+A

printf("%d", 7+A);
Which will generate:

Code:
#define A 7+A

printf("%d", 7+7+A);
And so on:

Code:
#define A 7+A

printf("%d", 7+7+7+7+7+7+7+7+7+7+7+7+...
And will hence crash the compiler. It is however possible to write macros which call themselves a limited number of times, often defined by the parameters passed.
  • String Hashing
The y_stringhash library, part of the new YSI framework, allows you to convert strings to numbers at compile time. As you should know, numbers are easier to compare to each other than strings, so having strings converted to (hopefully) unique numbers can be useful. As an example, let's imagine that we have a very small alphabet of just "ABC". The following words can all be written:

Code:
BAA
CAB
ABB
Now imagine a very simple hash algorithm, where A=1, B=2 and C=3 and the values are added (don't use this in real life because "AB" and "C" will both be 3, which is called a "collision" - two inputs that give the same output). For our example, the hashes are:

Code:
BAA = 4
CAB = 6
ABB = 5
Note that there are very few combinations in this example which do not give the same result as another combination, this is because it is a very weak hash algorithm. Cryptographic hashes are very complex algorithms which VERY rarely give the same output twice and are used to protect passwords so that someone entering the wrong password won't be allowed access while still hiding the real password from prying eyes. Less secure hashes, I personally use Bernstein, are used for the purposes covered here of comparing insecure strings (as opposed to passwords which are secure).

Now, to hash this string at compile time requires some clever macros. I'll give them first and explain them second:

Code:
#define HASH(%0) _HASH(%0,@,@)

#define _HASH(%0,%1) _DO_HASH_%0(%1)

#define _DO_HASH_A(%1) _HASH(%1)+1
#define _DO_HASH_B(%1) _HASH(%1)+2
#define _DO_HASH_C(%1) _HASH(%1)+3
#define _DO_HASH_@(%1) 0

printf("The hash of BAA is: " #HASH(B,A,A));
This may not look like the recursive macro we had before, none of those call themselves, but "_HASH" calls "_DO_HASH_%0", which then calls "_HASH" again - thus we have a loop. Now these macros need a LOT of understanding of exactly what it is that the pre-processor looks for when matching patterns:
  • Code:
    #HASH(B,A,A)
    That is the entry point, and will call the "HASH" macro with "B,A,A" in "%0".
  • Code:
    #define HASH(%0) _HASH(%0,@,@)
    That is the definition of "HASH", for our example this will call "_HASH" with "B,A,A,@,@" - the contents of "%0" plus two more characters (which I'll come to).
  • Code:
    #define _HASH(%0,%1) _DO_HASH_%0(%1)
    This macro expects something followed by a comma, followed by more stuff, followed by a close bracket. This macro was called with "B,A,A,@,@" - the thing before the first found comma is "B", so "%0" is "B", everything else ("A,A,@,@") comes after the first comma and before the close bracket so are stored in "%1".

    This macro then combines the contents of "%0" with the string "_DO_HASH_" to generate a new macro name, in this case "_DO_HASH_B"
  • Code:
    #define _DO_HASH_B(%1) _HASH(%1)+2
    This is the "_DO_HASH_B" macro, after this is run, our generated code will look like:

    Code:
    printf("The hash of BAA is: " #_HASH(A,A,@,@)+2);
  • The above steps are repeated, this time calling "_HASH" with "A,A,@,@", putting "A" in "%0" and "A,@,@" in "%1". After two more calls the code looks like:

    Code:
    printf("The hash of BAA is: " #_HASH(@,@)+1+1+2);
  • This time is a little different. Now when "_HASH" is called "%0" will be "@" and "%1" will also be "@" (though that no longer matters). The macro generated this time is "_DO_HASH_@", the code for which is:

    Code:
    #define _DO_HASH_@(%1) 0
    This is how the macro is ended - this does not call "_HASH" again, so we have an end to the hash:

    Code:
    printf("The hash of BAA is: " #0+1+1+2);
As you might expect this doesn't actually do the maths and instead gives:

Code:
printf("The hash of BAA is: 0+1+1+2");
But if you used the code in another context, for example on a switch case statement, it would do the maths:

Code:
switch (/* TODO */)
{
    case HASH(B,A,A):
        break;
    case HASH(C,A,B):
        break;
    case HASH(A,B,B):
        break;
}
Note that the "TODO" is because you can't use this method to hash normal strings, you would need to write a corresponding PAWN function:

Code:
new
    str[] = "BAA";
switch (Hash(str))
{
    case HASH(B,A,A):
        break;
    case HASH(C,A,B):
        break;
    case HASH(A,B,B):
        break;
}
Code:
Hash(str[])
{
    new
        i = -1,
        hash;
    for ( ; ; )
    {
        switch (str[++i])
        {
            case '\0':
                return hash;
            case 'A':
                hash += 1;
            case 'B':
                hash += 2;
            case 'C':
                hash += 3;
        }
    }
}
The good thing about the switch code is that it will alert you to collisions, as it will have two case statements with the same value. y_stringhash has a much more complete example with three different (much better) hash algorithms and all upper and lower case letters, numbers, spaces and underscores. The define for a space in the above code would be:

Code:
#define _DO_HASH_(%1) _HASH(%1)+0
Assuming that space is assigned a value of 0. This is because doing:

Code:
HASH( ,A);
Will make "%0" in "_HASH" empty, and thus the generated macro name will be "_DO_HASH_" + nothing, i.e. "_DO_HASH_".
hemselves a limited number of times, often defined by the parameters passed.

Note that all the examples of this have no spacing between the comma and the letters - this is very important.
  • Function Generation
In the string hashing example the end condition was the '@' symbol (this does mean that you can't hash a string with the '@' symbol). In the code below the end condition is based on having two symbols of the same name - one defined by the pre-processor and one defined by the compiler. This is a combination of all the techniques discussed so far in this section.

You don't really need to know WHY this code exists, but if you're interested, it's part of the new YSI text system, based on the YSI ini system for loading data. The ini system uses callbacks for loading data and the text system stores the text for different libraries in different sections of an ini file. For this reason a method is needed for specifying which ini sections to load by generating functions to load them - this is where this code comes in.

The end result of the macros below is a new psudo-operator - the "text" operator, used in a very similar manner to "new":

Code:
text core[ysi_text];
text core[ysi_properties], core[ysi_race];
That will tell the text system to load the [ysi_text], [ysi_properties] and [ysi_race] sections from the "core" language file.

The code for these macros is:

Code:
// Symbol 1
#define _:%0,) %0)

// Symbol 2
stock _DO_ALL_TEXT() {}

// Symbol 3
stock LoadData(identifier[], text[])
{
    // Do something here.
    printf("\"%s\" = \"%s\"", identifier, text);
}

// Symbol 4
#define _DO_ONE_TEXT(%0,%1) forward %0_%1_yX@(i[],t[]);public %0_%1_yX@(i[],t[]){LoadData(i,t);

// Symbol 5
#define _DO_ALL_TEXT(_:%0[%1],%2) }_DO_ONE_TEXT(%0,%1)_DO_ALL_TEXT(_:%2)

// Symbol 6
#define text%0[%1]%2; stock %0_%1_@yX(){_DO_ALL_TEXT(_:%0[%1]%2,,);}
The "_DO_ALL_TEXT" FUNCTION is the fallback for the final case where the macro stops matching correctly, signaling the end of the list of items. When the end has not been reached this is instead a recursive macro call. The simplest way to explain the code is to manually expand the macros and show the resulting code at every step. For each step below the symbol currently being evaluated (all numbered above) is listed in bold, with the contents of the macro parameters. When there are multiple macros which could be evaluated at once, always start with the inner most ones before the outer ones where possible (if one isn't contained in the other, the order should make no difference).
  • Start

    This is just the start point for the first version with only one text item.

    Code:
    text core[ysi_text];
  • Expand 6
    %0 = core
    %1 = ysi_text
    %2 =

    As there is only one text item "%2" (all items after the first) is empty. This creates the start of a function which only exists to be closed later and then ignored.

    Code:
    stock core_ysi_text_@yX(){_DO_ALL_TEXT(_:core[ysi_text],,);}
  • Expand 5
    %0 = core
    %1 = ysi_text
    %2 = ,

    Because of the double comma in the first macro, "%2" is now a comma (the only text after the first comma and before the close bracket was another comma).

    Code:
    stock core_ysi_text_@yX(){}_DO_ONE_TEXT(core,ysi_text)_DO_ALL_TEXT(_:,);}
  • Expand 1
    %0 =

    This gets rid of the trailing comma in the "_DO_ALL_TEXT".

    Code:
    stock core_ysi_text_@yX(){_DO_ONE_TEXT(core,ysi_text)_DO_ALL_TEXT();}
  • Expand 4
    %0 = core
    %1 = ysi_text

    This closes one function and starts a new one. Note that the name of this function and the one already created are subtly different, and both should be fairly unique.

    Code:
    stock core_ysi_text_@yX(){}forward core_ysi_text_yX@(i[],t[]);public core_ysi_text_yX@(i[],t[]){LoadData(i,t);_DO_ALL_TEXT();}
  • End

    There are no macros left to expand. The "_DO_ALL_TEXT" which is left doesn't contain a comma, so it is instead a call to symbol 2. The resulting code, expanded for readability, is:

    Code:
    stock _DO_ALL_TEXT() {}
    
    stock LoadData(identifier[], text[])
    {
        // Do something here.
        printf("\"%s\" = \"%s\"", identifier, text);
    }
    
    stock core_ysi_text_@yX()
    {
    }
    
    forward core_ysi_text_yX@(i[],t[]);
    public core_ysi_text_yX@(i[],t[])
    {
        LoadData(i,t);
        _DO_ALL_TEXT();
    }
The second version of the code evaluates as:
  • Start

    This is just the start point for the first version with only one text item.

    Code:
    text core[ysi_properties], core[ysi_race];
  • Expand 6
    %0 = core
    %1 = ysi_properties
    %2 = , core[ysi_race]

    Note that "%2" contains the comma as the delimiter was a square bracket, not a comma.

    Code:
    stock core_ysi_properties_@yX(){_DO_ALL_TEXT(_:core[ysi_properties], core[ysi_race],,);}
  • Expand 5
    %0 = core
    %1 = ysi_properties
    %2 = core[ysi_race],,

    Again we see the double comma persisting.

    Code:
    stock core_ysi_properties_@yX(){}_DO_ONE_TEXT(core,ysi_properties)_DO_ALL_TEXT(_: core[ysi_race],,);}
  • Expand 4
    %0 = core
    %1 = ysi_properties

    This is the same as before as this macro does not rely on the additional items.

    Code:
    stock core_ysi_properties_@yX(){}forward core_ysi_properties_yX@(i[],t[]);public core_ysi_properties_yX@(i[],t[]){LoadData(i,t);_DO_ALL_TEXT(_: core[ysi_race],,);}
  • Expand 5 again
    %0 = core
    %1 = ysi_race
    %2 = ,

    This is now the same as when this was called in the first example.

    Code:
    stock core_ysi_properties_@yX(){}forward core_ysi_properties_yX@(i[],t[]);public core_ysi_properties_yX@(i[],t[]){LoadData(i,t);}_DO_ONE_TEXT( core,ysi_race)_DO_ALL_TEXT(_:,);}
  • Expand 1
    %0 =

    This gets rid of the trailing comma in the "_DO_ALL_TEXT", again making it a function call, not a macro call.

    Code:
    stock core_ysi_properties_@yX(){}forward core_ysi_properties_yX@(i[],t[]);public core_ysi_properties_yX@(i[],t[]){LoadData(i,t);}_DO_ONE_TEXT( core,ysi_race)_DO_ALL_TEXT();}
  • Expand 4
    %0 = core
    %1 = ysi_race

    Same again

    Code:
    stock core_ysi_properties_@yX(){}forward core_ysi_properties_yX@(i[],t[]);public core_ysi_properties_yX@(i[],t[]){LoadData(i,t);}forward  core_ysi_race_yX@(i[],t[]);public  core_ysi_race_yX@(i[],t[]){LoadData(i,t);_DO_ALL_TEXT();}
  • End

    That's it again, the result is:

    Code:
    stock _DO_ALL_TEXT() {}
    
    // Symbol 3
    stock LoadData(identifier[], text[])
    {
        // Do something here.
        printf("\"%s\" = \"%s\"", identifier, text);
    }
    
    stock core_ysi_properties_@yX()
    {
    }
    
    forward core_ysi_properties_yX@(i[],t[]);
    public core_ysi_properties_yX@(i[],t[])
    {
        LoadData(i,t);
    }
    
    forward core_ysi_race_yX@(i[],t[]);
    public core_ysi_race_yX@(i[],t[])
    {
        LoadData(i,t);
        _DO_ALL_TEXT();
    }
You can extend this method if desired:

Code:
text core[ysi_text];
text core[ysi_properties], core[ysi_race,ysi_commands];
That will tell the text system to load the [ysi_text], [ysi_properties], [ysi_race] and [ysi_commands] sections from the "core" language file. Hopefully the example above has shown you how to evaluate macros to see what they do, so the code for these macros will not be explained:

Code:
#define _:%0,) %0)

stock _DO_ALL_TEXT() {}

stock _DO_ONE_FILE(a)
{
    #pragma unused a
}

stock LD(identifier[], text[], a)
{
    #pragma unused a
    // Do something here.
    printf("\"%s\" = \"%s\"", identifier, text);
}

#define _DO_ONE_TEXT(%0,%1) forward %0_%1_yX@(i[],t[]);public %0_%1_yX@(i[],t[]){new %0;LD(i,t,%0);

#define _DO_ONE_FILE(_:%0,%1,%2) }_DO_ONE_TEXT(%0,%1)_DO_ONE_FILE(_:%0,%2)

#define _DO_ALL_TEXT(_:%0[%1],%2) _DO_ONE_FILE(_:%0,%1,);_DO_ALL_TEXT(_:%2)

#define text%0[%1]%2; stock %0_%1_@yX(){_DO_ALL_TEXT(_:%0[%1]%2,,);}
Reply


Messages In This Thread
PAWN Pre-Processor - Pre-processor alternatives - Part 5/7 - by Misiur - 14.04.2015, 21:13

Forum Jump:


Users browsing this thread: 1 Guest(s)