From e19ff271d0cea2cc92559ccbd759b682ada0d8cb Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 2 Mar 2026 18:46:22 +0000 Subject: [PATCH] feat: add C & Zig bindings for tcp connector and acceptor --- build.zig | 2 + include/thespian/c/tcp.h | 31 ++++++++++ include/thespian/tcp.hpp | 11 ++++ src/c/tcp.cpp | 126 +++++++++++++++++++++++++++++++++++++++ src/instance.cpp | 15 +++++ src/thespian.zig | 51 ++++++++++++++++ test/tcp_c_api.cpp | 31 ++++++++++ test/tests.cpp | 1 + test/tests.hpp | 1 + 9 files changed, 269 insertions(+) create mode 100644 include/thespian/c/tcp.h create mode 100644 src/c/tcp.cpp create mode 100644 test/tcp_c_api.cpp diff --git a/build.zig b/build.zig index a6fc05a..e423a3f 100644 --- a/build.zig +++ b/build.zig @@ -60,6 +60,7 @@ pub fn build(b: *std.Build) void { "src/c/signal.cpp", "src/c/timeout.cpp", "src/c/unx.cpp", + "src/c/tcp.cpp", "src/c/trace.cpp", "src/cbor.cpp", "src/executor_asio.cpp", @@ -124,6 +125,7 @@ pub fn build(b: *std.Build) void { "test/tests.cpp", "test/timeout_test.cpp", "test/unx_c_api.cpp", + "test/tcp_c_api.cpp", }, .flags = &cppflags }); tests.linkLibrary(lib); tests.linkLibrary(asio_dep.artifact("asio")); diff --git a/include/thespian/c/tcp.h b/include/thespian/c/tcp.h new file mode 100644 index 0000000..8f73f38 --- /dev/null +++ b/include/thespian/c/tcp.h @@ -0,0 +1,31 @@ +#pragma once + +// NOLINTBEGIN(modernize-use-trailing-return-type) +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +struct thespian_tcp_acceptor_handle; +struct thespian_tcp_acceptor_handle * +thespian_tcp_acceptor_create(const char *tag); +uint16_t thespian_tcp_acceptor_listen(struct thespian_tcp_acceptor_handle *, + struct in6_addr ip, uint16_t port); +int thespian_tcp_acceptor_close(struct thespian_tcp_acceptor_handle *); +void thespian_tcp_acceptor_destroy(struct thespian_tcp_acceptor_handle *); + +struct thespian_tcp_connector_handle; +struct thespian_tcp_connector_handle * +thespian_tcp_connector_create(const char *tag); +int thespian_tcp_connector_connect(struct thespian_tcp_connector_handle *, + struct in6_addr ip, uint16_t port); +int thespian_tcp_connector_cancel(struct thespian_tcp_connector_handle *); +void thespian_tcp_connector_destroy(struct thespian_tcp_connector_handle *); + +#ifdef __cplusplus +} +#endif +// NOLINTEND(modernize-use-trailing-return-type) diff --git a/include/thespian/tcp.hpp b/include/thespian/tcp.hpp index 076580c..15a4af3 100644 --- a/include/thespian/tcp.hpp +++ b/include/thespian/tcp.hpp @@ -49,4 +49,15 @@ struct connector { connector_ref ref; }; +// C++ helpers used by the C binding layer + +auto acceptor_listen(acceptor_impl *h, const in6_addr &ip, port_t port) + -> port_t; +void acceptor_close(acceptor_impl *h); +void destroy_acceptor(acceptor_impl *h); + +void connector_connect(connector_impl *h, const in6_addr &ip, port_t port); +void connector_cancel(connector_impl *h); +void destroy_connector(connector_impl *h); + } // namespace thespian::tcp diff --git a/src/c/tcp.cpp b/src/c/tcp.cpp new file mode 100644 index 0000000..befc834 --- /dev/null +++ b/src/c/tcp.cpp @@ -0,0 +1,126 @@ +#include +#include +#include + +using ::port_t; +using thespian::tcp::acceptor_impl; +using thespian::tcp::connector_impl; + +extern "C" { + +auto thespian_tcp_acceptor_create(const char *tag) + -> struct thespian_tcp_acceptor_handle * { + try { + auto *h = thespian::tcp::acceptor::create(tag).ref.release(); + return reinterpret_cast(h); // NOLINT + } catch (const std::exception &e) { + thespian_set_last_error(e.what()); + return nullptr; + } catch (...) { + thespian_set_last_error("unknown thespian_tcp_acceptor_create error"); + return nullptr; + } +} + +auto thespian_tcp_acceptor_listen(struct thespian_tcp_acceptor_handle *handle, + in6_addr ip, uint16_t port) -> uint16_t { + try { + port_t p = thespian::tcp::acceptor_listen( + reinterpret_cast(handle), ip, port); // NOLINT + return static_cast(p); + } catch (const std::exception &e) { + thespian_set_last_error(e.what()); + return 0; + } catch (...) { + thespian_set_last_error("unknown thespian_tcp_acceptor_listen error"); + return 0; + } +} + +auto thespian_tcp_acceptor_close(struct thespian_tcp_acceptor_handle *handle) + -> int { + try { + thespian::tcp::acceptor_close( + reinterpret_cast(handle)); // NOLINT + return 0; + } catch (const std::exception &e) { + thespian_set_last_error(e.what()); + return -1; + } catch (...) { + thespian_set_last_error("unknown thespian_tcp_acceptor_close error"); + return -1; + } +} + +void thespian_tcp_acceptor_destroy( + struct thespian_tcp_acceptor_handle *handle) { + try { + thespian::tcp::destroy_acceptor( + reinterpret_cast(handle)); // NOLINT + } catch (const std::exception &e) { + thespian_set_last_error(e.what()); + } catch (...) { + thespian_set_last_error("unknown thespian_tcp_acceptor_destroy error"); + } +} + +auto thespian_tcp_connector_create(const char *tag) + -> struct thespian_tcp_connector_handle * { + try { + auto *h = thespian::tcp::connector::create(tag).ref.release(); + return reinterpret_cast( // NOLINT + h); + } catch (const std::exception &e) { + thespian_set_last_error(e.what()); + return nullptr; + } catch (...) { + thespian_set_last_error("unknown thespian_tcp_connector_create error"); + return nullptr; + } +} + +auto thespian_tcp_connector_connect( + struct thespian_tcp_connector_handle *handle, in6_addr ip, uint16_t port) + -> int { + try { + thespian::tcp::connector_connect( + reinterpret_cast(handle), // NOLINT + ip, port); + return 0; + } catch (const std::exception &e) { + thespian_set_last_error(e.what()); + return -1; + } catch (...) { + thespian_set_last_error("unknown thespian_tcp_connector_connect error"); + return -1; + } +} + +auto thespian_tcp_connector_cancel(struct thespian_tcp_connector_handle *handle) + -> int { + try { + thespian::tcp::connector_cancel( + reinterpret_cast(handle)); // NOLINT + return 0; + } catch (const std::exception &e) { + thespian_set_last_error(e.what()); + return -1; + } catch (...) { + thespian_set_last_error("unknown thespian_tcp_connector_cancel error"); + return -1; + } +} + +void thespian_tcp_connector_destroy( + struct thespian_tcp_connector_handle *handle) { + try { + thespian::tcp::destroy_connector( + reinterpret_cast(handle)); // NOLINT + } catch (const std::exception &e) { + thespian_set_last_error(e.what()); + } catch (...) { + thespian_set_last_error("unknown thespian_tcp_connector_destroy error"); + } +} + +} // extern "C" diff --git a/src/instance.cpp b/src/instance.cpp index 00ae6bd..2b74e68 100644 --- a/src/instance.cpp +++ b/src/instance.cpp @@ -1267,6 +1267,21 @@ void connector::connect(in6_addr ip, port_t port, in6_addr lip, port_t lport) { void connector::cancel() { ref->cancel(); } +// C API helpers - visibility requires complete types + +auto acceptor_listen(acceptor_impl *h, const in6_addr &ip, port_t port) + -> port_t { + return h->listen(ip, port); +} +void acceptor_close(acceptor_impl *h) { h->close(); } +void destroy_acceptor(acceptor_impl *h) { delete h; } + +void connector_connect(connector_impl *h, const in6_addr &ip, port_t port) { + h->connect(ip, port, in6addr_any, 0); +} +void connector_cancel(connector_impl *h) { h->cancel(); } +void destroy_connector(connector_impl *h) { delete h; } + } // namespace tcp namespace unx { diff --git a/src/thespian.zig b/src/thespian.zig index 7a6e641..0991615 100644 --- a/src/thespian.zig +++ b/src/thespian.zig @@ -7,6 +7,8 @@ const c = @cImport({ @cInclude("thespian/c/timeout.h"); @cInclude("thespian/c/signal.h"); @cInclude("thespian/c/unx.h"); + @cInclude("thespian/c/tcp.h"); + @cInclude("netinet/in.h"); }); const c_posix = if (builtin.os.tag != .windows) @cImport({ @cInclude("thespian/backtrace.h"); @@ -797,6 +799,55 @@ pub const unx_connector = struct { } }; +pub const tcp_acceptor = struct { + handle: *c.struct_thespian_tcp_acceptor_handle, + + const Self = @This(); + + pub fn init(tag_: []const u8) !Self { + return .{ .handle = c.thespian_tcp_acceptor_create(tag_) orelse return log_last_error(error.ThespianTcpAcceptorInitFailed) }; + } + + pub fn listen(self: *const Self, ip: c.in6_addr, port: u16) !u16 { + const ret = c.thespian_tcp_acceptor_listen(self.handle, ip, port); + if (ret == 0) return error.ThespianTcpAcceptorListenFailed; + return ret; + } + + pub fn close(self: *const Self) !void { + const ret = c.thespian_tcp_acceptor_close(self.handle); + if (ret < 0) return error.ThespianTcpAcceptorCloseFailed; + } + + pub fn deinit(self: *const Self) void { + c.thespian_tcp_acceptor_destroy(self.handle); + } +}; + +pub const tcp_connector = struct { + handle: *c.struct_thespian_tcp_connector_handle, + + const Self = @This(); + + pub fn init(tag_: []const u8) !Self { + return .{ .handle = c.thespian_tcp_connector_create(tag_) orelse return log_last_error(error.ThespianTcpConnectorInitFailed) }; + } + + pub fn connect(self: *const Self, ip: c.in6_addr, port: u16) !void { + const ret = c.thespian_tcp_connector_connect(self.handle, ip, port); + if (ret < 0) return error.ThespianTcpConnectorConnectFailed; + } + + pub fn cancel(self: *const Self) !void { + const ret = c.thespian_tcp_connector_cancel(self.handle); + if (ret < 0) return error.ThespianTcpConnectorCancelFailed; + } + + pub fn deinit(self: *const Self) void { + c.thespian_tcp_connector_destroy(self.handle); + } +}; + pub const timeout = struct { handle: ?*c.struct_thespian_timeout_handle, diff --git a/test/tcp_c_api.cpp b/test/tcp_c_api.cpp new file mode 100644 index 0000000..d5c2c18 --- /dev/null +++ b/test/tcp_c_api.cpp @@ -0,0 +1,31 @@ +#include "tests.hpp" +#include +#include + +using namespace thespian; + +// simple smoke test of tcp C API: create + destroy handles and listen on any +// port + +auto tcp_c_api(thespian::context &ctx, bool &result, thespian::env_t env) + -> thespian::result { + (void)ctx; + (void)env; + + struct thespian_tcp_acceptor_handle *a = thespian_tcp_acceptor_create("tag"); + check(a != nullptr); + uint16_t port = thespian_tcp_acceptor_listen(a, in6addr_any, 0); + // port may be zero if something went wrong; ignore for smoke. + (void)port; + thespian_tcp_acceptor_close(a); + thespian_tcp_acceptor_destroy(a); + + struct thespian_tcp_connector_handle *c = + thespian_tcp_connector_create("tag"); + check(c != nullptr); + // don't attempt to connect, simply exercise create/destroy + thespian_tcp_connector_destroy(c); + + result = true; + return ok(); +} diff --git a/test/tests.cpp b/test/tests.cpp index 0c3dffa..b661e4e 100644 --- a/test/tests.cpp +++ b/test/tests.cpp @@ -112,6 +112,7 @@ extern "C" auto runtestcase(const char *name) -> int { tests["spawn_exit"] = spawn_exit; tests["timeout_test"] = timeout_test; tests["unx_c_api"] = unx_c_api; + tests["tcp_c_api"] = tcp_c_api; env_t env{}; env_t log_env{}; diff --git a/test/tests.hpp b/test/tests.hpp index 791ab01..680e2db 100644 --- a/test/tests.hpp +++ b/test/tests.hpp @@ -28,3 +28,4 @@ testcase perf_spawn; testcase spawn_exit; testcase timeout_test; testcase unx_c_api; +testcase tcp_c_api;