Interfejsy API I/O w internecie są asynchroniczne, ale w większości języków systemowych są synchroniczne. Podczas kompilowania kodu na potrzeby WebAssembly musisz połączyć jeden typ interfejsu API z innym. Do tego służy Asyncify. Z tego posta dowiesz się, kiedy i jak używać narzędzia Asyncify oraz jak działa ta funkcja.
Wejście-wyjście w językach systemowych
Zacznijmy od prostego przykładu w języku C. Załóżmy, że chcesz odczytać imię i nazwisko użytkownika z pliku i przywitać go wiadomością „Cześć, (nazwa użytkownika)!”:
#include <stdio.h>
int main() {
FILE *stream = fopen("name.txt", "r");
char name[20+1];
size_t len = fread(&name, 1, 20, stream);
name[len] = '\0';
fclose(stream);
printf("Hello, %s!\n", name);
return 0;
}
Przykład nie robi zbyt wiele, ale pokazuje coś, co znajdziesz w aplikacji o dowolnej wielkości: aplikacja odczytuje dane wejściowe ze świata zewnętrznego, przetworzy je wewnętrznie i zapisze dane wyjściowe z powrotem na zewnątrz. Wszystkie takie interakcje ze światem zewnętrznym odbywają się za pomocą kilku funkcji, które nazywamy funkcjami wejścia-wyjścia, a które w skrócie nazywamy I/O.
Aby odczytać nazwę z C, musisz wykonać co najmniej 2 kluczowe wywołania I/O: fopen
, aby otworzyć plik, oraz fread
, aby odczytać z niego dane. Po pobraniu danych możesz użyć innej funkcji wejścia-wyjścia printf
, aby wydrukować wynik w konsoli.
Na pierwszy rzut oka te funkcje wyglądają dość prosto i nie musisz się zastanawiać nad mechanizmami, które są potrzebne do odczytu lub zapisu danych. W zależności od środowiska może się jednak działo sporo:
- Jeśli plik wejściowy znajduje się na dysku lokalnym, aplikacja musi wykonać serię operacji dostępu do pamięci i dysku, aby zlokalizować plik, sprawdzić uprawnienia, otworzyć go do odczytu, a następnie odczytać blok po bloku, aż do uzyskania żądanej liczby bajtów. Może to być dość powolne, w zależności od szybkości dysku i żądanego rozmiaru.
- Plik wejściowy może też znajdować się w lokalizacji podłączonej do sieci, co oznacza, że problem dotyczy także stosu sieciowego, co zwiększa złożoność, czas oczekiwania i liczbę potencjalnych ponownych prób każdej operacji.
- Na koniec warto dodać, że nawet polecenie
printf
nie zawsze drukuje na konsoli, ponieważ może zostać przekierowane do pliku lub lokalizacji sieciowej. W takim przypadku trzeba wykonać te same czynności.
Krótko mówiąc, operacje wejścia/wyjścia mogą być powolne i nie da się przewidzieć, ile czasu zajmie wykonanie określonego wywołania po pobieżnym przejrzeniu kodu. Podczas wykonywania tej operacji cała aplikacja będzie wyglądać na zablokowaną i nie będzie reagować na działania użytkownika.
Nie dotyczy to języka C ani C++. Większość systemowych języków programowania udostępnia wszystkie dane wejściowe i wyjściowe w formie synchronicznych interfejsów API. Jeśli na przykład przetłumaczysz przykład na Rust, interfejs API może wyglądać na prostszy, ale obowiązują te same zasady. Wystarczy, że wykonasz wywołanie i sychronicznie poczekasz na wynik, podczas gdy funkcja wykonuje wszystkie kosztowne operacje i ostatecznie zwraca wynik w jednym wywołaniu:
fn main() {
let s = std::fs::read_to_string("name.txt");
println!("Hello, {}!", s);
}
Co się jednak stanie, gdy spróbujesz skompilować dowolny z tych przykładów na WebAssembly i przetłumaczyć go na potrzeby internetu? Na przykład, co może oznaczać operacja „odczyt pliku”? Musi odczytać dane z niektórego miejsca do przechowywania.
Asynchroniczny model sieci
W internecie jest wiele różnych opcji przechowywania, które możesz zmapować, np. pamięć podręczną (obiekty JS), localStorage
, IndexedDB, pamięć po stronie serwera i nowy interfejs File System Access API.
Jednak tylko 2 z tych interfejsów API – pamięć podręczna i localStorage
– można używać w sposób synchroniczny. Oba te interfejsy API mają najbardziej ograniczone opcje dotyczące tego, co i jak długo można przechowywać. Wszystkie pozostałe opcje
udostępniają tylko asynchroniczne interfejsy API.
Jest to jedna z podstawowych właściwości wykonywania kodu w internecie: każda czasochłonna operacja, która obejmuje operacje wejścia-wyjścia, musi być asynchroniczna.
Dzieje się tak, ponieważ internet jest od zawsze jednowątkowy, a każdy kod użytkownika, który ma wpływ na interfejs użytkownika, musi działać w tym samym wątku co interfejs. Musi konkurować z innymi ważnymi zadaniami, takimi jak układ, renderowanie i obsługa zdarzeń. Nikt nie chciałby, żeby kod JavaScript lub WebAssembly mógł uruchamiać operację odczytu pliku i blokować wszystko inne – całą kartę lub w przeszłości całą przeglądarkę – w czasie od milisekund do kilku sekund, aż do zakończenia.
Zamiast tego kod może tylko zaplanować operację wejścia/wyjścia wraz z wywołaniem zwrotnym, które zostanie wykonane po jej zakończeniu. Takie wywołania zwrotne są wykonywane w ramach pętli zdarzeń przeglądarki. Nie będę tutaj podawać szczegółów, ale jeśli chcesz się dowiedzieć, jak działa pętla zdarzeń, zapoznaj się z artykułem na temat zadań, mikrozadań, kolejek i harmonogramów, w których znajdziesz szczegółowe omówienie tego tematu.
W skrócie: przeglądarka uruchamia wszystkie elementy kodu w nieskończonej pętli, pobierając je z kolejki pojedynczo. Gdy zostanie wywołane jakieś zdarzenie, przeglądarka umieszcza odpowiedni moduł obsługi w kole, a w następnej iteracji pętli pobiera go z niej i wykonuje. Ten mechanizm umożliwia symulowanie współbieżności i wykonywanie wielu operacji równoległych przy użyciu tylko jednego wątku.
Ważne, aby pamiętać o tym mechanizmie, to podczas wykonywania niestandardowego kodu JavaScript (lub WebAssembly) pętla zdarzeń jest blokowana i mimo że nie ma możliwości reagowania na zewnętrzne moduły obsługi, zdarzenia, wejścia-wyjścia itp. Jedynym sposobem na odzyskanie wyników wejścia/wyjścia jest zarejestrowanie wykonania wywołania zwrotnego, zakończenie kodu i przekazanie kontroli w taki sposób, aby przeglądarka mogła zachować kontrolę. Gdy operacja wejścia/wyjścia się zakończy, twój handler stanie się jednym z tych zadań i zostanie wykonany.
Jeśli np. chcesz przepisać powyższe przykłady w nowoczesnej wersji JavaScriptu i zamiast tego odczytać nazwę z zewnętrznego adresu URL, użyjesz interfejsu Fetch API i składni async-await:
async function main() {
let response = await fetch("name.txt");
let name = await response.text();
console.log("Hello, %s!", name);
}
Mimo że wygląda to na działanie synchroniczne, pod maską każdy element await
jest w istocie skrótem dla wywołań zwrotnych:
function main() {
return fetch("name.txt")
.then(response => response.text())
.then(name => console.log("Hello, %s!", name));
}
W tym przykładzie bez cukru, który jest nieco bardziej przejrzysty, żądanie jest uruchamiane, a odpowiedzi są subskrybowane w ramach pierwszego wywołania zwrotnego. Gdy przeglądarka otrzyma początkową odpowiedź (tylko nagłówki HTTP), wywołuje tę funkcję odwoływania asinkronicznie. Wywołanie zwrotne zaczyna odczytywać treść jako tekst za pomocą funkcji response.text()
i subskrybuje wynik za pomocą kolejnego wywołania zwrotnego. Gdy fetch
pobierze całą zawartość, wywoła ostatnią funkcję z powrotu, która wypisuje na konsoli „Cześć, (username)!”.
Dzięki asynchronicznemu charakterowi tych kroków oryginalna funkcja może zwrócić kontrolę przeglądarce, gdy tylko zaplanowane zostaną operacje wejścia/wyjścia, i pozostawić cały interfejs użytkownika w stanie gotowości do obsługi innych zadań, takich jak renderowanie, przewijanie itp., podczas gdy operacje wejścia/wyjścia będą wykonywane w tle.
Na koniec warto wspomnieć, że nawet proste interfejsy API, takie jak „sleep”, które powodują, że aplikacja czeka przez określony czas w sekundach, są też formą operacji wejścia–wyjścia:
#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");
Oczywiście. Możesz przetłumaczyć go w prosty sposób, aby zablokować bieżący wątek do czasu wygaśnięcia limitu czasu:
console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");
W fakcie właśnie to robi Emscripten w swojej domyślnej implementacji funkcji „sleep”, ale jest to bardzo niewydajne, ponieważ blokuje cały interfejs użytkownika i nie pozwala na przetwarzanie innych zdarzeń. Zazwyczaj nie należy tego robić w kodzie produkcyjnym.
Zamiast tego bardziej idiomatyczne wywołanie „sleep” w JavaScriptu polegałoby na wywołaniu setTimeout()
i subskrybowaniu go za pomocą metody obsługi:
console.log("A");
setTimeout(() => {
console.log("B");
}, 1000);
Co łączy wszystkie te przykłady i interfejsy API? W każdym przypadku idiomatyczny kod w pierwotnym języku systemowym używa blokującego interfejsu API do operacji wejścia/wyjścia, podczas gdy odpowiedni przykład w internecie używa asynchronicznego interfejsu API. Podczas kompilowania kodu na potrzeby internetu trzeba jakoś przekształcić te 2 modele wykonywania, a WebAssembly nie ma jeszcze wbudowanej możliwości, która umożliwiłaby to zrobienie.
Rozwiązanie problemu za pomocą Asyncify
Właśnie w tym momencie do akcji wkraczają skrypty Asyncify. Asyncify to funkcja kompilacji obsługiwana przez Emscripten, która umożliwia wstrzymanie całego programu i jego asynchroniczne wznowienie w późniejszym czasie.
Użycie w języku C/C++ z Emscriptenem
Jeśli w przypadku ostatniego przykładu chcesz użyć Asyncify do zaimplementowania asynchronicznego uśpienia, możesz to zrobić w ten sposób:
#include <stdio.h>
#include <emscripten.h>
EM_JS(void, async_sleep, (int seconds), {
Asyncify.handleSleep(wakeUp => {
setTimeout(wakeUp, seconds * 1000);
});
});
…
puts("A");
async_sleep(1);
puts("B");
EM_JS
to makro, które pozwala definiować fragmenty kodu JavaScript w taki sposób, jakby były funkcjami C. Użyj w środku funkcji Asyncify.handleSleep()
, która informuje Emscripten o zawieszeniu programu i udostępnia moduł obsługi wakeUp()
, który należy wywołać po zakończeniu operacji asynchronicznej. W powyższym przykładzie moduł obsługi jest przekazywany do setTimeout()
, ale można go używać w dowolnym innym kontekście, który akceptuje wywołania zwrotne. Na koniec możesz wywołać async_sleep()
w dowolnym miejscu, tak jak zwykłe sleep()
lub dowolny inny synchroniczny interfejs API.
Podczas kompilowania takiego kodu musisz poprosić Emscripten o włączenie funkcji Asyncify. W tym celu przekaż -s ASYNCIFY
oraz -s ASYNCIFY_IMPORTS=[func1,
func2]
z podobną do tablic listą funkcji, które mogą być asynchroniczne.
emcc -O2 \
-s ASYNCIFY \
-s ASYNCIFY_IMPORTS=[async_sleep] \
...
Dzięki temu Emscripten wie, że wywołania tych funkcji mogą wymagać zapisywania i przywracania stanu, więc kompilator wstrzykuje kod pomocniczy wokół takich wywołań.
Teraz, gdy wykonasz ten kod w przeglądarce, zobaczysz płynny log wyjściowy, w którym B pojawi się po krótkim opóźnieniu za A.
A
B
Możesz też zwracać wartości z funkcji Asyncify. Musisz tylko zwrócić wynik funkcji handleSleep()
i przekazać go do wywołania zwrotnego wakeUp()
. Jeśli na przykład zamiast odczytywać z pliku chcesz pobrać liczbę z zdalnego zasobu, możesz użyć fragmentu kodu podobnego do tego poniżej, aby wysłać żądanie, zawiesić kod C i wznowić jego działanie po pobraniu treści odpowiedzi. Wszystko to odbywa się płynnie, tak jakby wywołanie było synchroniczne.
EM_JS(int, get_answer, (), {
return Asyncify.handleSleep(wakeUp => {
fetch("answer.txt")
.then(response => response.text())
.then(text => wakeUp(Number(text)));
});
});
puts("Getting answer...");
int answer = get_answer();
printf("Answer is %d\n", answer);
W przypadku interfejsów API opartych na obietnicach, takich jak fetch()
, możesz nawet połączyć Asyncify z funkcją async-await JavaScriptu zamiast używać interfejsu API opartego na wywołaniu zwrotnym. W tym celu zamiast Asyncify.handleSleep()
wybierz Asyncify.handleAsync()
. Dzięki temu zamiast planować wywołanie zwrotne wakeUp()
, możesz przekazać funkcję JavaScriptu async
i użyć wewnątrz niej await
oraz return
. Dzięki temu kod wygląda jeszcze bardziej naturalnie i synchronicznie, nie tracąc przy tym żadnych zalet asynchronicznego wejścia-wyjścia.
EM_JS(int, get_answer, (), {
return Asyncify.handleAsync(async () => {
let response = await fetch("answer.txt");
let text = await response.text();
return Number(text);
});
});
int answer = get_answer();
Oczekywanie wartości złożonych
W tym przykładzie nadal ograniczasz się do liczb. Co zrobić, jeśli chcesz zastosować oryginalny przykład, w którym próbuję pobrać nazwę użytkownika z pliku jako ciąg znaków? Możesz to zrobić.
Emscripten udostępnia funkcję Embind, która umożliwia konwersję wartości JavaScript na C++ i odwrotnie. Obsługuje też Asyncify, więc możesz wywoływać await()
w zewnętrznych Promise
, a będzie to działać tak samo jak await
w asynchronicznym kodzie JavaScript:
val fetch = val::global("fetch");
val response = fetch(std::string("answer.txt")).await();
val text = response.call<val>("text").await();
auto answer = text.as<std::string>();
Przy użyciu tej metody nie musisz nawet przekazywać parametru ASYNCIFY_IMPORTS
jako flagi kompilacji, ponieważ jest on już domyślnie uwzględniony.
Aplikacja Emscripten działa doskonale. A co z innymi zestawami narzędzi i językami?
Wykorzystanie z innych języków
Załóżmy, że w kodzie Rust masz podobne wywołanie synchroniczne, które chcesz zmapować na asynchroniczny interfejs API w internecie. Okazuje się, że Ty też możesz to zrobić.
Najpierw musisz zdefiniować taką funkcję jako zwykły import za pomocą bloku extern
(lub składni wybranego języka dla funkcji obcych).
extern {
fn get_answer() -> i32;
}
println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);
I skompiluj kod do WebAssembly:
cargo build --target wasm32-unknown-unknown
Teraz musisz zmodyfikować plik WebAssembly, dodając do niego kod do przechowywania i przywracania stosu. W C/C++ robi to za nas, ale nie używa się go tutaj, więc ten proces jest nieco bardziej ręczny.
Na szczęście przekształcenie Asyncify jest całkowicie niezależne od narzędzia. Może przekształcać dowolne pliki WebAssembly niezależnie od tego, jaki kompilator został wyprodukowany. Przekształcenie jest dostarczane oddzielnie w ramach optymalizatora wasm-opt
z łańcucha narzędzi Binaryen i można je wywołać w ten sposób:
wasm-opt -O2 --asyncify \
--pass-arg=asyncify-imports@env.get_answer \
[...]
Przekaż --asyncify
, aby włączyć przekształcenie, a następnie użyj metody --pass-arg=…
, aby uzyskać rozdzielaną przecinkami listę funkcji asynchronicznych, w których stan programu ma zostać zawieszony, a następnie wznowiony.
Pozostaje tylko udostępnić kod środowiska wykonawczego, który będzie wykonywać te zadania, czyli wstrzymywać i wznawiać kod WebAssembly. Tak jak w C / C++, kod ten znalazłby się w Emscripten, ale teraz potrzebujesz niestandardowego kodu JavaScript do obsługi dowolnych plików WebAssembly. Utworzyliśmy właśnie bibliotekę.
Znajdziesz go na GitHubie pod adresem https://github.com/GoogleChromeLabs/asyncify lub w npm pod nazwą asyncify-wasm
.
Symuluje on standardowe API instancjowania WebAssembly, ale w ramach własnej przestrzeni nazw. Jedyna różnica polega na tym, że w standardowym interfejsie WebAssembly API jako importu możesz udostępniać tylko funkcje synchroniczne, natomiast w ramach otoki Asyncify możesz udostępniać również importy asynchroniczne:
const { instance } = await Asyncify.instantiateStreaming(fetch('app.wasm'), {
env: {
async get_answer() {
let response = await fetch("answer.txt");
let text = await response.text();
return Number(text);
}
}
});
…
await instance.exports.main();
Gdy spróbujesz wywołać taką funkcję asynchroniczną – taką jak get_answer()
w przykładzie powyżej – od strony WebAssembly, biblioteka wykryje zwrócony element Promise
, zawiesi działanie i zapisze stan aplikacji WebAssembly, zasubskrybuje obietnicę, a później bez problemu przywróci stos i stan wywołań i kontynuuje wykonywanie kodu tak, jakby nic się nie stało.
Ponieważ każda funkcja w module może wywołać wywołanie asynchroniczne, wszystkie eksporty mogą też być asynchroniczne, więc również zostaną owinięte. W przykładzie powyżej zauważyłeś, że aby uzyskać informację o tym, kiedy zadanie dobiega końca, musisz await
wynik funkcji instance.exports.main()
.
Jak to wszystko działa?
Gdy Asyncify wykryje wywołanie jednej z funkcji ASYNCIFY_IMPORTS
, rozpocznie operację asynchroniczną, zapisze cały stan aplikacji, w tym stos wywołań i wszystkie tymczasowe wartości lokalne, a później po zakończeniu tej operacji przywraca całą pamięć i stos wywołań oraz wznawia pracę z tego samego miejsca i z takim samym stanem, jakby program nigdy się nie zatrzymał.
Jest to całkiem podobne do funkcji asynchronicznego oczekiwania w języku JavaScript, którą przedstawiliśmy wcześniej, ale w przeciwieństwie do JavaScriptu nie wymaga żadnej specjalnej składni ani obsługi środowiska wykonawczego z danego języka. Zamiast tego działa przez przekształcanie zwykłych funkcji synchronicznych w czasie kompilacji.
Podczas kompilowania wcześniejszego przykładu wywołania asynchronicznego:
puts("A");
async_sleep(1);
puts("B");
Asyncify przekształca ten kod w postać mniej więcej taką: (pseudokod, rzeczywista transformacja jest bardziej skomplikowana):
if (mode == NORMAL_EXECUTION) {
puts("A");
async_sleep(1);
saveLocals();
mode = UNWINDING;
return;
}
if (mode == REWINDING) {
restoreLocals();
mode = NORMAL_EXECUTION;
}
puts("B");
Na początku parametr mode
ma wartość NORMAL_EXECUTION
. W związku z tym przy pierwszym wykonaniu tak przekształconego kodu zostanie oceniona tylko część do async_sleep()
. Gdy tylko zaplanowana zostanie operacja asynchroniczna, Asyncify zapisuje wszystkie zmienne lokalne i odwija stos, wracając z każdej funkcji aż do góry, co pozwala przywrócić kontrolę nad pętlą zdarzeń przeglądarki.
Gdy async_sleep()
zostanie rozwiązany, kod obsługi Asyncify zmieni wartość mode
na REWINDING
i ponownie wywoła funkcję. Tym razem gałąź „normalnego wykonania” jest pomijana, ponieważ już wykonała zadanie podczas poprzedniego wywołania, a ja chcę uniknąć dwukrotnego drukowania „A”. Zamiast tego przechodzi od razu do gałęzi „przewijania wstecz”. Po osiągnięciu tego punktu przywraca wszystkie zapisane lokalne, zmienia tryb z powrotem na „normalny” i kontynuuje wykonywanie kodu tak, jakby nigdy nie został zatrzymany.
Koszty transformacji
Niestety przekształcenie asynchroniczne nie jest całkowicie bezpłatne, ponieważ wymaga wstrzyknięcia dużej ilości kodu pomocniczego do przechowywania i przywracania wszystkich takich elementów lokalnych, poruszania się po stosie wywołań w różnych trybach i tak dalej. Próbuje zmodyfikować tylko funkcje oznaczone jako asynchroniczne na wierszu poleceń, a także ich potencjalnych wywołujących, ale obciążenie rozmiarem kodu może nadal wynosić około 50% przed kompresją.
Nie jest to idealne rozwiązanie, ale w wielu przypadkach jest akceptowalne, gdy alternatywą jest brak funkcji lub konieczność znacznego przeredagowania oryginalnego kodu.
Pamiętaj, aby zawsze włączać optymalizacje w przypadku wersji końcowych, aby nie zwiększać jeszcze bardziej ich rozmiaru. Możesz też zaznaczyć opcje optymalizacji dotyczące Asyncify, aby zmniejszyć obciążenie, ograniczając przekształcenia tylko do określonych funkcji lub tylko do bezpośrednich wywołań funkcji. Wywołania asynchroniczne mają też niewielki wpływ na wydajność w czasie wykonywania, ale dotyczy to tylko samych wywołań. Jednak w porównaniu z kosztem faktycznej pracy jest to zwykle nieistotne.
Demonstracje w rzeczywistych warunkach
Teraz, gdy znasz już proste przykłady, przejdę do bardziej skomplikowanych scenariuszy.
Jak wspomniano na początku tego artykułu, jedną z opcji przechowywania w internecie jest asynchroniczny interfejs File System Access API. Umożliwia ona dostęp do rzeczywistego systemu plików hosta z poziomu aplikacji internetowej.
Z drugiej strony istnieje standard de facto o nazwie WASI, który służy do obsługi wejść/wyjść WebAssembly w konsoli i na serwerze. Został zaprojektowany jako docel kompilacji dla języków systemowych i wyświetla wszystkie rodzaje systemów plików oraz inne operacje w tradycyjnej formie asynchronicznej.
Co jeśli można by je ze sobą powiązać? Następnie można skompilować dowolną aplikację w dowolnym języku źródłowym za pomocą dowolnego zestawu narzędzi obsługującego docel WASI i uruchomić ją w piaskownicy w internecie, zachowując przy tym możliwość działania na rzeczywistych plikach użytkownika. Asyncify Ci to umożliwi.
W tym pokazie skompilowałem bibliotekę coreutils w Rust z kilkoma drobnymi poprawkami do WASI, przekazanymi za pomocą transformacji Asyncify, oraz zaimplementowałem asynchroniczne wiązania z WASI do interfejsu File System Access API po stronie JavaScript. Po połączeniu z elementem terminala Xterm.js zapewnia realistyczne środowisko, które działa w karcie przeglądarki i obsługuje rzeczywiste pliki użytkownika – tak jak prawdziwy terminal.
Możesz go sprawdzić na żywo na stronie https://wasi.rreverser.com/.
Zastosowania asynchronizacji nie ograniczają się tylko do zegarów i systemów plików. Możesz też użyć bardziej wyspecjalizowanych interfejsów API w internecie.
Asyncify pozwala na przykład zmapować bibliotekę libusb – prawdopodobnie najpopularniejszą bibliotekę natywną do pracy z urządzeniami USB – na interfejs WebUSB API, który zapewnia asynchroniczny dostęp do takich urządzeń w internecie. Po zmapowaniu i skompilowaniu mam standardowe testy i przykłady libusb do uruchomienia na wybranych urządzeniach bezpośrednio w piaskownicy strony internetowej.
To chyba artykuł na inny post na blogu.
Te przykłady pokazują, jak skutecznym narzędziem może być Asyncify, który wypełni luki i umożliwi przeniesienie wszystkich rodzajów aplikacji do sieci, zapewniając dostęp z wielu platform, podział na piaskownice i lepsze zabezpieczenia, a wszystko to bez utraty funkcji.