Przykładowe komendy przeprowadzające ten proces wyglądają następująco:
1 |
iarchive –x tplib.a modul.o |
wyciąga plik modul.o z biblioteki
1 |
ar –cru libtplib.a *.o |
tworzy nową bibliotekę z wszystkimi modułami
Tworzenie interfejsu binarnego
W rzadkich przypadkach może zaistnieć konieczność stworzenia biblioteki będącej interfejsem dla biblioteki zewnętrznej. Dla bibliotek języka C proces ten jest relatywnie prosty, dla bibliotek C++ może się okazać dużo bardziej skomplikowany. W zależności od charakteru biblioteki rozsądnym może się okazać napisanie w języku C kodu „obudowującego” (wrappera) dla biblioteki C++, co pozwala zamaskować różnice między kompilatorami jeśli chodzi o ABI.
W niektórych sytuacjach może to jednak być nie do zaakceptowania. Wtedy to trzeba napisać wrappery w C++, co z kolei wymaga dobrego zrozumienia niskopoziomowej implementacji języka (jak obiekty są tworzone w pamięci, jak poprzez wirtualne tablice wskaźników do funkcji obsługiwany jest polimorfizm, jak rzucane i chwytane są wyjątki) i wykracza poza zakres niniejszego tekstu. W dalszej części tego rozdziału przedstawione są metody, którymi można się posłużyć, aby wykonać większość zadań pojawiających się w czasie migracji bibliotek w formie binarnej.
Wywoływanie i powrót z funkcji
Pierwszą rzeczą, którą trzeba zrozumieć przed rozpoczęciem tworzenia interfejsów binarnych jest mechanizm wywoływania funkcji i powrotu z nich. Chodzi tu o poznanie rejestrów używanych do przekazywania parametrów i zwracania wyników oraz zasad rządzących modyfikacjami zawartości rejestrów bez zachowywania ich dotychczasowego stanu dokonywanymi przez funkcje. Parametry wejściowe funkcji są najpierw zapisywane do rejestrów, a następnie na stosie.
Tab. 21.
Element | Opis |
Argumenty wejściowe | Rejestry od r0 do r3. Dla parametrów 64-bitowych rejestry mogą być grupowane w pary r0:r1 oraz r2:r3. Pierwszym parametrem może być adres: – wskaźnik this w C++ – adres dużej/złożonej wartości zwracanej |
Wartość zwracana | Rejestr r0 lub para r0:r1 dla wartości 64-bitowych; r0 może zawierać adres zwracanych danych w przypadku dużych lub złożonych wartości zwracanych. |
Rejestry robocze | Rejestry od r0 do r3 oraz r12 mogą być modyfikowane przez wywoływaną funkcję bez zachowywania ich wartości (bezpowrotnie). |
Rejestry zachowywane | Zawartość rejestrów od r4 do r11 w razie modyfikacji musi być zachowywana i odtwarzana przed powrotem z funkcji. |
Rejestry specjalne | r13 to wskaźnik stosu; r14 do rejestr adresu powrotu z procedury (link register); r15 to licznik programu. |
Jak widać, przy powrocie z funkcji, rejestry od r4 do r11 muszą zawierać te same wartości, co w momencie wywołania jej, podobnie jak wskaźnik stosu i link register. Rejestr r0 przechowuje wartość zwracaną. Rejestry od r1, r2, r3 i r12 mogą zawierać dowolne dane.
Użycie kompilatora do tworzenia interfejsu
Kompilator środowiska, z którego następuje migracja może zostać użyty do skonstruowania interfejsu wywoływania i powrotu z funkcji, który z kolei może zastosować nowy kompilator. Można to osiągnąć wykorzystując zdolność kompilatora do generowania kodu asemblera z kodu w C i C++, tworząc w starym zestawie narzędziowym działający interfejs asemblerowy, wywoływalny z C lub C++, który można następnie zintegrować z nowymi narzędziami.
WSKAZÓWKA
Należy zwrócić uwagę na to, że opisana procedura zakłada, że dwa kompilatory generują kod z niekompatybilnymi interfejsami. Jeśli jest inaczej, należy się posłużyć metodami opisanymi we wcześniejszych rozdziałach tekstu, aby zintegrować kompatybilne biblioteki z projektem. |
Dla przykładu rozważmy przypadek, w którym biblioteka zewnętrzna zawiera funkcję foo o prototypie przedstawionym poniżej.
1 2 3 4 5 6 |
typedef struct s { int a; char *str; } S; S foo (int a, char *str); |
Funkcja przyjmuje dwa argumenty, z których jeden jest wskaźnikiem i zwraca strukturę. Założeniem jest, że z jakiegoś powodu budowa struktury jest różna dla dwóch kompilatorów, co jest przykładem najgorszego możliwego scenariusza.
Po pierwsze, tworzymy w C funkcję „opakowującą” (wrapper), jako podstawę interfejsu i zapisujemy w pliku wrapper.c.
1 2 3 4 5 |
S foo_w (int a, char *str) { S s = foo (a,str); return (s); } |
WSKAZÓWKA
Plik z kodem źródłowym należy skompilować dwa razy, raz za pomocą kompilatora środowiska IAR i raz za pomocą GNU, w obu przypadkach korzystając z możliwości generacji kodu asemblerowego bez optymalizacji przez kompilator. Wyłączenie optymalizacji jest ważne, ponieważ jeśli tego nie zrobimy, kompilator może wygenerować kod wyjątkowo trudny do zaadaptowania. |