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:gcc_va_args

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 __a_size(n)           ((sizeof(n) + sizeof(char *) – 1) & ~(sizeof(char *) – 1))

#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:

void foo(int num_ints,)
{
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