Eine plattformübergreifende C++11-HTTP/HTTPS-Bibliothek mit nur einem Datei-Header.
Die Einrichtung ist äußerst einfach. Fügen Sie einfach die Datei httplib.h in Ihren Code ein!
Wichtig
Diese Bibliothek verwendet „blockierende“ Socket-E/A. Wenn Sie nach einer Bibliothek mit „nicht blockierendem“ Socket-I/O suchen, ist dies nicht die richtige für Sie.
#define CPPHTTPLIB_OPENSSL_SUPPORT#include "path/to/httplib.h"// HTTPhttplib::Server svr;// HTTPShttplib::SSLServer svr; svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) { res.set_content("Hallo Welt!", "text/plain"); }); svr.listen("0.0.0.0", 8080);
#define CPPHTTPLIB_OPENSSL_SUPPORT#include "path/to/httplib.h"// HTTPhttplib::Client cli("http://cpp-httplib-server.yhirose.repl.co");// HTTPShttplib::Client cli(" https://cpp-httplib-server.yhirose.repl.co");auto res = cli.Get("/hi"); res->status; res->body;
SSL-Unterstützung ist mit CPPHTTPLIB_OPENSSL_SUPPORT
verfügbar. libssl
und libcrypto
sollten verknüpft sein.
Notiz
cpp-httplib unterstützt derzeit nur Version 3.0 oder höher. Weitere Informationen finden Sie auf dieser Seite.
Tipp
Für macOS: cpp-httplib kann jetzt Systemzertifikate mit CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
verwenden. CoreFoundation
und Security
sollten mit -framework
verknüpft werden.
#define CPPHTTPLIB_OPENSSL_SUPPORT#include "path/to/httplib.h"// Serverhttplib::SSLServer svr("./cert.pem", "./key.pem");// Clienthttplib::Client cli("https: //localhost:1234"); // scheme + hosthttplib::SSLClient cli("localhost:1234"); // hosthttplib::SSLClient cli("localhost", 1234); // Host, Port// Verwenden Sie Ihr CA-Bundlecli.set_ca_cert_path("./ca-bundle.crt");// Zertifikatsüberprüfung deaktivierencli.enable_server_certificate_verification(false);// Hostüberprüfung deaktivierencli.enable_server_host_verification(false);
Notiz
Bei der Verwendung von SSL scheint es unmöglich, SIGPIPE in allen Fällen zu vermeiden, da SIGPIPE auf einigen Betriebssystemen nur pro Nachricht unterdrückt werden kann, es jedoch keine Möglichkeit gibt, die OpenSSL-Bibliothek dazu zu bringen, dies für ihre interne Kommunikation zu tun. Wenn Ihr Programm vermeiden muss, auf SIGPIPE beendet zu werden, besteht die einzige völlig allgemeine Möglichkeit möglicherweise darin, einen Signalhandler für SIGPIPE einzurichten, der es selbst verarbeitet oder ignoriert.
#include <httplib.h>int main(void) { unter Verwendung des Namensraums httplib; Server-SVR; svr.Get("/hi", [](const Request& req, Response& res) { res.set_content("Hallo Welt!", "text/plain"); }); // Vergleichen Sie den Anforderungspfad mit einem regulären Ausdruck // und extrahieren Sie seine Captures svr.Get(R"(/numbers/(d+))", [&](const Request& req, Response& res) { auto zahlen = req.matches[1]; res.set_content(numbers, "text/plain"); }); // Erfassen Sie das zweite Segment des Anforderungspfads als „id“-Pfadparameter svr.Get("/users/:id", [&](const Request& req, Response& res) { auto user_id = req.path_params.at("id"); res.set_content(user_id, „text/plain“); }); // Werte aus HTTP-Headern und URL-Abfrageparametern extrahieren svr.Get("/body-header-param", [](const Request& req, Response& res) { if (req.has_header("Content-Length")) { auto val = req.get_header_value("Content-Length" ); } if (req.has_param("key")) { auto val = req.get_param_value("key"); } res.set_content(req.body, "text/plain"); }); svr.Get("/stop", [&](const Request& req, Response& res) { svr.stop(); }); svr.listen("localhost", 1234); }
Die Methoden Post
, Put
, Delete
und Options
werden ebenfalls unterstützt.
int port = svr.bind_to_any_port("0.0.0.0"); svr.listen_after_bind();
// Mounten / in ./www-Verzeichnisauto ret = svr.set_mount_point("/", "./www");if (!ret) { // Das angegebene Basisverzeichnis existiert nicht...}// Mounten / public in ./www-Verzeichnisret = svr.set_mount_point("/public", "./www");// Mounten Sie /public in ./www1- und ./www2-Verzeichnissenret = svr.set_mount_point("/public", "./www1"); // 1. Suchauftragret = svr.set_mount_point("/public", "./www2"); // 2. Suchreihenfolge// Mount entfernen /ret = svr.remove_mount_point("/");// Mount entfernen /publicret = svr.remove_mount_point("/public");
// Benutzerdefinierte Dateierweiterung und MIME-Typzuordnungenssvr.set_file_extension_and_mimetype_mapping("cc", "text/xc"); svr.set_file_extension_and_mimetype_mapping("cpp", "text/xc"); svr.set_file_extension_and_mimetype_mapping("hh", "text/xh");
Die folgenden Zuordnungen sind integriert:
Verlängerung | MIME-Typ | Verlängerung | MIME-Typ |
---|---|---|---|
CSS | Text/CSS | mpga | Audio/MPEG |
csv | Text/CSV | weba | Audio/Webm |
txt | Text/einfach | wav | Audio/Welle |
vtt | text/vtt | otf | Schriftart/otf |
html, htm | text/html | ttf | Schriftart/ttf |
apng | Bild/apng | Wow | Schriftart/woff |
avif | Bild/avif | woff2 | Schriftart/woff2 |
bmp | Bild/bmp | 7z | application/x-7z-compressed |
GIF | Bild/Gif | Atom | application/atom+xml |
png | Bild/PNG | anwendung/pdf | |
SVG | image/svg+xml | mjs, js | Anwendung/Javascript |
webp | Bild/webp | json | application/json |
ico | Bild/X-Symbol | RSS | application/rss+xml |
tif | Bild/tiff | Teer | application/x-tar |
tiff | Bild/tiff | xhtml, xht | application/xhtml+xml |
JPEG, JPG | Bild/JPEG | xslt | application/xslt+xml |
mp4 | Video/mp4 | xml | application/xml |
mpeg | Video/mpeg | gz | application/gzip |
webm | Video/Webm | Reißverschluss | Anwendung/zip |
mp3 | Audio/mp3 | wasm | Anwendung/wasm |
Warnung
Diese statischen Dateiservermethoden sind nicht threadsicher.
// Der Handler wird unmittelbar vor dem Senden der Antwort an einen Client aufgerufen.vr.set_file_request_handler([](const Request &req, Response &res) { ... });
svr.set_logger([](const auto& req, const auto& res) { your_logger(req, res); });
svr.set_error_handler([](const auto& req, auto& res) { auto fmt = "<p>Fehlerstatus: <span style='color:red;'>%d</span></p>"; char buf [BUFSIZ]; snprintf(buf, sizeof(buf), fmt, res.status); res.set_content(buf, "text/html"); });
Der Ausnahmehandler wird aufgerufen, wenn ein Benutzer-Routing-Handler einen Fehler auslöst.
svr.set_Exception_handler([](const auto& req, auto& res, std::Exception_ptr ep) { auto fmt = "<h1>Error 500</h1><p>%s</p>"; char buf[BUFSIZ] ; versuche { std::rethrow_Exception(ep); } Catch (std::Exception &e) { snprintf(buf, sizeof(buf), fmt, e.what()); } Catch (...) { // Siehe den folgenden HINWEIS snprintf(buf, sizeof(buf), fmt, "Unknown Exception"); } res.set_content(buf, "text/html"); res.status = StatusCode::InternalServerError_500; });
Vorsicht
Wenn Sie den catch (...)
für einen erneut ausgelösten Ausnahmezeiger nicht bereitstellen, führt eine nicht abgefangene Ausnahme letztendlich zum Absturz des Servers. Seien Sie vorsichtig!
svr.set_pre_routing_handler([](const auto& req, auto& res) { if (req.path == "/hello") { res.set_content("world", "text/html"); return Server::HandlerResponse::Handled; } return Server::HandlerResponse::Unhandled; });
svr.set_post_routing_handler([](const auto& req, auto& res) { res.set_header("ADDITIONAL_HEADER", "value"); });
svr.Post("/multipart", [&](const auto& req, auto& res) { auto size = req.files.size(); auto ret = req.has_file("name1"); const auto& file = req. get_file_value("name1"); // file.filename; // file.content_type;};
svr.Post("/content_receiver", [&](const Request &req, Response &res, const ContentReader &content_reader) { if (req.is_multipart_form_data()) { // HINWEIS: „content_reader“ blockiert, bis jedes Formulardatenfeld gelesen wurde MultipartFormDataItems-Dateien; content_reader( [&](const MultipartFormData &file) { files.push_back(file); return true; }, [&](const char *data, size_t data_length) { files.back().content.append(data, data_length); return true; }); } anders { std::string body; content_reader([&](const char *data, size_t data_length) { body.append(data, data_length); return true; }); } });
const size_t DATA_CHUNK_SIZE = 4; svr.Get("/stream", [&](const Request &req, Response &res) { auto data = new std::string("abcdefg"); res.set_content_provider( data->size(), // Inhaltslänge „text/plain“, // Inhaltstyp [&, data](size_t offset, size_t length, DataSink &sink) { const auto &d = *data; sink.write(&d[offset], std::min(length, DATA_CHUNK_SIZE)); return true; // „false“ zurückgeben, wenn Sie den Vorgang abbrechen möchten. }, [data](bool success) { Daten löschen; }); });
Ohne Inhaltslänge:
svr.Get("/stream", [&](const Request &req, Response &res) { res.set_content_provider( "text/plain", // Inhaltstyp [&](size_t offset, DataSink &sink) { if (/* es sind noch Daten vorhanden */) { std::vector<char> data; // Daten vorbereiten... sink.write(data.data(), data.size()); } anders { sink.done(); // Keine Daten mehr } return true; // „false“ zurückgeben, wenn Sie den Vorgang abbrechen möchten. }); });
svr.Get("/chunked", [&](const Request& req, Response& res) { res.set_chunked_content_provider( "text/plain", [](size_t offset, DataSink &sink) { sink.write("123", 3); sink.write("345", 3); sink.write("789", 3); sink.done(); // Keine weiteren Daten return true; // „false“ zurückgeben, wenn Sie den Vorgang abbrechen möchten. } ); });
Mit Trailer:
svr.Get("/chunked", [&](const Request& req, Response& res) { res.set_header("Trailer", "Dummy1, Dummy2"); res.set_chunked_content_provider( "text/plain", [](size_t offset, DataSink &sink) { sink.write("123", 3); sink.write("345", 3); sink.write("789", 3); sink.done_with_trailer({ {"Dummy1", "DummyVal1"}, {"Dummy2", "DummyVal2"} }); return true; } ); });
svr.Get("/content", [&](const Request &req, Response &res) { res.set_file_content("./path/to/conent.html"); }); svr.Get("/content", [&](const Request &req, Response &res) { res.set_file_content("./path/to/conent", "text/html"); });
Standardmäßig sendet der Server eine 100 Continue
Antwort für einen Expect: 100-continue
Header.
// „417 Expectation Failed“ senden Response.svr.set_expect_100_continue_handler([](const Request &req, Response &res) { return StatusCode::ExpectationFailed_417; });
// Einen endgültigen Status senden, ohne die Nachricht zu lesen body.svr.set_expect_100_continue_handler([](const Request &req, Response &res) { return res.status = StatusCode::Unauthorized_401; });
svr.set_keep_alive_max_count(2); // Standard ist 5svr.set_keep_alive_timeout(10); // Standard ist 5
svr.set_read_timeout(5, 0); // 5 Sekundensvr.set_write_timeout(5, 0); // 5 Sekundensvr.set_idle_interval(0, 100000); // 100 Millisekunden
svr.set_payload_max_length(1024 * 1024 * 512); // 512 MB
Notiz
Wenn der Inhaltstyp des Anforderungstexts „www-form-urlencoded“ ist, sollte die tatsächliche Nutzlastlänge CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH
nicht überschreiten.
Weitere Informationen finden Sie unter Server-Beispiel und Client-Beispiel.
ThreadPool
wird als Standard- Aufgabenwarteschlange verwendet und die Standard-Thread-Anzahl beträgt 8 oder std::thread::hardware_concurrency()
. Sie können es mit CPPHTTPLIB_THREAD_POOL_COUNT
ändern.
Wenn Sie die Thread-Anzahl zur Laufzeit festlegen möchten, gibt es keine bequeme Möglichkeit ... Aber hier erfahren Sie, wie.
svr.new_task_queue = [] { return new ThreadPool(12); };
Sie können auch einen optionalen Parameter bereitstellen, um die maximale Anzahl ausstehender Anforderungen zu begrenzen, d accept()
, aber noch darauf warten, von Arbeitsthreads bedient zu werden.
svr.new_task_queue = [] { return new ThreadPool(/*num_threads=*/12, /*max_queued_requests=*/18); };
Das Standardlimit ist 0 (unbegrenzt). Sobald das Limit erreicht ist, beendet der Listener die Clientverbindung.
Sie können je nach Bedarf Ihre eigene Thread-Pool-Implementierung bereitstellen.
Klasse YourThreadPoolTaskQueue: public TaskQueue {public: YourThreadPoolTaskQueue(size_t n) { pool_.start_with_thread_count(n); } virtual bool enqueue(std::function<void()> fn) override { /* Gibt true zurück, wenn die Aufgabe tatsächlich in die Warteschlange gestellt wurde, oder false *, wenn der Aufrufer die entsprechende Verbindung trennen muss. */ return pool_.enqueue(fn); } Virtual Void Shutdown() überschreiben { pool_.shutdown_gracefully(); }Privat: YourThreadPool pool_; }; svr.new_task_queue = [] { return new YourThreadPoolTaskQueue(12); };
#include <httplib.h>#include <iostream>int main(void) { httplib::Client cli("localhost", 1234); if (auto res = cli.Get("/hi")) { if (res->status == StatusCode::OK_200) { std::cout << res->body << std::endl; } } else { auto err = res.error(); std::cout << "HTTP-Fehler: " << httplib::to_string(err) << std::endl; } }
Tipp
Konstruktor mit Schema-Host-Port-String wird jetzt unterstützt!
httplib::Client cli("localhost"); httplib::Client cli("localhost:8080"); httplib::Client cli("http://localhost"); httplib::Client cli("http://localhost:8080"); httplib::Client cli("https://localhost"); httplib::SSLClient cli("localhost");
Hier ist die Liste der Fehler von Result::error()
.
Enum-Fehler { Erfolg = 0, Unbekannt, Verbindung, BindIPAddress, Lesen, Schreiben, ExceedRedirectCount, Abgesagt, SSLConnection, SSLLoadingCerts, SSLServerVerification, Nicht unterstützteMultipartBoundaryChars, Kompression, ConnectionTimeout, };
httplib::Headers headers = { { "Accept-Encoding", "gzip, deflate" } };auto res = cli.Get("/hi", headers);
oder
auto res = cli.Get("/hi", {{"Accept-Encoding", "gzip, deflate"}});
oder
cli.set_default_headers({ { "Accept-Encoding", "gzip, deflate" } });auto res = cli.Get("/hi");
res = cli.Post("/post", "text", "text/plain"); res = cli.Post("/person", "name=john1¬e=coder", "application/x-www-form-urlencoded");
httplib::Params params; params.emplace("name", "john"); params.emplace("note", "coder");auto res = cli.Post("/post", params);
oder
httplib::Params params{ { "name", "john" }, { "note", "coder" } };auto res = cli.Post("/post", params);
httplib::MultipartFormDataItems items = { { "text1", "text default", "", "" }, { "text2", "aωb", "", "" }, { "file1", "hnennlnlnon", "hello.txt", "text/plain" }, { "file2", "{n "world", truen}n", "world.json", "application/json" }, { "file3", "", "", "application/octet-stream" }, };auto res = cli.Post("/multipart", items);
res = cli.Put("/resource/foo", "text", "text/plain");
res = cli.Delete("/resource/foo");
res = cli.Options("*"); res = cli.Options("/resource/foo");
cli.set_connection_timeout(0, 300000); // 300 Millisekundencli.set_read_timeout(5, 0); // 5 Sekundencli.set_write_timeout(5, 0); // 5 Sekunden
std::string body;auto res = cli.Get("/large-data", [&](const char *data, size_t data_length) { body.append(data, data_length); return true; });
std::string body;auto res = cli.Get( "/stream", Headers(), [&](const Response &response) { EXPECT_EQ(StatusCode::OK_200, Response.status); return true; // „false“ zurückgeben, wenn Sie die Anfrage abbrechen möchten. }, [&](const char *data, size_t data_length) { body.append(data, data_length); return true; // „false“ zurückgeben, wenn Sie die Anfrage abbrechen möchten. });
std::string body = ...;auto res = cli.Post( "/stream", body.size(), [](size_t-Offset, size_t-Länge, DataSink &sink) { sink.write(body.data() + Offset, Länge); return true; // „false“ zurückgeben, wenn Sie die Anfrage abbrechen möchten. }, "text/plain");
auto res = cli.Post( "/stream", [](size_t offset, DataSink &sink) { sink.os << "chunked data 1"; sink.os << "chunked data 2"; sink.os << "chunked data 3"; sink.done(); return true; // „false“ zurückgeben, wenn Sie die Anfrage abbrechen möchten. }, "text/plain");
httplib::Client cli(url, port);// prints: 0 / 000 bytes => 50% completeauto res = cli.Get("/", [](uint64_t len, uint64_t total) { printf("%lld / %lld Bytes => %d%% abgeschlossenn", len, gesamt, (int)(len*100/total)); return true; // „false“ zurückgeben, wenn Sie die Anfrage abbrechen möchten.} );
// Basisauthentifizierungcli.set_basic_auth("user", "pass");// Digest Authenticationcli.set_digest_auth("user", "pass");// Bearer-Token-Authentifizierungcli.set_bearer_token_auth("token");
Notiz
OpenSSL ist für die Digest-Authentifizierung erforderlich.
cli.set_proxy("host", port);// Basisauthentifizierungcli.set_proxy_basic_auth("user", "pass");// Digest-Authentifizierungcli.set_proxy_digest_auth("user", "pass");// Bearer-Token-Authentifizierungcli.set_proxy_bearer_token_auth ("passieren");
Notiz
OpenSSL ist für die Digest-Authentifizierung erforderlich.
httplib::Client cli("httpbin.org");auto res = cli.Get("/range/32", { httplib::make_range_header({{1, 10}}) // 'Range: bytes=1- 10'});// res->status sollte 206 sein.// res->body sollte „bcdefghijk“ sein.
httplib::make_range_header({{1, 10}, {20, -1}}) // 'Bereich: Bytes=1-10, 20-'httplib::make_range_header({{100, 199}, {500, 599 }}) // 'Bereich: Bytes=100-199, 500-599'httplib::make_range_header({{0, 0}, {-1, 1}}) // 'Bereich: Bytes=0-0, -1'
httplib::Client cli("localhost", 1234); cli.Get("/hello"); // mit „Verbindung: schließen“cli.set_keep_alive(true); cli.Get("/world"); cli.set_keep_alive(false); cli.Get("/last-request"); // mit „Verbindung: schließen“
httplib::Client cli("yahoo.com");auto res = cli.Get("/"); res->status; // 301cli.set_follow_location(true); res = cli.Get("/"); res->status; // 200
Notiz
Diese Funktion ist unter Windows noch nicht verfügbar.
cli.set_interface("eth0"); // Schnittstellenname, IP-Adresse oder Hostname
Der Server kann die Komprimierung auf die folgenden MIME-Typ-Inhalte anwenden:
alle Texttypen außer text/event-stream
image/svg+xml
Anwendung/Javascript
application/json
application/xml
application/xhtml+xml
Die „gzip“-Komprimierung ist mit CPPHTTPLIB_ZLIB_SUPPORT
verfügbar. libz
sollte verlinkt sein.
Die Brotli-Komprimierung ist mit CPPHTTPLIB_BROTLI_SUPPORT
verfügbar. Notwendige Bibliotheken sollten eingebunden werden. Weitere Einzelheiten finden Sie unter https://github.com/google/brotli.
cli.set_compress(true); res = cli.Post("/resource/foo", "...", "text/plain");
cli.set_decompress(false); res = cli.Get("/resource/foo", {{"Accept-Encoding", "gzip, deflate, br"}}); res->body; // Komprimierte Daten
poll
statt select
Der Systemaufruf select
wird standardmäßig verwendet, da er umfassender unterstützt wird. Wenn Sie möchten, dass cpp-httplib stattdessen poll
verwendet, können Sie dies mit CPPHTTPLIB_USE_POLL
tun.
Unix Domain Socket-Unterstützung ist unter Linux und macOS verfügbar.
// Serverhttplib::Server svr("./my-socket.sock"); svr.set_address_family(AF_UNIX).listen("./my-socket.sock", 80);// Clienthttplib::Client cli("./my-socket.sock"); cli.set_address_family(AF_UNIX);
„my-socket.sock“ kann ein relativer oder ein absoluter Pfad sein. Ihre Anwendung muss über die entsprechenden Berechtigungen für den Pfad verfügen. Sie können unter Linux auch eine abstrakte Socket-Adresse verwenden. Um eine abstrakte Socket-Adresse zu verwenden, stellen Sie dem Pfad ein Nullbyte ('x00') voran.
$ ./split.py -husage: split.py [-h] [-e EXTENSION] [-o OUT] Dieses Skript teilt httplib.h in .h und .cc parts.optionale Argumente: -h, --help show Diese Hilfemeldung und Exit -e EXTENSION, --extension EXTENSION Erweiterung der Implementierungsdatei (Standard: cc) -o OUT, --out OUT, wohin die Dateien geschrieben werden sollen (Standard: out)$ ./split.pySchreiben Sie out/httplib.h und out/httplib.cc aus
Dockerfile für statischen HTTP-Server ist verfügbar. Die Portnummer dieses HTTP-Servers ist 80 und er stellt statische Dateien aus dem Verzeichnis /html
im Container bereit.
> docker build -t cpp-httplib-server ....> docker run --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server HTTP wird auf Port 80 0.0.0.0 bereitgestellt ... 192.168.65.1 - - [31.08.2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"192.168.65.1 - - [31.08.2024 :21:34:26 +0000] „GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..."192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET /favicon.ico HTTP/1.1" 404 152 " -" "Mozilla/5.0 ..."
Vom Docker Hub
> docker run --rm -it -p 8080:80 -v ./docker/html:/html yhirose4dockerhub/cpp-httplib-server ...> docker run --init --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server HTTP wird auf Port 80 0.0.0.0 bereitgestellt ... 192.168.65.1 - - [31.08.2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"192.168.65.1 - - [31.08.2024 :21:34:26 +0000] „GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..."192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET /favicon.ico HTTP/1.1" 404 152 " -" "Mozilla/5.0 ..."
g++ 4.8 und niedriger können diese Bibliothek nicht erstellen, da <regex>
in den Versionen fehlerhaft ist.
Fügen Sie httplib.h
vor Windows.h
ein oder schließen Sie Windows.h
ein, indem Sie vorher WIN32_LEAN_AND_MEAN
definieren.
#include <httplib.h>#include <Windows.h>
#define WIN32_LEAN_AND_MEAN#include <Windows.h>#include <httplib.h>
Notiz
cpp-httplib unterstützt offiziell nur das neueste Visual Studio. Es funktioniert möglicherweise mit früheren Versionen von Visual Studio, aber ich kann es nicht mehr überprüfen. Pull-Requests sind für die älteren Versionen von Visual Studio immer willkommen, es sei denn, sie verstoßen gegen die C++11-Konformität.
Notiz
Windows 8 oder niedriger, Visual Studio 2013 oder niedriger sowie Cygwin und MSYS2 einschließlich MinGW werden weder unterstützt noch getestet.
MIT-Lizenz (© 2024 Yuji Hirose)
Diese Leute haben großartige Beiträge geleistet, um diese Bibliothek von einem einfachen Spielzeug auf ein völlig neues Niveau zu heben!