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:
#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:
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:
#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):
#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;
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;
}
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 );
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 );
sql = "SELECT * FROM Pracownicy;";
execute_sql_statement( sql, db );
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:
#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ę:
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:
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ć:
Przykład z kolorowaniem:#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:
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.
#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 );
}
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:
1. Przygotowanie katalogu roboczegoNa 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.BuildRepozytorium 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 CosmopolitanBoost.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_optionsMają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_optionsTworzymy prosty program (zapisany jako
main_program_options.cpp):
#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 CosmopolitanProgram 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 uruchomieniowyNa 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:
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:
#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):
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:
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:
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):
#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.
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.
Uproszczony obrazek, dla tych, których poprzedni schemat przytłoczył:
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.
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.