• Język Lua w MorphOS-ie

26.12.2012 10:04, autor artykułu: Grzegorz Kraszewski
odsłon: 3849, powiększ obrazki, wersja do wydruku,

Wprowadzenie

Każdy użytkownik komputera po przekroczeniu pewnego stopnia zaawansowania zaczyna doceniać fakt istnienia w systemie operacyjnym języka skryptowego. W sposób szczególny doceniają to amigowcy, albowiem ARexx - język przyjęty dla AmigaOS jako standardowy - posiada cechy wyjątkowe. Oprócz klasycznej funkcji, polegającej na automatyzacji zadań wykonywanych w linii komend, ARexx potrafi sterować aplikacjami poprzez specjalne porty komunikacyjne zwane potocznie – jakże by inaczej – portami ARexxa. W ten sposób prostym skryptem można połączyć ze sobą kilka programów tworząc z nich jeden super-program na potrzeby konkretnego zadania. Ponieważ standard się przyjął i praktycznie każdy program użytkowy na Amigę, a obecnie również i MorphOS-a jest wyposażony w port Arexxa, potęga takiego rozwiązania jest ogromna.

ARexx jednakże nie jest standardem języka skryptowego w MorphOS-ie. Przyczyna jest oczywista, jest to produkt będący elementem komercyjnego AmigaOS. Można by pomyśleć o zakupie licencji, gdyby nie fakt, że kody źródłowe zaginęły, zdaje się, w pomroce dziejów. Wiadomo też skądinąd, że ARexx był napisany w asemblerze procesora M68k. Nawet AmigaOS 4 zawiera te same pliki wykonywalne Arexxa (dla procesora M68k), co AmigaOS 2.0, w którym to język pojawił się po raz pierwszy. Mimo tego, że można uruchomić i używać ARexxa pod MorphOS-em kopiując pliki z AmigaOS, języka tego nie znajdziemy w standardowej dystrybucji systemu.

Warto też zauważyć, że napisany w 1987 roku ARexx jest implementacją języka REXX, którego historia sięga roku 1979. Przez 32 lata, jakie minęły od tego czasu, języki skryptowe i techniki programowania nie stały w miejscu. Mimo swoich niezwykłych możliwości, ARexx jako język programowania stał się językiem archaicznym. Dzięki temu, że standard portów ARexxa jest nieźle udokumentowany, istnieje możliwość rozszerzenia innych języków skryptowych o możliwość komunikowania się z aplikacjami przez porty ARexxa. Jako pierwszy w MorphOS-ie (a także w AmigaOS 4) zdobył taką możliwość popularny język skryptowy Python. Jest to jednak język dość ociężały (interpreter Pythona zajmuje pod MorphOS-em ponad 2 MB), borykający się ze wzajemną niekompatybilnością kolejnych wersji. Nie każdemu musi się też podobać to, że składnia Pythona wymusza określone formatowanie kodu, co jest zaskakujące dla programisty znającego praktycznie każdy inny popularny język programowania.

Jeżeli nie Python, to co? Poszukując lekkiej, szybkiej, łatwej do nauczenia się i elastycznej alternatywy dla ARexxa natknąłem się na język Lua. Lua ma dość egzotyczne pochodzenie - powstała bowiem w Brazylii (po portugalsku „Lua” oznacza Księżyc). Prace rozpoczęto w 1993 roku. Standard języka ustalił się wraz z wersją 4.0 w roku 2000, ostatnie większe zmiany zaszły w wersji 5.0 w roku 2003, a obecna wersja stabilna 5.1.4 pochodzi z roku 2008. Świadczy to o stabilności języka i zachęciło mnie do jego przeniesienia na MorphOS-a.

Lua jest językiem przystępnym. Tak zwana krzywa uczenia się nie jest stroma i łatwo zacząć programowanie. Z drugiej strony nie jest to na pewno język prymitywny. Po wstępnej zabawie w proste procedurki i oswojeniu się z językiem, programista może sięgnąć po techniki takie jak klasy, dziedziczenie, enkapsulację danych, przestrzenie nazw, moduły, a nawet bardziej egzotyczne techniki, np. programowanie funkcjonalne. Przy rozwiązywaniu prostych problemów można spokojnie pozostać na terenie klasycznego programowania proceduralnego. Lua jest też językiem lekkim. Biblioteka lua.library zawierająca interpreter liczy sobie niecałe 150 kB. Ponieważ język ten znalazł bardzo wiele zastosowań, szczególnie jako język wbudowywany w aplikacje, jest dość popularny. W sieci znajdziemy wiele stron i forów poświęconych Lua i programowaniu w tym języku, ukazało się też drukiem kilka książek na jego temat, niestety na razie bez polskich tłumaczeń.

Port

Standardowy interpreter języka Lua jest napisany w przenośnym języku C. Jego skompilowanie na MorphOS-ie jest trywialne: make, configure i gotowe. Wtedy dostajemy jednak po prostu kolejny interpreter języka skryptowego jako polecenie konsoli. Zero jakiejkolwiek integracji z systemem. MorphOS-owy port Lua ma postać biblioteki systemowej nazwanej lua.library. Ma to sens, bo Lua może być używana w niespotykany np. dla ARexxa sposób - mianowicie można interpreter wbudować w aplikację. Dzięki temu można w Lua pisać np. wewnętrzną logikę w grach, przetwarzać teksty, pozwolić użytkownikom na pisanie rozszerzeń do programu, tworzyć złożone pliki konfiguracyjne i tak dalej. Na innych systemach operacyjnych najczęściej linkuje się statycznie Lua z programem. Pod systemem MorphOS wystarczy otworzyć lua.library, dzięki temu wszystkie programy korzystają z tej samej kopii interpretera.

Kolejną ważną cechą Lua są moduły, które rozszerzają język o nowe możliwości. Moduły można pisać w Lua, można je także kompilować np. w C. Moduły mogą być ładowane dynamicznie na żądanie, standardowy interpreter zawiera kod dynamicznego ładowania dla Windows (biblioteki DLL) i systemów uniksowych (shared objects). W wersji dla systemu MorphOS zaimplementowałem dynamiczne moduły korzystające z systemowych bibliotek (*.library). Dodatkowo wszystkie moduły standardowe Lua przeniosłem do bibliotek zewnętrznych, co dodatkowo odchudziło interpreter. Rzadko kiedy w skrypcie potrzebne są wszystkie moduły standardowe, przez ich przeniesienie do plików zewnętrznych zużycie pamięci jeszcze się zmniejsza. Oczywiście skoro moduły Lua są systemowymi bibliotekami MorphOS-a, wszystkie skrypty ładujące dany moduł korzystają z tej samej kopii modułu w pamięci, moduł może też zostać usunięty z pamięci, gdy nie jest już potrzebny.

Lua jako zamiennik ARexxa

Ponieważ Lua bardzo łatwo rozszerza się poprzez moduły, dodanie obsługi portów ARexxa odbywa się przez załadowanie odpowiedniego modułu nazwanego ipc.module. Moduł ten dodaje do języka dwie nowe funkcje: address() i rx(). Pierwsza jest odpowiednikiem arexxowego polecenia ADDRESS i ustala nazwę portu, do którego będą kierowane wywołania. Analogicznie do ARexxa obsługiwany jest też wirtualny port „COMMAND”, który kieruje polecenia do DOS-a. Druga z funkcji służy do wydawania poleceń. Nie ma ona arexxowego odpowiednika. ARexx ma tę unikalną cechę, że komendy programów można pisać wprost w skrypcie, interpreter potrafi sam je ze skryptu „wyłowić”. W Lua aż tak miło nie ma, jednak nie jest to wielki problem. Oto prosty przykład, rysujący kilka kresek w programie TVPaint:

require("ipc")
address("rexx_TVPaint")

rx("tv_SetDrawMode", "Color")
rx("tv_SetAPen", 0, 0, 0) 

for i = 0, 100, 10 do
  rx("tv_Line", 100 + i, 100, 300 + i, 300)
end

Jak widać, w prostych zastosowaniach przejście z ARexxa na Lua nie jest trudne. Lua potrafi też oczywiście odbierać wyniki poleceń ARexxa. Obsługiwany jest zarówno wynik RC (poprzez globalną zmienną „rc” w Lua) jak i RESULT (jako wynik polecenia rx()), przy czym nie jest wymagane użycie polecenia OPTIONS RESULTS. Oto przykład - odczytanie wartości ARGB piksela w TVPaincie. Można je dopisać na końcu poprzedniego skryptu:

require("base")
require("string")

argb = rx("tv_GetPixel", 100, 101)
a, r, g, b = string.match(argb, "(%d+) (%d+) (%d+) (%d+)") 
print(a, r, g, b)

Kod ten pokazuje też użycie modułu standardowego string.module do parsowania łańcuchów tekstowych. Wyniki z portów ARexxa są zawsze przekazywane jako teksty, w tym przypadku tekst zawiera 4 liczby rozdzielone spacjami. W pokazany wyżej sposób, można rozbić ten tekst na cztery liczby. Moduł string umożliwia przetwarzanie tekstów z wykorzystaniem wyrażeń regularnych znanych np. z Perla, choć są tu drobne różnice.

Uważny czytelnik zauważy, że przykładowy skrypt jest nieco beztroski, bo nie sprawdza, czy program, mający być celem wysyłanych komend, jest uruchomiony. Do sprawdzenia tego w Arexxie używaliśmy polecenia WaitForPort. Moduł ipc.module posiada również funkcję waitforport(), której używa się na przykład w taki sposób:

ipc.waitforport("rexx_TVPaint", 10)

Po umieszczeniu tego kodu na początku skryptu, Lua będzie czekać maksymalnie 10 sekund na uruchomienie TVPainta, a jeżeli to nie nastąpi, skrypt zakończy działanie z komunikatem błędu.

Niestety Lua nie będzie prawdopodobnie w stanie zastąpić ARexxa w stu procentach, szczególnie jeżeli chodzi o starsze programy. Niektóre z nich wysyłają polecenia ARexxa bezpośrednio do portu „REXX”, nie korzystając z pośrednictwa programu rx. Lua mogłaby tworzyć taki port i przechwytywać polecenia zakładając, że są one napisane w Lua. Takie rozwiązanie być może miałoby szansę działać np. z programem AmIRC (polecenia definiowane w zakładce „Zdarzenia”). Niektóre inne aplikacje mogą jednak wysyłać polecenia, które nie są edytowalne przez użytkownika. Wtedy nie dość, że nie zostałyby rozpoznane przez interpreter Lua, to niemożliwe byłoby jednoczesne używanie Lua i ARexxa. W systemie nie mogą istnieć dwa porty o tej samej nazwie. Pewnym, nieco hakerskim rozwiązaniem, byłaby bezpośrednia modyfikacja (edytorem szesnastkowym) programu RexxMast tak, aby tworzył port o innej nazwie, np. „REX2”. Wtedy Lua przechwytywałby polecenia do swojego portu „REXX”, a jeżeli byłyby niezrozumiałe, przekazywałby je dalej do wykonania ARexxowi. Być może kiedyś zaimplementuję takie rozwiązanie.

Użycie Lua wewnątrz programu

Lua używany jako zamiennik ARexxa, komunikuje się z programem niejako z zewnątrz. Nic nie stoi jednak na przeszkodzie, żeby pisząc program, np. w C, jego część napisać w Lua, tworząc wewnątrz programu instancję interpretera. Jakie mogą być powody takiego działania? Są problemy programistyczne, które w Lua może być po prostu wygodniej rozwiązać. Przykładem mogą być bardziej złożone operacje na dynamicznych tekstach. Fragment w Lua można też ładować z pliku, co pozwoli użytkownikowi na zmianę działania programu. Takie podejście wykorzystuje (niestety na razie nie w pełni ukończony) program do IRC – Sermonatrix. Reakcję programu zarówno na komendy własne, jak i pojawiające się na kanale IRC teksty użytkownik może oskryptować w Lua.

Wymiana danych między kodem w C (czy innym języku) a skryptem Lua jest bardzo prosta i wykonywana za pomocą funkcji z biblioteki lua.library. Parametry dla funkcji w Lua są umieszczane na specjalnym stosie (nie należy go mylić ze sprzętowym stosem procesora) tworzonym przez Lua. Wyniki funkcji (których w Lua może być wiele) są, po jej wykonaniu, do wzięcia z tegoż stosu.

Dla przykładu rozwiążmy następujący problem. Mamy łańcuch tekstowy zawierający działanie matematyczne, na przykład „62 - 71 * 12 + 102 / 3”. Działanie to jest za każdym razem inne, wpisywane przez użytkownika. Trzeba wyznaczyć jego liczbowy wynik. Napisanie tego w C jest oczywiście możliwe, niemniej z pewnością zajmie trochę czasu. W Lua zadanie można rozwiązać na przykład tak:

require('string')
require('base')

function muldiv(op1, oper, op2)
  if (oper == '*') then x = op1 * op2
  else x = op1 / op2
  end
  return x
end

function addsub(op1, oper, op2)
  if (oper == '+') then x = op1 + op2
  else x = op1 - op2
  end
  return x
end

function kalkulator(wyr)
  repeat
    wyr2 = string.gsub(wyr, '(%-?%d+)%s*([%*/])%s*(%-?%d+)', muldiv, 1)
    brak_zmiany = (wyr == wyr2)
    wyr = wyr2
  until brak_zmiany
  repeat
    wyr2 = string.gsub(wyr, '(%-?%d+)%s*([%+%-])%s*(%-?%d+)', addsub, 1)
	brak_zmiany = (wyr2 == wyr)
    wyr = wyr2
  until brak_zmiany
  return tonumber(wyr)
end

Funkcja kalkulator() działa w ten sposób, że wyszukuje kolejne działania od lewej do prawej i każde znalezione działanie elementarne wraz z jego argumentami zastępuje jego wynikiem. Aby zachować kolejność działań, w pierwszej pętli przeglądane są mnożenia i dzielenia. Po ich eliminacji – dodawania i odejmowania. Ostatecznie po eliminacji wszystkich działań pozostaje jedna liczba, czyli końcowy wynik. A oto jak wygląda załadowanie, przekazanie parametru, wykonanie i odebranie wyniku z poziomu programu w języku C:

if (LuaBase = OpenLibrary("lua.library", 50))
{
  LuaState *L;
  struct LuaMemoryData lmd;

  if (L = LuaNewState(NULL, NULL))
  {
    char *wyrazenie = "8 - -7 * 2 + 10 / 4";
    LONG wynik;

    lmd.Buffer = Skrypt;
    lmd.Length = sizeof(Skrypt) - 1;

    if (LuaLoad(L, LUA_READER_MEMORY, &lmd, "kalkulator") == 0)
    {
      LuaPCall(L, 0, 0, 0);
      LuaGetGlobal(L, "kalkulator");
      LuaPushString(L, wyrazenie);
      LuaPCall(L, 1, 1, 0);
      wynik = LuaToInteger(L, -1);
      Printf("%s = %ld\n", wyrazenie, wynik);
    }
    else PutStr(LuaToString(L, -1));

    LuaClose(L);
  }
  CloseLibrary(LuaBase);
}

Po klasycznym otwarciu biblioteki, funkcja LuaNewState() tworzy interpreter. Następnie funkcja LuaLoad() ładuje skrypt ze zwykłej tablicy typu char, w której umieszczony jest skrypt Lua pokazany wyżej. Pierwsze wywołanie LuaPCall() wykonuje nam cały skrypt. Ponieważ jednak zawiera on wyłącznie definicje funkcji, to wykonanie spowoduje tylko załadowanie modułów zewnętrznych wyszczególnionych przez require() oraz zapamiętanie funkcji. Kolejnym krokiem jest wrzucenie na stos Lua funkcji kalkulator(), a następnie jej parametru, czyli wyrażenia do obliczenia. Drugie wywołanie LuaPCall() wykonuje naszą robotę. Dwie jedynki jako argumenty oznaczają, że przesyłamy jeden argument do Lua i odbieramy jeden wynik.

Oczywiście - jeżeli obliczenie wyrażenia będziemy wykonywać wielokrotnie, powtarzać trzeba tylko cztery linie kodu, począwszy od LuaGetGlobal(). Co więcej, jeżeli zachodzi taka potrzeba, można w jednym skrypcie umieścić wiele funkcji i po jego interpretacji wywoływać te funkcje wedle uznania. Gdybyśmy z jakichś względów woleli ładować skrypt z pliku (np. aby umożliwić użytkownikowi jego modyfikację), w kodzie zmieni się tylko tyle, że w LuaLoad() pojawi się stała LUA_READER_FILE, a zamiast wskaźnika na strukturę LuaMemoryData opisującej bufor w pamięci, damy po prostu ścieżkę do skryptu.

Nic nie stoi na przeszkodzie, aby w takim skrypcie wewnętrznym aplikacji wysyłać komendy Arexxa do innych programów. Możliwości, jakie się otwierają, są nieograniczone.

Warto się przyjrzeć ile „kosztuje” takie użycie skryptu Lua, w sensie zapotrzebowania na pamięć i szybkości wykonania. Wstępną ceną uruchomienia skryptu jest załadowanie do pamięci biblioteki lua.library (150 kB), base.module (20 kB) i string.module (22 kB) - razem 192 kB pamięci. Są to jednakże biblioteki współdzielone, zatem jeżeli zostały wcześniej otwarte przez inny program, żadna dodatkowa pamięć nie zostanie zajęta. Zużycie pamięci przez interpreter wykonujący skrypt można precyzyjnie zmierzyć programem Lua Explorador, który mierzy na bieżąco w czasie wykonywania skryptu aktualne i szczytowe zużycie pamięci. Dla skryptu kalkulatora szczytowe zużycie wynosi 14 kB, uwzględnia to pamięć niezbędną na załadowanie skryptu i przetłumaczenie go na kod „maszyny wirtualnej” Lua.

Jeżeli chodzi o szybkość wykonania, to oczywiście kod w Lua będzie znacznie wolniejszy niż procedura w C. Jeżeli jednak przetwarzane wyrażenie jest wprowadzane przez użytkownika, to kwestia czy będzie ono obliczane przez jedną milisekundę, czy 20 milisekund ma znaczenie wtórne. Sprawa przedstawiałaby się inaczej, gdyby nasz program musiał wczytywać z pliku i przetwarzać setki tysięcy takich wyrażeń, wtedy z pewnością dodatkowa praca włożona w napisanie parsera w języku C, przyniosłaby efekt.

W przypadku używania fragmentów w Lua w programie kompilowanym zawsze idziemy na pewien kompromis – ułatwiamy sobie i przyspieszamy pisanie programu, kosztem szybkości jego wykonania i wymagań pamięciowych. Wymagania pamięciowe nie są jednak, jak pokazałem powyżej, ogromne, natomiast szybkość wykonania nie zawsze jest sprawą krytyczną. W wielu przypadkach jest to cena, jaką warto zapłacić.

Plany na przyszłość

Port Lua dla MorphOS-a znajduje się wciąż w wersji 0. Oznacza to, że jakiś rozwój jest na pewno planowany. Przede wszystkim trzeba przeportować brakujące jeszcze moduły standardowe Lua – math, os, table, coroutine i debug. Pracy wymaga również Lua Explorador. Nieco bardziej ambitnym zamierzeniem jest umożliwienie pisania w Lua kompletnych programów używających MUI. Chodzi przy tym nie tylko o proste programiki bazujące jedynie na notyfikacjach. Bardzo ważne jest umożliwienie pisania w Lua własnych klas MUI, bo tylko w ten sposób można w pełni wykorzystać potencjał MUI i jego możliwości. Przydatne będzie z pewnością udostępnienie poprzez moduły innych systemowych bibliotek, chociażby locale.library.

Wierzę w to, że za jakiś czas Lua będzie językiem, w którym da się zrobić wszystko to, co w C, oczywiście z wyjątkiem rzeczy „blisko systemu” takich jak biblioteki współdzielone czy sterowniki urządzeń. Różnica będzie też w wydajności, wszak Lua jest językiem interpretowanym. Istnieje jednak prężnie się rozwijający projekt LuaJIT – interpreter Lua używający technik dynamicznej kompilacji. Jest on tak napisany, że można łatwo zastąpić nim standardowy interpreter. Dynamiczna kompilacja – w dużym uproszczeniu – polega na tym, że fragmenty programu wykonywane więcej niż raz, są tłumaczone na kod maszynowy procesora i zapamiętywane. Przy kolejnym wykonaniu fragmentu nie następuje już interpretacja programu Lua, zamiast tego procesor wykonuje swój natywny kod z pełną prędkością. Świetnie dla MorphOS-a się składa, że LuaJIT obsługuje również procesory PowerPC. W przyszłości z pewnością przymierzę się do zastosowania LuaJIT w morphosowym porcie języka.

Odnośniki

Strona domowa języka Lua
Podstawowa dokumentacja i specyfikacja języka
Baza wiedzy o Lua rozwijana na zasadzie wiki
Pierwsze wydanie książki "Programming in Lua" w wersji elektronicznej
Strona o Lua po polsku, jest tam również tłumaczenie specyfikacji języka, ale niedokończone
Najnowsza wersja Lua dla MorphOS-a

Artykuł oryginalnie pojawił się w szóstym numerze Polskiego Pisma Amigowego.

    tagi: Lua, programowanie
dodaj komentarz
Na stronie www.PPA.pl, podobnie jak na wielu innych stronach internetowych, wykorzystywane są tzw. cookies (ciasteczka). Służą ona m.in. do tego, aby zalogować się na swoje konto, czy brać udział w ankietach. Ze względu na nowe regulacje prawne jesteśmy zobowiązani do poinformowania Cię o tym w wyraźniejszy niż dotychczas sposób. Dalsze korzystanie z naszej strony bez zmiany ustawień przeglądarki internetowej będzie oznaczać, że zgadzasz się na ich wykorzystywanie.
OK, rozumiem