Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Inne artykuły

Cosmopolitan - skompilowane programy w C/C++ działające multiplatformowo bez narzutu na wydajność

[artykuł]

Wprowadzenie

Ucząc się na studiach informatycznych kolejnych języków, słyszałem wiele rzeczy. Jedną z nich było to, że C++ jest językiem kompilowanym, przez co nawet napisawszy w pełni przenośny kod (czyli zgodny ze standardem i używający bibliotek multiplatformowych), i tak musimy go przekompilować, przenosząc się na inny system operacyjny - a nawet w ramach tego samego systemu, gdy chcemy używać innego kompilatora. W kolejnym semestrze był przedmiot poświęcony językowi Java, na którym wykładowca zachwalał, że Java nie ma wielu wad C++, w tym że kod skompilowany jest przenośny - a to zdanie jest kłamstwem powtarzanym bezrefleksyjnie, z przekonaniem o jego prawdziwości. Kod pośredni Javy (bytecode) sam w sobie nie jest przenośny w sensie binarnym - wykonuje go przecież interpreter, czyli JVM (Java Virtual Machine), który dla każdego systemu operacyjnego trzeba dostarczyć osobno (zwykle napisany w C lub C++), tak więc skompilowany kod języka Java nie jest przenośny.

Na studiach - już nieoficjalnie, nie od wykładowców - słyszy się też o emulatorze Wine, dzięki któremu na Linuksie da się uruchamiać programy z Windowsa, w tym gry (na YouTube nietrudno znaleźć filmy tłumaczące krok po kroku, jak odpalić w ten sposób np. Wiedźmina III). W międzyczasie Microsoft wypuszcza WSL (Windows Subsystem for Linux), pozwalający odpalić Ubuntu wewnątrz Windowsa.

Widząc to wszystko, część osób nachodzi refleksja: skoro to ten sam sprzęt i ta sama architektura, to czemu skompilowany kod C++ jest aż tak nieprzenośny? Czy nie da się zbudować binarki działającej na kilku systemach naraz? Na szczęście ktoś nie tylko zadał sobie to pytanie, ale i odpowiedział na nie konkretnym narzędziem: GitHub - jart/cosmopolitan: build-once run-anywhere c library.

Czym jest Cosmopolitan

Cosmopolitan Libc to projekt, który pozwala skompilować kod C lub C++ raz i uruchamiać go bez zmian na wielu systemach - w przeciwieństwie do Javy, bez żadnej maszyny wirtualnej ani interpretera. Zamiast tego projekt tak konfiguruje standardowe GCC i Clang, że na wyjściu dostajemy jeden plik działający natywnie na Linuksie, macOS, Windowsie, FreeBSD, OpenBSD, NetBSD, a nawet bezpośrednio jako obraz rozruchowy BIOS/UEFI - i to zarówno na architekturze x86_64, jak i ARM64. Najciekawszym jest, że "binarka" wykonuje się niemalże bez narzutów w stosunku do kodu skompilowanego normalnym g++.

Ta sztuczka nazywa się APE (Actually Portable Executable) - format poliglotowy, którego nagłówek jest jednocześnie poprawnym skryptem powłoki (sh), sektorem rozruchowym BIOS-u oraz nagłówkiem plików wykonywalnych ELF (Linux/BSD), Mach-O (macOS) i  PE  (Windows). Przy starcie taki plik jest najpierw interpretowany jako skrypt powłoki, który rozpoznaje bieżący system i architekturę, po czym doklejone są (lub wczytywane w locie, w nowszych wersjach loadera) właściwe dla niego bajty. Jako ciekawostkę można dodać, że plik APE jest równocześnie poprawnym archiwum ZIP - katalog plików ZIP trzyma się na końcu pliku, więc nie koliduje to z nagłówkiem - co Cosmopolitan wykorzystuje np. do doklejania zależności czy dodatkowych zasobów wprost do binarki (więcej o tym w sekcji bonusowej niżej). Samo Cosmopolitan Libc jest wydane na licencji ISC. Cały mechanizm tego "sztuczkowania" formatu opisano dokładniej we wpisie autora projektu (dostępny z oficjalnego repozytorium na GitHubie).
Skrót APE nie ma nic wspólnego z formatem plików dźwiękowych Monkey's Audio (rozszerzenie .ape) - to zwykła kolizja nazw, dwa zupełnie niezwiązane ze sobą projekty.

Używanie frameworka Cosmopolitan na przykładach

Poniżej przykłady kompilacji od prostych kodów w C i C++, po kody C++ używające bibliotek header-only przez projekty używające CMake'a, aż wreszcie biblioteka boost i manager pakietów VCPKG.

Pobranie frameworka

Wystarczy pobrać najnowszy pakiet z Index of /pub/cosmocc/ (na moment pisania artykułu jest to cosmocc-4.0.2.zip - wersja 4.0.2 z 5 stycznia 2025, wciąż oznaczona jako najnowsza w oficjalnym repozytorium; warto sprawdzić GitHub - jart/cosmopolitan: releases, czy w międzyczasie nie pojawiło się coś nowszego):
mkdir -p cosmocc
cd cosmocc
wget https://cosmo.zip/pub/cosmocc/cosmocc.zip # pobiera zawsze najnowsza wersje
unzip cosmocc.zip
Mając pobrane archiwum możemy dodać programy wykonywalne do zmiennej środowiskowej zawierającej ścieżki do programów wykonywalnych. "Binarki" (oczywiście przenośne między systematmi) znajdują się (przy założeniu, że jesteśmy w tym samym katalogu co po wykonaniu powyższych komend) w ${PWD}/cosmocc/bin.

Dla wygody w dalszych przykładach przyjmijmy jedną zmienną wskazującą na katalog toolchaina:
export COSMOCC="${PWD}/cosmocc"

Dzięki temu polecenia w dalszej części artykułu (np. $COSMOCC/bin/x86_64-unknown-cosmo-c++) nie są przywiązane do konkretnej ścieżki na Twoim komputerze - wystarczy ustawić $COSMOCC raz, zgodnie z tym, gdzie faktycznie masz rozpakowany toolchain.

Prosty przykład w C

Przykładowy kod w C:
C/C++
// hello.c
#include <stdio.h>

int main() {
   
printf( "hello world\n" );
}
Kompilujemy go przez:
./bin/cosmocc -o hello hello.c

i uruchamiamy na dowolnym z obsługiwanych systemów przez:
./hello

Co ciekawe, sam
cosmocc
 jest binarką APE - czyli tym samym narzędziem, które generuje ale jest zbudowany tak, że można się nim posłużyć na dowolnym z obsługiwanych systemów bez osobnego pobierania wersji "pod dany OS".

Dodatkowe flagi na potrzeby logowania

Do podstawowego użytku to wystarczy, ale warto znać dwie przydatne flagi diagnostyczne wbudowane w runtime Cosmopolitan:
logowanie wywołań systemowych
./hello --strace
znacznie bardziej gadatliwy log wywołań funkcji
./hello --ftrace

Rozmiar APE vs rozmiar normalny

A ile waży taka przenośna binarka?
➜ ./bin/cosmocc -o hello hello.c
➜ gcc -o hello_gcc hello.c
➜ ls -laht hello*
-rwxrwxr-x 1 agh agh  16K Sep 23 14:04 hello_gcc
-rwxr-xr-x 1 agh agh 400K Sep 23 14:04 hello
-rwx--x--x 1 agh agh 674K Sep 23 14:04 hello.aarch64.elf
-rwx--x--x 1 agh agh 1.1M Sep 23 14:04 hello.com.dbg
Rozmiar można trochę zmniejszyć przez użycie trybu tiny - przez zmienną środowiskową
export MODE=tiny
 lub przez argument:
➜ ./bin/cosmocc -mtiny -o hello hello.c
➜ strip hello_gcc
➜ ls -laht hello*
-rwxrwxr-x 1 agh agh  15K Sep 23 14:08 hello_gcc
-rwxr-xr-x 1 agh agh 192K Sep 23 14:08 hello
-rwx--x--x 1 agh agh  69K Sep 23 14:08 hello.aarch64.elf
-rwx--x--x 1 agh agh 128K Sep 23 14:08 hello.com.dbg
Te liczby zgadzają się z tym, co pisze sama dokumentacja toolchaina: w trybie domyślnym najmniejsze możliwe binarki mają "rzędu setek KB", bo runtime na sztywno dolinkowuje spore, przydatne deweloperom mechanizmy --strace i --ftrace; -mtiny usuwa je i celuje w około 180 KB dla fat binarki oraz około 68 KB dla wersji jednoarchitekturowej - u mnie wyszło odpowiednio 192 KB i 69 KB, czyli bardzo blisko. Warto jednak wiedzieć, co się w zamian traci: dokumentacja wprost ostrzega, że w trybie tiny memmove() przestaje korzystać z wektoryzacji, malloc() gorzej skaluje się na wielu rdzeniach, a część funkcji przestaje wykrywać błędy wywołującego - to tryb pod rozmiar, nie pod programowanie/debugowanie.

Jak widać: APE jest cięższe od zwyczajnego dynamicznie linkowanego binarium z tego samego systemu, ale w trybie tiny mieści się w podobnym rzędzie wielkości - a w zamian dostajemy plik działający na siedmiu różnych środowiskach zamiast jednego.

Przykład w C++20

Wcześniej był przykład w C, ale przecież możemy bez problemu budować programy w C++. W tym celu wziąłem ostatni kod z artykułu o std::ranges korzystający wyłącznie ze standardu C++20:
C/C++
#include <iostream>
#include <vector>
#include <ranges>
using namespace std;

template < typename Data >
void printCollectionDetails( const Data & data )
{
   
if( !ranges::empty( data ) )
   
{
       
std::cout << "Data(size:" << ranges::size( data )
        <<
"),dataBeginPtr=" << ranges::cdata( data )
        <<
",firstElement=" << * ranges::begin( data )
        <<
",lastElement=" << *( ranges::end( data ) - 1 )
        <<
")\n";
   
}
}

int main()
{
   
const vector < string > workingDays = { "Poniedzialek", "Wtorek", "Piatek" };
   
printCollectionDetails( workingDays );
   
   
const double numbers[ ] = { 3.14, 2.71 };
   
printCollectionDetails( numbers );
   
   
ranges::ref_view rv( workingDays );
   
printCollectionDetails( rv );
}
Oto wynik jego kompilacji i uruchomienia:
➜ ./bin/cosmoc++ --std=c++26 main.cpp -o main_cosmoc && ./main_cosmoc
Data(size:3),dataBeginPtr=0x7ccf7c3953e0,firstElement=Poniedzialek,lastElement=Piatek)
Data(size:2),dataBeginPtr=0x7fff732da200,firstElement=3.14,lastElement=2.71)
Data(size:3),dataBeginPtr=0x7ccf7c3953e0,firstElement=Poniedzialek,lastElement=Piatek)

Budowanie bibliotek

Odpalenie samego "hello world" bez możliwości skompilowania całych bibliotek byłoby jedynie ciekawostką, trudną do zastosowania w praktyce. Cosmopolitan pozwala jednak budować w ten sposób także typowe projekty korzystające z autotools - wystarczy ustawić odpowiednie zmienne środowiskowe:
export CC=x86_64-unknown-cosmo-cc
export CXX=x86_64-unknown-cosmo-c++
./configure --prefix=/opt/cosmos/x86_64
make -j
make install
Warto pamiętać, że wszystkie zależności projektu też muszą zostać zbudowane w ten sposób - korzystanie z gotowych pakietów dystrybucji (np. z apt) mija się z celem, bo nie będą to binaria APE.

A co jeśli różne systemy mają wymagać innej implementacji tej samej zależności w ramach jednego pliku? Skoro dynamiczne linkowanie jest wykluczone, nie da się tego zrobić tak jak by się to zrobiło "normalnie" (osobna, prekompilowana biblioteka .so/.dll ładowana zależnie od systemu). Cosmopolitan rozwiązuje to jednak inaczej - w runtime, wewnątrz jednego, wspólnego kodu. Sam Cosmopolitan Libc udostępnia funkcje takie jak IsLinux(), IsWindows(), IsXnu() (macOS), IsFreebsd(), IsOpenbsd() czy IsNetbsd() (zdefiniowane w libc/dce.h - "Detect Compile Environment"), z których sam intensywnie korzysta do przełączania się między implementacjami tego samego wywołania systemowego na różnych platformach. Można z nich skorzystać dokładnie tak samo we własnym kodzie - jeśli jakaś zależność wymaga innego zachowania na różnych systemach, tę różnicę obsłuży się gałęzią
if( IsWindows() ) { ... } else { ... }
 w jednym, wspólnie skompilowanym pliku, a nie osobnymi bibliotekami dobieranymi przez linker.

Sposób linkowania: statyczny czy dynamiczny?

Zawsze statycznie - i to nie przez przeoczenie. Twórca projektu wprost odpowiadał w dyskusjach na GitHubie (np. w GitHub - jart/cosmopolitan pull request #490: unveil polyfill), że Cosmopolitan nie wspiera i nie planuje wspierać dynamicznego linkowania (.so/.dll). To logiczna konsekwencja samej idei: gdyby finalny plik zależał od bibliotek współdzielonych systemu hosta, przestałby być przenośny między systemami. Dlatego budując cokolwiek pod autotools, warto dopisać coś na kształt --disable-shared --enable-static (lub odpowiednik we własnym systemie budowania), żeby oszczędzić sobie błędów linkera. Jedynym wyłomem od tej zasady jest wąska funkcja cosmo_dlopen(), pozwalająca statycznie zlinkowanemu programowi doładować w locie natywną bibliotekę hosta (np. sterownik graficzny) - to jednak zupełnie inny mechanizm niż tradycyjne dynamiczne linkowanie i osobny temat, patrz GitHub - jart/cosmopolitan issue #278: dynamic loading.

SQLite - baza danych w C

Jako eksperyment zbudowałem bibliotekę SQLite3 dla przykładowego kodu (skopiowanego z innego mojego artykułu):
C/C++
#include <stdio.h>
#include <stdlib.h>
#include "sqlite/sqlite3.h"

static int callback( void * not_used, int argc, char * * argv, char * * az_col_name );

static void execute_sql_statement( const char * sql, sqlite3 * db );

int main()
{
   
sqlite3 * db = NULL;
   
   
/* Otwarcie bazy danych */
   
if( sqlite3_open( "pracownicy.db", & db ) != SQLITE_OK )
   
{
       
fprintf( stderr, "Can't open database: %s\n", sqlite3_errmsg( db ) );
       
       
sqlite3_close( db );
       
return EXIT_FAILURE;
   
}
   
   
/* Utworzenie tabeli */
   
const char * sql =
   
"CREATE TABLE IF NOT EXISTS Pracownicy ("
    "ID     INTEGER PRIMARY KEY NOT NULL,"
    "Imie   TEXT            NOT NULL,"
    "Wiek   INTEGER         NOT NULL,"
    "Adres  TEXT,"
    "Pensja REAL"
    ");"
;
   
   
execute_sql_statement( sql, db );
   
   
/* Dodanie pracowników */
   
sql =
   
"INSERT INTO Pracownicy "
    "(ID, Imie, Wiek, Adres, Pensja) "
    "VALUES (1, 'Tadeusz', 24, 'Krakow', 2000.01);"
   
    "INSERT INTO Pracownicy "
    "(ID, Imie, Wiek, Adres, Pensja) "
    "VALUES (2, 'Symeon', 25, 'Texas', 15000.00);"
   
    "INSERT INTO Pracownicy "
    "(ID, Imie, Wiek, Adres, Pensja) "
    "VALUES (3, 'Rodion', 23, 'Norway', 20000.00);"
;
   
   
execute_sql_statement( sql, db );
   
   
/* Wyświetlenie zawartości tabeli */
   
sql = "SELECT * FROM Pracownicy;";
   
   
execute_sql_statement( sql, db );
   
   
/* Zamknięcie bazy */
   
sqlite3_close( db );
   
   
return EXIT_SUCCESS;
}

static int callback( void * not_used, int argc, char * * argv, char * * az_col_name )
{
   
( void ) not_used;
   
   
for( int i = 0; i < argc; ++i )
       
 printf( "%s = %s\n", az_col_name[ i ], argv[ i ] != NULL ? argv[ i ]
        :
"NULL" );
   
   
printf( "\n" );
   
   
return 0;
}

static void execute_sql_statement( const char * sql, sqlite3 * db )
{
   
char * err_msg = NULL;
   
if( sqlite3_exec( db, sql, callback, NULL, & err_msg ) != SQLITE_OK )
   
{
       
fprintf( stderr, "SQL error: %s\n", err_msg );
       
sqlite3_free( err_msg );
   
}
}
kompilacja i uruchomienie:
➜ ./bin/cosmocc --std=c23 main_soci.c sqlite/sqlite3.c -o main_soci
cc1: note: rewrote 2 switch statements
➜  cosmocc ./main_soci
ID = 1
Imie = Tadeusz
Wiek = 24
Adres = Krakow
Pensja = 2000.01

ID = 2
Imie = Symeon
Wiek = 25
Adres = Texas
Pensja = 15000.0

ID = 3
Imie = Rodion
Wiek = 23
Adres = Norway
Pensja = 20000.0

SOCI - biblioteka do obsługi wielu baz danych - nieudana próba

Z kolei jednak nie udało mi się w łatwy sposób zbudować biblioteki SOCI - build wywraca się już na poziomie samego libc++: nagłówek aligned_union.h odwołuje się do (usuniętego w nowszych wersjach standardu) aligned_storage, którego w wersji libc++ dołączonej do Cosmopolitan brakuje, a błąd kaskadowo psuje rozpoznawanie nazw w całym projekcie - dalsze odwołania do std:: są odczytywane jako soci::std, więc reszta standardowej biblioteki (string, vector, make_signed...) przestaje być widoczna. Zgłoszone jako GitHub - jart/cosmopolitan issue #1526: SOCI libc++ aligned_union breakage.

Biblioteka libharu w C - generowanie PDF-ów

Bibliotekę napisaną w C, używającą CMake'a udało się użyć w przenośnym APE. Przerobiłem jeden z przykładów z artykułu o bibliotece libharu. Oto kod:
C/C++
#include <stdio.h>
#include <stdlib.h>
#include <hpdf.h>

void error_handler( HPDF_STATUS error_no, HPDF_STATUS detail_no, void * user_data ) {
   
printf( "ERROR: error_no=%04X, detail_no=%u\n",( HPDF_UINT ) error_no,( HPDF_UINT ) detail_no );
}

const char * generateRandomText( int length ) {
   
static char buffer[ 256 ];
   
const char charset[ ] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ";
   
for( int i = 0; i < length; ++i ) {
       
buffer[ i ] = charset[ rand() %( sizeof( charset ) - 1 ) ];
   
}
   
buffer[ length ] = '\0';
   
return buffer;
}

int main() {
   
HPDF_Doc pdf = HPDF_New( error_handler, NULL );
   
if( !pdf ) {
       
printf( "error: cannot create PdfDoc object\n" );
       
return 1;
   
}
   
   
HPDF_Page page1 = HPDF_AddPage( pdf );
   
HPDF_REAL pageWidth = HPDF_Page_GetWidth( page1 );
   
HPDF_REAL pageHeight = HPDF_Page_GetHeight( page1 );
   
   
HPDF_Font font = HPDF_GetFont( pdf, "Helvetica", NULL );
   
HPDF_Page_SetFontAndSize( page1, font, 24 );
   
   
HPDF_Page_BeginText( page1 );
   
{
       
const char * text = "Przykladowy tekst wysrodkowany";
       
const char * text1 = "tekst przesuniety zwyczajnie";
       
const char * text2 = "tekst przesuniety nadzwyczajnie";
       
const char * text3 = "do nowej linii";
       
const char * text4 = "do jeszcze kolejnej nowej linii";
       
       
HPDF_REAL textWidth = HPDF_Page_TextWidth( page1, text );
       
       
HPDF_Page_TextOut(
       
page1,
       
( pageWidth - textWidth ) / 2.,
       
50,
       
text
        );
       
       
HPDF_Page_MoveTextPos( page1, 50, 100 );
       
HPDF_Page_ShowText( page1, text1 );
       
       
HPDF_Page_MoveTextPos2( page1, 0, 30 );
       
HPDF_Page_ShowText( page1, text2 );
       
       
HPDF_Page_MoveToNextLine( page1 );
       
       
HPDF_Page_SetWordSpace( page1, 2 );
       
HPDF_Page_ShowText( page1, text3 );
       
       
HPDF_Page_ShowTextNextLine( page1, text4 );
       
       
HPDF_Page_SetFontAndSize( page1, font, 44 );
       
       
HPDF_Page_TextRect(
       
page1,
       
0,
       
pageHeight,
       
pageWidth,
       
pageHeight / 3. * 2.,
       
generateRandomText( 20 ),
       
HPDF_TALIGN_CENTER,
       
NULL
        );
   
}
   
   
HPDF_Page_EndText( page1 );
   
   
HPDF_SaveToFile( pdf, "pdf2.pdf" );
   
HPDF_Free( pdf );
}
A bibliotekę budowałem w następujący sposób (CMake + cosmocc, tylko statycznie, bez ZLIB/PNG - są opcjonalne):
git clone --depth 1 https://github.com/libharu/libharu.git
cd libharu
mkdir build && cd build

cmake .. \
 -DCMAKE_C_COMPILER="$(which cosmocc)" \
 -DCMAKE_AR="$(which cosmoar)" \
 -DBUILD_SHARED_LIBS=OFF \
 -DCMAKE_BUILD_TYPE=Release \
 -DCMAKE_INSTALL_PREFIX="$(pwd)/../zbudowano" \
 -DM_LIB=""

cmake --build . -j$(nproc)
make install
Kilka pułapek, na które warto zwrócić uwagę:
  • Cosmopolitan nie ma osobnej libm (funkcje matematyczne siedzą w libc), dlatego trzeba wymusić -DM_LIB="" - inaczej CMake kończy konfigurację błędem M_LIB-NOTFOUND.
  • ZLIB i PNG CMake nie znajduje (to normalne przy toolchainie Cosmo) - biblioteka i tak się buduje, tylko bez kompresji i wsparcia PNG.
  • Przy tworzeniu archiwum .a CMake musi dostać pełną ścieżkę do cosmoar (stąd $(which cosmoar)). Samo -DCMAKE_AR=cosmoar kończy się błędem Error running link command: no such file or directory.

Potem kompilowałem i uruchamiałem program z tą biblioteką:
./bin/x86_64-unknown-cosmo-c++ \
 -isystem /ścieżka/do/libharu/zbudowano/include \
  main_libharu.cpp \
 -L/ścieżka/do/libharu/zbudowano/lib \
 -lhpdf \
 -o pdf_demo.com

./pdf_demo.com
Uwaga: zwykłe cosmoc++ (bez prefiksu architektury) domyślnie buduje fat binary (x86_64 + aarch64). Ponieważ libhpdf.a powstała tylko pod jedną architekturę, linkowanie aarch64 kończy się błędem:
cosmoc++: x86_64 succeeded but aarch64 failed to link executable
.../ld.bfd: cannot find -lhpdf: No such file or directory
Dlatego do linkowania z biblioteką zbudowaną "zwykłym" cosmocc trzeba użyć kompilatora jednolitego (tutaj x86_64-unknown-cosmo-c++). Po uruchomieniu powstaje plik pdf2.pdf. Ten sam mechanizm - jednoarchitekturowa biblioteka + fat kompilator = błąd linkowania - wraca jeszcze przy {fmt} niżej.
Czy da się mieć libhpdf.a jako prawdziwy fat binary? Tak, ale CMake trzeba uruchomić dwa razy - raz z -DCMAKE_C_COMPILER=.../x86_64-unknown-cosmo-cc, raz z -DCMAKE_C_COMPILER=.../aarch64-unknown-cosmo-cc, każdorazowo do osobnego katalogu instalacji. Program, który z tego korzysta, kompiluje się i linkuje wtedy analogicznie: dwa razy, każdy raz jednoarchitekturowym kompilatorem c++ i odpowiadającą mu wersją libhpdf.a, a dwa gotowe pliki wykonywalne łączy się na końcu w jeden fat plik APE narzędziem apelink (patrz sekcja o vcpkg niżej). To dokładnie ten sam schemat, którym posługuje się GitHub - rapidforge-io/mruby-cosmo przy budowaniu zależności takich jak zlib czy OpenSSL - tam też każda biblioteka jest budowana "dual-arch", osobno dla każdej architektury.

POCO - próba nieudana

POCO to duży, modularny zestaw bibliotek C++ (sieć, wątki, XML, JSON, kryptografia, bazy danych i wiele innych) - na cpp0x.pl pojawiło się wiele artykułów na temat komponentów tej biblioteki:

Próba zbudowania POCO (nawet w okrojonej konfiguracji - tylko Foundation + Net) pod cosmocc zakończyła się jednak niepowodzeniem. Główne przyczyny to:
  • konflikty przeciążeń w NumberFormatter (na tej platformie long i Poco::Int64 to ten sam typ),
  • problemy z modelem wątków Cosmopolitan (SCHED_OTHER i założenia POSIX w Thread_POSIX.h).

Foundation mocno opiera się na wątkach, więc nie da się go łatwo "wyciąć".

{fmt} - formatowanie i kolorowanie w konsoli

Kolejną próbą było sprawdzenie popularnej biblioteki C++ do formatowania tekstu - {fmt}. Oferuje ona to co #include <format> ale też dodatkowo m.in. kolorowanie wyjścia w terminalu, czego standardowe std::format (C++20) nie ma.

Budowanie {fmt} pod Cosmopolitan:
git clone --depth 1 https://github.com/fmtlib/fmt.git
cd fmt

cmake -S . -B build \
 -DCMAKE_CXX_COMPILER=$COSMOCC/bin/x86_64-unknown-cosmo-c++ \
 -DCMAKE_AR=$(which ar) \
 -DCMAKE_RANLIB=$(which ranlib) \
 -DBUILD_SHARED_LIBS=OFF \
 -DCMAKE_BUILD_TYPE=Release \
 -DCMAKE_INSTALL_PREFIX="$(pwd)/zbudowano" \
 -DFMT_TEST=OFF \
 -DFMT_DOC=OFF \
 -DFMT_OS=OFF

cmake --build build -j$(nproc)
cmake --install build

Na co trzeba uważać:
  • cosmoar wymaga obu architektur (x86_64 + aarch64). Ponieważ budujemy tylko jedną, używamy zwykłego systemowego ar i ranlib.
  • Flaga -DFMT_OS=OFF wyłącza problematyczny moduł systemowy (stałe POSIX typu O_CREAT nie dają się podstawić w enumach pod Cosmopolitan).
  • Kompilator musi być w wersji jednolitej (x86_64-unknown-cosmo-c++), a nie fat cosmoc++ - z dokładnie tego samego powodu, co przy libharu wyżej: biblioteka jest zbudowana tylko pod jedną architekturę, więc fat kompilator próbujący dolinkować ją też pod aarch64 skończy błędem brakującego -lfmt.

Przykład z kolorowaniem:
C/C++
// main_fmt.cpp
#include <fmt/core.h>
#include <fmt/color.h>

int main() {
   
fmt::print( fg( fmt::color::red ), "Czerwony tekst\n" );
   
fmt::print( fg( fmt::color::green ) | fmt::emphasis::bold, "Zielony i pogrubiony\n" );
   
fmt::print( fg( fmt::color::yellow ) | bg( fmt::color::blue ), "Żółty na niebieskim tle\n" );
   
   
fmt::print( fmt::emphasis::italic, "Kursywa\n" );
   
fmt::print( fmt::emphasis::underline, "Podkreślenie\n" );
   
fmt::print( fmt::emphasis::strikethrough, "Przekreślenie\n" );
   
   
fmt::print( fg( fmt::color::cyan ) | fmt::emphasis::bold | fmt::emphasis::underline,
   
"Cyan + bold + underline\n" );
   
   
int x = 42;
   
fmt::print( fg( fmt::color::magenta ), "Wartość = {}\n", x );
}

Kompilacja programu:
$COSMOCC/bin/x86_64-unknown-cosmo-c++ \
 -isystem $COSMOCC/fmt/zbudowano/include \
  main_fmt.cpp \
 -L$COSMOCC/fmt/zbudowano/lib \
 -lfmt \
 -o main_fmt.com
Po uruchomieniu ./main_fmt.com otrzymujemy kolorowe wyjście w terminalu - dokładnie to, czego nie da się zrobić samym std::format.

Alternatywa: tryb header-only:
Jeśli nie chcemy budować biblioteki, {fmt} działa też w trybie header-only:
x86_64-unknown-cosmo-c++ \
 -I$COSMOCC/fmt/include \
 -DFMT_HEADER_ONLY=1 \
  main_fmt.cpp \
 -o main_fmt.com
Oba warianty (statyczna biblioteka i header-only) działają poprawnie pod Cosmopolitan.

GoogleTest - popularna biblioteka do testów

Budowanie GoogleTest:
git clone --depth 1 --branch v1.18.0 https://github.com/google/googletest.git
cd googletest

cmake -S . -B build \
 -DCMAKE_C_COMPILER=$COSMOCC/bin/x86_64-unknown-cosmo-cc \
 -DCMAKE_CXX_COMPILER=$COSMOCC/bin/x86_64-unknown-cosmo-c++ \
 -DCMAKE_AR=/usr/bin/ar \
 -DCMAKE_RANLIB=/usr/bin/ranlib \
 -DCMAKE_BUILD_TYPE=Release \
 -DBUILD_SHARED_LIBS=OFF \
 -Dgtest_build_tests=OFF \
 -Dgtest_build_samples=OFF \
 -Dgtest_disable_pthreads=ON \
 -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \
 -DCMAKE_INSTALL_PREFIX=$COSMOCC/googletest/zbudowano

cmake --build build -j$(nproc)
cmake --install build
Dwie flagi zasługują na komentarz:
  • -Dgtest_disable_pthreads=ON - ten sam rodzaj problemu z wątkami, co przy POCO wyżej (więcej o tym w sekcji "Kiedy Cosmopolitan się nie sprawdzi"). GoogleTest sam w sobie prawie nie potrzebuje wątków, więc najprościej je po prostu wyłączyć, zamiast walczyć z modelem pthread Cosmopolitan.
  • -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY - standardowa sztuczka przy egzotycznych/cross-toolchainach: CMake w swoim kroku wykrywania kompilatora domyślnie próbuje skonsolidować mały plik wykonywalny, co przy nietypowym linkerze bywa zawodne. Ta flaga każe CMake zadowolić się samą kompilacją do biblioteki statycznej.

Skoro GoogleTest jest już zbudowany, możemy sprawdzić, czy działa również program skompilowany przy pomocy Cosmopolitan. Zamiast testować trywialne dodawanie dwóch liczb, zróbmy coś odrobinę ciekawszego i wykorzystajmy kilka funkcji matematycznych.
C/C++
#include <cmath>
#include <numbers>
#include <gtest/gtest.h>

TEST( MathTest, SinAndCos ) {
   
constexpr double pi = std::numbers::pi;
   
   
EXPECT_NEAR( std::sin( pi / 2.0 ), 1.0, 1e-12 );
   
EXPECT_NEAR( std::cos( pi ), - 1.0, 1e-12 );
}

TEST( MathTest, PythagoreanIdentity ) {
   
constexpr double x = 0.73;
   
   
EXPECT_NEAR( std::sin( x ) * std::sin( x ) + std::cos( x ) * std::cos( x ), 1.0,
   
1e-12 );
}

TEST( MathTest, PiIsApproximatelyPi ) {
   
EXPECT_NEAR( std::numbers::pi, 3.14, 0.002 );
}
  • Pierwszy test sprawdza dwie dobrze znane wartości funkcji trygonometrycznych. Dla π/2 sinus powinien być równy 1, natomiast dla π cosinus powinien być równy -1.
  • W drugim teście sprawdzamy klasyczną tożsamość:
    sin²(x) + cos²(x) = 1

    Jest to również dobry przykład tego, dlaczego w testach wartości zmiennoprzecinkowych nie zawsze warto używać EXPECT_EQ. Wynik obliczeń może różnić się od wartości idealnej o niewielki błąd numeryczny, dlatego GoogleTest udostępnia EXPECT_NEAR, który pozwala określić dopuszczalną różnicę.
  • Na koniec mamy test, który można potraktować jako żartobliwy test jednostkowy:
    EXPECT_NEAR(std::numbers::pi, 3.14, 0.002);

    Sprawdzamy w nim, czy π jest "w przybliżeniu równe" 3.14. Oczywiście rzeczywista wartość jest nieco większa, ale przy przyjętej tolerancji test przechodzi.

Kompilacja testu:
Test możemy skompilować za pomocą tego samego kompilatora Cosmopolitan:
$COSMOCC/bin/x86_64-unknown-cosmo-c++ \
 -std=c++20 \
 -I$COSMOCC/googletest/zbudowano/include \
  main_tests.cpp \
  $COSMOCC/googletest/zbudowano/lib/libgtest_main.a \
  $COSMOCC/googletest/zbudowano/lib/libgtest.a \
 -o math_test
Warto zwrócić uwagę na gtest_main. Dzięki dołączeniu biblioteki libgtest_main.a nie musimy pisać własnej funkcji main(). Punkt wejścia dostarcza sam GoogleTest.
Po kompilacji otrzymujemy program math_test, który możemy uruchomić bezpośrednio:
./math_test

Wynik:
Running main() from /path/to/cosmocc/googletest/googletest/src/gtest_main.cc
[==========] Running 3 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 3 tests from MathTest
[ RUN      ] MathTest.SinAndCos
[       OK ] MathTest.SinAndCos (0 ms)
[ RUN      ] MathTest.PythagoreanIdentity
[       OK ] MathTest.PythagoreanIdentity (0 ms)
[ RUN      ] MathTest.PiIsApproximatelyPi
[       OK ] MathTest.PiIsApproximatelyPi (0 ms)
[----------] 3 tests from MathTest (0 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 3 tests.
Wszystkie trzy testy zakończyły się sukcesem. Co ciekawe, w całym przykładzie nie korzystamy z żadnych zewnętrznych bibliotek dynamicznych - GoogleTest został wcześniej zbudowany jako biblioteka statyczna, a test jest linkowany z libgtest_main.a oraz libgtest.a.
Możemy więc powiedzieć, że mamy działający zestaw testów C++ możliwy do zbudowania i uruchomienia na różnych systemach bez rekompilacji.

Boost 1.92.0

Boost to jeden z najbardziej znanych zestawów bibliotek C++ - dla tych, którzy nazwę słyszeli, ale nie mieli okazji zajrzeć głębiej: to nie jedna biblioteka, tylko kolekcja ponad 150 niezależnych modułów (m.in. programowanie sieciowe, wątki, wyrażenia regularne, parsery, kontenery, obsługa dat) - część z nich trafiła później, w mniej lub bardziej zmienionej formie, do standardu C++. Pełną listę komponentów wersji 1.92.0 znajdziesz tutaj: Boost 1.92.0 - lista bibliotek.
Poniższa procedura pokazuje kompletną ścieżkę budowania wybranych bibliotek Boost 1.92.0 przy użyciu toolchainu Cosmopolitan cosmocc, a następnie wykorzystania ich w programie skompilowanym dla Cosmopolitan libc.
Buduję tylko jedną z bibliotek na próbę, przez co niektóre biblioteki mogą się nie zbudować mimo sukcesu z tą jedną.

Założenia:
  • Linux jako system hosta,
  • architektura x86-64,
  • Cosmopolitan libc wraz z cosmocc,
  • Boost 1.92.0,
  • statyczne biblioteki Boost,
  • Boost.Build (b2),
  • przykład z Boost.Program_options.

1. Przygotowanie katalogu roboczego
Na początku warto przyjąć prostą strukturę katalogów:
workspace/
├── cosmocc/
└── boost-1.92.0/
Przechodzimy do katalogu roboczego:
mkdir -p ~/workspace
cd ~/workspace
Jeżeli Cosmopolitan nie jest jeszcze dostępny, należy przygotować go zgodnie z dokumentacją projektu i umieścić w wybranym katalogu.

Jeśli podążasz za całym artykułem, masz już ustawioną zmienną $COSMOCC z sekcji "Pobranie frameworka" - upewnij się tylko, że wskazuje na katalog z rozpakowanym toolchainem (tutaj: workspace/cosmocc). Jeśli zaczynasz od tej sekcji, ustaw ją teraz:
export COSMOCC="$HOME/workspace/cosmocc"

Toolchain powinien zawierać między innymi $COSMOCC/bin/x86_64-unknown-cosmo-c++ i $COSMOCC/bin/x86_64-unknown-cosmo-ar. Możemy to sprawdzić:
"$COSMOCC/bin/x86_64-unknown-cosmo-c++" --version
"$COSMOCC/bin/x86_64-unknown-cosmo-ar" --version

2. Pobranie źródeł Boost
Źródła Boost 1.92.0 pobieramy z repozytorium Git:
cd ~/workspace

git clone --recursive --branch boost-1.92.0 \
    https://github.com/boostorg/boost.git \
    boost-1.92.0
Boost na GitHubie to "superprojekt" - sam katalog boost-1.92.0 jest niemal pusty, a każda z ponad stu bibliotek (w tym libs/program_options) jest osobnym submodułem gita. Bez --recursive pobierzemy więc same "wskaźniki" do submodułów, bez ich rzeczywistej zawartości, i build zakończy się błędami o brakujących plikach. Z tego samego powodu przy takim klonowaniu lepiej nie stosować płytkiego --depth 1 - dla pełnego zestawu submodułów i tak trzeba pobrać większość danych, więc płytki klon niewiele by tu pomógł.
Przechodzimy do katalogu źródeł:
cd boost-1.92.0


3. Przygotowanie Boost.Build
Repozytorium Boost zawiera źródła Boost.Build, ale przed właściwym buildem należy przygotować program b2 (system budowy biblioteki Boost). Na tym etapie nie korzystamy jeszcze z kompilatorów Cosmopolitan - b2 to zwykłe, natywne narzędzie budujące uruchamiane na hoście, samo w sobie nie musi więc być przenośne. Dopiero ono, w kolejnym kroku, użyje kompilatora cosmocc do zbudowania właściwych bibliotek Boosta.
W katalogu Boost uruchamiamy:
./bootstrap.sh

Bootstrap przygotowuje lokalne narzędzia Boost.Build i buduje program b2. Po zakończeniu powinien być dostępny:
./b2 --version

To właśnie b2 będzie później odpowiedzialne za konfigurację toolsetu, kompilację źródeł Boost, tworzenie bibliotek statycznych i staging wynikowych artefaktów.
Bootstrap a właściwy build Boost Ważne jest rozdzielenie dwóch etapów: bootstrap.sh → b2 → budowanie bibliotek Boost. bootstrap.sh nie buduje bibliotek Boost. Przygotowuje narzędzie Boost.Build, którego używamy dopiero w następnym kroku.

4. Konfiguracja toolsetu Cosmopolitan
Boost.Build obsługuje wiele toolsetów, między innymi GCC. Cosmocc korzysta z GCC-compatible toolchainu, dlatego możemy skonfigurować go jako własną odmianę toolsetu GCC. Tworzymy plik ~/user-config.jam z następującą zawartością:
using gcc : cosmo
    : /path/to/cosmocc/bin/x86_64-unknown-cosmo-c++
    : <archiver>/path/to/cosmocc/bin/x86_64-unknown-cosmo-ar
;
W praktycznym środowisku zamiast /path/to/cosmocc należy użyć lokalizacji własnej instalacji Cosmocc. Istotne są dwa elementy: gcc : cosmo definiuje toolset gcc-cosmo, natomiast <archiver>... wskazuje program ar, którego Boost.Build ma używać do tworzenia bibliotek statycznych.
Dlaczego jawnie wskazujemy ar? To jeden z ważniejszych szczegółów całej konfiguracji. W przypadku standardowej instalacji GCC Boost.Build zazwyczaj potrafi samodzielnie znaleźć odpowiedni archiver. Przy Cosmocc automatyczne wykrywanie może jednak nie wskazać właściwego programu. Cosmopolitan dostarcza właściwy archiver w postaci x86_64-unknown-cosmo-ar, dlatego konfigurujemy go jawnie przez <archiver>/path/to/cosmocc/bin/x86_64-unknown-cosmo-ar. Nie należy w tym celu zmieniać systemowego /usr/bin/ar ani zastępować go symlinkiem do narzędzi Cosmopolitan - konfiguracja powinna dotyczyć wyłącznie toolsetu gcc-cosmo.

5. Budowanie Boost.Program_options
Mając skonfigurowany toolset, przechodzimy od razu do właściwego builda - bez osobnego "suchego przebiegu": jeśli coś jest nie tak z konfiguracją, i tak zobaczymy to w logu budowania.
./b2 \
   --with-program_options \
   --build-dir=build-cosmo \
   --stagedir=stage-cosmo \
    toolset=gcc-cosmo \
    variant=release \
    link=static \
    threading=single \
   -j"$(nproc)"
W tym przypadku interesuje nas przede wszystkim libs/program_options/ oraz wynikowa biblioteka stage-cosmo/lib/libboost_program_options.a. Boost.Build może przy okazji zbudować zależności potrzebne przez program_options, dlatego w logu może pojawić się również boost_container - nie jest to oznaka problemu.
Dlaczego osobny build-dir i stagedir? Warto zachować rozdzielenie build-cosmo/ i stage-cosmo/ od standardowych katalogów używanych przez inne konfiguracje Boost (np. build-gcc//stage-gcc/). Pozwala to równolegle utrzymywać różne konfiguracje bez mieszania obiektów i bibliotek zbudowanych różnymi toolchainami.
Warto rzucić okiem na log budowania i sprawdzić, czy pojawia się w nim wpis wskazujący na gcc-cosmo, a archiver wskazuje konkretny program (.../x86_64-unknown-cosmo-ar), a nie sam katalog bin. Błędne wykrycie katalogu zamiast programu ar prowadzi do błędu Permission denied podczas wykonywania gcc.archive.

6. Minimalny test Boost.Program_options
Tworzymy prosty program (zapisany jako main_program_options.cpp):
C/C++
#include <boost/program_options.hpp>
#include <iostream>

int main( int argc, char * * argv )
{
   
namespace po = boost::program_options;
   
   
po::options_description desc( "Options" );
   
   
desc.add_options()
    (
"help", "show help" );
   
   
po::variables_map vm;
   
   
po::store(
   
po::parse_command_line( argc, argv, desc ),
   
vm
    );
   
   
po::notify( vm );
   
   
if( vm.count( "help" ) )
       
 std::cout << desc << '\n';
   
}

7. Kompilacja i linkowanie z Cosmopolitan
Program kompilujemy za pomocą tego samego kompilatora Cosmopolitan, którego używaliśmy w konfiguracji Boost:
"$COSMOCC/bin/x86_64-unknown-cosmo-c++" \
   -std=c++17 \
   -I/path/to/boost-1.92.0 \
    main_program_options.cpp \
   -L/path/to/boost-1.92.0/stage-cosmo/lib \
   -lboost_program_options \
   -o main_program_options
W praktyce /path/to/boost-1.92.0 należy zastąpić lokalizacją źródeł Boost albo - jeszcze lepiej - użyć zmiennej export BOOST_ROOT="$HOME/workspace/boost-1.92.0" i wtedy:
"$COSMOCC/bin/x86_64-unknown-cosmo-c++" \
   -std=c++17 \
   -I"$BOOST_ROOT" \
    main_program_options.cpp \
   -L"$BOOST_ROOT/stage-cosmo/lib" \
   -lboost_program_options \
   -o main_program_options
Taka forma jest preferowana w dokumentacji i skryptach, ponieważ nie zakłada konkretnego katalogu domowego ani nazwy użytkownika.

8. Test uruchomieniowy
Na koniec uruchamiamy program:
./main_program_options --help

Powinniśmy otrzymać:
Options:
 --help                show help
To jest właściwy test końcowy całej procedury - nie sprawdzamy już tylko, czy Boost.Build zakończył się sukcesem, ale potwierdzamy cały łańcuch: źródła Boost → bootstrap.sh → Boost.Build/b2 → toolset gcc-cosmo (kompilator i archiver Cosmopolitan) → libboost_program_options.a → linker Cosmopolitan → main_program_options → ./main_program_options --help. Jeżeli program poprawnie się uruchamia i wyświetla oczekiwane opcje, mamy praktyczne potwierdzenie, że Boost.Program_options został zbudowany i zlinkowany przy użyciu Cosmopolitan toolchainu.

vcpkg - multiplatformowy manager pakietów

Teoretycznie powinno się dać: vcpkg pozwala zdefiniować własny triplet i plik toolchain, a cosmocc to "tylko" kolejny kompilator ze specyficznym targetem (x86_64-unknown-cosmo-cc). W praktyce nie natrafiłem jednak na gotowy, sprawdzony przykład takiego tripletu - ani w dokumentacji projektu, ani w społeczności - więc na razie łatwiej i pewniej jest budować zależności ręcznie, bezpośrednio narzędziami cosmocc (tak jak wyżej, dla libharu i {fmt}), niż przecierać szlak z vcpkg bez żadnego punktu odniesienia.

Tam, gdzie zależy nam na prawdziwej "fat" binarce (x86_64 + aarch64 naraz), społeczność radzi sobie inaczej: buduje zależności osobno dla każdej architektury (kompilatorami x86_64-unknown-cosmo-cc / aarch64-unknown-cosmo-cc), a gotowe pliki ELF scala w jeden plik APE narzędziem apelink. W praktyce wygląda to np. tak (zweryfikowany przykład z realnego projektu, gdzie do binarki dokłada się bibliotekę X11):
x86_64-unknown-cosmo-cc -o x11test x11test.c -I output/include -L output/lib -lX11 -lxcb -lXau
apelink -o x11test.com -l /opt/cosmo/.cosmocc/3.3.5/bin/ape-x86_64.elf x11test
Najpierw zwykła kompilacja/linkowanie pod jedną architekturę, a potem apelink doszywa do wyniku loader APE (plik ape-x86_64.elf z toolchaina) i zamienia zwykły plik ELF w samodzielny, uruchamialny .com. Dla prawdziwej binarki fat trzeba by powtórzyć kompilację pod aarch64 i przekazać apelinkowi oba loadery oraz oba pliki wynikowe - pełnej, kompletnej składni dla tego wariantu nie udało mi się jednak nigdzie znaleźć w całości (log budowania w dokumentacji toolchaina urywa się dokładnie w tym miejscu), więc zanim oprzesz na tym coś ważnego, sprawdź samodzielnie apelink --help.
Dobrym, kompletnym przykładem połączenia tego wszystkiego (osobne architektury + statyczne biblioteki zewnętrzne) jest GitHub - rapidforge-io/mruby-cosmo - projekt statycznie budujący mruby (czyli lekką implementację Rubiego stworzoną z myślą o osadzaniu w innych programach i urządzeniach - podobną rolą do tego, czym Lua jest dla C/C++) razem z OpenSSL i libcurl w jeden plik APE.

Zanim ręcznie zabierzesz się za budowanie czegoś większego tym sposobem, warto sprawdzić, czy ktoś już tego nie zrobił: GitHub - ahgamut/superconfigure to zbiór gotowych skryptów budujących w ten właśnie sposób (dwie architektury + apelink) całe mnóstwo popularnego oprogramowania jako fat binarki APE - m.in. bash, zsh, GNU Coreutils, sed, vim, emacs, curl, a nawet gcc i Python. Gotowe binarki można pobrać z zakładki Releases tego repozytorium, a jeśli danego programu tam nie ma, jego skrypt budujący (w katalogu config/) to dobry punkt wyjścia do napisania własnego.

Kiedy Cosmopolitan się nie sprawdzi

Cosmopolitan powstał i rozwija się przede wszystkim z myślą o programach konsolowych/terminalowych oraz usługach sieciowych (np. redbean opisany niżej) - nie o natywnych aplikacjach okienkowych. Potwierdza to zresztą sam autor projektu w GitHub - jart/cosmopolitan issue #35: cross platform GUI / 2D software rendering: tworzenie GUI na Windowsie jest już możliwe, ale rozszerzenie tego na pozostałe platformy oznaczone zostało etykietą "duże przedsięwzięcie, ograniczone zasoby" i pozostaje otwartym tematem, a nie gotową funkcją. Próba z SOCI wyżej napotkała z kolei zupełnie inny rodzaj problemu - w dołączonej wersji libc++.

To nie znaczy, że nikt nie próbował dalej. W tym notatki JoshuaWierenga o budowaniu X11/SDL2 pod cosmocc gist-cie znajdziesz krok po kroku, jak (na Linuksie) skompilować pod cosmocc biblioteki Xlib/libxcb oraz SDL2 (z niewielkimi łatkami w kodzie źródłowym tych bibliotek) i odpalić prosty przykład okienkowy jako APE - to właśnie stamtąd pochodzi przykład użycia apelink wyżej. Autor tej notatki zaznacza jednak, że odpowiednik na Windowsie (GDI) bywa dużo bardziej bolesny.

Inne rzeczy, o których warto pamiętać, zanim wybierzesz Cosmopolitan do projektu:
  • brak dynamicznego linkowania (patrz wyżej) - żadnej biblioteki nie da się "podmienić" bez przebudowy całego programu, jak również możemy mieć problem z używaniem projektów o licencji LGPL
  • każdą zależność trzeba samodzielnie zbudować tym samym toolchainem - nie skorzystamy z gotowych pakietów systemowych
  • binarka jest cięższa niż typowa, dynamicznie linkowana wersja dla jednego systemu (patrz sekcja o rozmiarach wyżej);
  • na części dystrybucji Linuksa i pod WSL-em można trafić na problemy z binfmt_misc - patrz kolejna sekcja;
  • starsze powłoki (Zsh poniżej wersji 5.9) mogą nie radzić sobie z binarnymi danymi w skrypcie powłoki, jakim częściowo jest plik APE.

Warto wspomnieć, że temat GUI pod Cosmopolitan eksploruje też zespół Qt: na Qt Contributors Summit 2024 i 2025 Cristian Adam pokazywał eksperymentalne budowanie Qt (w tym Qt Creator) z Cosmopolitan Libc – na razie przez serwer VNC QPA (http://localhost:5900), bez natywnych okien; to wciąż work in progress, nie gotowa ścieżka ( QtCS2024, QtCS25).

Wątki - najczęstszy realny problem

Rzeczowo, z tego co sami napotkaliśmy wyżej (POCO, GoogleTest): Cosmopolitan ma własną, wbudowaną implementację pthread (przez długi czas jej w ogóle nie było - starsze dyskusje na GitHubie wspominają, że z tego powodu nawet Python 3.7+ nie dawał się budować, dopóki autor nie dodał wątków). Wątki jako takie dziś działają, ale to reimplementacja, nie natywne wątki hosta - i tu leżał problem zarówno w POCO (SCHED_OTHER, założenia Thread_POSIX.h), jak i w GoogleTest (musieliśmy dać -Dgtest_disable_pthreads=ON). W praktyce: jeśli biblioteka mocno opiera się o wątki i zakłada konkretne, "glibcowe" zachowanie POSIX-owego pthreada (polityki szeregowania, konkretne działanie TLS itp.), warto się liczyć z tym, że pod Cosmopolitan może wymagać łatek albo wyłączenia wątków w ogóle - tak jak zrobiliśmy to dla GoogleTest.

Wątki to jednak tylko jeden przykład szerszego zjawiska: nie każde wywołanie systemowe jest zaimplementowane identycznie (albo w ogóle) na wszystkich siedmiu wspieranych platformach. Zamiast zgadywać, które konkretnie funkcje działają gdzie, najlepiej zajrzeć do oficjalnej listy funkcji standardowych i niestandardowych z adnotacją, na których systemach każda z nich jest realnie wspierana (dostępna ze strony domowej projektu, do której odnośniki znajdują się w oficjalnym repozytorium na GitHubie).

Testy wydajnościowe

Skoro Cosmopolitan nie jest maszyną wirtualną ani interpreterem, tylko generuje zwykły, natywny kod maszynowy (dla kilku architektur naraz, sklejonych w jeden plik) - z założenia obliczenia czysto procesorowe (bez wywołań systemowych w pętli) powinny działać z wydajnością bardzo bliską zwykłemu gcc/clang. Żeby to sprawdzić w praktyce, posłużmy się klasycznym problemem - sitem Eratostenesa:
C/C++
// sieve.cpp - Sito Eratostenesa jako obciazenie CPU do testu wydajnosci
#include <cstdio>
#include <vector>
#include <chrono>

int main() {
   
const long long N = 500'000'000LL;
   
   
auto start = std::chrono::steady_clock::now();
   
   
std::vector < bool > is_composite( N + 1, false );
   
long long count = 0;
   
for( long long i = 2; i <= N; ++i ) {
       
if( !is_composite[ i ] ) {
           
++count;
           
if( i * i <= N ) {
               
for( long long j = i * i; j <= N; j += i ) {
                   
is_composite[ j ] = true;
               
}
            }
        }
    }
   
   
auto end = std::chrono::steady_clock::now();
   
double seconds = std::chrono::duration < double >( end - start ).count();
   
   
std::printf( "Liczb pierwszych <= %lld: %lld\n", N, count );
   
std::printf( "Czas: %.3f s\n", seconds );
   
return 0;
}

Zestawienie wyników (500 000 000, ta sama maszyna, wszystko poza ostatnim wierszem na Linuksie):
g++ bez optymalizacji (-O0) 62,791 s
g++ -O2 3,646 s
g++ -O3 (i strip) 3,616 s
cosmoc++ bez podanej flagi optymalizacji 10,881 s
cosmoc++ -O3 3,682 s
cosmoc++ -O3 -mtiny 3,519 s
cosmoc++ -O2 -mtiny 3,540 s
ten sam plik pod Windowsem (Docker, obraz dockurr/windows) 4,510 s
Na maszynie:
Processor: Intel® Core™ i7-8750H CPU @ 2.20GHz (12 wątków)
Memory: 48 GiB RAM
Laptop: Lenovo Legion Y530-15ICH
Wniosek: hipoteza się potwierdza. Dla kodu czysto procesorowego, przy porównywalnym poziomie optymalizacji (-O2/-O3), cosmoc++ i zwykłe g++ dają praktycznie ten sam czas (3,5-3,7 s) - różnice są rzędu pojedynczych procent, nie wielokrotności. Nawet pod Windowsem uruchomionym w Dockerze wynik (4,51 s) jest w tym samym rzędzie wielkości - dodatkowy narzut bierze się raczej z samej wirtualizacji niż z APE. Największa różnica widoczna w tabeli to nie zasługa ani wada Cosmopolitan, tylko przypomnienie, żeby zawsze podawać jawną flagę optymalizacji: zwykłe g++ bez -O (czyli -O0) jest bez porównania wolniejsze niż cosmoc++ bez żadnej flagi - co sugeruje, że domyślny tryb cosmoc++ i tak stosuje jakiś podstawowy poziom optymalizacji, choć wyraźnie niższy niż -O2/-O3.

Cosmopolitan vs Wine i WSL

Wracając do wątku z wprowadzenia - o dziwo, Cosmopolitan potrafi wejść w drogę zarówno Wine'owi, jak i WSL-owi, i to z zabawnego powodu. Na części dystrybucji Linuksa mechanizm jądra binfmt_misc jest domyślnie skonfigurowany tak, by obce formaty plików wykonywalnych (w tym pliki .exe z Windowsa) przekazywać do obsługi przez Wine, a próba uruchomienia świeżo skompilowanej binarki kończy się błędem w stylu run-detectors: unable to find an interpreter. Rozwiązaniem, opisanym wprost w GitHub - jart/cosmopolitan: cosmocc toolchain README, jest zarejestrowanie w binfmt_misc osobnej, dedykowanej reguły dla plików APE:
sudo wget -O /usr/bin/ape https://cosmo.zip/pub/cosmos/bin/ape-$(uname -m).elf
sudo chmod +x /usr/bin/ape
sudo sh -c "echo ':APE:M::MZqFpD::/usr/bin/ape:' >/proc/sys/fs/binfmt_misc/register"
sudo sh -c "echo ':APE-jart:M::jartsr::/usr/bin/ape:' >/proc/sys/fs/binfmt_misc/register"
(ten sam efekt daje też uruchomienie gotowego skryptu ape/apeinstall.sh z repozytorium projektu). Jeśli to nie pomoże, można binfmt_misc wyłączyć całkowicie:
sudo sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/cli'     # usuwa interpreter MZ z Ubuntu
sudo sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/status'  # usuwa WSZYSTKIE wpisy binfmt_misc
Pod WSL-em problem jest podobny (WSL próbuje uruchamiać pliki z nagłówkiem MZ jako natywne binarki Win32), a oficjalnym rozwiązaniem jest wyłączenie konkretnie interopu z Windowsem:
sudo sh -c "echo -1 > /proc/sys/fs/binfmt_misc/WSLInterop"

A czy w takiej sytuacji nie wystarczy po prostu jawnie odpalić plik przez powłokę, np. sh ./plik_cosmopolitan, zamiast ./plik_cosmopolitan? Krótka odpowiedź: bardzo prawdopodobne, że tak. Nagłówek APE jest przecież poprawnym skryptem sh - jawne wywołanie przez sh (albo bash) pomija jądrowy mechanizm rozpoznawania formatu (execve() + binfmt_misc), więc Wine w ogóle nie wchodzi do gry. Sama dokumentacja projektu poleca zresztą analogiczny trik (bash -c './hello.com') jako obejście innego, pokrewnego problemu (niepoprawna obsługa binarnych danych w skrypcie przez starsze Zsh/fish) - to dobra przesłanka, że ten sam patent zadziała i tutaj, choć wprost pod kątem Wine/WSL nigdzie tego nie znalazłem opisanego.

Ekosystem wokół Cosmopolitan

Sam format APE stał się fundamentem kilku innych, ciekawych projektów tego samego autora i społeczności:
  • redbean - jednoplikowy serwer WWW, w którym strony (w Lua  + SQLite) trzymane są wewnątrz tego samego archiwum ZIP/APE co sam serwer;
  • llamafile - łączy Cosmopolitan Libc z llama.cpp, pozwalając rozprowadzać lokalne modele LLM jako pojedynczy, samodzielny plik wykonywalny na kilku systemach naraz. Projekt wystartował jako Mozilla-Ocho/llamafile, a rozwijany jest dalej pod skrzydłami Mozilla.ai.

Gotowe llamafile z modelem w środku można pobrać i odpalić od razu. Przykład z oficjalnego README (mały Qwen3.5 0.8B):
curl -LO https://huggingface.co/mozilla-ai/llamafile_0.10/resolve/main/Qwen3.5-0.8B-Q8_0.llamafile
chmod +x Qwen3.5-0.8B-Q8_0.llamafile
./Qwen3.5-0.8B-Q8_0.llamafile
Po uruchomieniu program sam otwiera przeglądarkę z czatem pod http://localhost:8080, a przy okazji wystawia też lokalne API kompatybilne z OpenAI.
Da się też złożyć własną binarkę z wybranym modelem - np. polskim Bielik-1.5B-v3.0-Instruct (albo innym z rodziny SpeakLeash / Bielik). Oficjalna instrukcja Mozilli: Creating llamafiles. Ważne: budujemy i używamy runtime z mozilla-ai/llamafile (llama.cpp z łatami pod Cosmopolitan i zipalign), a nie „gołego” upstreamowego llama.cpp. Gotowy runtime, plik GGUF i plik .args skleja się narzędziem zipalign w jeden plik APE - model ląduje pod ścieżką /zip/..., dokładnie jak zasoby w sekcji bonusowej niżej.

Podobnie łatwo w kilka poleceń uruchamia się redbean - wspomniany wyżej jednoplikowy serwer WWW:
curl -o redbean.com https://redbean.dev/redbean-2.2.com
chmod +x redbean.com
echo '<h1>Witaj z redbean!</h1>' > index.html
zip redbean.com index.html
./redbean.com
Po uruchomieniu strona jest dostępna pod http://localhost:8080 - a doklejenie pliku index.html przez zwykły zip to dokładnie ten sam mechanizm, który opisujemy niżej w sekcji bonusowej o pakowaniu własnych danych do wnętrza pliku APE.

Bonus: własne dane wewnątrz pliku APE

Skoro plik APE to jednocześnie archiwum ZIP, dokumentacja toolchaina opisuje wprost, jak z tego skorzystać do dołączania własnych zasobów. Do gotowego pliku wynikowego można dokleić dowolny plik (lub cały katalog) - projekt poleca zmodyfikowaną wersję Info-ZIP z cosmo.zip, żeby na pewno nie zepsuć nagłówka APE:
wget -O zip https://cosmo.zip/pub/cosmos/bin/zip
chmod +x zip
./zip program.com plik.txt
# albo cały katalog:
./zip -r program.com katalog/
Zawartość archiwum można podejrzeć zwykłym unzip:
unzip -l program.com


W kodzie pliki są widoczne pod specjalną ścieżką /zip/... - działają standardowe funkcje POSIX (access, fopen, fread, stat itd.), jakby to był zwykły plik na dysku:
C/C++
if( access( "/zip/plik.txt", F_OK ) == 0 ) {
   
fprintf( stderr, "/zip/plik.txt jest dostępny jako zasób\n" );
}

Przykład: program „fortune” z cytatami w środku binarki

Poniżej kompletny, krótki przykład. Program losuje jeden cytat z pliku wbudowanego w binarkę.

Plik cytaty.txt:
"Niesprawiedliwy to ktoś, kto źle używa swoich dóbr, albo uzurpuje sobie cudze dobra, albo do tego co posiada, doszedł w sposób niesprawiedliwy." - Św. Izydor z Sewilii
"Musicie od siebie wymagać, nawet gdyby inni od was nie wymagali." - Św. Jan Paweł II
"Najbardziej twórczą ze wszystkich prac jest praca nad sobą, która pozwala odnajdywać urok młodości. Nie ma większego bogactwa w narodzie nad światłych obywateli." - Św. Jan Paweł II
"Człowiek jest wielki nie przez to, co posiada, lecz przez to, kim jest; nie przez to, co ma, lecz przez to, czym dzieli się z innymi." - Św. Jan Paweł II
Kod (fortune.cpp):
C/C++
#include <fstream>
#include <iostream>
#include <random>
#include <string>
#include <vector>

int main() {
   
std::ifstream plik( "/zip/cytaty.txt" );
   
if( !plik ) {
       
std::cerr << "Brak /zip/cytaty.txt - najpierw: ./zip fortune.com cytaty.txt\n";
       
return 1;
   
}
   
   
std::vector < std::string > cytaty;
   
for( std::string linia; std::getline( plik, linia ); )
   
if( !linia.empty() )
       
 cytaty.push_back( linia );
   
   
if( cytaty.empty() ) {
       
std::cerr << "Plik cytatow jest pusty\n";
       
return 1;
   
}
   
   
std::mt19937 rng( std::random_device { }() );
   
std::uniform_int_distribution < std::size_t > los( 0, cytaty.size() - 1 );
   
std::cout << cytaty[ los( rng ) ] << '\n';
}
Kompilacja i doklejenie danych:
./bin/cosmoc++ -o fortune.com fortune.cpp
./zip fortune.com cytaty.txt
./fortune.com
Po usunięciu oryginalnego cytaty.txt z dysku program nadal działa - cytaty są już wewnątrz binarki. To samo podejście działa z gotową bazą SQLite: wystarczy zbudować plik .db raz, dokleić go przez
./zip program.com pracownicy.db
 i w kodzie otworzyć jako sqlite3_open("/zip/pracownicy.db", &db) (tryb tylko do odczytu). Dzięki temu baza nie jest tworzona od zera przy każdym uruchomieniu.

Ograniczenia i redbean

Mechanizm /zip/ jest z założenia tylko do odczytu. Zwykły program nie może w runtime dopisywać ani zmieniać plików w archiwum (wymagałoby to ręcznego przepisywania central directory ZIP-a). Jeśli potrzebujesz pliku, który sam się modyfikuje - np. serwuje strony, zapisuje dane do wbudowanej bazy SQLite i nadal pozostaje jednym plikiem - to właśnie robi redbean. Redbean to jednoplikowy serwer WWW napisany na Cosmopolitan: w środku trzyma Lua, SQLite i Twoje assety, a cały plik nadal jest poprawnym APE + ZIP.
Do doklejania plików używaj zawsze zipa z cosmo.zip - zwykły systemowy zip (szczególnie GUI-owe narzędzia na Windowsie) potrafi zniszczyć nagłówek APE.

Dodatek: jak zbudowane są binarki

Warto wiedzieć, z czego składają się pliki binarne podstawowych systemów - to ułatwia zrozumienie, jak w ogóle da się skleić je wszystkie w jeden plik.

Linux - ELF

Linux (i pozostałe uniksopodobne systemy z naszej listy) używa formatu ELF (Executable and Linkable Format). Plik zaczyna się od 4-bajtowej sygnatury 0x7F 'E' 'L' 'F', po której następuje nagłówek opisujący m.in. architekturę i typ pliku, a dalej tablica nagłówków programu (segmenty ładowane do pamięci przy starcie) oraz - zwykle na końcu - tablica nagłówków sekcji (używana głównie przy konsolidacji i debugowaniu). To właśnie fakt, że jądro Linuksa sprawdza tylko te pierwsze 4 bajty, pozwala APE "udawać" ELF-a, mimo że reszta pliku zawiera też dane potrzebne innym systemom.
Standardowy, poglądowy schemat układu pliku ELF z Wikipedii - nagłówek na początku, segmenty programu (ładowane wprost do pamięci) oraz sekcje wykorzystywane głównie przy konsolidacji i debugowaniu.
Standardowy, poglądowy schemat układu pliku ELF z Wikipedii - nagłówek na początku, segmenty programu (ładowane wprost do pamięci) oraz sekcje wykorzystywane głównie przy konsolidacji i debugowaniu.

Windows - PE

Windows używa formatu PE (Portable Executable). Tu historia jest ciekawsza: plik zaczyna się od starego nagłówka DOS MZ (sygnatura 'MZ'), zawierającego m.in. mały, prawdziwy program DOS-owy (zwykle tylko wypisujący "This program cannot be run in DOS mode") oraz - pod adresem wskazanym w polu e_lfanew - wskaźnik do właściwego nagłówka PE (sygnatura 'PE\0\0'), po którym dopiero idą nagłówek COFF, nagłówek opcjonalny i tablica sekcji. Ta warstwowa budowa (DOS MZ na początku, właściwy format gdzieś dalej, adresowany przez wskaźnik) jest dokładnie tym mechanizmem, który APE wykorzystuje najchętniej - w tym samym miejscu da się umieścić coś, co jednocześnie jest poprawnym początkiem skryptu powłoki i poprawnym nagłówkiem MZ, a właściwa zawartość dla danego systemu i tak czeka dalej w pliku.
Pełny, szczegółowy schemat 32-bitowego pliku PE - od nagłówka DOS MZ i stubu, przez nagłówek PE (COFF + Optional Header), po tablicę i zawartość poszczególnych sekcji (.text, .data itd.).
Pełny, szczegółowy schemat 32-bitowego pliku PE - od nagłówka DOS MZ i stubu, przez nagłówek PE (COFF + Optional Header), po tablicę i zawartość poszczególnych sekcji (.text, .data itd.).

Uproszczony obrazek, dla tych, których poprzedni schemat przytłoczył:
Ten sam układ w wersji „z lotu ptaka” - nagłówek DOS, nagłówek PE i sekcje, bez rozbijania na poszczególne pola.
Ten sam układ w wersji „z lotu ptaka” - nagłówek DOS, nagłówek PE i sekcje, bez rozbijania na poszczególne pola.

Pliki ZIP

Archiwum ZIP to w gruncie rzeczy prosty kontener: ciąg dowolnej liczby plików, z których każdy może być skompresowany niezależnie od pozostałych. Dla każdego pliku archiwum trzyma osobno jego dane oraz mały nagłówek z nazwą i metadanymi, a całość - w odróżnieniu od ELF-a czy PE, gdzie kluczowy nagłówek siedzi na samym początku - uzupełnia jeszcze jeden, zbiorczy element opisujący wszystkie spakowane pliki naraz.

Same dane poszczególnych plików mogą przy tym być spakowane na kilka różnych sposobów - ZIP jako format wspiera m.in. klasyczny Deflate, ale też np. BZip2, LZMA czy nowszy Zstandard. Cosmopolitan trzyma się tu najbardziej uniwersalnego wariantu: mechanizm /zip/ opiera się na wbudowanym zlibie, czyli Deflate (oraz zwykłym, nieskompresowanym Store) - dokładnie na tym, co rozumie każdy standardowy program typu unzip.

Format ZIP  ma jedną właściwość, dzięki której cały mechanizm APE-jako-archiwum w ogóle jest możliwy: kluczowa struktura opisująca zawartość archiwum - tzw. central directory - znajduje się na samym końcu pliku, a nie na początku. Każdy skompresowany plik ma wprawdzie także swój lokalny nagłówek (local file header) bezpośrednio przed swoimi danymi, ale to właśnie central directory na końcu zawiera pełną listę plików wraz z ich offsetami i jest tym, co przegląda program odczytujący archiwum (np. unzip). Dzięki temu czytnik ZIP-a szuka tej struktury, licząc od końca pliku wstecz - nie obchodzi go, co znajduje się na samym początku. Można więc doczepić dowolną ilość dowolnych danych (np. cały skompilowany program APE) przed właściwą zawartością archiwum, a mimo to plik pozostanie poprawnym, w pełni działającym ZIP-em. To samo zjawisko przy okazji tłumaczy, dlaczego samorozpakowujące się archiwa (self-extracting archives) w ogóle mogły powstać na długo przed Cosmopolitan - to ten sam trik, tylko wykorzystany do jednego konkretnego formatu wykonywalnego zamiast siedmiu naraz.
Schemat struktury pliku ZIP: lokalne nagłówki i dane plików, na końcu central directory oraz end of central directory
Schemat struktury pliku ZIP: lokalne nagłówki i dane plików, na końcu central directory oraz end of central directory

Bibliografia

W momencie pisania artykułu strona domowa projektu Cosmopolitan miała wygasły certyfikat SSL. Przeglądarki klasyfikują ją jako niebezpieczną, dlatego – dla lepszego pozycjonowania strony – nie przywołujemy oficjalnej strony frameworka w aktualnej wersji artykułu. Stronę domową (wraz z dokumentacją API, listą wspieranych funkcji, opisem formatu APE i loadera oraz wpisem o trzeciej edycji Cosmopolitan) można znaleźć wchodząc na oficjalne repozytorium projektu na GitHubie – znajdują się tam odnośniki do strony autora.