GCC i funkcje o zmiennej liczbie argumentów
Niniejszy wpis jest poświęcony zagadnieniu wykorzystania funkcji o zmiennej liczbie argumentów w swoich projektach. Najpopularniejszym przykładem funkcji posiadającą taką właściwość jest funkcja printf, której prototyp wygląda mniej-więcej tak:
int printf(char *fmt, ...);
Notacja ‘…’ w polu argumentów funkcji informuje kompilator o tym, iż funkcja może przybierać dowolną liczbę argumentów. Powstaje podstawowe pytanie: w jaki sposób takie argumenty są przekazywane do funkcji skoro ich liczba nie jest wprost określona? Jak się do nich odwoływać?
Twórcy kompilatora GCC rozwiązali sprawę przekazując takie parametry w formie obszaru pamięci zorganizowanego w specyficzny sposób. Na potrzeby rozważań ustalmy, iż funkcja, którą będziemy badać będzie posiadała następujący prototyp:
void foo(char *arg, ...);
Jeżeli kolejne argumenty funkcji oznaczymy przez arg0 … arg n to zostaną one umieszczone w pamięci w sposób przedstawiony na poniższym rysunku:
Wyjaśnienia wymaga zapis a_sizeof(x). Jest to makro zwracające rozmiar zmiennej (w bajtach) będącej jego argumentem w postaci ‘wyrównanej’ do architektury dla której kompilujemy oprogramowanie. Przykładowo, jeśli architektura jest 32 bitowa to wartość dla zmiennej 1-bajtowej wynosi 4, a dla zmiennej 5-bajtowej wynosi 8. Powyższy rysunek mówi nam, iż każdy kolejny argument jest umieszczony po poprzednim w odległości równej a_sizeof(x), gdzie x to argument poprzedni.
Skoro wiemy już jak zmienne są rozmieszczone w pamięci to możemy zdefiniować sobie makra do poruszania się po nich i odczytywania ich wartości. Zestaw makr przygotowanych przeze mnie ma następującą postać:
#define va_start(ptr, v) (ptr = (va_list)(&v) + __a_size(v))
#define va_arg(ptr, t) ( *(t *)((ptr += __a_size(v)) – __a_size(t)))
#define va_end(ptr) (ptr = (va_list) 0)
typedef char * va_list
Makro __a_size(n) jest odpowiednikiem notacji a_sizeof(n) z rysunku. Makro va_start(ptr, v) ustawia wskaźnik ptr, tak aby wskazywał na pierwszą zmienną ze zbioru. Aby poprawnie ustawić wskaźnik niezbędny jest adres ostatniej ‘normalnej’ zmiennej (na rysunku jest zmienna arg). Makro va_arg(ptr, t) powoduje zwrócenie zmiennej wskazywanej przez ptr oraz ustawienie ptr tak by wskazywał na następną zmienną. Parametr t makra określa typ zmiennej jaką chcemy zwrócić. Jest on niezbędny, gdyż od niego zależy ‘odległość’ do następnej zmiennej w pamięci. Makro va_end(ptr) służy do wyczyszczenia wartości wskaźnika ptr.
Wyposażeni w taki zestaw makr możemy zacząć naszą przygodę z funkcjami o zmiennej liczbie argumentów. Dla przykładu napiszemy funkcję wypisującą wartość wszystkich zmiennych typu int podanych jako argumenty. Listing funkcji jest następujący:
{
int i, x;
va_list va;
va_start(va, num_ints);
for(i = 0; i < num_ints; i++) {
x = va_arg(va, int);
printf("arg: %d, val: %d\n", i, x);
}
}
Przykładowe wywołanie:
foo(3, 1, 2, 3);
I jego rezultat:
arg: 0, val: 1 arg: 1, val: 2 arg: 2, val: 3