Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions pkgs/c/chriskohlhoff.asio.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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、遗留宏式协程和
Expand Down Expand Up @@ -146,6 +156,16 @@ module;
#include <asio/ip/udp.hpp>
#include <asio/ip/address.hpp>
#include <asio/ip/address_v6.hpp>
#include <asio/error.hpp>
// 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 <asio/file_base.hpp>
#include <asio/stream_file.hpp>
#include <asio/random_access_file.hpp>
#endif
#include <asio/experimental/promise.hpp>
#include <asio/experimental/channel_error.hpp>
#include <asio/experimental/channel.hpp>
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -218,13 +250,25 @@ 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;
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;
Expand All @@ -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 {
Expand Down
68 changes: 68 additions & 0 deletions tests/examples/asio-module/tests/errors.cpp
Original file line number Diff line number Diff line change
@@ -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<asio::error::basic_errors>);
static_assert(std::is_enum_v<asio::error::netdb_errors>);
static_assert(std::is_enum_v<asio::error::addrinfo_errors>);
static_assert(std::is_enum_v<asio::error::misc_errors>);

// --- 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;
}
84 changes: 84 additions & 0 deletions tests/examples/asio-module/tests/file.cpp
Original file line number Diff line number Diff line change
@@ -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<void> {
// --- 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<asio::stream_file, asio::basic_stream_file<>>);
static_assert(std::is_same_v<asio::random_access_file,
asio::basic_random_access_file<>>);
static_assert(std::is_base_of_v<asio::file_base, asio::stream_file>);

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
46 changes: 43 additions & 3 deletions tests/examples/asio-module/tests/network.cpp
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<asio::ip::tcp::endpoint> 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;
}
Loading