Pustaka HTTP/HTTPS lintas platform khusus header file tunggal C++11.
Pengaturannya sangat mudah. Cukup sertakan file httplib.h dalam kode Anda!
Penting
Perpustakaan ini menggunakan soket I/O 'memblokir'. Jika Anda mencari perpustakaan dengan I/O soket 'non-pemblokiran', ini bukan yang Anda inginkan.
#define CPPHTTPLIB_OPENSSL_SUPPORT#include "path/to/httplib.h"// HTTPhttplib::Server svr;// HTTPShttplib::SSLServer svr; svr.Get("/hi", [](const httplib::Permintaan &, httplib::Respon &res) { res.set_content("Halo Dunia!", "teks/polos"); }); 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");res otomatis = cli.Get("/hi"); res->status; res->tubuh;
Dukungan SSL tersedia dengan CPPHTTPLIB_OPENSSL_SUPPORT
. libssl
dan libcrypto
harus ditautkan.
Catatan
cpp-httplib saat ini hanya mendukung versi 3.0 atau lebih baru. Silakan lihat halaman ini untuk mendapatkan informasi lebih lanjut.
Tip
Untuk macOS: cpp-httplib sekarang dapat menggunakan sertifikat sistem dengan CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
. CoreFoundation
dan Security
harus dihubungkan dengan -framework
.
#define CPPHTTPLIB_OPENSSL_SUPPORT#include "path/to/httplib.h"// Serverhttplib::SSLServer svr("./cert.pem", "./key.pem");// Clienthttplib::Client cli("https: //host lokal:1234"); // skema + hosthttplib::SSLClient cli("localhost:1234"); // hosthttplib::SSLClient cli("localhost", 1234); // host, port// Gunakan CA bundlecli.set_ca_cert_path("./ca-bundle.crt");// Nonaktifkan verifikasi sertifikatcli.enable_server_certificate_verification(false);// Nonaktifkan verifikasi hostcli.enable_server_host_verification(false);
Catatan
Saat menggunakan SSL, tampaknya mustahil untuk menghindari SIGPIPE dalam semua kasus, karena pada beberapa sistem operasi, SIGPIPE hanya dapat disembunyikan per pesan, namun tidak ada cara untuk membuat pustaka OpenSSL melakukan hal tersebut untuk komunikasi internalnya. Jika program Anda perlu menghindari penghentian pada SIGPIPE, satu-satunya cara yang sepenuhnya umum adalah dengan menyiapkan pengendali sinyal agar SIGPIPE dapat menangani atau mengabaikannya sendiri.
#termasukint main(batal) { menggunakan namespace httplib; Server svr; svr.Get("/hi", [](const Permintaan&permintaan,Respon&res) { res.set_content("Halo Dunia!", "teks/polos"); }); // Cocokkan jalur permintaan dengan ekspresi reguler // dan ekstrak tangkapannya svr.Get(R"(/numbers/(d+))", [&](const Permintaan& permintaan, Respons& res) { nomor otomatis = req.matches[1]; res.set_content(angka, "teks/polos"); }); // Tangkap segmen kedua dari jalur permintaan sebagai parameter jalur "id". svr.Get("/users/:id", [&](const Permintaan& permintaan, Respons& res) { auto user_id = req.path_params.at("id"); res.set_content(user_id, "teks/polos"); }); // Ekstrak nilai dari header HTTP dan parameter kueri URL svr.Get("/body-header-param", [](const Permintaan& permintaan, Respons& res) { if (req.has_header("Panjang Konten")) { auto val = req.get_header_value("Panjang Konten" ); } if (req.has_param("kunci")) { auto val = req.get_param_value("kunci"); } res.set_content(req.body, "teks/polos"); }); svr.Get("/stop", [&](const Permintaan&permintaan,Respon&res) { svr.stop(); }); svr.listen("localhost", 1234); }
Metode Post
, Put
, Delete
dan Options
juga didukung.
int port = svr.bind_to_any_port("0.0.0.0"); svr.listen_after_bind();
// Pasang / ke ./www direktoriauto ret = svr.set_mount_point("/", "./www");if (!ret) {// Direktori dasar yang ditentukan tidak ada...}// Pasang / publik ke ./www direktoriret = svr.set_mount_point("/public", "./www");// Pasang /publik ke ./www1 dan ./www2 direktoriret = svr.set_mount_point("/public", "./ www1"); // Urutan pertama ke searchret = svr.set_mount_point("/public", "./www2"); // Perintah pencarian ke-2// Hapus mount /ret = svr.remove_mount_point("/");// Hapus mount /publicret = svr.remove_mount_point("/public");
// Ekstensi file yang ditentukan pengguna dan tipe MIME pemetaanssvr.set_file_extension_and_mimetype_mapping("cc", "text/xc"); svr.set_file_extension_and_mimetype_mapping("cpp", "teks/xc"); svr.set_file_extension_and_mimetype_mapping("hh", "teks/xh");
Berikut ini adalah pemetaan bawaan:
Perpanjangan | Tipe MIME | Perpanjangan | Tipe MIME |
---|---|---|---|
css | teks/css | mpga | audio/mpeg |
csv | teks/csv | weba | audio/webm |
txt | teks/polos | wav | audio/gelombang |
vtt | teks/vtt | sebaliknya | font/otf |
html, htm | teks/html | ttf | font/ttf |
apng | gambar/apng | huh | font/woff |
avif | gambar/avif | woff2 | font/woff2 |
bmp | gambar/bmp | 7z | application/x-7z-compressed |
gif | gambar/gif | atom | aplikasi/atom+xml |
png | gambar/png | aplikasi/pdf | |
svg | gambar/svg+xml | mjs, js | aplikasi/javascript |
webp | gambar/webp | json | aplikasi/json |
ico | gambar/ikon-x | rss | aplikasi/rss+xml |
tif | gambar/tiff | ter | aplikasi/x-tar |
bertengkar | gambar/tiff | xhtml, xht | aplikasi/xhtml+xml |
jpeg, jpg | gambar/jpeg | xslt | aplikasi/xslt+xml |
mp4 | video/mp4 | xml | aplikasi/xml |
mpeg | video/mpeg | gz | aplikasi/gzip |
webm | video/webm | ritsleting | aplikasi/zip |
mp3 | audio/mp3 | wasm | aplikasi/wasm |
Peringatan
Metode server file statis ini tidak aman untuk thread.
// Penangan dipanggil tepat sebelum respons dikirim ke klienvr.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 = "Status Kesalahan: %d
"; char buf [BUFSIZ]; snprintf(buf, sizeof(buf), fmt, res.status); res.set_content(buf, "teks/html"); });
Pengendali pengecualian dipanggil jika pengendali perutean pengguna membuat kesalahan.
svr.set_Exception_handler([](const auto& req, auto& res, std::Exception_ptr ep) { auto fmt = "Error 500
%s
"; char buf[BUFSIZ] ; coba { std::rethrow_Exception(ep); } catch (std::pengecualian &e) { snprintf(buf, sizeof(buf), fmt, e.what()); } catch (...) { // Lihat CATATAN berikut snprintf(buf, sizeof(buf), fmt, "Unknown Exception"); } res.set_content(buf, "teks/html"); res.status = StatusCode::InternalServerError_500; });
Peringatan
jika Anda tidak menyediakan blok catch (...)
untuk penunjuk pengecualian yang ditampilkan kembali, pengecualian yang tidak tertangkap akan menyebabkan server mogok. Hati-hati!
svr.set_pre_routing_handler([](const auto& req, auto& res) { if (req.path == "/hello") { res.set_content("dunia", "teks/html"); return Server::HandlerResponse::Ditangani; } kembali Server::HandlerResponse::Tidak tertangani; });
svr.set_post_routing_handler([](const otomatis& permintaan, otomatis& res) { res.set_header("ADDITIONAL_HEADER", "nilai"); });
svr.Post("/multipart", [&](const auto& req, auto& res) { ukuran otomatis = req.files.size(); auto ret = req.has_file("name1"); const auto& file = req. get_file_value("nama1"); // file.nama file; // file.content_type; // file.content;});
svr.Posting("/content_receiver", [&](const Permintaan &req, Respon &res, const ContentReader &content_reader) { if (req.is_multipart_form_data()) { // CATATAN: `content_reader` diblokir hingga setiap kolom data formulir dibaca file MultipartFormDataItems; konten_pembaca( [&](const MultipartFormData &file) { file.push_back(file); kembali benar; }, [&](const char *data, size_t data_length) { files.back().content.append(data, data_length); kembali benar; }); } kalau tidak { std::string tubuh; content_reader([&](const char *data, size_t data_length) { body.append(data, data_length); kembali benar; }); } });
ukuran konstan_t DATA_CHUNK_SIZE = 4; svr.Get("/stream", [&](const Permintaan &req, Respons &res) { data otomatis = new std::string("abcdefg"); res.set_content_provider( data->ukuran(), // Panjang konten "teks/polos", // Tipe konten [&, data](size_t offset, size_t length, DataSink &sink) { const auto &d = *data; sink.write(&d[offset], std::min(panjang, DATA_CHUNK_SIZE)); kembali benar; // kembalikan 'false' jika Anda ingin membatalkan proses. }, [data](bool sukses) { hapus data; }); });
Tanpa panjang konten:
svr.Get("/stream", [&](const Permintaan &permintaan, Respon &res) { res.set_content_provider( "teks/polos", // Tipe konten [&](size_t offset, DataSink &sink) { jika (/* masih ada data */) { std::vektordata; // menyiapkan data... tenggelam.tulis(data.data(), data.ukuran()); } kalau tidak { tenggelam.selesai(); // Tidak ada data lagi } mengembalikan nilai benar; // kembalikan 'false' jika Anda ingin membatalkan proses. }); });
svr.Get("/chunked", [&](const Permintaan&permintaan,Respon&res) { res.set_chunked_content_provider( "teks/polos", [](ukuran_t offset, Sink Data & sink) { tenggelam.write("123", 3); tenggelam.write("345", 3); tenggelam.write("789", 3); tenggelam.selesai(); // Tidak ada lagi data yang menghasilkan nilai true; // kembalikan 'false' jika Anda ingin membatalkan proses. } ); });
Dengan cuplikan:
svr.Get("/chunked", [&](const Permintaan&permintaan,Respon&res) { res.set_header("Trailer", "Dummy1, Dummy2"); res.set_chunked_content_provider( "teks/polos", [](ukuran_t offset, Sink Data & sink) { tenggelam.write("123", 3); tenggelam.write("345", 3); tenggelam.write("789", 3); tenggelam.selesai_dengan_trailer({ {"Dummy1", "DummyVal1"}, {"Dummy2", "DummyVal2"} }); kembali benar; } ); });
svr.Get("/content", [&](const Permintaan &permintaan, Respon &res) { res.set_file_content("./path/to/conent.html"); }); svr.Get("/content", [&](const Permintaan &permintaan, Respon &res) { res.set_file_content("./path/to/conent", "teks/html"); });
Secara default, server mengirimkan respons 100 Continue
untuk header Expect: 100-continue
.
// Kirim '417 Ekspektasi Gagal' respon.svr.set_expect_100_continue_handler([](const Permintaan &req, Respons &res) { return StatusCode::ExpectationFailed_417; });
// Kirim status akhir tanpa membaca pesan 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); // Defaultnya adalah 5svr.set_keep_alive_timeout(10); // Standarnya adalah 5
svr.set_read_timeout(5, 0); // 5 detiksvr.set_write_timeout(5, 0); // 5 detiksvr.set_idle_interval(0, 100000); // 100 milidetik
svr.set_payload_max_length(1024*1024*512); // 512MB
Catatan
Jika tipe konten isi permintaan adalah 'www-form-urlencoded', panjang payload sebenarnya tidak boleh melebihi CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH
.
Silakan lihat contoh Server dan contoh Klien.
ThreadPool
digunakan sebagai antrian tugas default , dan jumlah thread default adalah 8, atau std::thread::hardware_concurrency()
. Anda dapat mengubahnya dengan CPPHTTPLIB_THREAD_POOL_COUNT
.
Jika Anda ingin mengatur jumlah thread saat runtime, tidak ada cara yang mudah... Tapi inilah caranya.
svr.new_task_queue = [] { kembalikan ThreadPool baru(12); };
Anda juga dapat memberikan parameter opsional untuk membatasi jumlah maksimum permintaan yang tertunda, yaitu permintaan accept()
di-ed oleh pendengar tetapi masih menunggu untuk dilayani oleh thread pekerja.
svr.new_task_queue = [] { kembalikan ThreadPool baru(/*num_threads=*/12, /*max_queued_requests=*/18); };
Batas defaultnya adalah 0 (tidak terbatas). Setelah batas tercapai, pendengar akan mematikan koneksi klien.
Anda dapat menyediakan implementasi kumpulan thread Anda sendiri sesuai dengan kebutuhan Anda.
kelas YourThreadPoolTaskQueue : public TaskQueue {public: YourThreadPoolTaskQueue(size_t n) { pool_.start_with_thread_count(n); } virtual bool enqueue(std::functionfn) override { /* Mengembalikan nilai true jika tugas benar-benar dimasukkan ke dalam antrean, atau false * jika pemanggil harus memutuskan koneksi terkait. */ kembali pool_.enqueue(fn); } virtual void shutdown() timpa { pool_.shutdown_graceously(); }pribadi: Kumpulan ThreadPool Anda_; }; svr.new_task_queue = [] { kembalikan YourThreadPoolTaskQueue(12); };
#termasuk#termasuk int main(batal) { httplib::Klien klien("localhost", 1234); if (res otomatis = cli.Get("/hi")) { if (res->status == StatusCode::OK_200) { std::cout << res->tubuh << std::endl; } } else { kesalahan otomatis = res.error(); std::cout << "Kesalahan HTTP: " << httplib::to_string(err) << std::endl; } }
Tip
Konstruktor dengan string skema-host-port sekarang didukung!
httplib::Klien klien("localhost"); httplib::Klien klien("localhost:8080"); httplib::Klien klien("http://localhost"); httplib::Klien klien("http://localhost:8080"); httplib::Klien klien("https://localhost"); httplib::SSLClient cli("localhost");
Berikut adalah daftar kesalahan dari Result::error()
.
enum Kesalahan { Sukses = 0, Tidak dikenal, Koneksi, MengikatAlamat IP, Membaca, Menulis, MelebihiRedirectCount, Dibatalkan, Koneksi SSL, Sertifikat Pemuatan SSL, Verifikasi SSLServer, Karakter Batas Multibagian yang Tidak Didukung, Kompresi, Waktu Koneksi habis, };
httplib::Header header = { { "Terima-Encoding", "gzip, deflate" } };res otomatis = cli.Get("/hi", header);
atau
auto res = cli.Get("/hi", {{"Terima-Encoding", "gzip, deflate"}});
atau
cli.set_default_headers({ { "Terima-Encoding", "gzip, deflate" } });res otomatis = cli.Get("/hi");
res = cli.Post("/post", "teks", "teks/polos"); res = cli.Post("/orang", "nama=john1¬e=coder", "application/x-www-form-urlencoded");
httplib::Params params; params.emplace("nama", "john"); params.emplace("catatan", "coder");res otomatis = cli.Post("/post", params);
atau
httplib::Params params{ { "nama", "johan" }, { "catatan", "pembuat kode" } };res otomatis = cli.Post("/post", params);
httplib::Item MultipartFormDataItems = { { "teks1", "teks default", "", "" }, { "teks2", "aωb", "", "" }, { "file1", "hnennlnlnon", "hello.txt", "teks/polos" }, { "file2", "{n "world", truen}n", "world.json", "application/json" }, { "file3", "", "", "aplikasi/aliran oktet" }, };res otomatis = cli.Post("/multipart", item);
res = cli.Put("/resource/foo", "teks", "teks/polos");
res = cli.Hapus("/sumber daya/foo");
res = cli.Options("*"); res = cli.Options("/resource/foo");
cli.set_connection_timeout(0, 300000); // 300 milidetikcli.set_read_timeout(5, 0); // 5 detikcli.set_write_timeout(5, 0); // 5 detik
std::string body;res otomatis = cli.Get("/data besar", [&](const char *data, size_t data_length) { body.append(data, data_length); kembali benar; });
std::string body;auto res = cli.Get( "/stream", Header(), [&](const Respon &respons) { EXPECT_EQ(StatusCode::OK_200, respon.status); kembali benar; // kembalikan 'false' jika Anda ingin membatalkan permintaan. }, [&](const char *data, size_t data_length) { body.append(data, data_length); kembali benar; // kembalikan 'false' jika Anda ingin membatalkan permintaan. });
std::string body = ...;auto res = cli.Post( "/stream", body.size(), [](size_t offset, panjang size_t, DataSink &sink) { sink.write(body.data() + offset, panjang); kembali benar; // kembalikan 'false' jika Anda ingin membatalkan permintaan. }, "teks/polos");
res otomatis = cli.Post( "/stream", [](ukuran_t offset, Sink Data & sink) { sink.os << "data terpotong 1"; sink.os << "data terpotong 2"; sink.os << "data terpotong 3"; tenggelam.selesai(); kembali benar; // kembalikan 'false' jika Anda ingin membatalkan permintaan. }, "teks/polos");
httplib::Klien cli(url, port);// cetakan: 0/000 byte => 50% completeauto res = cli.Get("/", [](uint64_t len, uint64_t total) { printf("%lld / %lld byte => %d%% selesain", len, total, (int)(len*100/total)); kembali benar; // kembalikan 'false' jika Anda ingin membatalkan permintaan.} );
// Otentikasi Dasarcli.set_basic_auth("user", "pass");// Otentikasi Intisaricli.set_digest_auth("user", "pass");// Otentikasi Token Pembawacli.set_bearer_token_auth("token");
Catatan
OpenSSL diperlukan untuk Otentikasi Digest.
cli.set_proxy("host", port);// Otentikasi Dasarcli.set_proxy_basic_auth("pengguna", "lulus");// Otentikasi Intisaricli.set_proxy_digest_auth("pengguna", "lulus");// Otentikasi Token Pembawacli.set_proxy_bearer_token_auth ("lulus");
Catatan
OpenSSL diperlukan untuk Otentikasi Digest.
httplib::Klien cli("httpbin.org");auto res = cli.Get("/range/32", { httplib::make_range_header({{1, 10}}) // 'Rentang: byte=1- 10'});// res->status harus 206.// res->body harus "bcdefghijk".
httplib::make_range_header({{1, 10}, {20, -1}}) // 'Rentang: byte=1-10, 20-'httplib::make_range_header({{100, 199}, {500, 599 }}) // 'Rentang: byte=100-199, 500-599'httplib::make_range_header({{0, 0}, {-1, 1}}) // 'Rentang: byte=0-0, - 1'
httplib::Klien klien("localhost", 1234); cli.Dapatkan("/halo"); // dengan "Koneksi: tutup"cli.set_keep_alive(true); cli.Dapatkan("/dunia"); cli.set_keep_alive(salah); cli.Get("/permintaan terakhir"); // dengan "Koneksi: tutup"
httplib::Klien cli("yahoo.com");res otomatis = cli.Get("/"); res->status; // 301cli.set_follow_location(benar); res = cli.Dapatkan("/"); res->status; // 200
Catatan
Fitur ini belum tersedia di Windows.
cli.set_interface("eth0"); // Nama antarmuka, alamat IP atau nama host
Server dapat menerapkan kompresi pada konten tipe MIME berikut:
semua jenis teks kecuali teks/aliran peristiwa
gambar/svg+xml
aplikasi/javascript
aplikasi/json
aplikasi/xml
aplikasi/xhtml+xml
Kompresi 'gzip' tersedia dengan CPPHTTPLIB_ZLIB_SUPPORT
. libz
harus ditautkan.
Kompresi Brotli tersedia dengan CPPHTTPLIB_BROTLI_SUPPORT
. Perpustakaan yang diperlukan harus ditautkan. Silakan lihat https://github.com/google/brotli untuk detail lebih lanjut.
cli.set_compress(benar); res = cli.Post("/resource/foo", "...", "teks/polos");
cli.set_decompress(salah); res = cli.Get("/resource/foo", {{"Terima-Encoding", "gzip, deflate, br"}}); res->tubuh; // Data terkompresi
poll
alih-alih select
select
panggilan sistem digunakan sebagai default karena didukung lebih luas. Jika Anda ingin membiarkan cpp-httplib menggunakan poll
, Anda dapat melakukannya dengan CPPHTTPLIB_USE_POLL
.
Dukungan Unix Domain Socket tersedia di Linux dan macOS.
// 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" dapat berupa jalur relatif atau jalur absolut. Aplikasi Anda harus memiliki izin yang sesuai untuk jalur tersebut. Anda juga dapat menggunakan alamat soket abstrak di Linux. Untuk menggunakan alamat soket abstrak, tambahkan byte nol ('x00') ke jalurnya.
$ ./split.py -husage: split.py [-h] [-e EXTENSION] [-o OUT]Skrip ini membagi httplib.h menjadi bagian .h dan .cc. Argumen opsional: -h, --help show pesan bantuan ini dan keluar -e EXTENSION, --extension EXTENSION ekstensi file implementasi (default: cc) -o OUT, --out OUT tempat menulis file (default: out)$ ./split.pyWrote out/httplib .h dan keluar/httplib.cc
Dockerfile untuk server HTTP statis tersedia. Nomor port server HTTP ini adalah 80, dan melayani file statis dari direktori /html
di dalam container.
> buruh pelabuhan membangun -t cpp-httplib-server ....> buruh pelabuhan menjalankan --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server Melayani HTTP pada 0.0.0.0 port 80 ... 192.168.65.1 - - [31/Agt/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"192.168.65.1 - - [31/Agt/2024 :21:34:26 +0000] "GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0..."192.168.65.1 - - [31/Agt/2024:21:34:26 +0000] " DAPATKAN /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0..."
Dari Docker Hub
> menjalankan buruh pelabuhan --rm -it -p 8080:80 -v ./docker/html:/html yhirose4dockerhub/cpp-httplib-server ...> buruh pelabuhan menjalankan --init --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server Melayani HTTP pada 0.0.0.0 port 80 ... 192.168.65.1 - - [31/Agt/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"192.168.65.1 - - [31/Agt/2024 :21:34:26 +0000] "GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0..."192.168.65.1 - - [31/Agt/2024:21:34:26 +0000] " DAPATKAN /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0..."
g++ 4.8 dan yang lebih lama tidak dapat membangun perpustakaan ini karena
dalam versinya rusak.
Sertakan httplib.h
sebelum Windows.h
atau sertakan Windows.h
dengan mendefinisikan WIN32_LEAN_AND_MEAN
terlebih dahulu.
#termasuk#termasuk
#define WIN32_LEAN_AND_MEAN#termasuk#termasuk
Catatan
cpp-httplib secara resmi hanya mendukung Visual Studio terbaru. Ini mungkin berfungsi dengan Visual Studio versi sebelumnya, tetapi saya tidak dapat lagi memverifikasinya. Permintaan penarikan selalu diterima untuk versi Visual Studio yang lebih lama kecuali permintaan tersebut melanggar kesesuaian C++11.
Catatan
Windows 8 atau lebih rendah, Visual Studio 2013 atau lebih rendah, dan Cygwin dan MSYS2 termasuk MinGW tidak didukung atau diuji.
Lisensi MIT (© 2024 Yuji Hirose)
Orang-orang ini memberikan kontribusi besar untuk memoles perpustakaan ini ke tingkat yang lebih tinggi dari mainan sederhana!