From ceba8ca2690a0ad96d3eb516941fca9e872791d2 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 21:13:01 +0700 Subject: [PATCH 1/5] feat: add native socket RAII wrapper --- src/platform/native_socket.hpp | 71 ++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/platform/native_socket.hpp diff --git a/src/platform/native_socket.hpp b/src/platform/native_socket.hpp new file mode 100644 index 0000000..00447d1 --- /dev/null +++ b/src/platform/native_socket.hpp @@ -0,0 +1,71 @@ +#pragma once + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include + +namespace cpp_request::detail::platform { + +#ifdef _WIN32 +using NativeSocketHandle = SOCKET; +inline constexpr NativeSocketHandle kInvalidSocket = INVALID_SOCKET; +#else +using NativeSocketHandle = int; +inline constexpr NativeSocketHandle kInvalidSocket = -1; +#endif + +class NativeSocket final { +public: + using handle_type = NativeSocketHandle; + + constexpr NativeSocket() noexcept = default; + explicit constexpr NativeSocket(handle_type handle) noexcept + : handle_(handle) {} + + ~NativeSocket() noexcept; + + NativeSocket(const NativeSocket&) = delete; + NativeSocket& operator=(const NativeSocket&) = delete; + + NativeSocket(NativeSocket&& other) noexcept + : handle_(other.release()) {} + + NativeSocket& operator=(NativeSocket&& other) noexcept { + if (this != &other) { + reset(other.release()); + } + return *this; + } + + [[nodiscard]] constexpr bool valid() const noexcept { + return handle_ != kInvalidSocket; + } + + [[nodiscard]] explicit constexpr operator bool() const noexcept { + return valid(); + } + + [[nodiscard]] constexpr handle_type get() const noexcept { + return handle_; + } + + [[nodiscard]] handle_type release() noexcept { + return std::exchange(handle_, kInvalidSocket); + } + + void reset(handle_type replacement = kInvalidSocket) noexcept; + void close() noexcept { + reset(); + } + +private: + handle_type handle_{kInvalidSocket}; +}; + +[[nodiscard]] int last_socket_error() noexcept; + +} // namespace cpp_request::detail::platform From 28a57b6032f9348543aef465fd313a484f87ae4b Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 21:13:12 +0700 Subject: [PATCH 2/5] feat: implement native socket ownership --- src/platform/native_socket.cpp | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/platform/native_socket.cpp diff --git a/src/platform/native_socket.cpp b/src/platform/native_socket.cpp new file mode 100644 index 0000000..8ab66d4 --- /dev/null +++ b/src/platform/native_socket.cpp @@ -0,0 +1,45 @@ +#include "platform/native_socket.hpp" + +#ifndef _WIN32 +#include +#include +#endif + +namespace cpp_request::detail::platform { +namespace { + +void close_native_socket(NativeSocketHandle handle) noexcept { +#ifdef _WIN32 + ::closesocket(handle); +#else + ::close(handle); +#endif +} + +} // namespace + +NativeSocket::~NativeSocket() noexcept { + close(); +} + +void NativeSocket::reset(handle_type replacement) noexcept { + if (handle_ == replacement) { + return; + } + + if (valid()) { + close_native_socket(handle_); + } + + handle_ = replacement; +} + +int last_socket_error() noexcept { +#ifdef _WIN32 + return ::WSAGetLastError(); +#else + return errno; +#endif +} + +} // namespace cpp_request::detail::platform From 2b90b3e791f2c72ac460400c57812a847cf9cd93 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 21:13:30 +0700 Subject: [PATCH 3/5] test: cover native socket RAII semantics --- tests/native_socket_test.cpp | 156 +++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 tests/native_socket_test.cpp diff --git a/tests/native_socket_test.cpp b/tests/native_socket_test.cpp new file mode 100644 index 0000000..6641433 --- /dev/null +++ b/tests/native_socket_test.cpp @@ -0,0 +1,156 @@ +#include + +#include "platform/native_socket.hpp" + +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif + +namespace { + +using cpp_request::detail::platform::NativeSocket; +using cpp_request::detail::platform::NativeSocketHandle; +using cpp_request::detail::platform::kInvalidSocket; + +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_copy_assignable_v); +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_assignable_v); + +#ifdef _WIN32 +class WinsockScope final { +public: + WinsockScope() noexcept { + WSADATA data{}; + ready_ = ::WSAStartup(MAKEWORD(2, 2), &data) == 0; + } + + ~WinsockScope() noexcept { + if (ready_) { + ::WSACleanup(); + } + } + + [[nodiscard]] bool ready() const noexcept { + return ready_; + } + +private: + bool ready_{false}; +}; +#endif + +NativeSocketHandle create_test_socket() { +#ifdef _WIN32 + static WinsockScope winsock; + if (!winsock.ready()) { + return kInvalidSocket; + } + return ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); +#else + return ::socket(AF_INET, SOCK_STREAM, 0); +#endif +} + +bool is_closed(NativeSocketHandle handle) { +#ifdef _WIN32 + int socket_type = 0; + int length = sizeof(socket_type); + const int result = ::getsockopt( + handle, + SOL_SOCKET, + SO_TYPE, + reinterpret_cast(&socket_type), + &length); + return result == SOCKET_ERROR && ::WSAGetLastError() == WSAENOTSOCK; +#else + errno = 0; + return ::fcntl(handle, F_GETFD) == -1 && errno == EBADF; +#endif +} + +TEST(NativeSocketTest, DefaultConstructedSocketIsInvalid) { + const NativeSocket socket; + + EXPECT_FALSE(socket.valid()); + EXPECT_FALSE(static_cast(socket)); + EXPECT_EQ(socket.get(), kInvalidSocket); +} + +TEST(NativeSocketTest, MoveConstructionTransfersOwnership) { + const NativeSocketHandle raw = create_test_socket(); + ASSERT_NE(raw, kInvalidSocket); + + NativeSocket source{raw}; + NativeSocket destination{std::move(source)}; + + EXPECT_FALSE(source.valid()); + EXPECT_TRUE(destination.valid()); + EXPECT_EQ(destination.get(), raw); +} + +TEST(NativeSocketTest, MoveAssignmentClosesPreviousHandleAndTransfersOwnership) { + const NativeSocketHandle first = create_test_socket(); + const NativeSocketHandle second = create_test_socket(); + ASSERT_NE(first, kInvalidSocket); + ASSERT_NE(second, kInvalidSocket); + + NativeSocket destination{first}; + NativeSocket source{second}; + + destination = std::move(source); + + EXPECT_TRUE(is_closed(first)); + EXPECT_FALSE(source.valid()); + EXPECT_EQ(destination.get(), second); +} + +TEST(NativeSocketTest, ReleaseReturnsHandleWithoutClosingIt) { + const NativeSocketHandle raw = create_test_socket(); + ASSERT_NE(raw, kInvalidSocket); + + NativeSocket socket{raw}; + const NativeSocketHandle released = socket.release(); + + EXPECT_EQ(released, raw); + EXPECT_FALSE(socket.valid()); + EXPECT_FALSE(is_closed(raw)); + +#ifdef _WIN32 + EXPECT_EQ(::closesocket(raw), 0); +#else + EXPECT_EQ(::close(raw), 0); +#endif +} + +TEST(NativeSocketTest, ResetClosesCurrentHandle) { + const NativeSocketHandle raw = create_test_socket(); + ASSERT_NE(raw, kInvalidSocket); + + NativeSocket socket{raw}; + socket.reset(); + + EXPECT_FALSE(socket.valid()); + EXPECT_TRUE(is_closed(raw)); +} + +TEST(NativeSocketTest, DestructorClosesOwnedHandle) { + const NativeSocketHandle raw = create_test_socket(); + ASSERT_NE(raw, kInvalidSocket); + + { + NativeSocket socket{raw}; + ASSERT_TRUE(socket.valid()); + } + + EXPECT_TRUE(is_closed(raw)); +} + +} // namespace From 93b715653c0ac5d6bd6e7c933feb024174dec74e Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 21:13:42 +0700 Subject: [PATCH 4/5] build: wire native socket platform source --- src/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e57a175..b71a70b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(internal ${CMAKE_CURRENT_SOURCE_DIR}/dummy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/url.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/platform/native_socket.cpp ) add_library(${PROJECT_NAME}::internal ALIAS internal) @@ -13,3 +14,7 @@ target_include_directories(internal ) target_compile_features(internal PUBLIC cxx_std_17) + +if(WIN32) + target_link_libraries(internal PRIVATE ws2_32) +endif() From 9e0adedd13de2a9c15fbd32978879d66c5763015 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 21:13:52 +0700 Subject: [PATCH 5/5] test: wire native socket tests --- tests/CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f7b0790..839873a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,6 +2,12 @@ include(${CMAKE_SOURCE_DIR}/cmake/packages/google-test.cmake) add_executable(cpp_request_tests ${CMAKE_CURRENT_SOURCE_DIR}/url_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/native_socket_test.cpp +) + +target_include_directories(cpp_request_tests + PRIVATE + ${CMAKE_SOURCE_DIR}/src ) target_link_libraries(cpp_request_tests @@ -10,6 +16,10 @@ target_link_libraries(cpp_request_tests GTest::gtest_main ) +if(WIN32) + target_link_libraries(cpp_request_tests PRIVATE ws2_32) +endif() + target_compile_features(cpp_request_tests PRIVATE cxx_std_17) gtest_discover_tests(cpp_request_tests)