diff --git a/pkgs/c/chriskohlhoff.asio.lua b/pkgs/c/chriskohlhoff.asio.lua index da8d0e2..3fa095c 100644 --- a/pkgs/c/chriskohlhoff.asio.lua +++ b/pkgs/c/chriskohlhoff.asio.lua @@ -22,13 +22,23 @@ -- * 依赖未导出 ASIO_HAS_* 宏、平台专用头文件或 Boost 扩展的代码,需要 -- 改用标准/操作系统能力检测或另行扩展模块 wrapper。 -- +-- 平台条件导出:文件 I/O +-- * `asio::stream_file` / `asio::random_access_file` / `asio::file_base` +-- (及其 basic_ 模板)在 `ASIO_HAS_FILE` 成立时导出。这是 Asio 自己的 +-- 能力宏:Windows 下由 IOCP 提供,Linux 下需要 io_uring,macOS 下无后端 +-- (见 `asio/detail/config.hpp` 的 "// Files." 一节)。用 Asio 的宏而不是 +-- 在描述符里维护一张平台清单,是为了让它随上游一起演进。 +-- * 消费端**不能**用 `#if defined(ASIO_HAS_FILE)` 判断:宏不跨模块边界, +-- 而模块消费者不 include Asio 头文件,那个宏在它的 TU 里永远为假。 +-- 就本包的配置而言判据是确定的 —— 未开启 io_uring,所以 +-- `ASIO_HAS_FILE` ⇔ Windows。跨平台消费者按 `_WIN32` 分支即可 +-- (tests/examples/asio-module/tests/file.cpp 就是这么写的)。 +-- -- 未导出的组件 -- * SSL/TLS (`asio/ssl/*.hpp`):需要 OpenSSL/wolfSSL 等外部依赖。 -- * Unix 域套接字、POSIX 描述符和 Windows 句柄: -- `asio/local/*.hpp`、`asio/posix/*.hpp`、`asio/windows/*.hpp`。 --- * 串口、pipe 和文件 I/O:`asio/serial_port.hpp`、 --- `asio/*able_pipe.hpp`、`asio/stream_file.hpp`、 --- `asio/random_access_file.hpp`。 +-- * 串口和 pipe:`asio/serial_port.hpp`、`asio/*able_pipe.hpp`。 -- * spawn()/yield_context 有栈协程:需要 Boost.Context;本包禁用其自动 -- 检测,应改用 co_spawn + awaitable + use_awaitable。 -- * deadline_timer、generic protocol、execution、traits、遗留宏式协程和 @@ -146,6 +156,16 @@ module; #include #include #include +#include +// File I/O exists only where Asio has a backend for it: IOCP on Windows, or +// io_uring on Linux (asio/detail/config.hpp, "// Files."). ASIO_HAS_FILE is +// Asio's own answer to that question, so the guard here is the library's, not +// a platform list this descriptor would have to keep in sync. +#if defined(ASIO_HAS_FILE) +#include +#include +#include +#endif #include #include #include @@ -173,7 +193,19 @@ using ::std::coroutine_traits; export namespace asio::error { using ::asio::error::make_error_code; -using ::asio::error::operation_aborted; +// The four error enums and their enumerators. Exporting `operation_aborted` +// alone made every OTHER condition unreachable through the module — a consumer +// that wants to tell a connection refusal from a DNS failure had no name to +// compare against. `using enum` keeps that from becoming a hand-maintained +// list of ~40 enumerators that drifts on the next Asio release. +using ::asio::error::basic_errors; +using ::asio::error::netdb_errors; +using ::asio::error::addrinfo_errors; +using ::asio::error::misc_errors; +using enum ::asio::error::basic_errors; +using enum ::asio::error::netdb_errors; +using enum ::asio::error::addrinfo_errors; +using enum ::asio::error::misc_errors; } export namespace asio { @@ -218,6 +250,7 @@ using ::asio::consign; using ::asio::as_tuple; using ::asio::socket_base; using ::asio::connect; +using ::asio::async_connect; using ::asio::async_read; using ::asio::async_write; using ::asio::read; @@ -225,6 +258,17 @@ using ::asio::write; using ::asio::read_until; } +#if defined(ASIO_HAS_FILE) +export namespace asio { +using ::asio::file_base; +using ::asio::basic_file; +using ::asio::basic_stream_file; +using ::asio::basic_random_access_file; +using ::asio::stream_file; +using ::asio::random_access_file; +} +#endif + export namespace asio::experimental { using ::asio::experimental::channel; using ::asio::experimental::concurrent_channel; @@ -241,6 +285,9 @@ using ::asio::ip::udp; using ::asio::ip::address; using ::asio::ip::address_v4; using ::asio::ip::address_v6; +using ::asio::ip::make_address; +using ::asio::ip::make_address_v4; +using ::asio::ip::make_address_v6; } export namespace asio::this_coro { diff --git a/tests/examples/asio-module/tests/errors.cpp b/tests/examples/asio-module/tests/errors.cpp new file mode 100644 index 0000000..597a7e7 --- /dev/null +++ b/tests/examples/asio-module/tests/errors.cpp @@ -0,0 +1,68 @@ +// The error surface: the four Asio error enums and their enumerators reach a +// module consumer, and each one compares equal to the std::error_code that +// asio::error::make_error_code builds from it. +// +// Before this was exported the module offered exactly one condition, +// operation_aborted, which is enough to notice a cancellation and nothing else: +// telling a refused connection from a DNS failure had no name to compare +// against on the consumer's side. +import std; +import asio; + +int main() { + // --- the enum types themselves --- + static_assert(std::is_enum_v); + static_assert(std::is_enum_v); + static_assert(std::is_enum_v); + static_assert(std::is_enum_v); + + // --- enumerators, one per enum, round-tripped through make_error_code --- + const std::error_code aborted = asio::error::make_error_code(asio::error::operation_aborted); + const std::error_code refused = asio::error::make_error_code(asio::error::connection_refused); + const std::error_code timedout = asio::error::make_error_code(asio::error::timed_out); + const std::error_code unreach = asio::error::make_error_code(asio::error::host_unreachable); + const std::error_code netdown = asio::error::make_error_code(asio::error::network_unreachable); + const std::error_code nohost = asio::error::make_error_code(asio::error::host_not_found); + const std::error_code retryhost = asio::error::make_error_code(asio::error::host_not_found_try_again); + const std::error_code nosvc = asio::error::make_error_code(asio::error::service_not_found); + const std::error_code eof = asio::error::make_error_code(asio::error::eof); + + // An error_code built from an enumerator compares equal to that enumerator: + // this is the comparison real code writes (`if (ec == asio::error::eof)`), + // and it only works if the enumerator NAME crossed the module boundary. + if (aborted != asio::error::operation_aborted) return 1; + if (refused != asio::error::connection_refused) return 2; + if (timedout != asio::error::timed_out) return 3; + if (unreach != asio::error::host_unreachable) return 4; + if (netdown != asio::error::network_unreachable) return 5; + if (nohost != asio::error::host_not_found) return 6; + if (retryhost != asio::error::host_not_found_try_again) return 7; + if (nosvc != asio::error::service_not_found) return 8; + if (eof != asio::error::eof) return 9; + + // Distinct conditions must not collapse into each other. + if (refused == timedout || nohost == retryhost) return 10; + + // Every one of them is a real, non-empty condition. + for (const auto& ec : {aborted, refused, timedout, unreach, netdown, + nohost, retryhost, nosvc, eof}) { + if (!ec) return 11; + if (ec.message().empty()) return 12; + } + + // --- ip::make_address parses, and reports failure through error_code --- + std::error_code parse; + const auto v4 = asio::ip::make_address("192.0.2.7", parse); + if (parse || !v4.is_v4() || v4.to_string() != "192.0.2.7") return 13; + + const auto v6 = asio::ip::make_address("::1", parse); + if (parse || !v6.is_v6() || !v6.is_loopback()) return 14; + + (void)asio::ip::make_address("not-an-address", parse); + if (!parse) return 15; + + if (asio::ip::make_address_v4("10.0.0.1", parse).to_string() != "10.0.0.1" || parse) return 16; + if (!asio::ip::make_address_v6("::1", parse).is_loopback() || parse) return 17; + + return 0; +} diff --git a/tests/examples/asio-module/tests/file.cpp b/tests/examples/asio-module/tests/file.cpp new file mode 100644 index 0000000..7c14edd --- /dev/null +++ b/tests/examples/asio-module/tests/file.cpp @@ -0,0 +1,84 @@ +// Asio's file I/O over the module surface: asio::stream_file writes and reads +// back through a coroutine, asio::random_access_file reads at an offset, and +// asio::file_base's open flags compose. +// +// These names are exported only where Asio has a file backend. For THIS +// package's configuration the condition is exactly Windows: the backend is IOCP +// (ASIO_HAS_WINDOWS_RANDOM_ACCESS_HANDLE), and the only other one Asio offers is +// io_uring, which this package does not enable. `ASIO_HAS_FILE` itself is +// unusable as the discriminator here — a macro does not cross a module boundary +// and a module consumer includes no Asio header, so it is always false in this +// TU no matter what the package was built with. +import std; +import asio; + +#if defined(_WIN32) + +int main() { + namespace fs = std::filesystem; + + const auto path = (fs::temp_directory_path() / "mcpp_asio_module_file.bin").string(); + std::error_code rm; + fs::remove(path, rm); + + constexpr std::string_view payload = "mcpp-asio-file"; + + asio::io_context io; + int failure = 0; + + asio::co_spawn(io, [&]() -> asio::awaitable { + // --- write through stream_file, composing three file_base flags --- + { + asio::stream_file out(co_await asio::this_coro::executor, path, + asio::file_base::write_only + | asio::file_base::create + | asio::file_base::truncate); + const auto written = + co_await asio::async_write(out, asio::buffer(payload), asio::use_awaitable); + if (written != payload.size()) { failure = 1; co_return; } + out.close(); + } + + // --- read it back through stream_file --- + { + asio::stream_file in(co_await asio::this_coro::executor, path, + asio::file_base::read_only); + std::string got(payload.size(), '\0'); + const auto read = + co_await asio::async_read(in, asio::buffer(got), asio::use_awaitable); + if (read != payload.size() || got != payload) { failure = 2; co_return; } + } + + // --- random_access_file reads at an offset, leaving no file position --- + { + asio::random_access_file ra(co_await asio::this_coro::executor, path, + asio::file_base::read_only); + std::string tail(5, '\0'); // "-file", offset 9 + const auto read = co_await ra.async_read_some_at(9, asio::buffer(tail), + asio::use_awaitable); + if (read != 5 || tail != "-file") { failure = 3; co_return; } + } + }, asio::detached); + + io.run(); + fs::remove(path, rm); + + // The aliases are the basic_ templates with the default executor. + static_assert(std::is_same_v>); + static_assert(std::is_same_v>); + static_assert(std::is_base_of_v); + + return failure; +} + +#else + +int main() { + // No file backend in this configuration: asserting the names are absent is + // not something a TU can do, so this leg only records that the test ran. + std::println("asio module: file I/O not available in this configuration"); + return 0; +} + +#endif diff --git a/tests/examples/asio-module/tests/network.cpp b/tests/examples/asio-module/tests/network.cpp index c8d72f2..ce23ad3 100644 --- a/tests/examples/asio-module/tests/network.cpp +++ b/tests/examples/asio-module/tests/network.cpp @@ -1,5 +1,6 @@ -// TCP (acceptor/socket, async_read/async_write) and UDP (datagram send/receive) -// over the imported module surface. +// TCP (acceptor/socket, async_read/async_write), the free asio::async_connect +// over a sequence of candidate endpoints, and UDP (datagram send/receive) over +// the imported module surface. import std; import asio; @@ -98,5 +99,44 @@ int main() { }); udp_io.run(); - return !udp_failure && receive_done && send_done ? 0 : 8; + if (udp_failure || !receive_done || !send_done) return 8; + + // --- free asio::async_connect over a candidate sequence --- + // + // The member socket.async_connect above takes ONE endpoint; the free + // function takes a range and tries each in turn, which is what a consumer + // hands a resolver's results_type to. Assert that semantics directly: + // an unreachable candidate first, the live acceptor second. Kept hermetic + // (no DNS) so it cannot fail for reasons that have nothing to do with the + // module surface. + asio::io_context connect_io; + asio::ip::tcp::acceptor live(connect_io, {asio::ip::address_v4::loopback(), 0}); + asio::ip::tcp::socket accepted_side(connect_io); + asio::ip::tcp::socket connecting(connect_io); + live.async_accept(accepted_side, [](const std::error_code&) {}); + + const std::vector candidates{ + // Port 1 on loopback: nothing listens there, so this candidate is + // refused and async_connect must move on rather than give up. + {asio::ip::address_v4::loopback(), 1}, + live.local_endpoint(), + }; + + std::error_code connect_ec{std::make_error_code(std::errc::not_supported)}; + asio::ip::tcp::endpoint chosen; + bool connect_done = false; + + asio::async_connect(connecting, candidates, + [&](const std::error_code& ec, const asio::ip::tcp::endpoint& ep) { + connect_ec = ec; + chosen = ep; + connect_done = true; + }); + + connect_io.run(); + if (!connect_done || connect_ec) return 9; + if (chosen != live.local_endpoint()) return 10; // it must have skipped port 1 + if (!connecting.is_open()) return 11; + + return 0; }