PAWN Pre-Processor Część 1
DarkGL
19.12.2014
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.c...5175#post785175
http://darkgl.amxx.p...cessor-czesc-1/
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 preprocesor.
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 oddzielonymi "_", 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 pozycji 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ą oddzielane przecinkami, parametry makr są oddzielone 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 w pełni poprawny( oczywiście 2 nie zostanie wyświetlone ).
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 przecinek 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 naprawić 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 przed 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.
he1st
19.12.2014
+'ik za TuTorialek. Wszystko czytelne i w końcu zrozumiałem makra
Czekam na pozostałe 5 części, bo takie poradniki to można czytać i czytać.
grankee
20.12.2014
Widzę, że jest to część pierwsza więc czekam na więcej dyrektyw i ich objaśnienie w tak łopatologiczny sposób
Mimo, że tyle lat już piszę, to jestem całkowitym samoukiem więc sięgałem tylko po wiedzę, która była mi niezbędna do napisania czegoś dobrze działającego, toteż przyznaję się publicznie - makra jak i przede wszystkim operacje bitowe są dla mnie czarną magią i nie mogę tego zrozumieć, jak to działa i po co to w ogóle jest
Makra są dosyć proste i po przeczytaniu raz już to czaje, ale te operacje bitowe...może coś naskrobiesz?