Java Mobile w JavaScripcie

Tak się złożyło, że pod wpływem ostatnio panującej mody na Firefox OS od miesiąca klepię taki swój mały projekt. Mianowicie chodzi o emulator J2ME działający pod Firefox OS. Na razie efekt jest taki:

Można sobie pograć w Raymana. Prawda że fajne? Celem tego wpisu jest przedstawienie jakim cudem to działa.

Ale o czym ty w ogóle piszesz?

Tytułem wstępu, czym jest J2ME (a raczej Java ME, bo Oracle już dawno wywalił te dwójki z nazw swoich produktów, ale ja wolę taką nazwę), bo pewnie nie każdy o tym słyszał (a raczej słyszał, ale pod nieco inną nazwą). Otóż w dawnym czasach, gdy superkomputery miały moc obliczeniową współczesnych spinaczy do papieru, a przeciętną komórką można było zabić człowieka jedyne aplikacje, których można użyć w telefonie były te wbudowane w system. Prawda, że taka wizja nie wygląda zbyt ciekawie, kiedy dzisiaj można za jednym zamachem zrobić aplikację obejmującą niemal połowę rynku?

Była wtedy taka nieistniejąca już firma o nazwie Sun ([*]), która postanowiła rozwiązać ten problem i przy okazji zarobić trochę na opłatach licencyjnych. Mieli wtedy coś, co bardzo ułatwiło im osiągniecie tego celu, mieli Javę. Jedyne co musieli zrobić to upchać tę kobyłę do liczonej wtedy w kilobajtach pamięci telefonu komórkowego i zainteresować producentów.

Pierwsza wersja J2ME pojawiła w 1999. Układ był prosty, producenci telefonów płacą Sunowi, a ten daje pozwolenie i pomoc w zaimplementowaniu tego standardu w swoich telefonach. Pomysł wypalił, bo w ciągu kilku kolejny lat pojawiły się pierwsze urządzenia z implementacją J2ME, a wkrótce już trudno było kupić telefon, który by nie miał w sobie Javy. Aplikacje oparte na Javie w tamtych czasach po prostu opanowały komórki i ostatnią stronę Telemagazynu ("ostatnia strona Telemagazynu" to ówczesny AppStore).

Jednak nic nie trwa wiecznie, współcześnie o aplikacjach na J2ME słyszy się raczej w podaniach ludowych niż widzi na co dzień. Jak nietrudno się domyśleć to sprawka Androida i iOS, które opanowały rynek w ostatnich latach. Oczywiście dalej wielu ludzi ma featurephony i istnieją firmy, które tworzą aplikacje na J2ME, ale dzisiaj to już margines.

Historia historią, ale to jest blog techniczny

Generalnie w standardzie J2ME ważne są dwie główne specyfikacje: CLDC i MIDP. Ta pierwsza to najogólniej mówiąc skrójka J2SE (to ta, która ciągle woła o aktualizację). Oficjalny dokument odsyła nas do poczytania specyfikacji JVM 1.3 (Wirtualna Maszyna Javy, rdzeń tych wszystkich "jotek"), mówi co z tego standardu odrzucić, a następnie opisuje swoją bibliotekę standardową, która jest całkowicie kompatybilna z tą z "dużej" Javy, ale klas jest znacznie mniej, a te które zostały mają mniej metod niż ich starsi bracia (taki najbardziej dyrastyczny przykład: CLDC vs. J2SE). Wszystko po to, żeby gotowe implementacje mogły zajmować jak najmniej pamięci. Samo CLDC do tworzenia aplikacji się nie nadaje, bo jedynym sposobem na kontakt ze światem wewnętrznym jest standardowe wyjście. W zamierzeniu miała służyć za podstawę dla kolejnych specyfikacji dla różnych urządzeń. W przypadku komórek taką specyfikacją jest MIDP. Rozszerza CLDC o kolejne klasy, które pozwalają na komunikację aplikacji z urządzeniem czyli m.in. GUI, dźwięk, zbiornik na dane. Oprócz tych dwóch z biegiem latach powstało kilka innych specyfikacji, które są opcjonalne przy certyfikacji. Oprócz tego niektórzy producenci wprowadzili kilka własnych klas. Przez to mimo wszystko jakaś tam fragmentacja jednak istniała i niektórych aplikacji nie można było odpalić na każdym scertyfikowanym telefonie. Poza tym w 2009 pojawiła wersja 3.0 specyfikacji MIDP, ale do dzisiaj prawie nikt się nią nie zainteresował.

Uważny czytelnik po tym krótkim opisie domyśli się, że tak naprawdę każda implementacja zgodna z J2SE (a nawet ta karykatura na Androidzie) jest też zgodna z CLDC. Tak, właśnie dlatego każdy normalny człowiek pisze emulatory J2ME w Javie, bo do zaimplementowania pozostaje tak naprawdę tylko MIDP. A teraz zobaczmy, jak to wygląda w JavaScripcie...

Podstawy

Mimo że nie mam ułatwienia w postaci JVM (przynajmniej wedle mojej wiedzy czegoś takiego na Firefox OS nie ma) to postarałem się jak najbardziej oprzeć się na tym co jest w JS. Obiektowość jest (co prawda przez prototypowanie, ale to nie problem odpowiednio ją obudwać), zarządzanie pamięcią jest, wyjątki są... i to chyba tyle. Wszystkie klasy Javy trzymam przestrzeni nazw javaRoot, do tego dodaję znak dolara do nazw klas i pakietów (czyli np. java.lang.String mapuje się u mnie jako javaRoot.$java.$lang.$String). Z metodami jest nieco gorzej, bo w Javie np. metoda X, która przyjmuje jako argument Stringa to zupełnie inna metoda niż taka o tej samej nazwie i w tej samej klasie, ale przyjmująca jako argument Integera. Rozwiązałem to generując nazwy metod opierając się też na typach argumentów i zwracanej zmiennej. Przez to teraz istnieją takie potworki jak $setCommandListener$Ljavax_microedition_lcdui_CommandListener_$V. Zmniejsza to czytelność kodu, ale nic lepszego mi nie przyszło do głowy.

JAR

Paczką w której przenoszą się aplikacje J2ME podobnie jak wszystkie inne aplikacje w Javie jest format JAR. Jak pewnie wiecie JAR jest zwykłym ZIPem, w którym pliki są poukładane wedle pewnej logiki. Do otwarcia takiej paczki użyłem gotowego rozwiązania, które zwie się zip.js. Ważny fakt to że aplikacja może chcieć odczytać każdy plik z paczki. Żeby ułatwić sobie życie zawartość wszystkich plików trzymam w pamięci. Te JARy nie są duże, a najsłabszy znany telefon z Firefox OS ma mieć 512 MB RAM, więc chyba mogę sobie pozwolić na takie ułatwienie.

Wczytywanie klas

Kolejnym etapem obrobienia takiej aplikacji to wczytanie wszystkich klas. To kolejne ułatwienie z mojej strony, bo maszyny wirtualne zazwyczaj doczytują je dopiero, gdy są potrzebne, ale póki co takie rozwiązanie jest wystarczająco dobre. Jeden plik .class zawiera opis jednej klasy. Mimo że w pojedyńczym pliku z kodem źródłowym można zdefiniować kilka klas to kompilator stworzy nam kilka plików .class.

Opiszę ogólnie z czego się taki plik składa. Zaraz po nagłówkach (0xCAFEBABE) i tego typu pierdołach znajduje się dość ciekawa struktura, która jest podstawą dla prawie wszystkiego, co jest opisane w dalszej części pliku. Nazywa się to Constant Pool, jest to po prostu tablicą ze stałymi. Najbardziej podstawowym jest UTF8 czyli jak łatwo się domyśleć ciąg znaków. Nie mylić z Javowym typem String, takie stałe są osobnym typem i odwołują się po prostu do stałych UTF8. Poza tym jak łatwo się domyśleć są Inty, Floaty i parę innych typów. Z bardziej złożonych mamy klasy, pola, metody itd. Z powodu tego, że różne stałe odwołują się do siebie nawzajem po wczytaniu ich wszystkich przerabiam je na bardziej złożone struktury, żeby nie musieć potem skakać po tej całej tablicy.

Następnie pozostaje wczytać całą strukturę klasy. Tak jak mówiłem wszystko się odwołuje do Constant Pool czyli np. nazwa klasy jest podana jako indeks stałej typu ClassInfo. W dalszej części pliku są podane podstawowe informacje o klasie, pola, metody, jakie dana metoda łapie wyjątki i co najważniejsze - bytecode.

Kod bajtowy

To jest najważniejszy element całej tej zabawy, bo tak naprawdę głównym zadaniem maszyny wirtualnej jest wykonywanie tego kodu. Każda metoda ma swój, w zapisie symbolicznym przypomina trochę instrukcje Assemblera, tylko że zamiast operować na pamięci, operuje na obiektach, a zamiast rejestrów ma stos i zmienne lokalne. Po krótce wyjaśnię na jakiej zasadzie działa to w praktyce:

0 iload_1
1 ldc #59
3 iadd
4 istore_3
5 ireturn

Przede wszystkim każda metoda na początku wykonywania kodu w zmiennej lokalnej nr. 0 ma obiekt na którym ta metoda jest wołana (z wyjątkiem metod statycznych). W kolejnych slotach umieszczane są argumenty metody. Załóżmy, że podana tutaj metoda dostała jako argument liczbę 5. Pierwsza instrukcja wrzuca na stos zmienną lokalną nr. 1 (czyli liczbę 5), druga natomiast z wrzuca stałą z indeksem 59 (umówmy się, że leży tam liczba 9). Warto też zwrócić uwagę, że zajmuje dwa bajty (jeden, żeby zapisać kod instrukcji i drugi, żeby zapisać liczbę 59). Instrukcja iadd ściąga ze stosu dwie liczby, dodaje je ze sobą i wrzuca z powrotem wynik. Ostatnia instrukcja ściąga go ze stosu i zwraca jako wynik metody. Czyli nasza metoda zwraca liczbę 14. Jak pewnie się domyślacie te literki "i" na początku każdej operacji oznaczają, że tyczą się one tylko liczb całkowitych. Gdybyśmy chcieli zwrócić np. referencję do jakiegoś obiektu, użylibyśmy areturn.

No dobra, wszystko jasne tylko jak to wykonywać w JS? Pierwsze moje podejście polegało na generacji kodu JS i tworzenie metod konstruktorem Function. Czyli np. z iload_1 generował stack.push(locals[1]). Niestety pomysł upadł, gdy tylko pojawiły się instrukcje skoku. Brak goto w JS (komentarze w stylu "goto to zło" będą usuwane) kompletnie skomplikował sprawę. Biblioteki, które miały imitować jego działanie zawsze miały jakieś ograniczenia. Najgorsza jest świadomość, że kod JS przez przeglądarkę pewnie też jest kompilowany do jakiegoś swojego kodu, który na pewno zawiera instrukcje skoku.

Kolejne rozwiązanie to bezpośrednia interpretacja kodu. Tworzyłem po prostu funkcję, która w domknięciu dostawała tablicę z kodem, pulę stałych i co tam jeszcze było potrzebne. W momencie wykonanywania przygotowywała swój stos itd., a następnie interpretowała każdy bajt po kolei.

Trzecim rozwiązaniem, którego używam w tej chwili jest zmielenie kodu do własnej struktury, która jest tablicą funkcji. Każda instrukcja jest przerabiana na pojedyńczą funkcję. Fragment który wcześniej interpetował kod teraz po prostu wykonuje po kolei funkcje z tej tablicy.

Idealnie byłoby generować kod JS podobny do pierwotnego kodu w Javie, ale to odpada po pierwsze dlatego, że pisanie dekompilatora jest raczej trudne, a po drugie nawet te dekompilatory, które są nie zawsze potrafią wygenerować pierwotny kod. Oczywiście postaram się jakoś zbliżyć do tego, ale na razie obecna wydajność wydaje się wystarczająca.

Natywne klasy

Wszystkie klasy dostarczane razem z implementacją są napisane w JS. Oczywiście obudowałem je trochę, żeby imitowały te wygenerowane z pliku .class. Dzięki temu są dla siebie nawzajem widoczne z klasami Javy.

Typy prymitywne

Ponieważ w JS wszystkie liczby są tym samym typem zrobiłem takie oto mapowanie:

  • boolean -> number (JVM trakuje boole jak liczby)
  • integer -> number
  • short -> number
  • byte -> number
  • char -> number (na początku był stringiem, ale z tym były czasem problemy)
  • long -> js2me.Long (number ma zbyt małą precyzję, żeby trzymać w nim liczby 64-bitowe, dlatego trzymam w dwóch, musiałem przez to zaimplementować własną arytmetykę, koszmar)
  • float -> number
  • double -> js2me.Double (tak, akurat ten typ, który jest odpowiednikiem number musiałem ubrać w obiekt, bo long i double są czasem traktowane inaczej niż pozostałe typy i potrzebowałem rozróżnienia)

Oprócz tego odpowiednie instrukcje odpowiedzialne za arytmetykę integer, short i byte symulują w razie czego overflow. Co do floata to wydaje mi się, że nie będzie większej tragedii, jeśli będzie zbyt precyzyjny.

Wątki

To chyba najbardziej porąbana rzecz w tym projekcie. JS nie ma niczego, co by mogło nadawać się na wątki. Oczywiście każdy porządny programista JS natychmiast pomyśli o WebWorkerach, niestety podczas komunikacji z głównym wątkiem są przesyłane jedynie kopie obiektów. Zbyt dużo problemów, żeby się w to pakować, a Keon i tak ma jeden rdzeń. Rozwiązanie: symuluję wątki samemu. Np. kiedy jakaś metoda wywoła metodę sleep natywna implementacja ustawia globalną flagę js2me.suspendThread, metoda wywołująca dostaje sygnał, żeby kończyć imprezę. Wszystkie wygenerowane metody, gdy zobaczą aktywowaną powyższą flagę wrzucają informacje o bierzącym stanie wykonywania na stosik przyporządkowanym pod aktualny wątek. Sytuacja eskaluje coraz niżej, aż się nie skończy się drzewo wywołań. Wtedy mój stos ma wszystkie informacje, żeby potem móc dokładnie odtworzyć moment usypiania. Oczywiście sleep zostawia po sobie timeouta, który w odpowiednim czasie odtwarza stos. Głupie, ale skuteczne

Ta technika przydaje się również w przypadku ładowania obrazków (BTW są one normalnymi obrazkami HTML, które jako src mają ustawiany DataURI stworzony na podstawie danych z pliku), gdzie przeglądarka robi to asynchronicznie, a w J2ME dzieje się to synchronicznie. Wtedy usypiam wątek dopóki nie załaduje się obrazek.

Rozwój projektu

Co prawda roadmapa jest publicznie dostępna, ale zamierzam tutaj dokładniej opisać jak powstawał ten projekt. Jak widać w pliku, który pewnie teraz otworzyłeś na pierwszy ogień poszedł Hello World. Wzięty na szybko z jakiegoś tutoriala i skompilowany. Jak sama nazwa mówi ta aplikacja nie robi nic innego oprócz wyświetlania napsiu "Hello World". Nikt kto nie tworzył nigdy podobnego projektu nie wie jaka to radość zobaczyć taką pierdołę. Dojście do poziomu uruchomienia jej zajęło mi kilka dni.

Potem poszedłem trochę ambitniej i wziąłem jakąś gierkę napisaną J2ME lata temu. Powód był prosty, znałem ją, wiedziałem jak działa, miałem kod źródłowy, a złożoność nie była zbyt duża. Gdy tylko poczułem się na siłach poszukałem cudzych i większych projektów, ale koniecznie open source, żeby móc się wesprzeć kodem źródłowym. Klon Asteroids (licencja GPL), który znalazłem nawet dołączyłem do repozytorium jako swego rodzaju prezentację dla osoby, która by chciała uruchomić mój projekt. Potem próbowałem kolejne aplikacje, niektóre udało mi się doprowadzić do działania, przy innych się poddawałem. Potem postanowiłem zająć się wydajnością i wynalazłem FPC benchmark. Oczywiście musiałem się trochę pomęczyć, żeby go odpalić, ale w końcu ruszył i wskazał mi wynik na poziomie bardzo starych telefonów (FPC ma w necie bazę wyników), zdecydowanie starszych niż przeciętne telefony w czasach, gdy J2ME miał swoje lata świetności. Tym bardziej, że uruchamiałem go na komputerze stacjonarnym. Procesor miałem skręcony do 800 MHz. Keon ma rdzeń A5 (jedna z najsłabszych architektur) taktowany na 1GHz (czyli musiałbym zejść pewnie do jakichś 400 MHz, żeby się z nim zrównać wydajnością). Na mojej komórce (Scorpion 1GHz, wciąż trochę lepszy od A5) ten test trwał tak długo, że wolałem wyłączyć. Wtedy dopisałem czwartego Milestone'a i kompletnie zmieniłem sposób wykonywania kodu bajtowego (dokładny opis kilka podpunktów wyżej). W połączeniu z kilkoma innymi optymalizacjami osiągnąłem wynik kilkanaście razy lepszy i na razie odpuszczam sobie dalszą optymalizację, chociaż spodziewam się, że może być kiedyś potrzebna.

Obecny stan

Na razie projekt jest na takim poziomie, że całkiem przyzwoita ilość aplikacji działa. Nie mówię tylko o tych, które używałem przy rozwoju, miałem sporo sytuacji, kiedy ściągnąłem jakąś aplikację dla testów, a ona działała bez błędów albo z drobnymi błędami. Na ten moment z prawie 200 instrukcji mam zaimplementowane niemal wszystkie (wedle mojej rozpiski brakuje dwunastu). Na 145 klas i interfejsów prawie wszystkie są zaimplementowane znikomo lub częściowo (w pełni chyba tylko 10, poza tym odpada wiele interfejsów, bo tam zwykle nie ma co implementować). W każdym razie obecny stan oceniam na tyle pozytywnie, że można z tego zacząć kleić aplikację, bo na razie to głównie silnik z paroma końcówkami, żeby móc go testować. Poza tym kolega załapał się na darmowy telefon z Firefox OS i dobrze było by sprawdzić aplikację na żywym sprzęcie.

Przyszłość

  • Midlet suite - Czyli sytuacja, kiedy aplikacja ma kilka Midletów. Specyfikacja dopuszcza taką sytuację, ale w praktyce to jest prawie niespotykane. Na razie odpalam tylko pierwszy, lepszy z brzegu Midlet.
  • MIDI - Może to brzmi niewiarygodnie, ale Gecko nie obsługuje natwynie tego formatu. W sieci znalazłem MIDI.js (uwaga: migoczące kolory i wkurzająca muzyczka), ale trochę boję się o wydajność tego rozwiązania. Na razie w grach wykorzystujących ten format (czyli 99% gier J2ME) można posłuchać uspokajającej ciszy.
  • Blutetooth, SMS, TCP - Czyli wszystko co wymaga spięcia z API Firefox OS. Na razie wygodniej jest testować w przeglądarce i używanie symulatora to ostateczność
  • M3G - Te tajemnicze literki to jedno z bardziej wymagających rozszerzeń, potrzebne do uruchomienia gier w 3D.
  • ...i inne - Oprócz podanych wyżej jest sporo innych rozszerzeń, na szczęście rzadko wykorzystywane, ale wypadałoby kiedyś je dodać.

tl;dr; Zrobiłem emulator J2ME, tu jest kod: https://github.com/szatkus/js2me