Dlaczego na jednym rdzeniu w Rust zbudowaliśmy osobne powłoki dla każdej platformy
Aby wydać ten sam edytor na trzy systemy operacyjne, można stworzyć interfejs tylko raz. Poszliśmy tą drogą, ale potem z niej zawróciliśmy i na jednym rdzeniu zbudowaliśmy zestaw cienkich powłok dla poszczególnych platform.

Aby wydać ten sam edytor na Linuksa, macOS i Windows, trzeba stworzyć interfejs trzy razy.
Początkowo chcieliśmy uniknąć tego kosztu. Za pomocą jednego wieloplatformowego zestawu narzędzi wyświetlaliśmy ten sam interfejs w trzech systemach operacyjnych. Dzięki tej decyzji przeszliśmy pierwszą bramkę wydajnościową oraz osiągnęliśmy pierwszy kamień milowy dotyczący wprowadzania tekstu po koreańsku.
Potem zmieniły się priorytety.
Wydajność jest ważniejsza od projektu, a projekt ważniejszy od kosztów. Czas i koszty przestają być ograniczeniami.
Gdy ponownie spojrzeliśmy na problem według tych kryteriów, odpowiedź również się zmieniła.
Zrezygnowaliśmy z jednego interfejsu
Wieloplatformowy zestaw narzędzi ma wyraźne zalety. Funkcji stworzonej raz można używać w wielu systemach operacyjnych.
Trzeba było jednak zapłacić za to pewną cenę. Zużycie pamięci było większe o 40–60 MB, a uruchamianie trwało o 150–200 ms dłużej. Wyświetlanie znaków ustępowało natywnym mechanizmom tekstowym systemów operacyjnych, a charakterystycznych dla systemu efektów wizualnych nie dało się w pełni wykorzystać.
Kiedy koszty rozwoju były istotne, był to rozsądny wybór. Gdy jednak usunęliśmy koszty z listy ograniczeń, nie było już powodu, by godzić się na te niedogodności.
Postanowiliśmy więc stworzyć rdzeń przetwarzający dokumenty w Ruście, a powłokę odpowiedzialną za interfejs przygotować osobno dla każdego systemu operacyjnego.
Nie dlatego, że dotychczasowa technologia była zła. Kiedy zmieniają się priorytety, ten sam zestaw możliwości może prowadzić do innej odpowiedzi. Postanowiliśmy ponieść koszt trzykrotnego tworzenia funkcji, aby odzyskać wydajność, jakość tekstu i doświadczenie zgodne z charakterem systemu.
Nie wyrzuciliśmy jednak wszystkiego.
Zachowaliśmy cały zestaw zasad dotyczących pomiarów. Umieszczanie wskaźnika poza oknem, precyzyjne znajdowanie okna według nazwy i synchroniczne liczenie klatek. Przenieśliśmy również zasady projektowania obsługi tekstu koreańskiego. Systemowi operacyjnemu pokazujemy tylko wiersz, w którym znajduje się kursor, a składanych właśnie znaków nie stylizujemy. Kiedy stan przestaje się zgadzać, nie zgadujemy, lecz ponownie go synchronizujemy.
Kod można wyrzucić, ale sprawdzone zasady pozostają.
Ograniczyliśmy rolę powłoki
Jeśli powłoki poszczególnych platform zaczną przejmować zbyt wiele zadań, trzy produkty szybko zaczną się od siebie różnić. Dlatego jasno określiliśmy, co powłoka ma robić, a czego nie.
Powłoka tworzy okno i wyświetla znaki. Przekazuje do rdzenia dane z klawiatury i wskaźnika oraz zamienia obsługę wprowadzania tekstu koreańskiego na polecenia edycji. Stosuje również efekty wizualne systemu operacyjnego.
Nie interpretuje natomiast Markdownu. Poza zawijaniem wierszy nie ustala również układu, nie zapisuje plików ani nie weryfikuje licencji.
Nie ograniczyliśmy się do zapisania tej zasady w dokumentacji. Jeśli do powłoki trafi kod interpretujący Markdown, automatyczna kontrola zatrzyma kompilację.
Jeśli składnia zacznie być przetwarzana tylko w jednej powłoce, wyniki mogą się nieznacznie różnić między systemami operacyjnymi. Takie problemy są również trudne do wykrycia. Tabela może wyglądać inaczej tylko w jednej powłoce, choć rzeczywista przyczyna wcale nie musi znajdować się w kodzie, który ją wyświetla.
Zadbaliśmy o to, aby tylko rdzeń przetrwał długo
Rdzeń nie zależy od żadnej technologii interfejsu użytkownika. Można go testować samodzielnie w terminalu, nawet bez okna.
Rdzeń odpowiada za edycję dokumentów i interpretowanie Markdownu, cofanie zmian, wyszukiwanie, konspekt, zapisywanie plików, eksportowanie oraz weryfikowanie licencji.

Kiedy użytkownik naciska klawisz, powłoka przekazuje dane wejściowe do rdzenia. Rdzeń modyfikuje dokument i ponownie interpretuje tylko otoczenie zmienionego fragmentu. Następnie oblicza wyłącznie treść widoczną na ekranie i zwraca powłoce zmieniony wynik. Powłoka ponownie wyświetla tylko wiersze, których dotyczy zmiana, i kończy jedną klatkę.
Jeśli w tym procesie cały dokument jest odczytywany ponownie, nie jest to zwykły problem z wydajnością. To złamanie ustalonego na początku budżetu.
Poprawność również weryfikujemy tylko raz, w rdzeniu. Muszą przejść wszystkie 652 oficjalne przykłady CommonMark, a jeśli liczba zaliczonych przykładów spadnie, kompilacja kończy się niepowodzeniem. Przeprowadzamy także testy fuzzingowe, które nieustannie dostarczają nieprzewidziane dane wejściowe.
Sprawdzamy też, czy zapisany plik jest identyczny z oryginałem na poziomie bajtów. Jeśli współczynnik pełnego zapisu i ponownego odczytu jest niższy niż 1,0, nie oznacza to, że działamy wolno, lecz że utraciliśmy plik.
Nawet jeśli powłoka się zmieni, rdzeń pozostaje ten sam. W Linuksie i Windows łączymy rdzeń bezpośrednio, natomiast w macOS korzystamy z kodu integracyjnego w Swifcie. Powłokę macOS tworzymy przy użyciu AppKit i TextKit 2, a powłokę Windows przy użyciu DirectWrite.
Naszym celem jest zachowanie identycznych dokumentów przy jednoczesnym zapewnieniu w każdym systemie właściwych dla niego odczuć podczas pracy.
Dokument jest taki sam, ale sposób pracy się różni
Po wyznaczeniu granic stało się również jasne, co należy ujednolicić.
Ujednolicony musi być dokument. Niezależnie od systemu operacyjnego, w którym zostanie zapisany, zawartość pliku musi być identyczna na poziomie bajtów. Nie mogą się również zmieniać wyrównanie tabel, zakończenia wierszy ani końcowy znak nowego wiersza.
Poprawność Markdownu jest już weryfikowana w rdzeniu. Nie trzeba jej ponownie dowodzić przy tworzeniu każdej nowej powłoki. Na nowej platformie należy sprawdzić sposób pracy i wydajność.
Nie ujednolicamy natomiast doświadczeń, które użytkownik odczuwa bezpośrednio.
Znaki wyświetlamy za pomocą mechanizmu tekstowego każdego systemu operacyjnego. Korzystamy również bezpośrednio z systemowej metody wprowadzania tekstu koreańskiego. Efekty półprzezroczystości w macOS i Windows także wykorzystują funkcje systemowe, natomiast w Linuksie, który nie oferuje takich samych funkcji, samodzielnie renderujemy tylko niezbędne elementy.
Nie próbujemy na siłę ujednolicać położenia przycisków okna, postaci menu ani klawisza modyfikującego w skrótach. Wyświetlanie identycznych pikseli w różnych systemach operacyjnych nie jest naszym celem. Ważniejsze jest to, aby edytor zachowywał się naturalnie w każdym z nich.
Warunki wydania również określiliśmy osobno dla każdego systemu operacyjnego. Wydanie jest możliwe dopiero po spełnieniu kryteriów wydajnościowych i ukończeniu praktycznych testów z domyślną metodą wprowadzania tekstu koreańskiego.
Wiersz danej platformy pojawia się w tabeli wyników testów wydajnościowych dopiero po utworzeniu rzeczywistej kompilacji. Gdybyśmy z góry wpisali wiersz, którego nie zmierzyliśmy, trudno byłoby zaufać także pozostałym wierszom tabeli.
Ten wybór ma wyraźną cenę
Tę samą funkcję trzeba stworzyć trzy razy. Obsługę tekstu koreańskiego również trzeba zaimplementować trzy razy i utrzymywać trzy środowiska kompilacji.
W obecnie używanym środowisku nie możemy kompilować ani debugować wersji dla Windows i macOS. Potrzebujemy osobnego sprzętu i środowisk automatyzacji. Dlatego nie podajemy terminów dla tych dwóch platform.
Określiliśmy również warunki ponownego rozpatrzenia tej decyzji. Założeniem tej decyzji jest wyłączenie kosztów z ograniczeń. Jeśli nie zdołamy zdobyć potrzebnego sprzętu, opóźnimy jedynie harmonogram danej platformy. Wydanie wersji dla Linuksa będzie przebiegać bez zmian.
Decyzja potrzebuje nie tylko uzasadnienia, lecz także warunków jej ponownego rozpatrzenia. Dzięki temu, gdy sytuacja się zmieni, nie trzeba od początku powtarzać tej samej dyskusji.
To rdzeń musi przetrwać długo. Powłokę trzeba móc w każdej chwili wyrzucić.
Platformom powierzyliśmy tylko to, co można wyrzucić.