PAWN Pre-Processor Część 1

Jest to pierwsza część cyklu tutoriali na temat preprocesora autorstwa Y_Less przetłumaczona na język polski
Źródło http://forum.sa-mp.com/showthread.php?t=5709335

Sam tutorial dotyczy preprocesora obecnego w wersji pawn’a dla sa:mp jednak wiele rzeczy jest wspólnych , niektóre niestety działają tylko w sa:mp ale postanowiłem zostawić ich opis jaką ciekawostkę. Wszelkie uwagi co do tlumaczenia mile widziane. Podczas tłumaczenia dodawałem / zmieniałem rzeczy od siebie.
Za wszystkie błędy lub nieścisłości przepraszam czasami ciężko było przenieść znaczenie zdań z języka angielskiego na polski.

Zawartość

Część 1 – Obejmuje wprowadzenie do preprocesora oraz kilka ważnych rzeczy przydatnych podczas pisania makr.
Część 2 – Wyjaśnienie dokładnie czego szuka kompilator oraz typowych zastosowań makr.
Część 3 – Opis innych dostępnych dyrektyw (oprócz”#define”) oraz spojrzenie na definicje bez wartości podmiany.
Część 4 – Używanie stringów w preprocesorze.
Część 5 – Alternatywy dla preprocesora, wiele symboli i rekurencja.
Część 6 – Problemy z makrami oraz spacje.

Podstawowa podmiana

Na początek proste makra i jeszcze prostsze definicje. Idealny początek do wprowadzenia jak działa preprocesora.

Definicje

Definicja poniżej składa się tylko z tekstu do podmiany i tekstu na który podmieniasz.

#define MAX_PLAYERS                     500

Klasyka – definicja określa ilośc graczy na Twoim serwerze.Idealny przykład jak działają definicje. Preprocesor jak sama nazwa wskazuje wykonuje się przed głownym procesem ( kompilatorem ). Kompilator bierze napisany kod i konwertuje go do pliku AMXX, preprocesor generuje napisany kod. Przykład:

printf("%d", MAX_PLAYERS);

Preprocesor podczas wykonywania przekonwertuje ten kod do:

printf("%d", 500);

Jest to kod który zostanie przekazany głownemu kompilatorowi i to ten kod zostanie przekonwertowany do pliku wynikowego ( AMXX ). Jest to po prostu podmiana tekstu. Wszystkie makra są w tej samej formie:

#define <szukany text><spacja/spacje><text podmiany>

Warto zauważyć że spacja jest ważna – pierwsza spacja oznacza koniec stringu który będzie szukany, wszystko za spacją jest traktowane jako string na który preprocesor będzie podmieniał znalezione stringi! W przykładzie wyżej szukany string to „MAX_PLAYERS” a string podmiany to „500”.

Makra

Makro to coś w rodzaju funkcji – posiada parametry. Nazwy parametrów zaczynają się od „%0” do „%9” ( „%0” , „%1” , „%2” itp. itd. ) – nie można im nadać własnych nazw. Funkcja zwracająca maksymalną ilośc graczy pomnożoną przez liczbę wyglądała by tak:

MaxPlayersTimesNumber(number)
{
    return MAX_PLAYERS * number;
    // Pamiętaj że "MAX_PLAYERS" jest definicją więc kompilator skompiluje te wyrażenie jako:
    //  return (500) * number;
}

Makro robiące to samo wyglądało by tak:

#define MAX_PLAYERS_TIMES_NUMBER(%0)    MAX_PLAYERS * %0

Tym razem szukanym stringiem jest „MAX_PLAYERS_TIMES_NUMBER(%0)” a stringiem podmiany jest „MAX_PLAYERS * %0”. „%0” to specjalne wyrażenie – nie oznacza szukaj „%0”, oznacza szukaj czegokolwiek pomiędzy dwoma nawiasami . „%0” otrzymują tą samą wartość która była pomiędzy nawiasami w stringu który był podmieniany.
Przykład:

#define MAX_PLAYERS                     500
#define MAX_PLAYERS_TIMES_NUMBER(%0)    MAX_PLAYERS * %0

printf("%d", MAX_PLAYERS_TIMES_NUMBER(7));

Po wykonaniu preprocesora ( podamiana „%0” na „7” ) otrzymujemy:

#define MAX_PLAYERS                     500

printf("%d", MAX_PLAYERS * 7);

„MAX_PLAYERS” jest dodatkowo makrem więc otrzymujemy:

printf("%d", 500 * 7);

Warto zauwayżyć że kompilator jest „inteligentny” – jeśli widzi takie wyrażenie jak to tutaj gdzie nie mamy żadnych zmiennych , wyliczy sobie wartość , więc kod który finalnie dostajemy do kompilacji wygląda tak ( kompilator nie umie formatować stringów ):

printf("%d", 3500);

Można by to też zrobić tak:

#define MAX_PLAYERS                     500
#define MAX_PLAYERS_TIMES_NUMBER(%0)    MAX_PLAYERS * %0

new value = 7;
printf("%d", MAX_PLAYERS_TIMES_NUMBER(value));

Po wykonaniu preprocesora ( podamiana „%0” na „value” ) otrzymujemy:

#define MAX_PLAYERS                     500

new value = 7;
printf("%d", MAX_PLAYERS * value);

„MAX_PLAYERS” jest makrem więc otrzymujemy:

new value = 7;
printf("%d", 500 * value);

Ponieważ te wyrażenie używa zmiennej kompilator nie umie go wyliczyć. Więc jest to finalny kod który zostaje skompilowany.

Dlaczego ?

Więc dlaczego używać makr zamiast funkcji ( lub dlaczego używać funkcji zamiast makr )? Makra podmieniają tekst – więc wszedzie gdzie umieścisz makro tam zostanie dodany twój tekst. Jeśli masz makro w kodzie użyte 100 razy , kod zostanie wygenerowany 100 razy. Z drugiej strony jeśli masz 100 wywołań funkcji w swoim kodzie , kod zostanie dodany tylko raz mimo 100 wywołań. Funkcje są prawdopodbnie bardziej użyteczne jeśli masz dużo kodu – duże bloki kodu występujące 100 razy utworzą bardzo duży plik AMXX ! Makra są raczej używane przy małej ilości kodu – wywołanie funkcji zajmuje pamieć i czas procesora więc jeśli masz mały blok kodu nie opłaca się wywoływać funkcji , ale to nie jest zasadą ! Jeśli użyłbyś funkcji zamiast makra powyżej , skompilowany kod wygląał by tak:

MaxPlayersTimesNumber(number)
{
    return (500) * number;
}

Przykład 1:

printf("%d", MaxPlayersTimesNumber(7));

Przykład 2:

new value = 7;
printf("%d", MaxPlayersTimesNumber(value));

W obu przypadkach kompilator nie wie jak zoptymalizować kod.

Konwencja

Jedną z rzeczy które mogłeś zauważyć czytając ten poradnik jest nazewnictwo ,funkcja została nazwana „MaxPlayersTimesNumber” to samo makro zostało nazwane „MAX_PLAYERS_TIMES_NUMBER”.
To tylko konwencja – funkcje w tym poradnik będą miały nazwy pisane małymi literami oprócz pierwszych znaków wyrazów , makra za to będą miały nazwy pisane wielkimi literami z wyrazami odzielonymi „_”, jest to po to aby łatwo można było zorientować się czego teraz używamy bez sprawdzania definicji.

Kolejna konwencja to ustawianie stringu podmiany na pozcji 40 ( kiedy to możliwe ) – jest to po to aby ułatwić czytanie dużej ilości makr np.

#define DEFINITION_1                    1
#define MY_DEF                          2
#define SOME_OTHER_LONG_NAME_DEFINITION 3
#define A_MACRO(%0)                     3 * %0

Zamiast:

#define DEFINITION_1 1
#define MY_DEF 2
#define SOME_OTHER_LONG_NAME_DEFINITION 3
#define A_MACRO(%0) 3 * %0

Żadna z tych konwencji nie jest zasadą więc masz wolną ręke przy używaniu ich , jeśli chcesz możesz je zignorować. Ale zachęcał bym Cie to posiadania naprawdę dobrych powodów zanim je zignorujesz.

Składnia / Semantyka

Szybkie przypomnienie. „Składnia” jest to wygląd kodu, „Semantyka” oznacza to co ten kod robi . Składnia pętli for to: „for (; ; ) {}”, „semantyka” pętli for to: wykonaj się ileś razy na podstawie przekazanych parametrów. Wążna sprawą w następnej sekcji jest składnia i semantyka funkcji „printf”. Składnia to: „printf(string[], …);” – czyli string a następnie dowolna ilość parametrów, zawartość stringu nie wpływa na składnie – „printf(„%d”, 6, 7);” spełnia zasady składnie, ale 7 nie zostanie wyświetlona ponieważ string określa semantykę funkcji ( co ona naprawdę robi ).Kod się skompiluje ale nie będzie działał poprawnie , i jest to bardzo ważna różnica.

Parametry

Makro może posiadać kilka parametrów:

#define MULTIPLY_TWO_NUMBERS(%0,%1)     %0 * %1

W rzeczywistości makro może mieć nawet do 10 parametrów:

#define MULTIPLY_NUMBERS(%0,%1,%2,%3,%4,%5,%6,%7,%8,%9) %0 * %1 * %2 * %3 * %4 * %5 * %6 * %7 * %8 * %9

Niektórzy lubią stawiać spacje po przecinku w liście parametrów np.:

#define MULTIPLY_TWO_NUMBERS(%0, %1)     %0 * %1

Czegoś takiego nie można robić w makrach – tak jak było wcześniej powiedziane spacja oznacza koniec stringu do podmiany , więc preprocesor będzie szukał „MULTIPLY_TWO_NUMBERS(%0,”, a nie „MULTIPLY_TWO_NUMBERS(%0, %1)” i podmieni to na „%1) %0 * %1”.

Teraz kiedy wiesz już czym jest makro i czym są jego parametry możemy skupić się na różnicach parametrów makr i parametrów funkcji.

Po pierwsze – parametry makra i funkcji nie są tym samym i nie powinny być traktowane w ten sam sposób. Parametry funkcji są odzielane przecinkami , parametry makr są odzielone czymkolwiek chcesz.

Ten kod nie jest poprawny, podczas wywołania funkcji jest przekazywane za dużo parametrów:

MyFunc(a)
{
    return a;
}

main()
{
    printf("%d", MyFunc(1, 2));
}

Ten kod jest poprawny:

#define MY_FUNC(%0)                     %0

main()
{
    printf("%d", MY_FUNC(1, 2));
}

W przykładzie wyżej makro „MY_FUNC” szuka czegoś pomiędzy dwoma nawiasami poprzedzone „MY_FUNC”. W tym przykładzie zawartością pomiędzy nawiasami jest „1, 2”. Wyrażenie zawiera przecinek ale dla makra nie robi to różnicy. Kod po wykonynaniu preprocesora dla tego makra będzie wyglądał tak:

main()
{
    printf("%d", 1, 2);
}

Wygenerowany kod jest wpełni poprawny( oczywiście 2 nie zostanie wyświetlona ).

Jeśli parametry nie są odzielane przecinkami , jak móc używać więcej niż jednego ? Parametry są odzielane czymkolwiek chcesz żeby były odzielane np.:

#define MULTIPLY_TWO_NUMBERS(%0,%1)     %0 * %1

Kod wyżej będzie szukał „MULTIPLY_TWO_NUMBERS(” następnie wszystkiego do przecinka , przecinka , wszystkiego do zamykającego nawiasu.

printf("%d", MULTIPLY_TWO_NUMBERS(6, 7));

Kod wyżej zostanie podmieniony przez makro ( spacja tutaj jest dopuszczalna , nie jest dopuszczalna w deklaracji ) i otrzymamy taki kod:

printf("%d", 6 * 7);

Jednak przecinke nie jest zamykajacym nawiasem więc to też jest prawidłowe:

printf("%d", MULTIPLY_TWO_NUMBERS(6, 7, 8));

W tym przypadku parametr „%0” przyjmuje wartość 6 a parametr „%1” przyjmuje wartość „7,8” więc po wygenerowaniu kodu otrzymamy:

printf("%d", 6 * 7, 8);

Nawiasy

Skoro parametry są tak elastyczne jak możemy kontrolować to co generuje nam preprocesor ? Wszystkie makra wyżej były bardzo złe , nie używały nawiasów.

Przykład:

// Without brackets (first).
#define MULTIPLY_TWO_A(%0,%1)           %0 * %1

// With brackets (second).
#define MULTIPLY_TWO_B(%0,%1)           ((%0) * (%1))

main()
{
    // Two with first.
    printf("%d", MULTIPLY_TWO_A(6, 7));
    
    // Two with second.
    printf("%d", MULTIPLY_TWO_B(6, 7));
    
    // Three with first.
    printf("%d", MULTIPLY_TWO_A(6, 7, 8));
    
    // Three with second.
    printf("%d", MULTIPLY_TWO_B(6, 7, 8));
}

Po wygenerowaniu otrzymamy taki kod:

main()
{
    // VALID
    printf("%d", 6 * 7);
    
    // VALID
    printf("%d", ((6) * (7)));
    
    // VALID
    printf("%d", 6 * 7, 8));
    
    // INVALID!
    printf("%d", ((6) * (7, 8)));
}

Finalny kod pokazuje ważna różnice , po dodaniu nawiasów wygenerowany kod jest błędny ( składnia jest błędna ). Próbujemy mnożyć „6” przez „7,8” – co jest błędne więc użytkownik dostanie błąd przy kompilacji.

Inne użycie nawiasów to ustalanie priorytetów operatorów. Dzięki nawiasom możemy ustalać kolejność wykonywania operatorów np. „4 + 5 * 6” otrzymujemy „34”, nie „54”. Ponieważ * ma wyższy priorytet niż + więc „4 + 5 * 6” zostaje wykonane do „4 + 30” a potem „34”. Jeśli parametry były by wykonywane po kolei „4 + 5 * 6” staje się „9 * 6” a następnie „54”.

Przeanalizujmy taki kod

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

main()
{
    printf("%d", ADD_TWO(3, 3) * 7);
}

3 + 3 to 6 , 6 * 7 to 42 prawda? Nie! Zobaczmy wygenerowany kod.

main()
{
    printf("%d", 3 + 3 * 7);
}

Wiemy co się stanie mnożenie zostanie wykonane przed dodawaniem więc otrzymamy 24. Całość możemy naprawawić dodając nawiasy:

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

Kolejny przykład

#define MUL_TWO(%0,%1)                  (%0 * %1)

main()
{
    printf("%d", MUL_TWO(3 + 3, 7));
}

Dodaliśmy nawiasy więc wszystko powinno być ok ? Błąd ! Zobaczmy co wygenerował preprocesor:

main()
{
    printf("%d", (3 + 3 * 7));
}

Obliczenia są w nawiasach ale znowu mnożenie zostanie wykonane przez dodawaniem. Powinniśmy dodać jeszcze jeden poziom nawiasów dzięki czemu wszystkie operacje będa wykonywane poprawnie:

#define MUL_TWO(%0,%1)                  ((%0) * (%1))

PAMIĘTAJ: Owijaj makro i parametry makra w nawiasy. Istnieją sytuację kiedy nie trzeba tego robić ale o nich opowiem później.

Makra kilku linijkowe

Makro może mieć kilka linii dzięki użyciu „\”. Zasada jest prosta jeśli na końcu linii znajduje się znak \ makro jest kontynuowane w kolejnej linii. Makro nie może być kontynuowane w parametrach i nazwie z tych samych powodów z których nie możemy używać spacji. Uwaga: W tym poradniku znak kontynuacji jest umieszczany na pozycji 80:

#define MUL_TWO(%0,%1)                                                          \
    ((%0) * (%1))
#define MUL_TWO(%0,%1)                                                          \
    (                                                                           \
        (%0)                                                                    \
        *                                                                       \
        (%1)                                                                    \
    )
#define MUL_TWO(%0,%1)                                                          \
    (                                                                           \
        (                                                                       \
            %0                                                                  \
        )                                                                       \
        *                                                                       \
        (                                                                       \
            %1                                                                  \
        )                                                                       \
    )

Ostatnia linii makra nie posiada operatora konytnuacji.

Pułapka

Jest jeden bardzo ważny problem podczas używania makr zamiast funkcji:

Wersja funkcyjna:

PrintSquare(var)
{
    printf("%d", var * var);
}

main()
{
    new
        var = 2;
    PrintSquare(var++);
    printf("%d", var);
}

Wynik:
4
3

Wersja z makrami:

#define PRINT_SQUARE(%0)                printf("%d", (%0) * (%0))

main()
{
    new
        var = 2;
    PRINT_SQUARE(var++);
    printf("%d", var);
}

Możemy otrzymać:
4
4

Lub:
6
4

Ponieważ parametry przekazane do makra są inkrementowane , więc inkrementacja jest dodawana przy generowaniu kodu:

main()
{
    new
        var = 2;
    printf("%d", (var++) * (var++));
    printf("%d", var);
}

W takim przypadku w drugim printf zmienna var będzie zinkrementowana dwa razy – co jest błędne i nie wydarzy sie podczas użycia funkcji.

Kolejność wykonania dla operatora inkrementowania może zostać wykonana na dwa sposoby:

temp1 = var;
temp2 = var;
var   = var + 1;
var   = var + 1;
printf("%d", temp1 * temp2);

Lub:

temp1 = var;
var   = var + 1;
temp2 = var;
var   = var + 1;
printf("%d", temp1 * temp2);

Oba są technicznie prawidłowe – w obu przypadkach inkrementowanie jest wykonane po użyciu zmiennej , problemem jest tylko który sposób wybierze kompilator. Dlatego wynik może być „4” lub „6”.

Bądź bardzo uważny podczas używania makr z parametrami które modyfikują zmienne – dlatego nazwy makr są pisane bardzo często z dużych liter aby użytkownik wiedział że jest to makro i był bardzo uważny podczas jego używania.

4 komentarzy o “PAWN Pre-Processor Część 1

Dodaj komentarz