A glance at offshoring discussions

I’ve been reading through some discussions on LinkedIn, regarding offshoring practices as performed by companies located in the US and, oh boy, this topic does bring out some emotions. I don’t know how volatile these posts are, so instead of linking to them directly, I’ll just quote without mentioning any names. I hope they are easy enough to find given the titles.

Let’s start with this sneaky attempt at suggesting some general racial/national problems to analyze:

Should Gartner do research on the quality of code produced in India?

Would you find value if Gartner were to perform extensive research to provide insight into the question of whether Americans write better code than their Indian counterparts? Does code written in India have more OWASP Top Ten vulnerabilities than code written onshore? The perspectives of IT executives and how they make tradeoffs on quality for cost?

While most of the responses are balanced and in good faith, it’s not unusual to see a retaliatory “answer”:

I think Gartner should research quality of code produced in America, exclusively… and Indians should stop writing code on services projects for america.

Let’s take an another topic:

What european country do you suggest to outsourcing/offshoring software development?

While probably not intended to be so, I found these comments quite amusing:

Try outsourcing to the Midwest. (United states).
You’d be surprised at your cost savings over the life of the project.

Or:

How about outsourcing to USA.
Contact -[email removed for obvious reasons]

Way to go! Obviously, not everyone likes peaceful means of promotion/competition, and replies along these lines happen as well:

I’m sorry, with very few exceptions, any US company that outsources/offshores its software development should be boycotted.

The topic is “hot” right now. Different kinds of arguments related to the state of economy are being brought up and I believe what can be found in these posts is just a tip of the iceberg – the moods are probably much worse than that…

Uruchamianie programu w Linuksie

Note: this is the original paper as I wrote it a long time ago, when I still was a student. I’m publishing it here in Polish, hoping that someone will find it useful. English version is going to be released soon.

Po polsku

Poniższy wpis wyjaśnia jak uruchamiane są programy łączone dynamicznie w Linuksie, choć podaję też przykłady pochodzące z Solarisa. Grupą docelową nie są doświadczeni administratorzy, a raczej osoby dopiero zaczynające swoją przygodę z systemami wywodzącymi się od Uniksa, więc zostawiłem w nim potencjalnie trywialne fragmenty.

Wstęp

Pewnego razu znajomemu administratorowi przydarzyła się taka oto historia: próbując stworzyć sobie “system w systemie” (odseparowany przy pomocy chroota) zabrał się za tworzenie nowej struktury katalogowej dla zagnieżdżonego środowiska. Stworzył więc katalog /newroot, a następnie zaczął wypełniać go kopiami plików z systemu pierwotnego. O tyle niefortunnie, że zamiast skopiować (cp), przeniósł (mv) katalogi /bin oraz /lib (mv), skutecznie psując system. W efekcie tej pochopnej operacji, ważne programy zaczęły zachowywać się, na przykład, w następujący sposób:

host:/newroot/bin/# ./cp
-su: ./cp: Nie ma takiego pliku ani katalogu

Komunikat bardzo mylący, gdyż plik cp oczywiście istniał w katalogu /newroot/bin.

Jak się okazało, istnieje elegancki sposób wybrnięcia z tej sytuacji, lecz by wyjaśnić to zachowanie, musimy zrozumieć systemowe mechanizmy odpowiedzialne za obsługę bibliotek współdzielonych oraz ich rolę w działaniu całego środowiska. Na zakończenie, oprócz pokazania sposobu rozwiązania takiego problemu, pokażę alternatywną metodę częściowego zabezpieczenia się przed awariami związanymi z brakiem lub uszkodzeniem systemowych bibliotek.

Zakładam przy tym, że znasz pojęcia jądra, procesu, bibliotek współdzielonych, przestrzeni adresowej oraz mapowania plików do pamięci, gdyż ich znajomość jest potrzebna do zrozumienia dalszej części tekstu.

Struktura pliku programu

Aby program mógł zostać uruchomiony przez system operacyjny, musi być zapisany w pliku o odpowiedniej strukturze. Aktualnie w świecie systemów uniksowych króluje format ELF (Executable and Linking Format), więc poniższe informacje podane są w jego kontekście. Funkcjonujące dawniej formaty a.out i COFF pomijamy.

Zacznijmy od zbadania przykładowego pliku wykonywalnego (/bin/ls) przy pomocy narzędzia readelf, wypisującego w czytelnej dla człowieka formie informacje zgromadzone w nagłówku oraz sekcjach plików w formacie ELF.

Wynik działania programu powinien wyglądać tak:

% readelf -h /bin/ls
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4025d0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          87432 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         10
  Size of section headers:           64 (bytes)
  Number of section headers:         27
  Section header string table index: 26

Widzimy kolejno:

  1. “Magiczny numer” (Magic), dzięki któremu możliwe jest rozpoznanie formatu pliku
  2. Klasę (Class), określającą architekturę (32 lub 64 bity), do której przystosowany jest plik
  3. Interfejs binarny (OS/ABI), z którego korzysta plik wykonywalny
  4. Rodzaj maszyny (Machine), dla której przeznaczony jest kod
  5. Punkt wejścia (Entry point address), czyli adres, od którego zaczyna się wykonywanie programu
  6. Dane określające liczbę oraz rozmiar dodatkowych nagłówków i sekcji

Oprócz tych ogólnych informacji, w wykonywalnym pliku w formacie ELF znajdują się jeszcze metadane opisujące dokładne wymagania programu, takie jak adresy pamięci, pod którymi mają znaleźć się sekcje pliku oraz wymagane biblioteki wraz z importowanymi z nich symbolami. Przyjrzymy się im w dalszej części wpisu.

Uruchamianie

Uruchamiania łączonego dynamicznie programu jest dość skomplikowanym procesem, który zaczyna się w momencie wywołania funkcji z rodziny exec, zapewniającej różnorodne interfejsy do systemowego wywołania execve, którego rolą jest załadowanie do przestrzeni adresowej procesu kodu nowego programu oraz przygotowanie środowiska do jego wykonania. W przypadku Linuksa, proces ten można przedstawić skrótowo jako listę następujących kroków:

  1. Sprawdzenie formatu pliku wykonywalnego i, jeśli jest on obsługiwany, rozpoczęcie jego przetwarzania
  2. Odczyt części nagłówka pliku, opisującego segmenty programu
  3. Zwolnienie zasobów związanych z poprzednim kontekstem wykonania (m.in. usunięcie przyporządkowania stron pamięci, aktualizacja procedur obsługi sygnałów)
  4. Alokacja pamięci na stos trybu użytkownika, zmienne środowiskowe, argumenty podane z linii poleceń oraz kod i część danych uruchamianego programu
  5. Mapowanie segmentów pliku pod odpowiednie adresy w przestrzeni procesu
  6. Powtórzenie kroku 6 dla interpretera programu (wybranego na podstawie nagłówka)
  7. Zakończenie wykonania wywołania systemowego – proces zaczyna wykonywać kod interpretera, który po zakończeniu pracy wykonuje skok do punktu wejścia programu, rozpoczynając wykonanie właściwego kodu

Powyższy wykaz operacji jest bardzo uproszczony. Pełen opis z dokładnym uwzględnieniem operacji wykonywanych w trybie jądra znaleźć można m.in. w książce “Linux kernel” [1], oraz oczywiście w kodzie jądra.

Skorzystajmy ponownie z narzędzia readelf, by obejrzeć zawartość nagłówka programu (skrócona wersja poniżej):

rogram Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x0000000000000230 0x0000000000000230  R E    8
  INTERP         0x0000000000000270 0x0000000000400270 0x0000000000400270
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000013f4c 0x0000000000013f4c  R E    200000
  LOAD           0x0000000000014df0 0x0000000000614df0 0x0000000000614df0
                 0x00000000000006c0 0x0000000000000c60  RW     200000
  DYNAMIC        0x0000000000014e18 0x0000000000614e18 0x0000000000614e18
                 0x00000000000001c0 0x00000000000001c0  RW     8
  NOTE           0x000000000000028c 0x000000000040028c 0x000000000040028c
                 0x0000000000000020 0x0000000000000020  R      4
  GNU_EH_FRAME   0x0000000000011f3c 0x0000000000411f3c 0x0000000000411f3c
                 0x0000000000000694 0x0000000000000694  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     8
  GNU_RELRO      0x0000000000014df0 0x0000000000614df0 0x0000000000614df0
                 0x0000000000000210 0x0000000000000210  R      1
  PAX_FLAGS      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000         8

Oprócz wpisów określających części pliku oraz odpowiadające im adresy w przestrzeni adresowej procesu, interesuje nas linia INTERP, która określa interpreter programu, który ma zostać załadowany i uruchomiony przez jądro na końcu wywołania execve. Wpis ten ma znaczenie tylko w przypadku plików wykonywalnych (choć może pojawić się też w plikach obiektów). Oczywiście, jeśli interpreter nie zostanie znaleziony, lub jądro nie będzie mogło umieścić go w przestrzeni adresowej procesu, uruchomienie programu się nie powiedzie.

Dygresja: Interpreter znajduje się zazwyczaj w katalogach /lib lub /libexec, dlatego operacje na nich należy wykonywać z odpowiednią ostrożnością – uszkodzenie go może doprowadzić od poważnej awarii systemu.

Wiązanie dynamiczne

Skompilowane (łączone dynamicznie) programy, które można znaleźć w katalogach /bin, /usr/bin, itd., nie nadają się do bezpośredniego uruchomienia (załadowania kodu do pamięci i rozpoczęcie jego wykonywania przez procesor) z dwóch powodów:

  • Ich kod jest niezależny od miejsca w pamięci, w którym się znajduje (PIC – Position Independent Code), a co za tym idzie – początkowo część referencji do pamięci nie wskazuje na jej poprawne w danym kontekście fragmenty. Muszą one zostać odpowiednio ustawione.
  • W przestrzeni adresowej procesu nie jest dostępny wymagany kod, należący do bibliotek współdzielonych, a referencje do symboli (nazwanych adresów, pod którymi znajduje się kod lub obiekty) nie posiadają poprawnych wartości.

Nim sterowanie zostanie przekazane pod adres wskazany przez punkt wejściowy programu, powyższe elementy muszą zostać odpowiednio zainicjowane przez linker dynamiczny. Wykonywanie kodu właściwego programu może rozpocząć się dopiero po przeprowadzeniu odpowiedniego mapowania oraz serii relokacji. Chcąc skupić się na przedstawionym na początku problemie, omówimy tylko kwestię ładowania bibliotek.

Jak już wcześniej wspomniałem, w przestrzeni adresowej procesu musi znaleźć się mapowany kod. Przy pomocy narzędzia strace, śledzącego wywołania systemowe wykonywane przez proces, pokażemy, że linker faktycznie odpowiednio wypełnia przestrzeń adresową przy pomocy funkcji mmap. Wykonajmy strace /bin/ls 2>&1 | less i znajdźmy w wyniku poniższy fragment:

open("/lib/libc.so.6", O_RDONLY)        = 3
read(3, "\177ELF\2\1\1\3>\1\220\334\1"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1293456, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fdab4233000
mmap(NULL, 3399928, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fdab38fd000
mprotect(0x7fdab3a33000, 2093056, PROT_NONE) = 0
mmap(0x7fdab3c32000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x135000) = 0x7fdab3c32000
mmap(0x7fdab3c37000, 16632, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fdab3c37000
close(3)                                = 0

Co robi linker?

  • open() – otwiera plik biblioteki w trybie tylko do odczytu; będzie on dostępny pod deskryptorem o numerze 3
  • read() – odczytuje jego nagłówek, by zdobyć informacje o formacie pliku (czy to na pewno biblioteka?) oraz rozmieszczeniu danych
  • fstat() – sprawdza, czy otwarty plik jest zwykłym plikiem, co jest istotne z punktu widzenia późniejszych wywołań mmap(); również sprawdza jego rozmiar
  • mmap() – drugie wywołanie mapuje cały plik w trybie tylko do odczytu z ustawioną flagą pozwalającą na wykonanie – domyślnie kod nie jest zapisywalny; mapowanie jest prywatne, więc ewentualne zmiany nie zostaną wprowadzone do pliku
  • mmap() – trzecie wywołanie mapuje część pliku zezwalając na zapis do tego obszaru pamięci; można się spodziewać, że w tej części znajdują się statyczne dane używane przez bibliotekę
  • close() – zamyka plik

Pozostałe wywołania nie są w tym momencie istotne. Podobne fragmenty pojawią się w wyniku działania strace jeszcze kilka razy, kolejno dla wszystkich ładowanych bibliotek. Wprowadzane zmiany odniesień do pamięci w programie oczywiście nie są widoczne, bo przebiegają bez udziału jądra systemu operacyjnego. Po zakończeniu pracy linkera, zaczyna się wykonywanie kodu programu.

Dygresja: Rozmieszczenie wszystkich segmentów w przestrzeni adresowej procesu można w Linuksie odczytać z pliku /proc/PID/maps, gdzie PID oznacza identyfikator interesującego nas procesu.

Słowo o środowisku

Nim przystąpimy do rozwiązywania postawionego na początku problemu, musimy zdać sobie sprawdę z jednego faktu: linker dynamiczny wykonuje się w przestrzeni adresowej przeznaczonej dla nowo uruchamianego programu. Ma zatem dostęp do jego argumentów oraz zmiennych środowiskowych. Dzięki temu mamy możliwość ingerowania w jego działanie przy pomocy odpowiednich zmiennych środowiskowych. Dwie z nich są szczególnie interesujące:

  • LD_LIBRARY_PATH – określa listę ścieżek, które mają być dodatkowo przeszukiwane pod kątem potrzebnych bibliotek; umożliwia korzystanie z bibliotek znajdujących się w miejscach nie przeszukiwanych standardowo oraz nie określonych przez konfigurację linkera (/etc/ld.so.conf).
  • LD_PRELOAD – określa listę bibliotek, które mają być załadowane przed wszystkimi innymi, określonymi w nagłówkach programu; umożliwia zastępowanie symboli (mówiąc potocznie, umożliwia “podmianę” bibliotecznych funkcji oraz obiektów, gdyż symbole dopasowywane są tylko raz)

Rozwiązanie problemu

Znamy już mechanizm uruchamiania programów w systemach uniksowych, możemy więc przystąpić do analizy przedstawionego na początku wpisu problemu.

Możemy się już domyślić, że dziwnie brzmiący komunikat wyniknął z faktu, że jądro systemu nie odnalazło pliku zawierającego potrzebny mu interpreter, określony w nagłówku pliku programu. Gdyby problem sprowadzał się wyłącznie do braku biblioteki współdzielonej, zostalibyśmy o tym poinformowani w bardziej czytelny sposób, a rozwiązanie byłoby trywialne. Co można zrobić w takiej sytuacji?

  • Przenieść (mv, cp) plik linkera do pierwotnego katalogu
    Jeśli programy mv i cp są łączone dynamicznie, a tak było w tym przypadku, nie można ich użyć do tego celu (nie da się ich uruchomić!)
  • Stworzyć kopię linkera wyłącznie przy użyciu wewnętrznych poleceń powłoki
    Polecenia wewnętrzne, jak nazwa wskazuje, nie uruchamiają zewnętrznych programów. O ile w sytuacji awaryjnej mamy uruchomioną powłokę, można próbować z nich korzystać. Przykładowe zastosowanie poleceń wewnętrznych (read i echo) oraz mechanizmu przekierowań:

    % read tresc < plik
    % echo $tresc > plik2

    Niestety, jądro odmawia załadowania linkera z pliku, który nie ma ustawionych praw wykonania. Z kolei, by je ustawić potrzebny jest program chmod, którego nie możemy uruchomić z powodu braku interpretera. Powyższy przykład ma też dość poważną wadę polegającą na tym, że z dużym prawdopodobieństwem uszkodzi “kopiowany” plik.

Pech. W ogólnym przypadku, z poziomu działającego systemu nie jesteśmy w stanie zaradzić takiej sytuacji – potrzebne jest środowisko ratunkowe (np. płyta CD z możliwym do uruchomienia systemem). Rozwiązanie to, oczywiście, nie jest dobre – w przypadku maszyny pełniącej istotną funkcję nie chcemy powodować dodatkowych przestojów. Co jeszcze nam zostało?

  • Posiadać zestaw podstawowych narzędzi linkowanych statycznie
    Rzeczywiście, jeśli podstawowe narzędzia systemowe skompilowano statycznie, powyższy problem w ogóle się nie pojawi. Jest to doskonałe zabezpieczenie przed problemami związanymi z obsługą ważnych bibliotek współdzielonych.
  • Poinstruować system, gdzie szukać linkera dynamicznego
    Pomysł dobry, ale niewykonalny – taki mechanizm po prostu nie istnieje. Jednak użytkownicy GNU/Linuksa oraz Solarisa mogą skorzystać z dość specyficznej cechy dostępnych w ich systemach linkerów. Pamiętamy, że jest to w gruncie rzeczy normalny program (uruchamiany zazwyczaj przez jądro systemu) z odpowiednim punktem wejściowym. Okazuje się, że w tych systemach istnieje możliwość uruchomienia /lib/ld-linux*.so lub /lib/ld.so.1 z podaniem programu, który ma zostać załadowany i obsłużony przez linker! Zatem mając interesujące nas pliki w katalogu /newroot możemy użyć tej możliwości w następujący sposób:

    % export LD_LIBRARY_PATH=/newroot/lib
    % /newroot/lib/ld-2.3.6.so /newroot/bin/mv /newroot/lib /

    Czyli: ustawiamy niestandardową ścieżkę, pod którą linker ma szukać bibliotek (potrzebna, bo wskazuje miejsce, gdzie znajdują się wymagane przez mv pliki), a następnie uruchamiamy mv, przenosząc potrzebne pliki na ich miejsce! Voila! Przywróciliśmy system do stanu pełnej sprawności!

Do zastanowienia się

Zachęcam do zastanowienia się nad implikacjami faktów przedstawionych w tym wpisie. Pozwolę sobie pomóc, ale przy użyciu metody sokratejskiej:

  • Badając przy pomocy narzędzia ldd najważniejsze programy narzędziowe w systemie – jaka biblioteka pojawia się na liście najczęściej i co z tego wynika?
  • Kod biblioteczny mapowany jest do przestrzeni adresowej procesu z pliku. Czy nadpisanie pliku zawierającego kod biblioteki współdzielonej to dobry pomysł?
  • Zakładając, że powyższe to zły pomysł, to jak zaktualizować bibliotekę współdzieloną jeśli jest ona używana przez działający proces?
  • Rozważmy sytuację, w której program /bin/secret używa funkcji strcmp ze standardowej biblioteki języka C do porównania skrótu wprowadzonego hasła z zakodowanym wzorcem i na podstawie wyniku tej operacji zapewnia dostęp do ściśle tajnych informacji. Stwórz bibliotekę zawierającą funkcję strcmp, która zawsze zwraca wartość wskazującą identyczność argumentów, a następnie spraw, by /bin/secret akceptował każde wprowadzone hasło.
  • Jakie implikacje dla bezpieczeństwa systemu ma powyższy punkt? Czy linker dynamiczny honoruje ustawienia zmiennych LD_PRELOAD i LD_LIBRARY_PATH jeśli wykonywany jest program z ustawionym bitem SUID?

Bibliografia

  • [1] (Daniel P. Bovet, Marco Cesati, O’Reilly, Wydawnictwo RM, Warszawa 2001

git

I started to use git a few days ago. I’m still a fan of Mercurial, but git turned out to be “a must” due to the number of “Battle of Wesnoth” developers switching to it, and thanks to rather seamless integration with Subversion.

The other reason is that I’ve been looking for a DVCS tool to use as a front-end to an SVN repository I use at work, with a possibility of converting at least one other open-minded guy to that paradigm. Of course, in this case solid integration with the existing (and mandatory…) tool is a must, so git scored an another point.

So far, it seems to work, and reasonably fast at that. Getting it to run on Cygwin was a pain in the neck, though, because for some reason git opens its internal files in text-mode (ugh), which leads to problems with index getting corrupted (CRLF/LF) if Cygwin file systems are not mounted in binmode. Careful there.

I’ve also hit a problem with Subversion import, most likely caused by some branching mess in our development history (empty directories being created in branches/ with no data, then being deleted, then being recreated as proper branches). Don’t ask why it’s like that. I probably have a bug to report, once I have it tested on a simple test case. As it is, inspection of the branching history shows branches where nothing has been branched, and merges when a branch was created. Ouch.

Mercurial hosting

Those of you who are looking for free Mercurial hosting for your little libre projects – search no more. freeHg.org is out there and works quite well.

Kudos for showing me this site go to Maciek Ligenza. My little prototype has its home now. :-)

Prototyping

As my masters thesis is nearing completion, I dared to let my mind off it, and started to play a little bit with some ideas that kept haunting me for at least one year.

The ideas of a game – a space themed tactics in a mix of turn-based and real-time genre. I’ve been experimenting with user interface concepts that I have envisioned and the results are promising, as it turns out that my ideas are possible to implement, and quite easily at that! The final product, should it ever get “completed”, is going to be written in Python and PyOpenGL. The prototype uses pygame instead of PyOpenGL, because it’s faster to develop in and I need no fancy features to test simple concepts.

This little experiment reminds me what a great language for prototyping Python is!

Popular wisdom

If the code and the comments disagree, then both are probably wrong.

– Norm Schryer

Search relevancy

Compare.

Here vs here. Is this supposed to be Google’s competition? :D

The world’s most…

…scalable, available and most reliable database1:

*** ERROR from SQL [-1311]: The text of the view definition is 2018 bytes too long.

*** ERROR[15037] The maximum fragment length of a statement in SQL/MX is 4096. Please retype your statement with a newline embedded in it.

1 – according to its developer

Random rant

Any intelligent fool can make things bigger, more complex, and more violent. It takes a touch of genius — and a lot of courage — to move in the opposite direction. – Albert Einstein

My relatively short “professional career” has already taught me what “enterprisey” software means. I’ve known this word before, I’ve seen it used to describe systems that suffered from bloat and lacked relative simplicity. If software is hard, then creating “simple” software is even harder.

Why do people like complicated solutions? Why complex object models get introduced in simple applications that could be more intuitively expressed without them? Why monsters that internally convert one XML data format, to another and another exist..?

There’s so much to see and learn from!

QtConcurrent

I’m not going to discover America, but it looks like the evolution of (personal) computers in terms of raw CPU power and GHz values has slowed down to a crawl, while the overall processing power increases thanks to additional cores. Intensively parallel computing may be an exciting vision, but writing software that could make use of many CPU cores is challenge. It’s not a secret that multi-threading is harder than sequential programming, with all related algorithmic and hardware problems like deadlocks, race conditions, cache ping-pong, locking contention and so on – no wonder some people predict that Erlang is going to be the next Big Thing – concurrent programming based on message passing is more scalable and seems easier to get right.

Having all these things in mind, I’ve recently stumbled upon an interesting addition to the Trolltech’s Qt library (which I like very much): QtConcurrent is a project that makes multi-threaded programming with Qt much easier (on all supported platforms!). Not only does it implement asynchronous function calls and Google’s Map-Reduce algorithm, but also, according to the website, is capable of determining the number of available processors/cores and adjusts the number of threads accordingly! Neat.

I’m definitely going to use it in a little Secret project of mine (WIP).

Next Page »