Współdzielone biblioteki w OS4 - autor Hans-Jorg Frieden
Współdzielone biblioteki mają za zadanie dostarczyć programiście tworzącemu
aplikację interfejs, sposób porozumiewania się z systemem operacyjnym.
Praktycznie wszystko w AmigaOS jest zaimplementowane we współdzielonych
bibliotekach (z kilkoma wyjątkami). Klasyczne współdzielenie bibliotek
składa się ze struktury danych w pamięci (zwane jest to Library Base) i
tablicy skoków, która umieszczona jest na początku struktury danych.
Program, który chce wywołać funkcję biblioteczną najpierw musi pobrać
wskaźnik do Library Base przy użyciu wywołania execa OpenLibrary. To zwróci
wskaźnik do Library Base. Od kiedy tablica funkcji znajduje się na początku
Library Base, dostęp do funkcji następuje przez negatywny offset z Library
Base, z pierwszym wektorem zlokalizowanym na -6, drugim -12 itd. Te wektory
zawierają prostą instrukcje JMP z 68k, po której następują cztery bajty
funkcji adresowej (w związku z tym, że wektory są odległe od siebie o 6
bajtów). Program wykonuje bezpośredni skok podprocedury do tego wektora, który
następnie rozgałęzia się i wędruje do właściwej procedury.
Dwa miejsca w powyższym paragrafie są ważne: "bezpośredni" i "instrukcja JMP
68k". "Bezpośredni" jest istotny ponieważ wskazuje, że nie istnieje nic
pomiędzy samym programem, a kodem biblioteki. Nie następuje żadne
oddziaływanie systemu. Nie zajdzie sytuacja, że jakikolwiek zewnętrzny
mechanizm wedrze się do środka. Instrukcja "68k JMP" jest ważna z prostego
powodu: klasyczne programy spodziewają się znaleźć tę instrukcję, gdy
wykonują skok do kodu 68k. Program napisany pod PowerPC nie byłby w stanie
tego zrobić, gdyż nie isnieje kod PowerPC na końcu wywołania. Powstaje więc
dylemat: albo biblioteka jest pod 68k i nie może byc wykorzystana przez
PowerPC (chyba, że program jest przygotowany na emulację każdego wywołania
biblioteki), albo biblioteka jest pod PowerPC i nie może być wykorzystana
przez 68k. Skoro powzięliśmy wyzwanie przeniesienia systemu operacyjnego na
PowerPC, odnalezienie odpowiedzi na to pytanie jest bardzo ważne. Nasze
rozwiązanie jest dobre z kilku powodów: biblioteki powinny być albo pod
PowerPC lub pod 68k, lub obu typów. Program 68k powinien być zdolny wywołać
bibliotekę PowerPC bez potrzeby jego rekompilacji. Podobnie, program PowerPC
powinien potrafić wywołać bibliotekę bez potrzeby wiedzy na temat tego, czy
biblioteka jest pod 68k czy PowerPC. Idąc dalej, powinno być mozliwe,
zastąpienie funkcji/kodu 68k w bibliotece bez potrzeby rekompilacji programu
korzystającego z tej biblioteki.
Jedna z możliwości zakłada, że punktem wejścia każdej biblioteki jest zawsze
68k. To byłoby jednak wyciąganiem starej koncepcji bez powodu i w większości
wymagało by to zawsze wykorzystywania emulatora (nawet dla tylko kilku
instrukcji) w celu wywołania funkcji z biblioteki. Prostym jest więc, że nie
jest to dobre rozwiązanie. Wywołuje to niedopuszczalne koszty prędkościowe w
przypadku wywołania każdej funkcji. Szczególnie irytujące może to być przy
częstym dokonywaniu takich wywyołań. Założenie, że zawsze będziemy korzystać
z biblioteki PPC również nie jest dorbym rozwiązaniem. Stare programy nie
byłyby w stanie z nich korzystać, a posiadanie dwóch kopii biblioteki (PPC i
68k) również nie stanowi zbyt wielkiej pomocy, gdyż byłoby koszmarem dla
programistów.
Biblioteki Multi-Interface
Rozwiązanie, które zastosowaliśmy nazwaliśmy bibliotekami "multi-interface"
("multi-interface libraries"). Jak sugeruje nazwa, biblioteka taka potrafi
eksportować dowolną liczbę skoków do tabeli. Interfejs jest zawsze taki sam
jak biblioteka; zawiera tabelę wskaźników funkcji i własną część danych. Nie
ma instrukcji skoków. Program, który chce wywołać po prostu odczyta wskaźnik
funkcji i rozgałęzi się w tamtym kierunku (ten sposób wywoływania funkcji
przebiega z korzyścią dla architektury PowerPC).
Aby osiągnąć kompatybilność z aplikacjami, skok do tabeli znany z kodu 68k
nadal istnieje. Na bibliotece stricte 68k, nakierowuje to nadal na kod 68k.
Jakkolwiek możliwe jest przekierowanie wywołania do krótkiego odcinka, który
odcina się od emulatora do natywnej wersji wywołania. W ten sposób, nawet
stare programy uzyskają przyspieszenie poprzez wykorzystanie natywnych dla
PowerPC funkcji systemu.
Exec library jest przykładem biblioteki, która została całkowicie napisana
pod PowerPC, lecz umożliwia przekierowanie tablicy skoków. W związku z tym,
program napisany pod 68k, wywołujący np. "AllocVec" ostatecznie trafia do
wersji PowerPC tego wywołania, nawet tego nie zauważając. Dla aplikacji
napisanej pod 68k, jest to całkowicie przezroczyste.
Wykorzystanie interface'u
Interface posiada strukturę przypominającą język C i nazwę ciągu podaną w
ASCII. Struktura określa strukturę interface'u i jest czymś na styl
zamiennika starego stylu #pragma znanego z SAS/C czy StormC. Przypomina to
bardziej interface z języka Java lub klasy C++. Struktura interface'u
określa rozłożenie i dostępne funkcje/metody interface'u - nie ich
implementacje. W teorii, ta sama struktura interface'u może być dostarczona
do więcej niż jednej biblioteki lub jedna biblioteka może wyeksportować wiele
wersji/implementacji interface'u (później przyjrzymy się bardziej szczegółowo
temu zagadnieniu, a także w kolejnych artykułach na temat rozszerzeń OS4 i
architektury PCI, która w szczególny sposób wykorzystuje tę cechę).
Interface'y zazwyczaj podążają specyficznym schematem. Zwykle posiadają
część opisową oraz przyrostek "IFace". Dla przykładu, interface Execa
wywoływany jest przez "struct ExecIFace", Intuition - "struct IntuitionIFace"
itd. Każda biblioteka rozpoznaje przynajmniej dwa interface'y. Jednym z
nich jest wewnętrzny interface kontroli, który wykorzystywany jest tylko
przez system (w zależności od tego czy biblioteka jest "prawdziwą" biblioteką
czy urządzeniem, wyeksportuje LibraryManagerIFace lub DeviceManagerIFace).
Kolejny interface'em zazwyczaj zawiera główną funkcjonalność biblioteki.
Aby uzyskać informacje z interface'u, program musi wywołać funkcję Execa -
"GetInterface". Funkcja ta jako argument akceptuje zarówno wskaźnik library
base jaki ciąg nazwy ASCII i tworzy i/lub zwraca wskaźnik do interface'u,
który go żądał. W tym celu, interface'y są identyfikowane przez nazwę.
Większość bibliotek eksportuje główny interface pod nazwą "główny" ("main"),
lecz takie nazewnictwo nie jest wymagane. Dla przykładu, Exec library
eksportuje "główny" interface jako typ "ExecIFace", a interface "mmu" jako
typ "MMUIFace" (drugi interface kontroli wykorzystujący MMU na procesorze
PowerPC).
Jak zwykle, Exec library to specjalny przypadek, z racji, że dostarcza
wszystkich wymaganych funkcjonalności dla potrzeb operowania interface'em. W
tej sytuacji, wskaźnik "ExecIFace" ustawiany jest przez kod startowy C
library i jest natychmiast gotowy do użycia.
Wywołanie funkcji z interface'u odbywa się w prostej linii. Tablica skoków
zawiera wszystkie funkcje interface'u w formie wskaźników funkcji. Wywołanie
niektórych z nich wygląda podobnie do sposobu wywołania w C++ i poniekąd jest
również podobne do metody wywołania w języku Java. Dla przykładu,
przyjmijmy, że zmienna "IExec" zawiera wskaźnik do głównego interface'u Exec
library, który jest typem "struct ExecIFace". Wywołanie funkcji "AllocVec"
wyglądało by tak: memory = IExec->AllocVec(size, memory_flags);
Z czasu przed OS4 wyglądało by to w ten sposób: memory = AllocVec(size,
memory_flags);
Jak widzimy, jedyna różnica to przekierowanie zmiennej "IExec". W starym
systemie, mniej więcej to samo się działo (wywołanie wykonywane było przez
przekierowanie wskaźnika SysBase). Różnica ta umożliwia jednak programiście
wybrać tablicę skoków do której funkcja AllocVec powinna się odwoływać. Z
początku może wydawać się to niepotrzebną stratą czasu, jest jednak kilka
zalet tej metody wywołania: wywołanie jest wyraźne i dosłowne; programista
posiada pełną kontrolę nad tym co jest wywoływane. Nie ma potrzeby na
jakiekolwiek zabiegi zwane "library base swapping", dla przykładu, gdy
program chce wywołać pluginy.
Biblioteki stanowią ogromną pomoc. Nie ma powodów dla których dwie
biblioteki nie miałyby robić tego samego. Stary system wymagał zestawu nazw
funkcji rozdzielających. Wywołanie jest inne niż w starym systemie.
Umożliwia implementację natywnych bibliotek pod PowerPC przy zachowaniu
starej, znanej z 68k tablicy skoków. W systemie zastosowano podejście modelu
składowego zarówno dla systemu jak i aplikacji.
Przy ostatnim punkcie zatrzymamy się na chwilę. Model składowy posiada kilka
zalet tradycyjnego programowania. Jedną z nich jest to, że można odwoływać
się tylko do jednej metody (lub funkcji wywołania), które części składowe
rozumieją. Rozszerzenia AmigaOS4 i architektura PCI polega właśnie na tej
cesze. Z powodu wykorzystania interface'ów, możliwa jest transparentna
obsługa dowolnego typu amigowego kontrolera PCI, jak również chipsetu Articia
na płycie AmigaOne. Każde urządzenie PCI na szynie reprezentowane jest przez
interface typu "PCI_Device". Jednakże informacja przekazywana przez
interface tworzona jest oddzielnie dla każdego urządzenia, a w związku z tym
mogą zostać ukryte wszystkie specyficzne szczegóły implementacji właśnie w
interface'ie. Programista po prostu wywołuje pcidev->WriteConfigByte(.);
Interface wie jak sobie z tym radzić i nie ważne czy jest to karta sieciowa
wetknięta w Prometheusza, karta dźwiękowa w Mediatorze lub karta AGP za
mostkiem PCI->AGP na AmigaOne.
Kompatybilność interface'ów
Aby umożliwić funkcjonalność biblioteki 68k dla natywnych programów pod PPC,
Exec implementuje mechanizm tworzący interface bibliotek starego stylu. Dla
przykładu załóżmy, że program pod PowerPC chce skorzystać z biblioteki
"foo.library", która została napisana pod 68k i nie wie nic na temat
koncepcji interface'u. Posiada jednak tablicę skoków 68k, z której program
PowerPC może skorzystać poprzez zintegrowany w OS4 emulator. Program może
więc wywołać emulator dla wywołania funkcji biblioteki "foo.library", których
chce użyć. Jednak gdy autor foo.library zdecyduje, że chce mieć natywną
wersję tej biblioteki, program będzie musiał zostać przekompilowany lub
uruchomiony w trybie emulacji.
Oczywistym rozwiązaniem jest wygenerowanie "głównego" interface'u dla
foo.library (czyli "struct FooIFace), ze wszystkimi funkcjami z tablicy
skoków 68k jako interface'u funkcji. Interface po prostu zaemuluje wszystkie
punkty wejścia foo.library. Gdy Exec otrzyma wywołanie GetInterface, które w
jakiś sposób nie będzie mogło zostać zaakceptowane (czy to z powodu starej
biblioteki 68k czy też błędu GetInterface), wywołany zostanie RAMLIB (program
odpowiedzialny za wczytywanie urządzeń i bibliotek z dysku), który będzie
starał się dostać do interface'u. RAMLIB będzie próbował udostępnić to albo
szukając kompilowanego pliku interface'u w LIBS: lub przez generację
interface'u w locie z zainstalowanego pliku FD lub SFD.
W dalszym etapie, gdy foo.library jest zastępowana przez natywną wersją PPC,
GetInterface uruchomi wywołanie w Execu, a program automatycznie będzie
używał wersji PowerPC. Żadne przekompilowanie nie jest wymagane.
Podsumowanie
Obecnie, tylko "liznęliśmy" tematu dotyczącego tego, jakie możliwości daje
nowy system interface'u bibliotek. Wspomniano, że interface może zawierać
własną część z danymi, nie wiemy jednak jak tego użyć. Możliwym jest
sklonowanie interface'u w locie lub zmodyfikowanie go w zależności od jego
zawartości. Więcej na ten temat w następnych numerach CAM.
Autor: Hans-Jörg Frieden, Hyperion Entertainment
Club Amiga logo concept and artwork by Mark Rickan & Mohamed Moujami, winners of the Club Amiga Logo Contest.
Submitted text and images reproduced in Club Amiga Monthly are copyright by their respective authors.
All other text and images reproduced in Club Amiga Monthly are copyright 2003 Amiga Inc.
Content may not be reprinted or reproduced in part or in whole without express written consent of Amiga Inc.