|
^ Blog index    << Preprocesor C - szczegóły    >> BCM2835 SoC w Raspberry Pi Preprocessor C - zaawansowane wykorzystanie (preprocesor GNU) 2014-12-18    dr inż. Piotr Romaniuk Spis treści
Wstęp
Preprocesor może zostać wykorzystany do wygenerowania fragmentów kodu w czasie budowania programu
(compilation time). Przy zaawansowanym wykorzystaniu jego zdawałoby się skromnych możliwosci
można stworzyć pewien metajęzyk wspierający programowanie w C. Często pojawia się potrzeba sprawdzenia wersji i warunkowej konfiguracji/kompilacji fragmentów kodu. Przykładowo, załóżmy, że potrzeba zdefiniować położenie końcówki GPIO zależnie od rodzaju płytki: // określenie rodzaju płytki (w nagłówku specyfikującym): #define IDBOARD "CNC_ROUTER_HEAD3" // poniższe w nagłówku ogólnym: #if IDBOARD=="CNC_ROUTER_HEAD3" # define MOTOR_PIN 1 # define MOTOR_PORT 1 #elif IDBOARD=="CNC_ROUTER_CUTTER" # define MOTOR_PIN 3 # define MOTOR_PORT 1 #endif // podczas próby zbudowania preproecsor wygeneruje bład: test.c:114:12: error: token ""CNC_ROUTER_HEAD3"" is not valid in preprocessor expressions #define IDBOARD "CNC_ROUTER_HEAD3" ^ test.c:120:5: note: in expression of macro 'IDBOARD' #if IDBOARD=="CNC_ROUTER_HEAD3" Problemem okazuje się tu brak możliwości porównania tekstowego w #if - preprocesor nie ma takiego operatora, może porównywać tylko wartości całkowite. Aby to rozwiązać można zdefiniować typy płytek w postaci makr rozwijanych do unikalnych identyfikatorów: #define IDBOARD CNC_ROUTER_HEAD3 //identyfikatory dla różnych maszyn: #define CNC_ROUTER_HEAD3 1 #define CNC_ROUTER_CUTTER 2 #if IDBOARD==CNC_ROUTER_HEAD3 # define MOTOR_PIN 1 # define MOTOR_PORT 1 #elif IDBOARD==CNC_ROUTER_CUTTER # define MOTOR_PIN 3 # define MOTOR_PORT 1 #endif Przy użyciu makra często pojawia się niejednoznaczność czy należy po nim stawiać średnik (aby przypominało to składnię C) czy nie. Najlepiej byłoby gdyby to implementator makra tak je skonstruował aby brak średnika powodował błąd kompilacji. Problem staje się jeszcze poważniejszy, gdy dotyczy to makr wieloliniowych i ich współpracy z instrukcjami warunkowymi. Oczekuje się, że makro powinno dać się użyć w dowolnym miejscu (np. po if/else) i powinno się ono zachować tak jakby było funkcją. // brak ochrony spójności makra #define MULTILINE_MACRO( x ) fun( x );\ fun2() //problem: if( a>3 ) MULTILINE_MACRO(a); //skompiluje sie do: if( a>3 ) fun( x ); fun2(); Jak widać tylko pierwsza częśc makra (wywołanie fun()) będzie warunkowe, druga linia (tj. fun2()) będzie wywoływane zawsze. Z pewnoscią nie o to chodziło autorowi. Warto zauważyć, że wszystko się skompiluje ale nie będzie działać właściwie! Dodajmy więc nawiasy klamrowe: #define MULTILINE_MACRO( x ) {\ fun( x );\ fun2();\ } if( a>3 ) MULTILINE_MACRO(a); //skompiluje sie do: if( a>3 ) { fun( x ); fun2(); }; Jest już lepiej ale makro nie wymusza na programiście stawiania średnika. Do tego nie ma jednolitej reguły, jeśli bowiem jest else w instrukcji warunkowej to średnika nie należy stawiać bo prowadziłoby to do błędu: if( a>3 ) MULTILINE_MACRO(a); else ... //wygeneruje błąd: test1.c:136:3: error: 'else' without a previous 'if' Przeglądając źródła kernela Linuxa można natknąć się na rozwiązanie: #define MULTILINE_MACRO( x ) \ do{\ fun( x );\ fun2();\ }while(0) Taki kod wymusza postawienie średnika (zawsze) i jest pewność że ta pętla wykona się raz i tylko raz, a do tego w całości (jest niepodzielna), niezależnie od tego gdzie się znajdzie.
Wybieranie makra wartością parametru Przypuśćmy, że jakiś symbol rozwija się do 0 lub 1-ki. Chcielibyśmy, aby makro rozwijało się do różnych symboli zależnie od tego parametru. Wystarczy stworzyć dwa makra, których nazwy będą tworzone przez połaczenie stałej części (np. SELECT_) oraz wartości symbolu kontrolnego: //makro wybierające #define SELECT( sym_true, sym_false, ctrl ) _SELECT( sym_true, sym_false, ctrl ) #define _SELECT( sym_true, sym_false, ctrl ) SELECT_##ctrl( sym_true, sym_false ) //dwa makra do wybierania odpowiedniego argumentu #define SELECT_0( sym_true, sym_false ) sym_false #define SELECT_1( sym_true, sym_false ) sym_true //wykorzystanie #define SOME_CTRL_SYM 1 SELECT( SYMBOL_T, SYMBOL_F, SOME_CTRL_SYM ) => SYMBOL_T //a gdy symbol bedzie zdefiniowany jako 0: #define SOME_CTRL_SYM 0 SELECT( SYMBOL_T, SYMBOL_F, SOME_CTRL_SYM ) => SYMBOL_F Należy tu zwrócić uwagę, że symbol kontrolny może być wynikiem jakiegoś innego przetwarzania. W powyższym przykładzie jest zdefiniowany #definem aby zademonstrować jak najprościej jak jest używany. Podobnie wynik takiej selekcji może być rozwinięciem innego makra. Zastosowań jest więc wiele. Załóżmy że chcemy okreslić liczbę argumentów podanych do makra. Następnie w zależności od tej liczby wywoływać różne zachowania. (wystarczy połączyć jaki stały tekst z otrzymaną liczbą przez konkatenację i zdefiniować różne makra dla takich symboli). Można powiedzieć, że jest to pewnego rodzaju uproszczone przeciążanie (overloading). // niech GET_ARGS_NB( ) będzie makrem rozwijanym do liczby podanych mu argumentów // najpierw zdefiniujmy konkatenację (łączenie), które będzie przetwarzać argumenty (preprocesor tego nie robi, // gdy jest operator konkatenacji. #define _CONC( a, b ) a ## b #define CONC( a, b ) _CONC( a, b ) // zdefiniujmy dwie wersje zachowania w zależności od tego ile ejst argumentów (na razie dla 1 i 2) #define FN_ARGS_1( x ) some_fn_arg1( x ) #define FN_ARGS_2( x, y ) some_fn_args2( x, y ) // a teraz definicja przełącznika, głównej funkcji używanej w kodzie ze zmienną liczbą argumentów: #define SOME_FN_VARARGS( ... ) CONC( FN_ARGS_, GET_ARGS_NB( __VA_ARGS__ ) )( __VA_ARGS__ ) // jeśli makro wyznaczające liczbę argumentów pracuje poprawnie to poniższe wywołania rozwiną się odpowiednio: SOME_FN_VARARGS( x ) => some_fn_arg1( x ) SOME_FN_VARARGS( x, y ) => some_fn_args2( x, y ) Powyżej pokazane zostało jak może być wykorzystane makro wyznaczające liczbę argumentów, a teraz jak je zrobić: #define GET_6TH( n1, n2, n3, n4, n5, n6, ...) n6 #define GET_ARGS_NB( ... ) GET_6TH( __VA_ARGS__ , 5, 4, 3, 2, 1) Pomysł opiera się na tym, aby do listy argumentów dokleić kolejne liczby naturalne a potem zawsze czytać wartość z określonej pozycji. Im więcej będzie argumentów tym bardziej przesunięte będą te doklejone.
Sprawdzenie czy podane zostały argumenty Kolejny problem do rozwiązania przy pomocy preprocesora: sprawdzenie czy makro zostało wywołane bez argumentów. Podobnie jak poprzednio, można tu zaimplementować inne zachowanie dla wywołania z argumentami lub bez. #define GET_7TH( n1, n2, n3, n4, n5, n6, n7, ...) n7 #define IS_EMPTY( ... ) GET_7TH( 1000, ## __VA_ARGS__ , 0, 0, 0, 0, 0, 1, 1001) //wykorzystanie: IS_EMPTY( a ) => 0 IS_EMPTY( ) => 1
Liczba 1000 jest tylko po to aby był jakiś argument przed ## __VA_ARGS__ - konieczne aby się sklejało gdy brak argumentów,
liczba 1001 jest po to aby dostarczyć dodatkowy argument do makra GET_7TH. |
||||||