From b1296b315d1eea10315812a6cbdacacee6c6e90c Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sat, 14 Mar 2026 16:41:11 +0100 Subject: [PATCH 01/18] Implement serial communication functions for configuration, status, and control - Added `serialGetConfig` to retrieve serial port configuration. - Implemented `serialGetCts`, `serialGetDcd`, `serialGetDsr`, and `serialGetRi` to check modem status. - Introduced `serialMonitorPorts` for monitoring available COM ports. - Created functions for controlling DTR, RTS, and flow control settings. - Added `serialSendBreak` and `serialUpdateBaudrate` for sending break signals and updating baud rates. --- src/serial_get_baudrate.cpp | 28 +++++++ src/serial_get_cts.cpp | 28 +++++++ src/serial_get_data_bits.cpp | 28 +++++++ src/serial_get_dcd.cpp | 28 +++++++ src/serial_get_dsr.cpp | 28 +++++++ src/serial_get_flow_control.cpp | 36 +++++++++ src/serial_get_parity.cpp | 36 +++++++++ src/serial_get_ri.cpp | 28 +++++++ src/serial_get_stop_bits.cpp | 28 +++++++ src/serial_monitor_ports.cpp | 134 ++++++++++++++++++++++++++++++++ src/serial_send_break.cpp | 41 ++++++++++ src/serial_set_baudrate.cpp | 42 ++++++++++ src/serial_set_data_bits.cpp | 42 ++++++++++ src/serial_set_dtr.cpp | 27 +++++++ src/serial_set_flow_control.cpp | 63 +++++++++++++++ src/serial_set_parity.cpp | 54 +++++++++++++ src/serial_set_rts.cpp | 27 +++++++ src/serial_set_stop_bits.cpp | 42 ++++++++++ 18 files changed, 740 insertions(+) create mode 100644 src/serial_get_baudrate.cpp create mode 100644 src/serial_get_cts.cpp create mode 100644 src/serial_get_data_bits.cpp create mode 100644 src/serial_get_dcd.cpp create mode 100644 src/serial_get_dsr.cpp create mode 100644 src/serial_get_flow_control.cpp create mode 100644 src/serial_get_parity.cpp create mode 100644 src/serial_get_ri.cpp create mode 100644 src/serial_get_stop_bits.cpp create mode 100644 src/serial_monitor_ports.cpp create mode 100644 src/serial_send_break.cpp create mode 100644 src/serial_set_baudrate.cpp create mode 100644 src/serial_set_data_bits.cpp create mode 100644 src/serial_set_dtr.cpp create mode 100644 src/serial_set_flow_control.cpp create mode 100644 src/serial_set_parity.cpp create mode 100644 src/serial_set_rts.cpp create mode 100644 src/serial_set_stop_bits.cpp diff --git a/src/serial_get_baudrate.cpp b/src/serial_get_baudrate.cpp new file mode 100644 index 0000000..918bd75 --- /dev/null +++ b/src/serial_get_baudrate.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetBaudrate(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + return static_cast(dcb.BaudRate); + } + +} // extern "C" diff --git a/src/serial_get_cts.cpp b/src/serial_get_cts.cpp new file mode 100644 index 0000000..726e729 --- /dev/null +++ b/src/serial_get_cts.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetCts(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(h, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kGetModemStatusError); + } + + return (modem_status & MS_CTS_ON) ? 1 : 0; + } + +} // extern "C" diff --git a/src/serial_get_data_bits.cpp b/src/serial_get_data_bits.cpp new file mode 100644 index 0000000..53114ae --- /dev/null +++ b/src/serial_get_data_bits.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetDataBits(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + return static_cast(dcb.ByteSize); + } + +} // extern "C" diff --git a/src/serial_get_dcd.cpp b/src/serial_get_dcd.cpp new file mode 100644 index 0000000..808ed7e --- /dev/null +++ b/src/serial_get_dcd.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetDcd(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(h, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kGetModemStatusError); + } + + return (modem_status & MS_RLSD_ON) ? 1 : 0; + } + +} // extern "C" diff --git a/src/serial_get_dsr.cpp b/src/serial_get_dsr.cpp new file mode 100644 index 0000000..6ef9d09 --- /dev/null +++ b/src/serial_get_dsr.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetDsr(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(h, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kGetModemStatusError); + } + + return (modem_status & MS_DSR_ON) ? 1 : 0; + } + +} // extern "C" diff --git a/src/serial_get_flow_control.cpp b/src/serial_get_flow_control.cpp new file mode 100644 index 0000000..bef60c2 --- /dev/null +++ b/src/serial_get_flow_control.cpp @@ -0,0 +1,36 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetFlowControl(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + if (dcb.fOutxCtsFlow != 0 && dcb.fRtsControl == RTS_CONTROL_HANDSHAKE) + { + return 1; + } + if (dcb.fOutX != 0 && dcb.fInX != 0) + { + return 2; + } + return 0; + } + +} // extern "C" diff --git a/src/serial_get_parity.cpp b/src/serial_get_parity.cpp new file mode 100644 index 0000000..96c41f5 --- /dev/null +++ b/src/serial_get_parity.cpp @@ -0,0 +1,36 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetParity(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + switch (dcb.Parity) + { + case EVENPARITY: + return 1; + case ODDPARITY: + return 2; + default: + return 0; + } + } + +} // extern "C" diff --git a/src/serial_get_ri.cpp b/src/serial_get_ri.cpp new file mode 100644 index 0000000..782a531 --- /dev/null +++ b/src/serial_get_ri.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetRi(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(h, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kGetModemStatusError); + } + + return (modem_status & MS_RING_ON) ? 1 : 0; + } + +} // extern "C" diff --git a/src/serial_get_stop_bits.cpp b/src/serial_get_stop_bits.cpp new file mode 100644 index 0000000..91d497f --- /dev/null +++ b/src/serial_get_stop_bits.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetStopBits(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + return (dcb.StopBits == TWOSTOPBITS) ? 2 : 0; + } + +} // extern "C" diff --git a/src/serial_monitor_ports.cpp b/src/serial_monitor_ports.cpp new file mode 100644 index 0000000..bcacb27 --- /dev/null +++ b/src/serial_monitor_ports.cpp @@ -0,0 +1,134 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +std::mutex g_mutex; +std::thread g_thread; +HANDLE g_stop_event = nullptr; +std::atomic g_running{false}; + +auto enumerateComPorts() -> std::set +{ + std::set ports; + std::vector buffer(65536); + const DWORD len = QueryDosDeviceA(nullptr, buffer.data(), static_cast(buffer.size())); + if (len == 0) + { + return ports; + } + + const char *ptr = buffer.data(); + while (*ptr != '\0') + { + std::string name(ptr); + if (name.rfind("COM", 0) == 0 && name.size() >= 4) + { + ports.insert(name); + } + ptr += name.size() + 1; + } + return ports; +} + +void monitorLoop(void (*callback)(int event, const char *port)) +{ + std::set previous = enumerateComPorts(); + + while (g_running.load(std::memory_order_relaxed)) + { + const DWORD wait = WaitForSingleObject(g_stop_event, 500); + if (wait == WAIT_OBJECT_0) + { + break; + } + + std::set current = enumerateComPorts(); + + for (const auto &p : current) + { + if (previous.find(p) == previous.end()) + { + callback(1, p.c_str()); + } + } + + for (const auto &p : previous) + { + if (current.find(p) == current.end()) + { + callback(0, p.c_str()); + } + } + + previous = std::move(current); + } +} + +void stopMonitor() +{ + if (!g_running.load(std::memory_order_relaxed)) + { + return; + } + + g_running.store(false, std::memory_order_relaxed); + + if (g_stop_event != nullptr) + { + SetEvent(g_stop_event); + } + + if (g_thread.joinable()) + { + g_thread.join(); + } + + if (g_stop_event != nullptr) + { + CloseHandle(g_stop_event); + g_stop_event = nullptr; + } +} + +} // namespace + +extern "C" +{ + + MODULE_API auto serialMonitorPorts(void (*callback_fn)(int event, const char *port), + ErrorCallbackT error_callback) -> int + { + std::lock_guard lock(g_mutex); + + stopMonitor(); + + if (callback_fn == nullptr) + { + return 0; + } + + g_stop_event = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (g_stop_event == nullptr) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kMonitorError); + } + + g_running.store(true, std::memory_order_relaxed); + g_thread = std::thread(monitorLoop, callback_fn); + + return 0; + } + +} // extern "C" diff --git a/src/serial_send_break.cpp b/src/serial_send_break.cpp new file mode 100644 index 0000000..db43e1f --- /dev/null +++ b/src/serial_send_break.cpp @@ -0,0 +1,41 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSendBreak(int64_t handle, int duration_ms, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + if (duration_ms <= 0) + { + return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSendBreakError, + "Break duration must be > 0"); + } + + if (SetCommBreak(h) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kSendBreakError); + } + + Sleep(static_cast(duration_ms)); + + if (ClearCommBreak(h) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kSendBreakError); + } + + return 0; + } + +} // extern "C" diff --git a/src/serial_set_baudrate.cpp b/src/serial_set_baudrate.cpp new file mode 100644 index 0000000..071655b --- /dev/null +++ b/src/serial_set_baudrate.cpp @@ -0,0 +1,42 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetBaudrate(int64_t handle, int baudrate, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + if (baudrate < 300) + { + return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetBaudrateError, + "Invalid baudrate: must be >= 300"); + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + dcb.BaudRate = static_cast(baudrate); + + if (SetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kSetBaudrateError); + } + + return 0; + } + +} // extern "C" diff --git a/src/serial_set_data_bits.cpp b/src/serial_set_data_bits.cpp new file mode 100644 index 0000000..62482c4 --- /dev/null +++ b/src/serial_set_data_bits.cpp @@ -0,0 +1,42 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetDataBits(int64_t handle, int data_bits, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + if (data_bits < 5 || data_bits > 8) + { + return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetDataBitsError, + "Invalid data bits: must be 5-8"); + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + dcb.ByteSize = static_cast(data_bits); + + if (SetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kSetDataBitsError); + } + + return 0; + } + +} // extern "C" diff --git a/src/serial_set_dtr.cpp b/src/serial_set_dtr.cpp new file mode 100644 index 0000000..a8aeb5d --- /dev/null +++ b/src/serial_set_dtr.cpp @@ -0,0 +1,27 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetDtr(int64_t handle, int state, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + const DWORD func = state ? SETDTR : CLRDTR; + if (EscapeCommFunction(h, func) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kSetDtrError); + } + + return 0; + } + +} // extern "C" diff --git a/src/serial_set_flow_control.cpp b/src/serial_set_flow_control.cpp new file mode 100644 index 0000000..3e28fd1 --- /dev/null +++ b/src/serial_set_flow_control.cpp @@ -0,0 +1,63 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetFlowControl(int64_t handle, int mode, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + if (mode < 0 || mode > 2) + { + return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetFlowControlError, + "Invalid flow control mode: must be 0, 1, or 2"); + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + dcb.fOutxCtsFlow = FALSE; + dcb.fRtsControl = RTS_CONTROL_ENABLE; + dcb.fOutX = FALSE; + dcb.fInX = FALSE; + + switch (mode) + { + case 1: + dcb.fOutxCtsFlow = TRUE; + dcb.fRtsControl = RTS_CONTROL_HANDSHAKE; + break; + case 2: + dcb.fOutX = TRUE; + dcb.fInX = TRUE; + dcb.XonChar = 0x11; + dcb.XoffChar = 0x13; + dcb.XonLim = 2048; + dcb.XoffLim = 512; + break; + default: + break; + } + + if (SetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kSetFlowControlError); + } + + return 0; + } + +} // extern "C" diff --git a/src/serial_set_parity.cpp b/src/serial_set_parity.cpp new file mode 100644 index 0000000..542cd54 --- /dev/null +++ b/src/serial_set_parity.cpp @@ -0,0 +1,54 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetParity(int64_t handle, int parity, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + BYTE win_parity = NOPARITY; + switch (parity) + { + case 0: + win_parity = NOPARITY; + break; + case 1: + win_parity = EVENPARITY; + break; + case 2: + win_parity = ODDPARITY; + break; + default: + return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetParityError, + "Invalid parity: must be 0, 1, or 2"); + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + dcb.Parity = win_parity; + dcb.fParity = (parity != 0) ? TRUE : FALSE; + + if (SetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kSetParityError); + } + + return 0; + } + +} // extern "C" diff --git a/src/serial_set_rts.cpp b/src/serial_set_rts.cpp new file mode 100644 index 0000000..b94dca8 --- /dev/null +++ b/src/serial_set_rts.cpp @@ -0,0 +1,27 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetRts(int64_t handle, int state, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + const DWORD func = state ? SETRTS : CLRRTS; + if (EscapeCommFunction(h, func) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kSetRtsError); + } + + return 0; + } + +} // extern "C" diff --git a/src/serial_set_stop_bits.cpp b/src/serial_set_stop_bits.cpp new file mode 100644 index 0000000..b316a3d --- /dev/null +++ b/src/serial_set_stop_bits.cpp @@ -0,0 +1,42 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetStopBits(int64_t handle, int stop_bits, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + if (stop_bits != 0 && stop_bits != 1 && stop_bits != 2) + { + return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetStopBitsError, + "Invalid stop bits: must be 0, 1, or 2"); + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + dcb.StopBits = (stop_bits == 2) ? TWOSTOPBITS : ONESTOPBIT; + + if (SetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kSetStopBitsError); + } + + return 0; + } + +} // extern "C" From 758dd75068676e5b64c388833f430188b6cba2bd Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 26 Aug 2026 21:37:15 +0200 Subject: [PATCH 02/18] feat: bring Windows bindings to Linux feature parity --- .github/workflows/build_binary.yml | 215 +++++++++++++++------ .github/workflows/deno_tests.yml | 2 +- .github/workflows/publish_jsr.yml | 15 +- .github/workflows/test_unit_cpp.yml | 6 +- CMakeLists.txt | 161 +++++++++++++--- CMakePresets.json | 18 ++ README.md | 80 +++++++- jsr/README.md | 8 + jsr/jsr.json | 1 + scripts/verify_release_binary.ps1 | 95 ++++++++++ src/detail/common_types.hpp | 15 ++ src/detail/handle_state.hpp | 229 +++++++++++++++++++++++ src/detail/io_impl.hpp | 277 ++++++++++++++++++++++++++++ src/detail/win32_helpers.hpp | 141 +++++++------- src/get_version.cpp | 4 + src/serial_abort_read.cpp | 22 +++ src/serial_abort_write.cpp | 22 +++ src/serial_clear_buffer_in.cpp | 27 +++ src/serial_clear_buffer_out.cpp | 27 +++ src/serial_close.cpp | 11 +- src/serial_close.test.cpp | 18 +- src/serial_drain.cpp | 27 +++ src/serial_extended_api.test.cpp | 101 ++++++++++ src/serial_get_baudrate.cpp | 3 +- src/serial_get_cts.cpp | 2 +- src/serial_get_data_bits.cpp | 3 +- src/serial_get_dcd.cpp | 2 +- src/serial_get_dsr.cpp | 2 +- src/serial_get_flow_control.cpp | 3 +- src/serial_get_parity.cpp | 3 +- src/serial_get_ri.cpp | 2 +- src/serial_get_stop_bits.cpp | 3 +- src/serial_in_bytes_total.cpp | 20 ++ src/serial_in_bytes_waiting.cpp | 28 +++ src/serial_list_ports.cpp | 214 +++++++++++++++++++++ src/serial_monitor_ports.cpp | 143 +++++++------- src/serial_open.cpp | 103 ++++------- src/serial_open.test.cpp | 116 ++++++------ src/serial_out_bytes_total.cpp | 20 ++ src/serial_out_bytes_waiting.cpp | 31 ++++ src/serial_read.cpp | 216 +--------------------- src/serial_read.test.cpp | 18 +- src/serial_read_line.cpp | 16 ++ src/serial_read_until.cpp | 23 +++ src/serial_read_until_sequence.cpp | 34 ++++ src/serial_send_break.cpp | 8 +- src/serial_set_baudrate.cpp | 8 +- src/serial_set_data_bits.cpp | 8 +- src/serial_set_dtr.cpp | 3 +- src/serial_set_error_callback.cpp | 13 ++ src/serial_set_flow_control.cpp | 10 +- src/serial_set_parity.cpp | 8 +- src/serial_set_read_callback.cpp | 13 ++ src/serial_set_rts.cpp | 3 +- src/serial_set_stop_bits.cpp | 8 +- src/serial_set_write_callback.cpp | 13 ++ src/serial_write.cpp | 90 +-------- src/serial_write.test.cpp | 20 +- src/test_helpers/error_capture.hpp | 2 +- tests/serial_arduino.test.cpp | 8 +- 60 files changed, 2023 insertions(+), 719 deletions(-) create mode 100644 scripts/verify_release_binary.ps1 create mode 100644 src/detail/common_types.hpp create mode 100644 src/detail/handle_state.hpp create mode 100644 src/detail/io_impl.hpp create mode 100644 src/get_version.cpp create mode 100644 src/serial_abort_read.cpp create mode 100644 src/serial_abort_write.cpp create mode 100644 src/serial_clear_buffer_in.cpp create mode 100644 src/serial_clear_buffer_out.cpp create mode 100644 src/serial_drain.cpp create mode 100644 src/serial_extended_api.test.cpp create mode 100644 src/serial_in_bytes_total.cpp create mode 100644 src/serial_in_bytes_waiting.cpp create mode 100644 src/serial_list_ports.cpp create mode 100644 src/serial_out_bytes_total.cpp create mode 100644 src/serial_out_bytes_waiting.cpp create mode 100644 src/serial_read_line.cpp create mode 100644 src/serial_read_until.cpp create mode 100644 src/serial_read_until_sequence.cpp create mode 100644 src/serial_set_error_callback.cpp create mode 100644 src/serial_set_read_callback.cpp create mode 100644 src/serial_set_write_callback.cpp diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index 6f70fc2..87bd8aa 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -1,22 +1,22 @@ name: 'Build Binary' -description: | - This workflow builds the binary files for Windows. The build binaries are stored as artifact - and may be reused by other workflows. on: push: - branches: [ 'main' ] - tags: [ '*' ] - + branches: ['main'] + tags: ['*'] pull_request: - branches: [ '*' ] + branches: ['*'] jobs: - build-binary: - name: 'Build binary' - runs-on: windows-latest - permissions: - contents: write + generate-metadata: + name: 'Generate FFI metadata (x86_64-windows-msvc)' + runs-on: windows-2025 + env: + ASTREIN_VERSION: '1.2.0' + ASTREIN_SHA256: 'd8a4984dca05175a6523530bef5756ef9bf87d0e4e6d58981bee3b9980544149' + outputs: + package_version: ${{ steps.version.outputs.PACKAGE_VERSION }} + is_valid_package_version: ${{ steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION }} steps: - name: 'Checkout repository' @@ -29,87 +29,184 @@ jobs: with: cmake-version: '3.31.x' - - name: 'Configure CMake' + - name: 'Download ASTrein' + shell: pwsh run: | - cmake --preset windows-vs-release + $archive = Join-Path $env:RUNNER_TEMP 'astrein-windows-x86_64.zip' + $destination = Join-Path $env:RUNNER_TEMP 'astrein-package' + Invoke-WebRequest ` + -Uri "https://github.com/Katze719/ASTrein/releases/download/v$env:ASTREIN_VERSION/astrein-windows-x86_64.zip" ` + -OutFile $archive + + $actualHash = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant() + if ($actualHash -ne $env:ASTREIN_SHA256) { + throw "ASTrein checksum mismatch: expected $env:ASTREIN_SHA256, got $actualHash" + } - - name: 'Build' - id: build + Expand-Archive -Path $archive -DestinationPath $destination + $astrein = Join-Path $destination 'astrein/bin/astrein.exe' + & $astrein --version + "ASTREIN_EXECUTABLE=$astrein" >> $env:GITHUB_ENV + + - name: 'Configure metadata context' + shell: pwsh run: | - cmake --build --preset windows-vs-release --config Release --target cpp_bindings_windows + cmake -S . -B build/ffi -G Ninja ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_CXX_COMPILER=clang-cl ` + -DCPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT=ON ` + "-DCPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE=$env:ASTREIN_EXECUTABLE" ` + "-DCPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT=$env:GITHUB_WORKSPACE/dist/ffi/x86_64.ffi.json" + + - name: 'Generate metadata' + run: | + cmake --build build/ffi --target cpp_bindings_windows_ffi_json - - name: 'Set PACKAGE_VERSION from env.bat' + - name: 'Set package version' id: version shell: pwsh run: | - $content = Get-Content -Raw build/env.bat - echo "$content" - $m = [regex]::Match($content, 'PACKAGE_VERSION=(.+)') - $v = if ($m.Success) { $m.Groups[1].Value.Trim() } else { '0.0.0' } - echo "PACKAGE_VERSION=$v" >> $env:GITHUB_OUTPUT + $content = Get-Content -Raw build/ffi/env.bat + $match = [regex]::Match($content, 'PACKAGE_VERSION=(.+)') + $version = if ($match.Success) { $match.Groups[1].Value.Trim() } else { '0.0.0' } + "PACKAGE_VERSION=$version" >> $env:GITHUB_OUTPUT - - name: 'Copy DLL to stable path and upload artifact' + - name: 'Check package version' + id: check-tag shell: pwsh + env: + PACKAGE_VERSION: ${{ steps.version.outputs.PACKAGE_VERSION }} run: | - $dll = Get-ChildItem -Recurse -Path build -Filter cpp_bindings_windows.dll -File | Select-Object -First 1 - if (-not $dll) { throw "cpp_bindings_windows.dll not found under build/" } - New-Item -ItemType Directory -Force -Path build/out | Out-Null - Copy-Item -Force $dll.FullName -Destination build/out/cpp_bindings_windows.dll + if ($env:PACKAGE_VERSION -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(alpha|rc|beta|experimental)\.[1-9]\d*)?$') { + "IS_VALID_PACKAGE_VERSION=true" >> $env:GITHUB_OUTPUT + } else { + "IS_VALID_PACKAGE_VERSION=false" >> $env:GITHUB_OUTPUT + } - - name: 'Upload artifacts' + - name: 'Upload FFI metadata' uses: actions/upload-artifact@v4 with: if-no-files-found: error - name: cpp_bindings_windows - path: build/out/cpp_bindings_windows.dll + name: cpp-bindings-windows-ffi + path: dist/ffi/x86_64.ffi.json - - name: 'Check tag' - id: check-tag - shell: pwsh - env: - PACKAGE_VERSION: ${{ steps.version.outputs.PACKAGE_VERSION }} + build-binary: + name: 'Build x86_64-windows-msvc' + runs-on: windows-2025 + steps: + - name: 'Checkout repository' + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 'Setup CMake' + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: '3.31.x' + + - name: 'Configure release' run: | - $v = $env:PACKAGE_VERSION - if ($v -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(alpha|rc|beta|experimental)\.[1-9]\d*)?$') { - echo "IS_VALID_PACKAGE_VERSION=true" >> $env:GITHUB_OUTPUT - } else { - echo "IS_VALID_PACKAGE_VERSION=false" >> $env:GITHUB_OUTPUT + cmake --preset windows-vs-release ` + -DCPP_BINDINGS_WINDOWS_STATIC_MSVC_RUNTIME=ON + + - name: 'Build and test' + run: | + cmake --build --preset windows-vs-release --config Release ` + --target cpp_bindings_windows cpp_bindings_windows_tests ` + --parallel 4 + ctest --test-dir build -C Release ` + --output-on-failure ` + --output-junit test-report.xml + + - name: 'Stage and verify binary' + shell: pwsh + run: | + $dll = Get-ChildItem -Recurse -Path build -Filter cpp_bindings_windows.dll -File | + Select-Object -First 1 + if (-not $dll) { + throw 'cpp_bindings_windows.dll not found under build/' } - - name: 'Create GitHub Release' - if: github.ref_type == 'tag' && steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION == 'true' - uses: softprops/action-gh-release@v2 + New-Item -ItemType Directory -Force -Path dist/x86_64-windows-msvc | Out-Null + Copy-Item -Force $dll.FullName dist/x86_64-windows-msvc/cpp_bindings_windows.dll + ./scripts/verify_release_binary.ps1 ` + dist/x86_64-windows-msvc/cpp_bindings_windows.dll ` + x86_64-windows-msvc + + - name: 'Upload test report' + if: always() + uses: actions/upload-artifact@v4 with: - name: 'v${{ steps.version.outputs.PACKAGE_VERSION }}' - tag_name: ${{ github.ref_name }} - generate_release_notes: true - files: build/out/cpp_bindings_windows.dll + if-no-files-found: warn + name: test-report-x86_64-windows-msvc + path: build/test-report.xml - outputs: - package_version: ${{ steps.version.outputs.PACKAGE_VERSION }} - is_valid_package_version: ${{ steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION }} + - name: 'Upload binary' + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: cpp-bindings-windows-x86_64-windows-msvc + path: dist/x86_64-windows-msvc/cpp_bindings_windows.dll test-unit-cpp: name: 'Run: Test Unit C++' - needs: [ 'build-binary' ] + needs: ['build-binary'] uses: './.github/workflows/test_unit_cpp.yml' with: - artifact-name: cpp_bindings_windows - + artifact-name: cpp-bindings-windows-x86_64-windows-msvc permissions: contents: read checks: write + create-release: + name: 'Create GitHub release' + needs: ['generate-metadata', 'build-binary', 'test-unit-cpp'] + if: github.ref_type == 'tag' && needs.generate-metadata.outputs.is_valid_package_version == 'true' + runs-on: windows-2025 + permissions: + contents: write + + steps: + - name: 'Download binary' + uses: actions/download-artifact@v4 + with: + name: cpp-bindings-windows-x86_64-windows-msvc + path: release/binary + + - name: 'Download FFI metadata' + uses: actions/download-artifact@v4 + with: + name: cpp-bindings-windows-ffi + path: release/ffi + + - name: 'Name release assets' + shell: pwsh + run: | + Move-Item release/binary/cpp_bindings_windows.dll ` + release/cpp_bindings_windows-x86_64-windows-msvc.dll + Move-Item release/ffi/x86_64.ffi.json ` + release/cpp_bindings_windows-x86_64-windows-msvc.ffi.json + + - name: 'Create GitHub release' + uses: softprops/action-gh-release@v2 + with: + name: 'v${{ needs.generate-metadata.outputs.package_version }}' + tag_name: ${{ github.ref_name }} + generate_release_notes: true + files: | + release/cpp_bindings_windows-x86_64-windows-msvc.dll + release/cpp_bindings_windows-x86_64-windows-msvc.ffi.json + publish-jsr: name: 'Run: Publish JSR' - needs: [ 'build-binary', 'test-unit-cpp' ] + needs: ['generate-metadata', 'build-binary', 'test-unit-cpp'] uses: './.github/workflows/publish_jsr.yml' with: - publish: ${{ needs.build-binary.outputs.is_valid_package_version == 'true' }} - version: ${{ needs.build-binary.outputs.package_version }} - artifact-name: cpp_bindings_windows - + publish: ${{ needs.generate-metadata.outputs.is_valid_package_version == 'true' }} + version: ${{ needs.generate-metadata.outputs.package_version }} + artifact-name: cpp-bindings-windows-x86_64-windows-msvc + ffi-artifact-name: cpp-bindings-windows-ffi permissions: contents: read id-token: write diff --git a/.github/workflows/deno_tests.yml b/.github/workflows/deno_tests.yml index 471e293..9207b97 100644 --- a/.github/workflows/deno_tests.yml +++ b/.github/workflows/deno_tests.yml @@ -34,7 +34,7 @@ jobs: - name: Build run: | - cmake --build --preset windows-vs-release --config Release + cmake --build --preset windows-vs-release --config Release --target cpp_bindings_windows - name: Run Deno integration tests working-directory: integration_tests diff --git a/.github/workflows/publish_jsr.yml b/.github/workflows/publish_jsr.yml index 7ca8bcd..ef799f2 100644 --- a/.github/workflows/publish_jsr.yml +++ b/.github/workflows/publish_jsr.yml @@ -21,6 +21,11 @@ on: required: true type: string + ffi-artifact-name: + description: 'Name of the FFI metadata artifact' + required: true + type: string + permissions: contents: read id-token: write @@ -46,15 +51,23 @@ jobs: name: ${{ inputs.artifact-name }} path: artifacts + - name: 'Download FFI metadata' + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.ffi-artifact-name }} + path: artifacts/ffi + - name: 'Prepare files for JSR' shell: pwsh run: | New-Item -ItemType Directory -Force -Path ./jsr/bin | Out-Null Copy-Item -Force ./artifacts/cpp_bindings_windows.dll ./jsr/bin/x86_64.dll + Copy-Item -Force ./artifacts/ffi/x86_64.ffi.json ./jsr/bin/x86_64.ffi.json + Copy-Item -Force ./LICENSE ./jsr/LICENSE ./jsr/scripts/binary_to_json.ps1 ` artifacts/cpp_bindings_windows.dll ` - jsr/bin/x86_64.json windows-x86_64 + jsr/bin/x86_64.json x86_64-windows-msvc ./jsr/scripts/set_version.ps1 ` jsr/jsr.json ` diff --git a/.github/workflows/test_unit_cpp.yml b/.github/workflows/test_unit_cpp.yml index 78e7683..4ddf4be 100644 --- a/.github/workflows/test_unit_cpp.yml +++ b/.github/workflows/test_unit_cpp.yml @@ -14,13 +14,15 @@ on: jobs: test-unit-cpp: name: 'Test Unit C++' - runs-on: windows-latest + runs-on: windows-2025 env: TEST_REPORT_NAME: 'test_report.xml' steps: - name: 'Checkout repository' uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: 'Download artifact' uses: actions/download-artifact@v4 @@ -39,7 +41,7 @@ jobs: - name: 'Build tests' run: | - cmake --build --preset windows-vs-release --config Release + cmake --build --preset windows-vs-release --config Release --target cpp_bindings_windows_tests - name: 'Copy library artifact next to test exe' shell: pwsh diff --git a/CMakeLists.txt b/CMakeLists.txt index 672c1ca..50c1e36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,5 @@ cmake_minimum_required(VERSION 3.30) -# Windows-only project -if(NOT WIN32) - message(FATAL_ERROR "cpp-bindings-windows can only be built on Windows.") -endif() - # Export compile commands to root directory set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -28,25 +23,135 @@ project( LANGUAGES CXX ) +# Check after project() so cross-compilation toolchains can initialize WIN32. +if(NOT WIN32) + message(FATAL_ERROR "cpp-bindings-windows can only be built for Windows.") +endif() + file(WRITE "${CMAKE_BINARY_DIR}/env.bat" "set PACKAGE_VERSION=${GIT_DESCRIBE_NO_V}\n") # Set C++ standard -set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -# Enable C++23 module support -set(CMAKE_CXX_MODULE_STD 23) +# Enable C++26 module support +set(CMAKE_CXX_MODULE_STD 26) set(CMAKE_CXX_MODULE_EXTENSIONS OFF) +option( + CPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT + "Enable ASTrein JSON export for the cpp-core FFI headers" + OFF +) +option( + CPP_BINDINGS_WINDOWS_STATIC_MSVC_RUNTIME + "Statically link the MSVC runtime into the shared library" + OFF +) +set( + CPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE + "" + CACHE FILEPATH + "Path to the ASTrein executable used for FFI JSON export" +) +set( + CPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT + "${CMAKE_BINARY_DIR}/cpp_bindings_windows.ffi.json" + CACHE FILEPATH + "Output path for the generated cpp-core FFI API metadata" +) + CPMAddPackage( NAME cpp_core GITHUB_REPOSITORY Serial-IO/cpp-core - GIT_TAG v1.1.0 + GIT_TAG v2.0.1 OPTIONS "CMAKE_EXPORT_COMPILE_COMMANDS OFF" ) +if(CPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT) + if(CPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE) + set(_cpp_bindings_windows_astrein "${CPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE}") + else() + find_program(_cpp_bindings_windows_astrein NAMES astrein astrein.exe) + endif() + + if(NOT _cpp_bindings_windows_astrein) + message( + FATAL_ERROR + "CPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT=ON requires ASTrein. " + "Install astrein or set CPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE." + ) + endif() + + file( + GLOB_RECURSE _cpp_bindings_windows_ffi_headers + CONFIGURE_DEPENDS + "${cpp_core_SOURCE_DIR}/include/*.h" + "${cpp_core_SOURCE_DIR}/include/*.hpp" + ) + set(_cpp_bindings_windows_ffi_wrapper "${CMAKE_BINARY_DIR}/ffi.cpp") + get_filename_component( + _cpp_bindings_windows_ffi_output_dir + "${CPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT}" + DIRECTORY + ) + + file( + GENERATE + OUTPUT "${_cpp_bindings_windows_ffi_wrapper}" + CONTENT "#include \n" + ) + + add_library( + cpp_bindings_windows_ffi_ast_context + OBJECT + EXCLUDE_FROM_ALL + "${_cpp_bindings_windows_ffi_wrapper}" + ) + target_include_directories( + cpp_bindings_windows_ffi_ast_context + PRIVATE + "${cpp_core_SOURCE_DIR}/include" + ) + target_compile_definitions( + cpp_bindings_windows_ffi_ast_context + PRIVATE + cpp_bindings_windows_EXPORTS + ) + target_compile_features(cpp_bindings_windows_ffi_ast_context PRIVATE cxx_std_26) + + add_custom_command( + OUTPUT "${CPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT}" + COMMAND + ${CMAKE_COMMAND} -E make_directory + "${_cpp_bindings_windows_ffi_output_dir}" + COMMAND + "${_cpp_bindings_windows_astrein}" + --ffi + --compile-commands "${CMAKE_BINARY_DIR}/compile_commands.json" + --require-c-linkage + --require-default-visibility + --public-header "cpp_core/serial.h" + --api-root "${cpp_core_SOURCE_DIR}/include" + --output "${CPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT}" + "${_cpp_bindings_windows_ffi_wrapper}" + DEPENDS + "${_cpp_bindings_windows_astrein}" + "${CMAKE_BINARY_DIR}/compile_commands.json" + "${_cpp_bindings_windows_ffi_wrapper}" + ${_cpp_bindings_windows_ffi_headers} + COMMENT "Exporting cpp-core FFI API metadata with ASTrein" + VERBATIM + ) + + add_custom_target( + cpp_bindings_windows_ffi_json + DEPENDS "${CPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT}" + ) +endif() + # Generate version information generate_git_version( OUTPUT_DIR ${CMAKE_BINARY_DIR}/generated @@ -67,7 +172,7 @@ include(CTest) enable_testing() # Library sources: src/*.cpp only, exclude *.test.cpp and test_helpers/ -file(GLOB_RECURSE LIB_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") +file(GLOB_RECURSE LIB_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") list(FILTER LIB_SOURCES EXCLUDE REGEX ".*\\.test\\.cpp$") list(FILTER LIB_SOURCES EXCLUDE REGEX ".*/test_helpers/.*") @@ -84,27 +189,32 @@ set_target_properties( target_include_directories( cpp_bindings_windows - PUBLIC + PRIVATE + $ $ + $ ) target_link_libraries( cpp_bindings_windows - PUBLIC + PRIVATE cpp_core::cpp_core + setupapi ) -# cpp-core's `MODULE_API` macro checks for `cpp_windows_bindings_EXPORTS` on Windows. -# Our target is named `cpp_bindings_windows`, so CMake would otherwise define -# `cpp_bindings_windows_EXPORTS` and `MODULE_API` would resolve to dllimport. -target_compile_definitions(cpp_bindings_windows PRIVATE cpp_windows_bindings_EXPORTS) +target_compile_features(cpp_bindings_windows PRIVATE cxx_std_26) -target_compile_features(cpp_bindings_windows PUBLIC cxx_std_23) +if(MSVC AND CPP_BINDINGS_WINDOWS_STATIC_MSVC_RUNTIME) + set_property( + TARGET cpp_bindings_windows + PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" + ) +endif() # Test sources: src/*.test.cpp, tests/*.test.cpp, src/test_helpers/*.cpp (helpers excluded from lib) -file(GLOB SRC_UNIT_TESTS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.test.cpp") -file(GLOB TESTS_INTEGRATION "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.test.cpp") -file(GLOB TEST_HELPER_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/test_helpers/*.cpp") +file(GLOB SRC_UNIT_TESTS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.test.cpp") +file(GLOB TESTS_INTEGRATION CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.test.cpp") +file(GLOB TEST_HELPER_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/test_helpers/*.cpp") set(TEST_SOURCES ${SRC_UNIT_TESTS} ${TESTS_INTEGRATION} ${TEST_HELPER_SOURCES}) if(TEST_SOURCES) @@ -115,6 +225,7 @@ if(TEST_SOURCES) PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_BINARY_DIR}/generated + ${cpp_core_SOURCE_DIR}/include ) target_link_libraries( @@ -125,10 +236,14 @@ if(TEST_SOURCES) GTest::gtest_main ) - target_compile_features(cpp_bindings_windows_tests PRIVATE cxx_std_23) + target_compile_features(cpp_bindings_windows_tests PRIVATE cxx_std_26) include(GoogleTest) - gtest_discover_tests(cpp_bindings_windows_tests) + if(CMAKE_CROSSCOMPILING) + gtest_add_tests(TARGET cpp_bindings_windows_tests) + else() + gtest_discover_tests(cpp_bindings_windows_tests) + endif() endif() include(GNUInstallDirs) @@ -159,5 +274,3 @@ if(CMAKE_EXPORT_COMPILE_COMMANDS AND EXISTS "${CMAKE_BINARY_DIR}/compile_command COMMENT "Copying compile_commands.json to project root" ) endif() - - diff --git a/CMakePresets.json b/CMakePresets.json index 55d79ba..23ea253 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -35,6 +35,20 @@ "CMAKE_C_COMPILER": "cl", "CMAKE_CXX_COMPILER": "cl" } + }, + { + "name": "windows-mingw-release", + "displayName": "Windows MinGW x86-64 Release", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/mingw", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_SYSTEM_NAME": "Windows", + "CMAKE_SYSTEM_PROCESSOR": "x86_64", + "CMAKE_C_COMPILER": "x86_64-w64-mingw32-gcc", + "CMAKE_CXX_COMPILER": "x86_64-w64-mingw32-g++", + "CMAKE_RC_COMPILER": "x86_64-w64-mingw32-windres" + } } ], "buildPresets": [ @@ -49,6 +63,10 @@ { "name": "windows-ninja-msvc", "configurePreset": "windows-ninja-msvc" + }, + { + "name": "windows-mingw-release", + "configurePreset": "windows-mingw-release" } ] } diff --git a/README.md b/README.md index 0bd5b40..d16ace9 100644 --- a/README.md +++ b/README.md @@ -1 +1,79 @@ -# cpp-windows-bindings +# C++ Bindings for Windows + +[![Build](https://github.com/Serial-IO/cpp-bindings-windows/actions/workflows/build_binary.yml/badge.svg)](https://github.com/Serial-IO/cpp-bindings-windows/actions/workflows/build_binary.yml) +[![JSR](https://jsr.io/badges/@serial/cpp-bindings-windows)](https://jsr.io/@serial/cpp-bindings-windows) + +Windows DLL for serial communication. It implements the +[`cpp-core`](https://github.com/Serial-IO/cpp-core) interface and provides functions for discovering, monitoring, +opening, configuring, reading from, and writing to serial ports. + +## Requirements + +- CMake 3.30 or newer +- Git +- A compiler with sufficient C++26 support +- One of: + - Windows with Visual Studio 2022 and the C++ workload + - Linux with an x86-64 MinGW-w64 toolchain for cross-compilation + +CMake downloads `cpp-core` and GoogleTest automatically during configuration. + +## Build on Windows + +```powershell +git clone https://github.com/Serial-IO/cpp-bindings-windows.git +cd cpp-bindings-windows +cmake --preset windows-vs-release +cmake --build --preset windows-vs-release --config Release --target cpp_bindings_windows +``` + +The DLL is written below `build/Release/`. + +Official release and JSR artifacts currently target `x86_64-windows-msvc`. +Release DLLs statically include the MSVC runtime and expose the complete C API +described by `cpp-core` 2.0.1. + +## Cross-compile with MinGW + +The MinGW preset provides a local compile and link check from Linux: + +```sh +cmake --preset windows-mingw-release +cmake --build --preset windows-mingw-release \ + --target cpp_bindings_windows cpp_bindings_windows_tests +``` + +The DLL and test executable are written to `build/mingw/`. The tests must be +run on Windows (or in a compatible Windows runtime); cross-compilation alone +does not execute them. + +## Tests + +Build and run the C++ suite on Windows: + +```powershell +cmake --build --preset windows-vs-release --config Release --target cpp_bindings_windows_tests +ctest --test-dir build -C Release --output-on-failure +``` + +Tests that require a serial device use `SERIAL_TEST_PORT` and are skipped when +no suitable device is available. + +The optional Deno FFI smoke tests require Deno 2 and a built DLL: + +```powershell +cd integration_tests +deno task test +``` + +## FFI metadata + +Release and JSR packages include `x86_64-windows-msvc` API metadata generated +from the public `cpp-core` headers with +[ASTrein](https://github.com/Katze719/ASTrein). It describes exported symbols, +types, callbacks, default values, and API documentation for downstream FFI +adapter generators. + +## License + +This project is licensed under the [GNU Lesser General Public License v3.0](LICENSE). diff --git a/jsr/README.md b/jsr/README.md index e4b7be4..30909e7 100644 --- a/jsr/README.md +++ b/jsr/README.md @@ -5,6 +5,14 @@ Binaries are provided as a [package on JSR](https://jsr.io/@serial/cpp-bindings-windows). They are serialized as a base64 string inside the JSON file. +The package currently contains the `x86_64-windows-msvc` DLL. The release DLL +statically includes the MSVC runtime. + +It also includes cpp-core FFI API metadata generated with +[ASTrein](https://github.com/Katze719/ASTrein) at `bin/x86_64.ffi.json`. +It describes the exported C symbols, parameter and return types, callbacks, +default values, and API documentation used by downstream FFI adapter generators. + This package is primarily intended as a dependency for [`@serial/serial`](https://jsr.io/@serial/serial). However, it can also be used independently. diff --git a/jsr/jsr.json b/jsr/jsr.json index 1d0d7ab..05df6b7 100644 --- a/jsr/jsr.json +++ b/jsr/jsr.json @@ -9,6 +9,7 @@ "publish": { "include": [ "README.md", + "LICENSE", "jsr.json", "src/**", "bin/**" diff --git a/scripts/verify_release_binary.ps1 b/scripts/verify_release_binary.ps1 new file mode 100644 index 0000000..6ba93e9 --- /dev/null +++ b/scripts/verify_release_binary.ps1 @@ -0,0 +1,95 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Binary, + + [Parameter(Mandatory = $true)] + [ValidateSet("x86_64-windows-msvc")] + [string]$Target +) + +$ErrorActionPreference = "Stop" + +$binaryPath = (Resolve-Path $Binary).Path +$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio/Installer/vswhere.exe" +if (-not (Test-Path $vswhere)) { + throw "vswhere.exe was not found" +} + +$dumpbin = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` + -find "VC/Tools/MSVC/*/bin/Hostx64/x64/dumpbin.exe" | Select-Object -First 1 +if (-not $dumpbin) { + throw "dumpbin.exe was not found in the Visual Studio installation" +} + +$headers = (& $dumpbin /headers $binaryPath 2>&1) -join "`n" +if ($LASTEXITCODE -ne 0) { + throw "dumpbin /headers failed`n$headers" +} +if ($headers -notmatch "(?im)^\s*8664 machine \(x64\)") { + throw "Expected an x86-64 PE DLL for $Target" +} +if ($headers -notmatch "(?im)^\s*DLL\s*$") { + throw "Expected a PE DLL, not an executable" +} + +$dependents = (& $dumpbin /dependents $binaryPath 2>&1) -join "`n" +if ($LASTEXITCODE -ne 0) { + throw "dumpbin /dependents failed`n$dependents" +} +if ($dependents -match "(?i)(msvcp[^\s]*|vcruntime[^\s]*|ucrtbased)\.dll") { + throw "Release DLL unexpectedly depends on a dynamic MSVC C/C++ runtime`n$dependents" +} + +$exports = (& $dumpbin /exports $binaryPath 2>&1) -join "`n" +if ($LASTEXITCODE -ne 0) { + throw "dumpbin /exports failed`n$exports" +} + +$expectedExports = @( + "getVersion", + "serialAbortRead", + "serialAbortWrite", + "serialClearBufferIn", + "serialClearBufferOut", + "serialClose", + "serialDrain", + "serialGetBaudrate", + "serialGetCts", + "serialGetDataBits", + "serialGetDcd", + "serialGetDsr", + "serialGetFlowControl", + "serialGetParity", + "serialGetRi", + "serialGetStopBits", + "serialInBytesTotal", + "serialInBytesWaiting", + "serialListPorts", + "serialMonitorPorts", + "serialOpen", + "serialOutBytesTotal", + "serialOutBytesWaiting", + "serialRead", + "serialReadLine", + "serialReadUntil", + "serialReadUntilSequence", + "serialSendBreak", + "serialSetBaudrate", + "serialSetDataBits", + "serialSetDtr", + "serialSetErrorCallback", + "serialSetFlowControl", + "serialSetParity", + "serialSetReadCallback", + "serialSetRts", + "serialSetStopBits", + "serialSetWriteCallback", + "serialWrite" +) + +$missingExports = @($expectedExports | Where-Object { $exports -notmatch "(?m)\s$([regex]::Escape($_))\s*$" }) +if ($missingExports.Count -ne 0) { + throw "Release DLL is missing exports: $($missingExports -join ', ')" +} + +Write-Host "Verified $Target DLL: x86-64, static MSVC runtime, and $($expectedExports.Count) C API exports" diff --git a/src/detail/common_types.hpp b/src/detail/common_types.hpp new file mode 100644 index 0000000..e353a81 --- /dev/null +++ b/src/detail/common_types.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +#include + +namespace cpp_bindings_windows::detail +{ +using IoCallbackT = void (*)(int); +using StatusCodeValue = cpp_core::StatusCodeValue; +using cpp_core::StatusCode; + +inline std::atomic g_error_callback{nullptr}; +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/handle_state.hpp b/src/detail/handle_state.hpp new file mode 100644 index 0000000..d27ee59 --- /dev/null +++ b/src/detail/handle_state.hpp @@ -0,0 +1,229 @@ +#pragma once + +#include "common_types.hpp" + +#include +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace cpp_bindings_windows::detail +{ +enum class Operation +{ + kRead, + kWrite, +}; + +struct Win32HandleTraits +{ + using handle_type = HANDLE; // NOLINT(readability-identifier-naming) + + static constexpr auto invalid() noexcept -> handle_type + { + return nullptr; + } + + static auto close(handle_type handle) noexcept -> void + { + if (handle != nullptr && handle != INVALID_HANDLE_VALUE) + { + CloseHandle(handle); + } + } +}; + +using UniqueHandle = cpp_core::UniqueResource; + +struct HandleState +{ + std::atomic bytes_read_total{0}; + std::atomic bytes_written_total{0}; + std::atomic abort_read{false}; + std::atomic abort_write{false}; + std::mutex pending_io_mutex; + OVERLAPPED *pending_read = nullptr; + OVERLAPPED *pending_write = nullptr; +}; + +struct HandleContext +{ + HANDLE handle = nullptr; + std::shared_ptr state; +}; + +struct PendingIoStart +{ + BOOL completed = FALSE; + DWORD error = ERROR_SUCCESS; + bool aborted = false; +}; + +inline std::mutex g_handle_states_mutex; +inline std::unordered_map> g_handle_states; +inline std::atomic g_read_callback{nullptr}; +inline std::atomic g_write_callback{nullptr}; + +inline auto handleKey(HANDLE handle) -> std::uintptr_t +{ + return reinterpret_cast(handle); +} + +inline auto effectiveErrorCallback(ErrorCallbackT error_callback) -> ErrorCallbackT +{ + return error_callback != nullptr ? error_callback : g_error_callback.load(std::memory_order_acquire); +} + +inline auto ensureHandleState(HANDLE handle) -> std::shared_ptr +{ + std::lock_guard lock(g_handle_states_mutex); + auto &state = g_handle_states[handleKey(handle)]; + if (!state) + { + state = std::make_shared(); + } + return state; +} + +inline auto registerOpenedHandle(HANDLE handle) -> void +{ + (void)ensureHandleState(handle); +} + +inline auto removeHandleState(HANDLE handle) -> void +{ + std::lock_guard lock(g_handle_states_mutex); + g_handle_states.erase(handleKey(handle)); +} + +template +inline auto validateWin32Handle(int64_t handle, ErrorCallbackT error_callback, HANDLE *out_handle) -> ReturnType +{ + const auto callback = effectiveErrorCallback(error_callback); + if (handle <= 0) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + + if constexpr (sizeof(intptr_t) < sizeof(int64_t)) + { + if (handle > static_cast(std::numeric_limits::max())) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + } + + const auto native_handle = reinterpret_cast(static_cast(handle)); + if (native_handle == nullptr || native_handle == INVALID_HANDLE_VALUE) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + + *out_handle = native_handle; + return static_cast(StatusCode::kSuccess); +} + +template +inline auto acquireHandleContext(int64_t handle, ErrorCallbackT error_callback, HandleContext *out_context) + -> ReturnType +{ + HANDLE native_handle = nullptr; + const auto status = validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + out_context->handle = native_handle; + out_context->state = ensureHandleState(native_handle); + return static_cast(StatusCode::kSuccess); +} + +inline auto abortFlag(const std::shared_ptr &state, Operation operation) -> std::atomic & +{ + return operation == Operation::kRead ? state->abort_read : state->abort_write; +} + +inline auto pendingOperation(const std::shared_ptr &state, Operation operation) -> OVERLAPPED *& +{ + return operation == Operation::kRead ? state->pending_read : state->pending_write; +} + +inline auto requestAbort(HANDLE handle, const std::shared_ptr &state, Operation operation) -> void +{ + abortFlag(state, operation).store(true, std::memory_order_release); + + std::lock_guard lock(state->pending_io_mutex); + if (auto *pending = pendingOperation(state, operation); pending != nullptr) + { + (void)CancelIoEx(handle, pending); + } +} + +inline auto consumeAbort(const std::shared_ptr &state, Operation operation) -> bool +{ + return abortFlag(state, operation).exchange(false, std::memory_order_acq_rel); +} + +template +inline auto startPendingIo(const std::shared_ptr &state, Operation operation, OVERLAPPED *overlapped, + StartOperation &&start_operation) -> PendingIoStart +{ + std::lock_guard lock(state->pending_io_mutex); + if (consumeAbort(state, operation)) + { + return {.aborted = true}; + } + + pendingOperation(state, operation) = overlapped; + const BOOL completed = std::forward(start_operation)(); + return {.completed = completed, .error = completed != FALSE ? ERROR_SUCCESS : GetLastError()}; +} + +inline auto finishPendingIo(const std::shared_ptr &state, Operation operation, OVERLAPPED *overlapped) + -> bool +{ + std::lock_guard lock(state->pending_io_mutex); + auto &pending = pendingOperation(state, operation); + if (pending == overlapped) + { + pending = nullptr; + } + return consumeAbort(state, operation); +} + +inline auto noteBytesTransferred(const std::shared_ptr &state, Operation operation, int transferred_bytes) + -> void +{ + if (operation == Operation::kRead) + { + state->bytes_read_total.fetch_add(transferred_bytes, std::memory_order_relaxed); + if (const auto callback = g_read_callback.load(std::memory_order_acquire); callback != nullptr) + { + callback(transferred_bytes); + } + return; + } + + state->bytes_written_total.fetch_add(transferred_bytes, std::memory_order_relaxed); + if (const auto callback = g_write_callback.load(std::memory_order_acquire); callback != nullptr) + { + callback(transferred_bytes); + } +} + +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/io_impl.hpp b/src/detail/io_impl.hpp new file mode 100644 index 0000000..8aa11bb --- /dev/null +++ b/src/detail/io_impl.hpp @@ -0,0 +1,277 @@ +#pragma once + +#include "handle_state.hpp" +#include "win32_helpers.hpp" + +#include + +#include +#include +#include + +namespace cpp_bindings_windows::detail +{ +enum class IoOutcome +{ + kCompleted, + kTimedOut, + kAborted, + kError, +}; + +struct IoResult +{ + IoOutcome outcome = IoOutcome::kError; + int bytes_transferred = 0; + DWORD error = ERROR_SUCCESS; +}; + +inline auto multiplierTimeout(int timeout_ms, int multiplier) -> int +{ + if (multiplier <= 0) + { + return 0; + } + + const auto timeout = static_cast(cpp_core::clampTimeout(timeout_ms)) * multiplier; + return timeout > INT_MAX ? INT_MAX : static_cast(timeout); +} + +inline auto waitForPendingIo(HANDLE handle, const std::shared_ptr &state, Operation operation, + OVERLAPPED *overlapped, int timeout_ms) -> IoResult +{ + const DWORD wait_result = + WaitForSingleObject(overlapped->hEvent, static_cast(cpp_core::clampTimeout(timeout_ms))); + if (wait_result == WAIT_TIMEOUT) + { + (void)CancelIoEx(handle, overlapped); + DWORD ignored = 0; + (void)GetOverlappedResult(handle, overlapped, &ignored, TRUE); + if (finishPendingIo(state, operation, overlapped)) + { + return {.outcome = IoOutcome::kAborted}; + } + return {.outcome = IoOutcome::kTimedOut}; + } + + if (wait_result != WAIT_OBJECT_0) + { + const DWORD error = GetLastError(); + (void)CancelIoEx(handle, overlapped); + DWORD ignored = 0; + (void)GetOverlappedResult(handle, overlapped, &ignored, TRUE); + const bool aborted = finishPendingIo(state, operation, overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = error}; + } + + DWORD transferred = 0; + const BOOL completed = GetOverlappedResult(handle, overlapped, &transferred, FALSE); + const DWORD error = completed != FALSE ? ERROR_SUCCESS : GetLastError(); + const bool aborted = finishPendingIo(state, operation, overlapped); + if (aborted || error == ERROR_OPERATION_ABORTED) + { + return {.outcome = IoOutcome::kAborted}; + } + if (completed == FALSE) + { + return {.outcome = IoOutcome::kError, .error = error}; + } + return {.outcome = IoOutcome::kCompleted, .bytes_transferred = static_cast(transferred)}; +} + +inline auto readChunk(const HandleContext &context, unsigned char *buffer, int buffer_size, int timeout_ms) -> IoResult +{ + UniqueHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!event) + { + return {.outcome = IoOutcome::kError, .error = GetLastError()}; + } + + OVERLAPPED overlapped = {}; + overlapped.hEvent = event.get(); + DWORD transferred = 0; + const auto start = startPendingIo(context.state, Operation::kRead, &overlapped, [&] { + return ReadFile(context.handle, buffer, static_cast(buffer_size), &transferred, &overlapped); + }); + if (start.aborted) + { + return {.outcome = IoOutcome::kAborted}; + } + if (start.completed != FALSE) + { + const bool aborted = finishPendingIo(context.state, Operation::kRead, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kCompleted, + .bytes_transferred = static_cast(transferred)}; + } + if (start.error != ERROR_IO_PENDING) + { + const bool aborted = finishPendingIo(context.state, Operation::kRead, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = start.error}; + } + + return waitForPendingIo(context.handle, context.state, Operation::kRead, &overlapped, timeout_ms); +} + +inline auto writeChunk(const HandleContext &context, const unsigned char *buffer, int buffer_size, int timeout_ms) + -> IoResult +{ + UniqueHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!event) + { + return {.outcome = IoOutcome::kError, .error = GetLastError()}; + } + + OVERLAPPED overlapped = {}; + overlapped.hEvent = event.get(); + DWORD transferred = 0; + const auto start = startPendingIo(context.state, Operation::kWrite, &overlapped, [&] { + return WriteFile(context.handle, buffer, static_cast(buffer_size), &transferred, &overlapped); + }); + if (start.aborted) + { + return {.outcome = IoOutcome::kAborted}; + } + if (start.completed != FALSE) + { + const bool aborted = finishPendingIo(context.state, Operation::kWrite, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kCompleted, + .bytes_transferred = static_cast(transferred)}; + } + if (start.error != ERROR_IO_PENDING) + { + const bool aborted = finishPendingIo(context.state, Operation::kWrite, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = start.error}; + } + + return waitForPendingIo(context.handle, context.state, Operation::kWrite, &overlapped, timeout_ms); +} + +inline auto matchesSuffix(const unsigned char *buffer, int buffer_size, const unsigned char *terminator, + int terminator_size) -> bool +{ + return terminator_size > 0 && buffer_size >= terminator_size && + std::memcmp(buffer + buffer_size - terminator_size, terminator, static_cast(terminator_size)) == + 0; +} + +inline auto readImpl(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, + const unsigned char *terminator, int terminator_size, ErrorCallbackT error_callback) -> int +{ + const auto callback = effectiveErrorCallback(error_callback); + const auto buffer_status = cpp_core::validateBuffer(buffer, buffer_size, callback); + if (buffer_status < 0) + { + return buffer_status; + } + if (terminator_size > 0 && terminator == nullptr) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kBufferError), + "Invalid terminator"); + } + + HandleContext context; + const auto handle_status = acquireHandleContext(handle, callback, &context); + if (handle_status < 0) + { + return handle_status; + } + + auto *output = static_cast(buffer); + int total_read = 0; + while (total_read < buffer_size) + { + int chunk_size = 1; + if (terminator_size <= 0) + { + int waiting = 0; + if (!bytesWaiting(context.handle, &waiting)) + { + return failWin32(callback, static_cast(StatusCode::Control::kGetStateError)); + } + chunk_size = waiting > 0 ? std::min(waiting, buffer_size - total_read) : 1; + } + + const int current_timeout = + total_read == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); + const auto result = readChunk(context, output + total_read, chunk_size, current_timeout); + if (result.outcome == IoOutcome::kTimedOut) + { + return total_read; + } + if (result.outcome == IoOutcome::kAborted) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kAbortReadError), + "Read aborted"); + } + if (result.outcome == IoOutcome::kError) + { + SetLastError(result.error); + return failWin32(callback, static_cast(StatusCode::Io::kReadError)); + } + if (result.bytes_transferred <= 0) + { + return total_read; + } + + noteBytesTransferred(context.state, Operation::kRead, result.bytes_transferred); + total_read += result.bytes_transferred; + if (matchesSuffix(output, total_read, terminator, terminator_size)) + { + return total_read; + } + } + + return total_read; +} + +inline auto writeImpl(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int multiplier, + ErrorCallbackT error_callback) -> int +{ + const auto callback = effectiveErrorCallback(error_callback); + const auto buffer_status = cpp_core::validateBuffer(buffer, buffer_size, callback); + if (buffer_status < 0) + { + return buffer_status; + } + + HandleContext context; + const auto handle_status = acquireHandleContext(handle, callback, &context); + if (handle_status < 0) + { + return handle_status; + } + + const auto *input = static_cast(buffer); + int total_written = 0; + while (total_written < buffer_size) + { + const int current_timeout = + total_written == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); + const auto result = writeChunk(context, input + total_written, buffer_size - total_written, current_timeout); + if (result.outcome == IoOutcome::kTimedOut) + { + return total_written; + } + if (result.outcome == IoOutcome::kAborted) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kAbortWriteError), + "Write aborted"); + } + if (result.outcome == IoOutcome::kError) + { + SetLastError(result.error); + return failWin32(callback, static_cast(StatusCode::Io::kWriteError)); + } + if (result.bytes_transferred <= 0) + { + return total_written; + } + + noteBytesTransferred(context.state, Operation::kWrite, result.bytes_transferred); + total_written += result.bytes_transferred; + } + + return total_written; +} + +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/win32_helpers.hpp b/src/detail/win32_helpers.hpp index 6f404e6..964c5f7 100644 --- a/src/detail/win32_helpers.hpp +++ b/src/detail/win32_helpers.hpp @@ -1,9 +1,9 @@ #pragma once +#include "common_types.hpp" +#include "handle_state.hpp" + #include -#include -#include -#include #ifndef NOMINMAX #define NOMINMAX @@ -11,64 +11,98 @@ #include #include -#include -#include #include +#include +#include namespace cpp_bindings_windows::detail { - -// Win32 HANDLE traits for UniqueResource -struct Win32HandleTraits +inline auto win32ErrorToString(DWORD error) -> std::string { - using handle_type = HANDLE; + LPSTR buffer = nullptr; + const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; + const DWORD language_id = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); + const DWORD length = + FormatMessageA(flags, nullptr, error, language_id, reinterpret_cast(&buffer), 0, nullptr); + if (length == 0 || buffer == nullptr) + { + return "Unknown Win32 error (" + std::to_string(error) + ")"; + } - static constexpr auto invalid() noexcept -> handle_type + std::string message(buffer, length); + LocalFree(buffer); + while (!message.empty() && (message.back() == '\r' || message.back() == '\n')) { - return nullptr; + message.pop_back(); } + return message; +} - static auto close(handle_type h) noexcept -> void +inline auto utf8ToWide(const char *utf8) -> std::wstring +{ + if (utf8 == nullptr || *utf8 == '\0') { - if (h != INVALID_HANDLE_VALUE) - { - CloseHandle(h); - } + return {}; } -}; -using UniqueHandle = cpp_core::UniqueResource; + const int required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, -1, nullptr, 0); + if (required <= 0) + { + return {}; + } + + std::wstring wide(static_cast(required), L'\0'); + if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, -1, wide.data(), required) <= 0) + { + return {}; + } + wide.pop_back(); + return wide; +} -// Win32-specific error helpers -inline auto win32ErrorToString(DWORD err) -> std::string +inline auto normalizePortPath(std::wstring_view port) -> std::wstring { - LPSTR buffer = nullptr; - const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; - const DWORD lang_id = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); + if (port.starts_with(L"\\\\.\\")) + { + return std::wstring(port); + } + if (port.starts_with(L"COM") || port.starts_with(L"com")) + { + return L"\\\\.\\" + std::wstring(port); + } + return std::wstring(port); +} - const DWORD len = FormatMessageA(flags, nullptr, err, lang_id, reinterpret_cast(&buffer), 0, nullptr); - if (len == 0 || buffer == nullptr) +inline auto wideToUtf8(std::wstring_view wide) -> std::string +{ + if (wide.empty()) { - return "Unknown Win32 error"; + return {}; } - std::string msg(buffer, len); - LocalFree(buffer); + const int required = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), + nullptr, 0, nullptr, nullptr); + if (required <= 0) + { + return {}; + } - while (!msg.empty() && (msg.back() == '\r' || msg.back() == '\n')) + std::string utf8(static_cast(required), '\0'); + if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), utf8.data(), + required, nullptr, nullptr) <= 0) { - msg.pop_back(); + return {}; } - return msg; + return utf8; } -template -inline auto failWin32(Callback &&error_callback, cpp_core::StatusCodes code) -> Ret +template +inline auto failWin32(ErrorCallbackT error_callback, StatusCodeValue code) -> ReturnType { - const DWORD err = GetLastError(); - const std::string msg = win32ErrorToString(err); - cpp_core::invokeError(std::forward(error_callback), code, msg); - return static_cast(code); + const DWORD error = GetLastError(); + const std::string message = win32ErrorToString(error); + cpp_core::invokeError(effectiveErrorCallback(error_callback), code, message); + return static_cast(code); } inline auto bytesWaiting(HANDLE handle, int *out_bytes) -> bool @@ -80,41 +114,14 @@ inline auto bytesWaiting(HANDLE handle, int *out_bytes) -> bool *out_bytes = 0; DWORD errors = 0; - COMSTAT stat = {}; - if (ClearCommError(handle, &errors, &stat) == 0) + COMSTAT status = {}; + if (ClearCommError(handle, &errors, &status) == 0) { return false; } - if (stat.cbInQue > static_cast(INT_MAX)) - { - *out_bytes = INT_MAX; - } - else - { - *out_bytes = static_cast(stat.cbInQue); - } + *out_bytes = status.cbInQue > static_cast(INT_MAX) ? INT_MAX : static_cast(status.cbInQue); return true; } -// Combined int64_t -> HANDLE validation for the C API boundary. -// Checks numeric range, nullptr, and INVALID_HANDLE_VALUE. -template -inline auto validateWin32Handle(int64_t handle, Callback &&error_callback, HANDLE *out) -> Ret -{ - if (handle <= 0 || handle > std::numeric_limits::max() || handle > std::numeric_limits::max()) - { - return cpp_core::failMsg(std::forward(error_callback), - cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle"); - } - const HANDLE h = reinterpret_cast(static_cast(handle)); - if (h == nullptr || h == INVALID_HANDLE_VALUE) - { - return cpp_core::failMsg(std::forward(error_callback), - cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle"); - } - *out = h; - return static_cast(cpp_core::StatusCodes::kSuccess); -} - } // namespace cpp_bindings_windows::detail diff --git a/src/get_version.cpp b/src/get_version.cpp new file mode 100644 index 0000000..472a0b0 --- /dev/null +++ b/src/get_version.cpp @@ -0,0 +1,4 @@ +#include + +// Keep the inline C API definition in a library translation unit so Windows +// linkers emit the exported getVersion symbol because windows is a bit picky and stupid sometimes. diff --git a/src/serial_abort_read.cpp b/src/serial_abort_read.cpp new file mode 100644 index 0000000..e881bcb --- /dev/null +++ b/src/serial_abort_read.cpp @@ -0,0 +1,22 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API auto serialAbortRead(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + cpp_bindings_windows::detail::requestAbort(context.handle, context.state, + cpp_bindings_windows::detail::Operation::kRead); + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_abort_write.cpp b/src/serial_abort_write.cpp new file mode 100644 index 0000000..5cc7400 --- /dev/null +++ b/src/serial_abort_write.cpp @@ -0,0 +1,22 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API auto serialAbortWrite(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + cpp_bindings_windows::detail::requestAbort(context.handle, context.state, + cpp_bindings_windows::detail::Operation::kWrite); + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_clear_buffer_in.cpp b/src/serial_clear_buffer_in.cpp new file mode 100644 index 0000000..f5c6327 --- /dev/null +++ b/src/serial_clear_buffer_in.cpp @@ -0,0 +1,27 @@ +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialClearBufferIn(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + if (PurgeComm(context.handle, PURGE_RXCLEAR) == 0) + { + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + static_cast(cpp_core::StatusCode::Io::kClearBufferInError)); + } + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_clear_buffer_out.cpp b/src/serial_clear_buffer_out.cpp new file mode 100644 index 0000000..52b4373 --- /dev/null +++ b/src/serial_clear_buffer_out.cpp @@ -0,0 +1,27 @@ +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialClearBufferOut(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + if (FlushFileBuffers(context.handle) == 0 || PurgeComm(context.handle, PURGE_TXCLEAR) == 0) + { + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + static_cast(cpp_core::StatusCode::Io::kClearBufferOutError)); + } + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_close.cpp b/src/serial_close.cpp index 4ebf015..f5e20cb 100644 --- a/src/serial_close.cpp +++ b/src/serial_close.cpp @@ -14,8 +14,7 @@ extern "C" } HANDLE h = nullptr; - const auto handle_ok = - cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + const auto handle_ok = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); if (handle_ok < 0) { return handle_ok; @@ -23,11 +22,13 @@ extern "C" if (CloseHandle(h) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kCloseHandleError); + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Connection::kCloseHandleError); } - return 0; + cpp_bindings_windows::detail::removeHandleState(h); + return static_cast(cpp_core::StatusCode::kSuccess); } } // extern "C" diff --git a/src/serial_close.test.cpp b/src/serial_close.test.cpp index af2d38b..aff951e 100644 --- a/src/serial_close.test.cpp +++ b/src/serial_close.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include @@ -29,29 +29,29 @@ TEST_F(SerialCloseTest, CloseInvalidHandleZero) { int result = serialClose(0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::kSuccess)); } TEST_F(SerialCloseTest, CloseInvalidHandleNegative) { int result = serialClose(-1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::kSuccess)); } TEST_F(SerialCloseTest, CloseInvalidHandleNegativeLarge) { int result = serialClose(-12345, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::kSuccess)); } -TEST_F(SerialCloseTest, CloseInvalidHandleTooLarge) +TEST_F(SerialCloseTest, HandleAboveIntMaxIsNotRejectedByRangeValidation) { auto too_large_handle = static_cast(std::numeric_limits::max()) + 1; int result = serialClose(too_large_handle, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialCloseTest, CloseInvalidHandleIntMaxBoundary) @@ -59,14 +59,14 @@ TEST_F(SerialCloseTest, CloseInvalidHandleIntMaxBoundary) auto handle = static_cast(std::numeric_limits::max()); int result = serialClose(handle, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialCloseTest, CloseNoErrorCallback) { int result = serialClose(0, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::kSuccess)); } TEST_F(SerialCloseTest, CloseInvalidHandle) @@ -74,5 +74,5 @@ TEST_F(SerialCloseTest, CloseInvalidHandle) // Closing a value that is not a valid HANDLE int result = serialClose(9999, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kCloseHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kCloseHandleError)); } diff --git a/src/serial_drain.cpp b/src/serial_drain.cpp new file mode 100644 index 0000000..50dc524 --- /dev/null +++ b/src/serial_drain.cpp @@ -0,0 +1,27 @@ +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialDrain(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + if (FlushFileBuffers(context.handle) == 0) + { + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + static_cast(cpp_core::StatusCode::Io::kWriteError)); + } + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_extended_api.test.cpp b/src/serial_extended_api.test.cpp new file mode 100644 index 0000000..83d09f5 --- /dev/null +++ b/src/serial_extended_api.test.cpp @@ -0,0 +1,101 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace +{ +std::atomic g_last_error_code{0}; +std::atomic g_port_callback_count{0}; + +void globalErrorCallback(int code, const char * /*message*/) +{ + g_last_error_code.store(code, std::memory_order_relaxed); +} + +void listPortsCallback(const char * /*port*/, const char * /*path*/, const char * /*manufacturer*/, + const char * /*serial_number*/, const char * /*pnp_id*/, const char * /*location_id*/, + const char * /*product_id*/, const char * /*vendor_id*/) +{ + g_port_callback_count.fetch_add(1, std::memory_order_relaxed); +} + +constexpr auto kBufferError = static_cast(cpp_core::StatusCode::Io::kBufferError); +constexpr auto kInvalidHandleError = static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError); +} // namespace + +class SerialExtendedApiTest : public ::testing::Test +{ + protected: + void SetUp() override + { + g_last_error_code.store(0, std::memory_order_relaxed); + g_port_callback_count.store(0, std::memory_order_relaxed); + serialSetErrorCallback(nullptr); + serialSetReadCallback(nullptr); + serialSetWriteCallback(nullptr); + ASSERT_EQ(serialMonitorPorts(nullptr, nullptr), 0); + } + + void TearDown() override + { + serialSetErrorCallback(nullptr); + serialSetReadCallback(nullptr); + serialSetWriteCallback(nullptr); + (void)serialMonitorPorts(nullptr, nullptr); + } +}; + +TEST_F(SerialExtendedApiTest, GlobalErrorCallbackActsAsFallback) +{ + serialSetErrorCallback(globalErrorCallback); + + std::array buffer{}; + EXPECT_EQ(serialRead(-1, buffer.data(), static_cast(buffer.size()), 10, 1, nullptr), kInvalidHandleError); + EXPECT_EQ(g_last_error_code.load(std::memory_order_relaxed), kInvalidHandleError); +} + +TEST_F(SerialExtendedApiTest, ReadHelpersValidateTerminators) +{ + std::array buffer{}; + EXPECT_EQ(serialReadUntil(1, buffer.data(), static_cast(buffer.size()), 10, 1, nullptr, nullptr), + kBufferError); + EXPECT_EQ(serialReadUntilSequence(1, buffer.data(), static_cast(buffer.size()), 10, 1, nullptr, nullptr), + kBufferError); + + char empty_sequence[] = ""; + EXPECT_EQ( + serialReadUntilSequence(1, buffer.data(), static_cast(buffer.size()), 10, 1, empty_sequence, nullptr), + kBufferError); +} + +TEST_F(SerialExtendedApiTest, HandleBasedExtensionsRejectInvalidHandles) +{ + EXPECT_EQ(serialAbortRead(-1, nullptr), kInvalidHandleError); + EXPECT_EQ(serialAbortWrite(-1, nullptr), kInvalidHandleError); + EXPECT_EQ(serialInBytesTotal(-1, nullptr), kInvalidHandleError); + EXPECT_EQ(serialOutBytesTotal(-1, nullptr), kInvalidHandleError); +} + +TEST_F(SerialExtendedApiTest, ListPortsValidatesAndEnumerates) +{ + EXPECT_EQ(serialListPorts(nullptr, nullptr), kBufferError); + + const int result = serialListPorts(listPortsCallback, nullptr); + EXPECT_GE(result, 0); + EXPECT_EQ(result, g_port_callback_count.load(std::memory_order_relaxed)); +} diff --git a/src/serial_get_baudrate.cpp b/src/serial_get_baudrate.cpp index 918bd75..a4eece3 100644 --- a/src/serial_get_baudrate.cpp +++ b/src/serial_get_baudrate.cpp @@ -19,7 +19,8 @@ extern "C" dcb.DCBlength = sizeof(DCB); if (GetCommState(h, &dcb) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); } return static_cast(dcb.BaudRate); diff --git a/src/serial_get_cts.cpp b/src/serial_get_cts.cpp index 726e729..09c6860 100644 --- a/src/serial_get_cts.cpp +++ b/src/serial_get_cts.cpp @@ -19,7 +19,7 @@ extern "C" if (GetCommModemStatus(h, &modem_status) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kGetModemStatusError); + cpp_core::StatusCode::Control::kGetModemStatusError); } return (modem_status & MS_CTS_ON) ? 1 : 0; diff --git a/src/serial_get_data_bits.cpp b/src/serial_get_data_bits.cpp index 53114ae..458a4ad 100644 --- a/src/serial_get_data_bits.cpp +++ b/src/serial_get_data_bits.cpp @@ -19,7 +19,8 @@ extern "C" dcb.DCBlength = sizeof(DCB); if (GetCommState(h, &dcb) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); } return static_cast(dcb.ByteSize); diff --git a/src/serial_get_dcd.cpp b/src/serial_get_dcd.cpp index 808ed7e..0b904e5 100644 --- a/src/serial_get_dcd.cpp +++ b/src/serial_get_dcd.cpp @@ -19,7 +19,7 @@ extern "C" if (GetCommModemStatus(h, &modem_status) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kGetModemStatusError); + cpp_core::StatusCode::Control::kGetModemStatusError); } return (modem_status & MS_RLSD_ON) ? 1 : 0; diff --git a/src/serial_get_dsr.cpp b/src/serial_get_dsr.cpp index 6ef9d09..a67ea62 100644 --- a/src/serial_get_dsr.cpp +++ b/src/serial_get_dsr.cpp @@ -19,7 +19,7 @@ extern "C" if (GetCommModemStatus(h, &modem_status) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kGetModemStatusError); + cpp_core::StatusCode::Control::kGetModemStatusError); } return (modem_status & MS_DSR_ON) ? 1 : 0; diff --git a/src/serial_get_flow_control.cpp b/src/serial_get_flow_control.cpp index bef60c2..1394971 100644 --- a/src/serial_get_flow_control.cpp +++ b/src/serial_get_flow_control.cpp @@ -19,7 +19,8 @@ extern "C" dcb.DCBlength = sizeof(DCB); if (GetCommState(h, &dcb) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); } if (dcb.fOutxCtsFlow != 0 && dcb.fRtsControl == RTS_CONTROL_HANDSHAKE) diff --git a/src/serial_get_parity.cpp b/src/serial_get_parity.cpp index 96c41f5..f5ff9a1 100644 --- a/src/serial_get_parity.cpp +++ b/src/serial_get_parity.cpp @@ -19,7 +19,8 @@ extern "C" dcb.DCBlength = sizeof(DCB); if (GetCommState(h, &dcb) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); } switch (dcb.Parity) diff --git a/src/serial_get_ri.cpp b/src/serial_get_ri.cpp index 782a531..0bb6254 100644 --- a/src/serial_get_ri.cpp +++ b/src/serial_get_ri.cpp @@ -19,7 +19,7 @@ extern "C" if (GetCommModemStatus(h, &modem_status) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kGetModemStatusError); + cpp_core::StatusCode::Control::kGetModemStatusError); } return (modem_status & MS_RING_ON) ? 1 : 0; diff --git a/src/serial_get_stop_bits.cpp b/src/serial_get_stop_bits.cpp index 91d497f..ccb2c5b 100644 --- a/src/serial_get_stop_bits.cpp +++ b/src/serial_get_stop_bits.cpp @@ -19,7 +19,8 @@ extern "C" dcb.DCBlength = sizeof(DCB); if (GetCommState(h, &dcb) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); } return (dcb.StopBits == TWOSTOPBITS) ? 2 : 0; diff --git a/src/serial_in_bytes_total.cpp b/src/serial_in_bytes_total.cpp new file mode 100644 index 0000000..50b867a --- /dev/null +++ b/src/serial_in_bytes_total.cpp @@ -0,0 +1,20 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API auto serialInBytesTotal(int64_t handle, ErrorCallbackT error_callback) -> int64_t + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = + cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + return context.state->bytes_read_total.load(std::memory_order_relaxed); + } + +} // extern "C" diff --git a/src/serial_in_bytes_waiting.cpp b/src/serial_in_bytes_waiting.cpp new file mode 100644 index 0000000..291aeef --- /dev/null +++ b/src/serial_in_bytes_waiting.cpp @@ -0,0 +1,28 @@ +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialInBytesWaiting(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + int waiting = 0; + if (!cpp_bindings_windows::detail::bytesWaiting(context.handle, &waiting)) + { + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + static_cast(cpp_core::StatusCode::Control::kGetStateError)); + } + return waiting; + } + +} // extern "C" diff --git a/src/serial_list_ports.cpp b/src/serial_list_ports.cpp new file mode 100644 index 0000000..12cbbfb --- /dev/null +++ b/src/serial_list_ports.cpp @@ -0,0 +1,214 @@ +#include +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ +struct PortInfo +{ + std::string port; + std::string path; + std::string manufacturer; + std::string serial_number; + std::string pnp_id; + std::string location_id; + std::string product_id; + std::string vendor_id; +}; + +auto registryString(HKEY key, const wchar_t *value_name) -> std::optional +{ + DWORD type = 0; + DWORD size = 0; + if (RegQueryValueExW(key, value_name, nullptr, &type, nullptr, &size) != ERROR_SUCCESS || + (type != REG_SZ && type != REG_EXPAND_SZ) || size < sizeof(wchar_t)) + { + return std::nullopt; + } + + std::vector buffer(size / sizeof(wchar_t)); + if (RegQueryValueExW(key, value_name, nullptr, &type, reinterpret_cast(buffer.data()), &size) != + ERROR_SUCCESS) + { + return std::nullopt; + } + return std::wstring(buffer.data()); +} + +auto portName(HDEVINFO device_info_set, SP_DEVINFO_DATA *device_info) -> std::optional +{ + HKEY key = SetupDiOpenDevRegKey(device_info_set, device_info, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_QUERY_VALUE); + if (key == INVALID_HANDLE_VALUE) + { + return std::nullopt; + } + const auto value = registryString(key, L"PortName"); + RegCloseKey(key); + return value; +} + +auto deviceProperty(HDEVINFO device_info_set, SP_DEVINFO_DATA *device_info, DWORD property) + -> std::optional +{ + DWORD type = 0; + DWORD size = 0; + (void)SetupDiGetDeviceRegistryPropertyW(device_info_set, device_info, property, &type, nullptr, 0, &size); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || size < sizeof(wchar_t)) + { + return std::nullopt; + } + + std::vector buffer(size); + if (SetupDiGetDeviceRegistryPropertyW(device_info_set, device_info, property, &type, buffer.data(), size, + nullptr) == 0) + { + return std::nullopt; + } + return std::wstring(reinterpret_cast(buffer.data())); +} + +auto instanceId(HDEVINFO device_info_set, SP_DEVINFO_DATA *device_info) -> std::optional +{ + DWORD required = 0; + (void)SetupDiGetDeviceInstanceIdW(device_info_set, device_info, nullptr, 0, &required); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || required == 0) + { + return std::nullopt; + } + + std::vector buffer(required); + if (SetupDiGetDeviceInstanceIdW(device_info_set, device_info, buffer.data(), required, nullptr) == 0) + { + return std::nullopt; + } + return std::wstring(buffer.data()); +} + +auto asciiUpper(std::string value) -> std::string +{ + std::ranges::transform(value, value.begin(), + [](unsigned char character) { return static_cast(std::toupper(character)); }); + return value; +} + +auto hardwareId(std::string_view pnp_id, std::string_view prefix) -> std::string +{ + const std::string upper = asciiUpper(std::string(pnp_id)); + const auto position = upper.find(prefix); + if (position == std::string::npos || position + prefix.size() + 4 > upper.size()) + { + return {}; + } + return upper.substr(position + prefix.size(), 4); +} + +auto serialNumber(std::string_view pnp_id) -> std::string +{ + const auto separator = pnp_id.rfind('\\'); + if (separator == std::string_view::npos || separator + 1 >= pnp_id.size()) + { + return {}; + } + + std::string candidate(pnp_id.substr(separator + 1)); + return candidate.find('&') == std::string::npos ? candidate : std::string{}; +} + +auto optionalCString(const std::string &value) -> const char * +{ + return value.empty() ? nullptr : value.c_str(); +} +} // namespace + +extern "C" +{ + + MODULE_API auto serialListPorts(void (*callback_fn)(const char *port, const char *path, const char *manufacturer, + const char *serial_number, const char *pnp_id, + const char *location_id, const char *product_id, + const char *vendor_id), + ErrorCallbackT error_callback) -> int + { + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + if (callback_fn == nullptr) + { + return cpp_core::failMsg( + callback, static_cast(cpp_core::StatusCode::Io::kBufferError), + "Port callback must not be null"); + } + + const HDEVINFO device_info_set = SetupDiGetClassDevsW(&GUID_DEVCLASS_PORTS, nullptr, nullptr, DIGCF_PRESENT); + if (device_info_set == INVALID_HANDLE_VALUE) + { + return cpp_bindings_windows::detail::failWin32( + callback, static_cast(cpp_core::StatusCode::Monitor::kMonitorError)); + } + const auto cleanup = cpp_core::defer([&] { SetupDiDestroyDeviceInfoList(device_info_set); }); + + std::vector ports; + for (DWORD index = 0;; ++index) + { + SP_DEVINFO_DATA device_info = {}; + device_info.cbSize = sizeof(device_info); + if (SetupDiEnumDeviceInfo(device_info_set, index, &device_info) == 0) + { + if (GetLastError() == ERROR_NO_MORE_ITEMS) + { + break; + } + return cpp_bindings_windows::detail::failWin32( + callback, static_cast(cpp_core::StatusCode::Monitor::kMonitorError)); + } + + const auto port_name = portName(device_info_set, &device_info); + if (!port_name || port_name->size() < 4 || + (!port_name->starts_with(L"COM") && !port_name->starts_with(L"com"))) + { + continue; + } + + PortInfo info; + info.port = cpp_bindings_windows::detail::wideToUtf8(*port_name); + info.path = "\\\\.\\" + info.port; + if (const auto value = deviceProperty(device_info_set, &device_info, SPDRP_MFG)) + { + info.manufacturer = cpp_bindings_windows::detail::wideToUtf8(*value); + } + if (const auto value = deviceProperty(device_info_set, &device_info, SPDRP_LOCATION_INFORMATION)) + { + info.location_id = cpp_bindings_windows::detail::wideToUtf8(*value); + } + if (const auto value = instanceId(device_info_set, &device_info)) + { + info.pnp_id = cpp_bindings_windows::detail::wideToUtf8(*value); + info.serial_number = serialNumber(info.pnp_id); + info.vendor_id = hardwareId(info.pnp_id, "VID_"); + info.product_id = hardwareId(info.pnp_id, "PID_"); + } + ports.push_back(std::move(info)); + } + + std::ranges::sort(ports, {}, &PortInfo::port); + for (const auto &info : ports) + { + callback_fn(optionalCString(info.port), optionalCString(info.path), optionalCString(info.manufacturer), + optionalCString(info.serial_number), optionalCString(info.pnp_id), + optionalCString(info.location_id), optionalCString(info.product_id), + optionalCString(info.vendor_id)); + } + return static_cast(ports.size()); + } + +} // extern "C" diff --git a/src/serial_monitor_ports.cpp b/src/serial_monitor_ports.cpp index bcacb27..90eb811 100644 --- a/src/serial_monitor_ports.cpp +++ b/src/serial_monitor_ports.cpp @@ -1,11 +1,12 @@ #include -#include +#include "detail/handle_state.hpp" #include "detail/win32_helpers.hpp" -#include -#include +#include +#include #include +#include #include #include #include @@ -13,122 +14,116 @@ namespace { +std::mutex g_monitor_mutex; +std::mutex g_wait_mutex; +std::condition_variable_any g_wakeup; +std::jthread g_monitor_thread; -std::mutex g_mutex; -std::thread g_thread; -HANDLE g_stop_event = nullptr; -std::atomic g_running{false}; - -auto enumerateComPorts() -> std::set +auto enumerateComPorts() -> std::optional> { - std::set ports; std::vector buffer(65536); - const DWORD len = QueryDosDeviceA(nullptr, buffer.data(), static_cast(buffer.size())); - if (len == 0) + const DWORD length = QueryDosDeviceA(nullptr, buffer.data(), static_cast(buffer.size())); + if (length == 0) { - return ports; + return std::nullopt; } - const char *ptr = buffer.data(); - while (*ptr != '\0') + std::set ports; + const char *current = buffer.data(); + while (*current != '\0') { - std::string name(ptr); - if (name.rfind("COM", 0) == 0 && name.size() >= 4) + std::string name(current); + if (name.size() >= 4 && (name.starts_with("COM") || name.starts_with("com"))) { - ports.insert(name); + ports.insert(std::move(name)); } - ptr += name.size() + 1; + current += std::char_traits::length(current) + 1; } return ports; } -void monitorLoop(void (*callback)(int event, const char *port)) +auto stopMonitor() -> void { - std::set previous = enumerateComPorts(); + if (!g_monitor_thread.joinable()) + { + return; + } + g_monitor_thread.request_stop(); + g_wakeup.notify_all(); + if (g_monitor_thread.get_id() == std::this_thread::get_id()) + { + g_monitor_thread.detach(); + return; + } + g_monitor_thread.join(); +} - while (g_running.load(std::memory_order_relaxed)) +auto monitorLoop(std::stop_token stop_token, std::set previous, + void (*callback)(int event, const char *port), ErrorCallbackT error_callback) -> void +{ + std::unique_lock wait_lock(g_wait_mutex); + while (!stop_token.stop_requested()) { - const DWORD wait = WaitForSingleObject(g_stop_event, 500); - if (wait == WAIT_OBJECT_0) + (void)g_wakeup.wait_for(wait_lock, stop_token, std::chrono::milliseconds(500), [] { return false; }); + if (stop_token.stop_requested()) { break; } - std::set current = enumerateComPorts(); + wait_lock.unlock(); + auto current = enumerateComPorts(); + if (!current) + { + cpp_core::invokeError(error_callback, + static_cast(cpp_core::StatusCode::Monitor::kMonitorError), + cpp_bindings_windows::detail::win32ErrorToString(GetLastError())); + wait_lock.lock(); + continue; + } - for (const auto &p : current) + for (const auto &port : *current) { - if (previous.find(p) == previous.end()) + if (!previous.contains(port)) { - callback(1, p.c_str()); + callback(1, port.c_str()); } } - - for (const auto &p : previous) + for (const auto &port : previous) { - if (current.find(p) == current.end()) + if (!current->contains(port)) { - callback(0, p.c_str()); + callback(0, port.c_str()); } } - - previous = std::move(current); + previous = std::move(*current); + wait_lock.lock(); } } - -void stopMonitor() -{ - if (!g_running.load(std::memory_order_relaxed)) - { - return; - } - - g_running.store(false, std::memory_order_relaxed); - - if (g_stop_event != nullptr) - { - SetEvent(g_stop_event); - } - - if (g_thread.joinable()) - { - g_thread.join(); - } - - if (g_stop_event != nullptr) - { - CloseHandle(g_stop_event); - g_stop_event = nullptr; - } -} - } // namespace extern "C" { - MODULE_API auto serialMonitorPorts(void (*callback_fn)(int event, const char *port), - ErrorCallbackT error_callback) -> int + MODULE_API auto serialMonitorPorts(void (*callback_fn)(int event, const char *port), ErrorCallbackT error_callback) + -> int { - std::lock_guard lock(g_mutex); - + std::lock_guard lock(g_monitor_mutex); stopMonitor(); - if (callback_fn == nullptr) { - return 0; + return static_cast(cpp_core::StatusCode::kSuccess); } - g_stop_event = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (g_stop_event == nullptr) + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + auto initial_ports = enumerateComPorts(); + if (!initial_ports) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kMonitorError); + return cpp_bindings_windows::detail::failWin32( + callback, static_cast(cpp_core::StatusCode::Monitor::kMonitorError)); } - g_running.store(true, std::memory_order_relaxed); - g_thread = std::thread(monitorLoop, callback_fn); - - return 0; + g_monitor_thread = std::jthread(monitorLoop, std::move(*initial_ports), callback_fn, callback); + return static_cast(cpp_core::StatusCode::kSuccess); } } // extern "C" diff --git a/src/serial_open.cpp b/src/serial_open.cpp index 84f353c..73b3920 100644 --- a/src/serial_open.cpp +++ b/src/serial_open.cpp @@ -14,46 +14,8 @@ namespace { -auto utf8ToWide(const char *utf8) -> std::wstring -{ - if (utf8 == nullptr || utf8[0] == '\0') - { - return {}; - } - const int needed = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, nullptr, 0); - if (needed <= 0) - { - return {}; - } - std::wstring out(static_cast(needed), L'\0'); - const int written = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, out.data(), needed); - if (written <= 0) - { - return {}; - } - if (!out.empty() && out.back() == L'\0') - { - out.pop_back(); - } - return out; -} - -auto normalizePortPath(const wchar_t *port) -> std::wstring -{ - std::wstring p(port); - if (p.rfind(L"\\\\.\\", 0) == 0) - { - return p; - } - if (p.rfind(L"COM", 0) == 0 || p.rfind(L"com", 0) == 0) - { - return L"\\\\.\\" + p; - } - return p; -} - -auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Parity par, - cpp_core::StopBits sb) -> cpp_core::Status +auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Parity parity_value, + cpp_core::StopBits stop_bits_value) -> cpp_core::Status { DCB dcb = {}; dcb.DCBlength = sizeof(DCB); @@ -61,7 +23,7 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par if (GetCommState(handle, &dcb) == 0) { const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCodes::kGetStateError, + return cpp_core::fail(cpp_core::StatusCode::Control::kGetStateError, "GetCommState failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); } @@ -69,7 +31,7 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par dcb.ByteSize = static_cast(data_bits); dcb.fBinary = TRUE; - dcb.fParity = (par != cpp_core::Parity::kNone) ? TRUE : FALSE; + dcb.fParity = (parity_value != cpp_core::Parity::kNone) ? TRUE : FALSE; dcb.fOutxCtsFlow = FALSE; dcb.fOutxDsrFlow = FALSE; @@ -80,7 +42,7 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par dcb.fInX = FALSE; dcb.fRtsControl = RTS_CONTROL_ENABLE; - switch (par) + switch (parity_value) { case cpp_core::Parity::kNone: dcb.Parity = NOPARITY; @@ -92,14 +54,14 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par dcb.Parity = ODDPARITY; break; default: - return cpp_core::fail(cpp_core::StatusCodes::kSetStateError, "Invalid parity"); + return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, "Invalid parity"); } - if (sb == cpp_core::StopBits::kOne) + if (stop_bits_value == cpp_core::StopBits::kOne) { dcb.StopBits = ONESTOPBIT; } - else if (sb == cpp_core::StopBits::kTwo) + else if (stop_bits_value == cpp_core::StopBits::kTwo) { dcb.StopBits = TWOSTOPBITS; } @@ -107,7 +69,7 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par if (SetCommState(handle, &dcb) == 0) { const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCodes::kSetStateError, + return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, "SetCommState failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); } @@ -115,7 +77,7 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par if (SetCommTimeouts(handle, &timeouts) == 0) { const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCodes::kSetTimeoutError, + return cpp_core::fail(cpp_core::StatusCode::Configuration::kSetTimeoutError, "SetCommTimeouts failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); } @@ -128,52 +90,57 @@ extern "C" MODULE_API auto serialOpen(void *port, int baudrate, int data_bits, int parity, int stop_bits, ErrorCallbackT error_callback) -> intptr_t { - const auto params_ok = cpp_core::validateOpenParams(port, baudrate, data_bits, error_callback); + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + const auto params_ok = cpp_core::validateOpenParams(port, baudrate, data_bits, callback); if (params_ok < 0) { return params_ok; } - const auto par = static_cast(parity); + if (parity < static_cast(cpp_core::Parity::kNone) || parity > static_cast(cpp_core::Parity::kOdd)) + { + return cpp_core::failMsg(callback, cpp_core::StatusCode::Control::kSetStateError, + "Invalid parity: must be 0, 1, or 2"); + } + const auto parity_value = static_cast(parity); // stop_bits: 0 or 1 = one stop bit (0 kept for backward compat), 2 = two stop bits if (stop_bits != static_cast(cpp_core::StopBits::kOne) && stop_bits != 1 && stop_bits != static_cast(cpp_core::StopBits::kTwo)) { - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, + return cpp_core::failMsg(callback, cpp_core::StatusCode::Control::kSetStateError, "Invalid stop bits: must be 0, 1, or 2"); } - const auto sb = (stop_bits == static_cast(cpp_core::StopBits::kTwo)) ? cpp_core::StopBits::kTwo - : cpp_core::StopBits::kOne; + const auto stop_bits_value = (stop_bits == static_cast(cpp_core::StopBits::kTwo)) + ? cpp_core::StopBits::kTwo + : cpp_core::StopBits::kOne; const auto *port_utf8 = static_cast(port); - std::wstring port_wide = utf8ToWide(port_utf8); + std::wstring port_wide = cpp_bindings_windows::detail::utf8ToWide(port_utf8); if (port_wide.empty()) { - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kNotFoundError, + return cpp_core::failMsg(callback, cpp_core::StatusCode::Connection::kNotFoundError, "Port string is invalid or not valid UTF-8"); } - const std::wstring device_path = normalizePortPath(port_wide.c_str()); + const std::wstring device_path = cpp_bindings_windows::detail::normalizePortPath(port_wide); - const HANDLE raw_handle = - CreateFileW(device_path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr); + const HANDLE raw_handle = CreateFileW(device_path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr); // CreateFileW returns INVALID_HANDLE_VALUE on failure, normalize to nullptr // so UniqueHandle (sentinel = nullptr) treats it as invalid. - cpp_bindings_windows::detail::UniqueHandle handle( - (raw_handle == INVALID_HANDLE_VALUE) ? nullptr : raw_handle); + cpp_bindings_windows::detail::UniqueHandle handle((raw_handle == INVALID_HANDLE_VALUE) ? nullptr : raw_handle); if (!handle) { - return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kNotFoundError); + return cpp_bindings_windows::detail::failWin32(callback, + cpp_core::StatusCode::Connection::kNotFoundError); } - const auto settings = applyLineSettings(handle.get(), baudrate, data_bits, par, sb); + const auto settings = applyLineSettings(handle.get(), baudrate, data_bits, parity_value, stop_bits_value); if (!settings.has_value()) { - return static_cast(cpp_core::toCStatus(settings, error_callback)); + return static_cast(cpp_core::toCStatus(settings, callback)); } PurgeComm(handle.get(), PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT); @@ -181,10 +148,12 @@ extern "C" const intptr_t out = reinterpret_cast(handle.get()); if (out <= 0) { - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kInvalidHandleError, + return cpp_core::failMsg(callback, cpp_core::StatusCode::Connection::kInvalidHandleError, "Invalid handle generated"); } - return reinterpret_cast(handle.release()); + const HANDLE opened_handle = handle.release(); + cpp_bindings_windows::detail::registerOpenedHandle(opened_handle); + return reinterpret_cast(opened_handle); } } // extern "C" diff --git a/src/serial_open.test.cpp b/src/serial_open.test.cpp index c43d563..1c387c0 100644 --- a/src/serial_open.test.cpp +++ b/src/serial_open.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -40,155 +40,155 @@ TEST_F(SerialOpenTest, NullPortParameter) { intptr_t result = serialOpen(nullptr, 9600, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kNotFoundError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kNotFoundError)); EXPECT_NE(error_capture.last_message.find("nullptr"), std::string::npos); } TEST_F(SerialOpenTest, BaudrateTooLow) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 100, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 100, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); EXPECT_NE(error_capture.last_message.find("baudrate"), std::string::npos); } TEST_F(SerialOpenTest, BaudrateTooLowBoundary) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 299, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 299, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, BaudrateBoundaryValid) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 300, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 300, 8, 0, 1, error_callback); // COM99999 does not exist, but should pass baudrate validation (kNotFoundError, not kSetStateError) - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, DataBitsTooLow) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 4, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 4, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); EXPECT_NE(error_capture.last_message.find("data bits"), std::string::npos); } TEST_F(SerialOpenTest, DataBitsTooHigh) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 9, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 9, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidDataBits5) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 5, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 5, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidDataBits6) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 6, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 6, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidDataBits7) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 7, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 7, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidDataBits8) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, InvalidParity) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 5, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 5, 1, error_callback); EXPECT_LT(result, 0); } TEST_F(SerialOpenTest, ValidParityNone) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidParityEven) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 1, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 1, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidParityOdd) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 2, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 2, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, InvalidStopBits) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 3, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 3, error_callback); EXPECT_LT(result, 0); } TEST_F(SerialOpenTest, ValidStopBits0) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 0, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 0, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidStopBits1) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidStopBits2) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 2, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 2, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, NonExistentPort) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kNotFoundError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kNotFoundError)); } TEST_F(SerialOpenTest, VariousBaudrates) @@ -197,9 +197,9 @@ TEST_F(SerialOpenTest, VariousBaudrates) for (int baudrate : baudrates) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), baudrate, 8, 0, - 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)) + intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), baudrate, 8, 0, 1, + error_callback); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)) << "Baudrate " << baudrate << " should be valid"; } } @@ -208,5 +208,5 @@ TEST_F(SerialOpenTest, NoErrorCallbackNullPort) { intptr_t result = serialOpen(nullptr, 9600, 8, 0, 1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kNotFoundError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kNotFoundError)); } diff --git a/src/serial_out_bytes_total.cpp b/src/serial_out_bytes_total.cpp new file mode 100644 index 0000000..d479cb5 --- /dev/null +++ b/src/serial_out_bytes_total.cpp @@ -0,0 +1,20 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API auto serialOutBytesTotal(int64_t handle, ErrorCallbackT error_callback) -> int64_t + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = + cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + return context.state->bytes_written_total.load(std::memory_order_relaxed); + } + +} // extern "C" diff --git a/src/serial_out_bytes_waiting.cpp b/src/serial_out_bytes_waiting.cpp new file mode 100644 index 0000000..fb762dc --- /dev/null +++ b/src/serial_out_bytes_waiting.cpp @@ -0,0 +1,31 @@ +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialOutBytesWaiting(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + DWORD errors = 0; + COMSTAT comm_status = {}; + if (ClearCommError(context.handle, &errors, &comm_status) == 0) + { + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + static_cast(cpp_core::StatusCode::Control::kGetStateError)); + } + return comm_status.cbOutQue > static_cast(INT_MAX) ? INT_MAX : static_cast(comm_status.cbOutQue); + } + +} // extern "C" diff --git a/src/serial_read.cpp b/src/serial_read.cpp index d7e36d1..6892c52 100644 --- a/src/serial_read.cpp +++ b/src/serial_read.cpp @@ -1,221 +1,15 @@ #include -#include -#include -#include "detail/win32_helpers.hpp" - -#include - -namespace -{ -auto waitForRxChar(HANDLE handle, int timeout_ms) -> int -{ - timeout_ms = cpp_core::clampTimeout(timeout_ms); - - if (SetCommMask(handle, EV_RXCHAR) == 0) - { - return -1; - } - - OVERLAPPED ov = {}; - ov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (ov.hEvent == nullptr) - { - return -1; - } - DEFER - { - CloseHandle(ov.hEvent); - }; - - DWORD mask = 0; - const BOOL ok = WaitCommEvent(handle, &mask, &ov); - if (ok != 0) - { - return 1; - } - if (ok == 0) - { - const DWORD err = GetLastError(); - if (err != ERROR_IO_PENDING) - { - SetLastError(err); - return -1; - } - } - - const DWORD wait_rc = WaitForSingleObject(ov.hEvent, static_cast(timeout_ms)); - if (wait_rc == WAIT_TIMEOUT) - { - CancelIoEx(handle, &ov); - return 0; - } - if (wait_rc != WAIT_OBJECT_0) - { - CancelIoEx(handle, &ov); - SetLastError(ERROR_GEN_FAILURE); - return -1; - } - - DWORD bytes = 0; - if (GetOverlappedResult(handle, &ov, &bytes, FALSE) == 0) - { - return -1; - } - - return 1; -} - -auto readSome(HANDLE handle, unsigned char *dst, int size, int timeout_ms) -> int -{ - if (size <= 0) - { - return 0; - } - - OVERLAPPED ov = {}; - ov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (ov.hEvent == nullptr) - { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return -1; - } - DEFER - { - CloseHandle(ov.hEvent); - }; - - DWORD bytes_read = 0; - const BOOL ok = ReadFile(handle, dst, static_cast(size), &bytes_read, &ov); - if (ok != 0) - { - return static_cast(bytes_read); - } - - const DWORD err = GetLastError(); - if (err != ERROR_IO_PENDING) - { - SetLastError(err); - return -1; - } - - timeout_ms = cpp_core::clampTimeout(timeout_ms); - const DWORD wait_rc = WaitForSingleObject(ov.hEvent, static_cast(timeout_ms)); - if (wait_rc == WAIT_TIMEOUT) - { - CancelIoEx(handle, &ov); - return 0; - } - if (wait_rc != WAIT_OBJECT_0) - { - CancelIoEx(handle, &ov); - SetLastError(ERROR_GEN_FAILURE); - return -1; - } - - if (GetOverlappedResult(handle, &ov, &bytes_read, FALSE) == 0) - { - return -1; - } - - return static_cast(bytes_read); -} -} // namespace +#include "detail/io_impl.hpp" extern "C" { - MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, + + MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, ErrorCallbackT error_callback) -> int { - const auto buf_ok = cpp_core::validateBuffer(buffer, buffer_size, error_callback); - if (buf_ok < 0) - { - return buf_ok; - } - - HANDLE h = nullptr; - const auto handle_ok = - cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (handle_ok < 0) - { - return handle_ok; - } - - auto *buf = static_cast(buffer); - - int waiting = 0; - if (!cpp_bindings_windows::detail::bytesWaiting(h, &waiting)) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); - } - - if (waiting <= 0) - { - if (timeout_ms <= 0) - { - return 0; - } - const int ready = waitForRxChar(h, timeout_ms); - if (ready < 0) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kReadError); - } - if (ready == 0) - { - return 0; - } - } - - if (!cpp_bindings_windows::detail::bytesWaiting(h, &waiting)) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); - } - - if (waiting <= 0) - { - return 0; - } - - const int first_chunk = std::min(waiting, buffer_size); - int total = readSome(h, buf, first_chunk, timeout_ms); - if (total < 0) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kReadError); - } - if (total == 0) - { - total = readSome(h, buf, first_chunk, 10); - if (total < 0) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kReadError); - } - if (total == 0) - { - return 0; - } - } - - while (total < buffer_size) - { - if (!cpp_bindings_windows::detail::bytesWaiting(h, &waiting)) - { - return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kGetStateError); - } - if (waiting <= 0) - { - break; - } - const int chunk = std::min(waiting, buffer_size - total); - const int got = readSome(h, buf + total, chunk, 0); - if (got <= 0) - { - break; - } - total += got; - } - - return total; + return cpp_bindings_windows::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, nullptr, 0, + error_callback); } } // extern "C" diff --git a/src/serial_read.test.cpp b/src/serial_read.test.cpp index 840a7d6..70e1d69 100644 --- a/src/serial_read.test.cpp +++ b/src/serial_read.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -30,7 +30,7 @@ TEST_F(SerialReadTest, ReadNullBuffer) { int result = serialRead(1, nullptr, 10, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); EXPECT_NE(error_capture.last_message.find("buffer"), std::string::npos); } @@ -39,7 +39,7 @@ TEST_F(SerialReadTest, ReadZeroBufferSize) std::array buffer{}; int result = serialRead(1, buffer.data(), 0, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); } TEST_F(SerialReadTest, ReadNegativeBufferSize) @@ -47,7 +47,7 @@ TEST_F(SerialReadTest, ReadNegativeBufferSize) std::array buffer{}; int result = serialRead(1, buffer.data(), -1, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); } TEST_F(SerialReadTest, ReadInvalidHandleZero) @@ -55,7 +55,7 @@ TEST_F(SerialReadTest, ReadInvalidHandleZero) std::array buffer{}; int result = serialRead(0, buffer.data(), static_cast(buffer.size()), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialReadTest, ReadInvalidHandleNegative) @@ -63,16 +63,16 @@ TEST_F(SerialReadTest, ReadInvalidHandleNegative) std::array buffer{}; int result = serialRead(-1, buffer.data(), static_cast(buffer.size()), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } -TEST_F(SerialReadTest, ReadInvalidHandleTooLarge) +TEST_F(SerialReadTest, ReadHandleAboveIntMaxIsNotRejectedByRangeValidation) { std::array buffer{}; auto too_large = static_cast(std::numeric_limits::max()) + 1; int result = serialRead(too_large, buffer.data(), static_cast(buffer.size()), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialReadTest, ReadNoErrorCallback) @@ -80,5 +80,5 @@ TEST_F(SerialReadTest, ReadNoErrorCallback) std::array buffer{}; int result = serialRead(0, buffer.data(), static_cast(buffer.size()), 100, 0, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } diff --git a/src/serial_read_line.cpp b/src/serial_read_line.cpp new file mode 100644 index 0000000..c1c7092 --- /dev/null +++ b/src/serial_read_line.cpp @@ -0,0 +1,16 @@ +#include + +#include "detail/io_impl.hpp" + +extern "C" +{ + + MODULE_API auto serialReadLine(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, + ErrorCallbackT error_callback) -> int + { + static constexpr unsigned char kNewline = '\n'; + return cpp_bindings_windows::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, &kNewline, 1, + error_callback); + } + +} // extern "C" diff --git a/src/serial_read_until.cpp b/src/serial_read_until.cpp new file mode 100644 index 0000000..7a2ccc5 --- /dev/null +++ b/src/serial_read_until.cpp @@ -0,0 +1,23 @@ +#include + +#include "detail/io_impl.hpp" + +extern "C" +{ + + MODULE_API auto serialReadUntil(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, + void *until_char, ErrorCallbackT error_callback) -> int + { + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + if (until_char == nullptr) + { + return cpp_core::failMsg( + callback, static_cast(cpp_core::StatusCode::Io::kBufferError), + "Terminator pointer must not be null"); + } + + return cpp_bindings_windows::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, + static_cast(until_char), 1, callback); + } + +} // extern "C" diff --git a/src/serial_read_until_sequence.cpp b/src/serial_read_until_sequence.cpp new file mode 100644 index 0000000..32a8750 --- /dev/null +++ b/src/serial_read_until_sequence.cpp @@ -0,0 +1,34 @@ +#include + +#include "detail/io_impl.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialReadUntilSequence(int64_t handle, void *buffer, int buffer_size, int timeout_ms, + int multiplier, void *sequence, ErrorCallbackT error_callback) -> int + { + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + if (sequence == nullptr) + { + return cpp_core::failMsg( + callback, static_cast(cpp_core::StatusCode::Io::kBufferError), + "Sequence pointer must not be null"); + } + + const auto *sequence_bytes = static_cast(sequence); + const int sequence_size = static_cast(std::strlen(reinterpret_cast(sequence_bytes))); + if (sequence_size <= 0) + { + return cpp_core::failMsg( + callback, static_cast(cpp_core::StatusCode::Io::kBufferError), + "Sequence must not be empty"); + } + + return cpp_bindings_windows::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, + sequence_bytes, sequence_size, callback); + } + +} // extern "C" diff --git a/src/serial_send_break.cpp b/src/serial_send_break.cpp index db43e1f..c5dca34 100644 --- a/src/serial_send_break.cpp +++ b/src/serial_send_break.cpp @@ -17,14 +17,14 @@ extern "C" if (duration_ms <= 0) { - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSendBreakError, - "Break duration must be > 0"); + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Control::kSendBreakError, "Break duration must be > 0"); } if (SetCommBreak(h) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kSendBreakError); + cpp_core::StatusCode::Control::kSendBreakError); } Sleep(static_cast(duration_ms)); @@ -32,7 +32,7 @@ extern "C" if (ClearCommBreak(h) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kSendBreakError); + cpp_core::StatusCode::Control::kSendBreakError); } return 0; diff --git a/src/serial_set_baudrate.cpp b/src/serial_set_baudrate.cpp index 071655b..25243ca 100644 --- a/src/serial_set_baudrate.cpp +++ b/src/serial_set_baudrate.cpp @@ -17,7 +17,8 @@ extern "C" if (baudrate < 300) { - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetBaudrateError, + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetBaudrateError, "Invalid baudrate: must be >= 300"); } @@ -25,7 +26,8 @@ extern "C" dcb.DCBlength = sizeof(DCB); if (GetCommState(h, &dcb) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); } dcb.BaudRate = static_cast(baudrate); @@ -33,7 +35,7 @@ extern "C" if (SetCommState(h, &dcb) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kSetBaudrateError); + cpp_core::StatusCode::Configuration::kSetBaudrateError); } return 0; diff --git a/src/serial_set_data_bits.cpp b/src/serial_set_data_bits.cpp index 62482c4..a703604 100644 --- a/src/serial_set_data_bits.cpp +++ b/src/serial_set_data_bits.cpp @@ -17,7 +17,8 @@ extern "C" if (data_bits < 5 || data_bits > 8) { - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetDataBitsError, + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetDataBitsError, "Invalid data bits: must be 5-8"); } @@ -25,7 +26,8 @@ extern "C" dcb.DCBlength = sizeof(DCB); if (GetCommState(h, &dcb) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); } dcb.ByteSize = static_cast(data_bits); @@ -33,7 +35,7 @@ extern "C" if (SetCommState(h, &dcb) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kSetDataBitsError); + cpp_core::StatusCode::Configuration::kSetDataBitsError); } return 0; diff --git a/src/serial_set_dtr.cpp b/src/serial_set_dtr.cpp index a8aeb5d..f53d55e 100644 --- a/src/serial_set_dtr.cpp +++ b/src/serial_set_dtr.cpp @@ -18,7 +18,8 @@ extern "C" const DWORD func = state ? SETDTR : CLRDTR; if (EscapeCommFunction(h, func) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kSetDtrError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kSetDtrError); } return 0; diff --git a/src/serial_set_error_callback.cpp b/src/serial_set_error_callback.cpp new file mode 100644 index 0000000..0acae05 --- /dev/null +++ b/src/serial_set_error_callback.cpp @@ -0,0 +1,13 @@ +#include + +#include "detail/common_types.hpp" + +extern "C" +{ + + MODULE_API void serialSetErrorCallback(ErrorCallbackT error_callback) + { + cpp_bindings_windows::detail::g_error_callback.store(error_callback, std::memory_order_release); + } + +} // extern "C" diff --git a/src/serial_set_flow_control.cpp b/src/serial_set_flow_control.cpp index 3e28fd1..f493d5d 100644 --- a/src/serial_set_flow_control.cpp +++ b/src/serial_set_flow_control.cpp @@ -17,7 +17,8 @@ extern "C" if (mode < 0 || mode > 2) { - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetFlowControlError, + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetFlowControlError, "Invalid flow control mode: must be 0, 1, or 2"); } @@ -25,7 +26,8 @@ extern "C" dcb.DCBlength = sizeof(DCB); if (GetCommState(h, &dcb) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); } dcb.fOutxCtsFlow = FALSE; @@ -53,8 +55,8 @@ extern "C" if (SetCommState(h, &dcb) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kSetFlowControlError); + return cpp_bindings_windows::detail::failWin32( + error_callback, cpp_core::StatusCode::Configuration::kSetFlowControlError); } return 0; diff --git a/src/serial_set_parity.cpp b/src/serial_set_parity.cpp index 542cd54..2b444ca 100644 --- a/src/serial_set_parity.cpp +++ b/src/serial_set_parity.cpp @@ -28,7 +28,8 @@ extern "C" win_parity = ODDPARITY; break; default: - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetParityError, + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetParityError, "Invalid parity: must be 0, 1, or 2"); } @@ -36,7 +37,8 @@ extern "C" dcb.DCBlength = sizeof(DCB); if (GetCommState(h, &dcb) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); } dcb.Parity = win_parity; @@ -45,7 +47,7 @@ extern "C" if (SetCommState(h, &dcb) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kSetParityError); + cpp_core::StatusCode::Configuration::kSetParityError); } return 0; diff --git a/src/serial_set_read_callback.cpp b/src/serial_set_read_callback.cpp new file mode 100644 index 0000000..4f094aa --- /dev/null +++ b/src/serial_set_read_callback.cpp @@ -0,0 +1,13 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API void serialSetReadCallback(void (*callback_fn)(int bytes_read)) + { + cpp_bindings_windows::detail::g_read_callback.store(callback_fn, std::memory_order_release); + } + +} // extern "C" diff --git a/src/serial_set_rts.cpp b/src/serial_set_rts.cpp index b94dca8..fac86df 100644 --- a/src/serial_set_rts.cpp +++ b/src/serial_set_rts.cpp @@ -18,7 +18,8 @@ extern "C" const DWORD func = state ? SETRTS : CLRRTS; if (EscapeCommFunction(h, func) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kSetRtsError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kSetRtsError); } return 0; diff --git a/src/serial_set_stop_bits.cpp b/src/serial_set_stop_bits.cpp index b316a3d..5415780 100644 --- a/src/serial_set_stop_bits.cpp +++ b/src/serial_set_stop_bits.cpp @@ -17,7 +17,8 @@ extern "C" if (stop_bits != 0 && stop_bits != 1 && stop_bits != 2) { - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetStopBitsError, + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetStopBitsError, "Invalid stop bits: must be 0, 1, or 2"); } @@ -25,7 +26,8 @@ extern "C" dcb.DCBlength = sizeof(DCB); if (GetCommState(h, &dcb) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); } dcb.StopBits = (stop_bits == 2) ? TWOSTOPBITS : ONESTOPBIT; @@ -33,7 +35,7 @@ extern "C" if (SetCommState(h, &dcb) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kSetStopBitsError); + cpp_core::StatusCode::Configuration::kSetStopBitsError); } return 0; diff --git a/src/serial_set_write_callback.cpp b/src/serial_set_write_callback.cpp new file mode 100644 index 0000000..660becb --- /dev/null +++ b/src/serial_set_write_callback.cpp @@ -0,0 +1,13 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API void serialSetWriteCallback(void (*callback_fn)(int bytes_written)) + { + cpp_bindings_windows::detail::g_write_callback.store(callback_fn, std::memory_order_release); + } + +} // extern "C" diff --git a/src/serial_write.cpp b/src/serial_write.cpp index 62b2fdc..910a402 100644 --- a/src/serial_write.cpp +++ b/src/serial_write.cpp @@ -1,95 +1,15 @@ #include -#include -#include -#include "detail/win32_helpers.hpp" - -namespace -{ -auto writeSome(HANDLE handle, const void *src, int size, int timeout_ms) -> int -{ - if (size <= 0) - { - return 0; - } - - OVERLAPPED ov = {}; - ov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (ov.hEvent == nullptr) - { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return -1; - } - DEFER - { - CloseHandle(ov.hEvent); - }; - - DWORD bytes_written = 0; - const BOOL ok = WriteFile(handle, src, static_cast(size), &bytes_written, &ov); - if (ok != 0) - { - return static_cast(bytes_written); - } - - const DWORD err = GetLastError(); - if (err != ERROR_IO_PENDING) - { - SetLastError(err); - return -1; - } - - timeout_ms = cpp_core::clampTimeout(timeout_ms); - const DWORD wait_rc = WaitForSingleObject(ov.hEvent, static_cast(timeout_ms)); - if (wait_rc == WAIT_TIMEOUT) - { - CancelIoEx(handle, &ov); - return 0; - } - if (wait_rc != WAIT_OBJECT_0) - { - CancelIoEx(handle, &ov); - SetLastError(ERROR_GEN_FAILURE); - return -1; - } - - if (GetOverlappedResult(handle, &ov, &bytes_written, FALSE) == 0) - { - return -1; - } - - return static_cast(bytes_written); -} -} // namespace +#include "detail/io_impl.hpp" extern "C" { - MODULE_API auto serialWrite(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, + + MODULE_API auto serialWrite(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int multiplier, ErrorCallbackT error_callback) -> int { - const auto buf_ok = cpp_core::validateBuffer(buffer, buffer_size, error_callback); - if (buf_ok < 0) - { - return buf_ok; - } - - HANDLE h = nullptr; - const auto handle_ok = - cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (handle_ok < 0) - { - return handle_ok; - } - - const int written = writeSome(h, buffer, buffer_size, timeout_ms); - if (written < 0) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kWriteError); - } - - FlushFileBuffers(h); - - return written; + return cpp_bindings_windows::detail::writeImpl(handle, buffer, buffer_size, timeout_ms, multiplier, + error_callback); } } // extern "C" diff --git a/src/serial_write.test.cpp b/src/serial_write.test.cpp index 7bb183c..dd246cd 100644 --- a/src/serial_write.test.cpp +++ b/src/serial_write.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -31,7 +31,7 @@ TEST_F(SerialWriteTest, WriteNullBuffer) { int result = serialWrite(1, nullptr, 10, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); EXPECT_NE(error_capture.last_message.find("buffer"), std::string::npos); } @@ -40,7 +40,7 @@ TEST_F(SerialWriteTest, WriteZeroBufferSize) std::array buffer{}; int result = serialWrite(1, buffer.data(), 0, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); } TEST_F(SerialWriteTest, WriteNegativeBufferSize) @@ -48,7 +48,7 @@ TEST_F(SerialWriteTest, WriteNegativeBufferSize) std::array buffer{}; int result = serialWrite(1, buffer.data(), -1, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); } TEST_F(SerialWriteTest, WriteInvalidHandleZero) @@ -56,7 +56,7 @@ TEST_F(SerialWriteTest, WriteInvalidHandleZero) const char *buffer = "test"; int result = serialWrite(0, buffer, static_cast(strlen(buffer)), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialWriteTest, WriteInvalidHandleNegative) @@ -64,16 +64,16 @@ TEST_F(SerialWriteTest, WriteInvalidHandleNegative) const char *buffer = "test"; int result = serialWrite(-1, buffer, static_cast(strlen(buffer)), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } -TEST_F(SerialWriteTest, WriteInvalidHandleTooLarge) +TEST_F(SerialWriteTest, WriteHandleAboveIntMaxIsNotRejectedByRangeValidation) { const char *buffer = "test"; auto too_large = static_cast(std::numeric_limits::max()) + 1; int result = serialWrite(too_large, buffer, static_cast(strlen(buffer)), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialWriteTest, WriteEmptyStringZeroSize) @@ -81,7 +81,7 @@ TEST_F(SerialWriteTest, WriteEmptyStringZeroSize) const char *empty = ""; int result = serialWrite(1, empty, 0, 0, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); } TEST_F(SerialWriteTest, WriteNoErrorCallback) @@ -89,5 +89,5 @@ TEST_F(SerialWriteTest, WriteNoErrorCallback) std::array buffer{}; int result = serialWrite(0, buffer.data(), 1, 0, 0, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } diff --git a/src/test_helpers/error_capture.hpp b/src/test_helpers/error_capture.hpp index 9749538..987d0a9 100644 --- a/src/test_helpers/error_capture.hpp +++ b/src/test_helpers/error_capture.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include diff --git a/tests/serial_arduino.test.cpp b/tests/serial_arduino.test.cpp index 4ca19c3..f4e6535 100644 --- a/tests/serial_arduino.test.cpp +++ b/tests/serial_arduino.test.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include #include #ifndef NOMINMAX @@ -144,7 +144,7 @@ TEST(SerialInvalidHandleTest, InvalidHandleRead) { char buffer[256]; const int result = serialRead(-1, buffer, static_cast(sizeof(buffer)), 1000, 1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)) + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)) << "Should return error for invalid handle"; } @@ -152,12 +152,12 @@ TEST(SerialInvalidHandleTest, InvalidHandleWrite) { const char *data = "test"; const int result = serialWrite(-1, data, 4, 1000, 1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)) + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)) << "Should return error for invalid handle"; } TEST(SerialInvalidHandleTest, InvalidHandleClose) { const int result = serialClose(-1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::kSuccess)); } From 7dc39cb36ef7755b53d7e8044368f1a271398d5b Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 26 Aug 2026 21:43:49 +0200 Subject: [PATCH 03/18] ci: build Windows jobs with clang-cl --- .github/workflows/build_binary.yml | 7 ++++--- .github/workflows/deno_tests.yml | 4 ++-- .github/workflows/test_unit_cpp.yml | 4 ++-- CMakeLists.txt | 18 ++++++++---------- CMakePresets.json | 13 +++++++++++++ README.md | 2 +- integration_tests/ffi_bindings.ts | 3 +-- 7 files changed, 31 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index 87bd8aa..bffbe35 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -53,6 +53,7 @@ jobs: run: | cmake -S . -B build/ffi -G Ninja ` -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_C_COMPILER=clang-cl ` -DCMAKE_CXX_COMPILER=clang-cl ` -DCPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT=ON ` "-DCPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE=$env:ASTREIN_EXECUTABLE" ` @@ -107,15 +108,15 @@ jobs: - name: 'Configure release' run: | - cmake --preset windows-vs-release ` + cmake --preset windows-clang-release ` -DCPP_BINDINGS_WINDOWS_STATIC_MSVC_RUNTIME=ON - name: 'Build and test' run: | - cmake --build --preset windows-vs-release --config Release ` + cmake --build --preset windows-clang-release ` --target cpp_bindings_windows cpp_bindings_windows_tests ` --parallel 4 - ctest --test-dir build -C Release ` + ctest --test-dir build ` --output-on-failure ` --output-junit test-report.xml diff --git a/.github/workflows/deno_tests.yml b/.github/workflows/deno_tests.yml index 9207b97..22f8b25 100644 --- a/.github/workflows/deno_tests.yml +++ b/.github/workflows/deno_tests.yml @@ -30,11 +30,11 @@ jobs: - name: Configure CMake run: | - cmake --preset windows-vs-release + cmake --preset windows-clang-release - name: Build run: | - cmake --build --preset windows-vs-release --config Release --target cpp_bindings_windows + cmake --build --preset windows-clang-release --target cpp_bindings_windows - name: Run Deno integration tests working-directory: integration_tests diff --git a/.github/workflows/test_unit_cpp.yml b/.github/workflows/test_unit_cpp.yml index 4ddf4be..8449001 100644 --- a/.github/workflows/test_unit_cpp.yml +++ b/.github/workflows/test_unit_cpp.yml @@ -37,11 +37,11 @@ jobs: - name: 'Configure CMake' run: | - cmake --preset windows-vs-release + cmake --preset windows-clang-release - name: 'Build tests' run: | - cmake --build --preset windows-vs-release --config Release --target cpp_bindings_windows_tests + cmake --build --preset windows-clang-release --target cpp_bindings_windows_tests - name: 'Copy library artifact next to test exe' shell: pwsh diff --git a/CMakeLists.txt b/CMakeLists.txt index 50c1e36..1fda84c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,15 +30,14 @@ endif() file(WRITE "${CMAKE_BINARY_DIR}/env.bat" "set PACKAGE_VERSION=${GIT_DESCRIBE_NO_V}\n") -# Set C++ standard -set(CMAKE_CXX_STANDARD 26) +# The Windows bindings use the non-reflection cpp-core headers only. Keeping the +# implementation on C++23 lets the MSVC ABI build use the compiler shipped on +# GitHub's Windows image; cpp-core's reflection facilities require C++26 and a +# newer experimental compiler. +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -# Enable C++26 module support -set(CMAKE_CXX_MODULE_STD 26) -set(CMAKE_CXX_MODULE_EXTENSIONS OFF) - option( CPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT "Enable ASTrein JSON export for the cpp-core FFI headers" @@ -120,7 +119,7 @@ if(CPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT) PRIVATE cpp_bindings_windows_EXPORTS ) - target_compile_features(cpp_bindings_windows_ffi_ast_context PRIVATE cxx_std_26) + target_compile_features(cpp_bindings_windows_ffi_ast_context PRIVATE cxx_std_23) add_custom_command( OUTPUT "${CPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT}" @@ -198,11 +197,10 @@ target_include_directories( target_link_libraries( cpp_bindings_windows PRIVATE - cpp_core::cpp_core setupapi ) -target_compile_features(cpp_bindings_windows PRIVATE cxx_std_26) +target_compile_features(cpp_bindings_windows PRIVATE cxx_std_23) if(MSVC AND CPP_BINDINGS_WINDOWS_STATIC_MSVC_RUNTIME) set_property( @@ -236,7 +234,7 @@ if(TEST_SOURCES) GTest::gtest_main ) - target_compile_features(cpp_bindings_windows_tests PRIVATE cxx_std_26) + target_compile_features(cpp_bindings_windows_tests PRIVATE cxx_std_23) include(GoogleTest) if(CMAKE_CROSSCOMPILING) diff --git a/CMakePresets.json b/CMakePresets.json index 23ea253..2c2f306 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -36,6 +36,15 @@ "CMAKE_CXX_COMPILER": "cl" } }, + { + "name": "windows-clang-release", + "displayName": "Windows Clang-CL Release", + "inherits": "default", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang-cl", + "CMAKE_CXX_COMPILER": "clang-cl" + } + }, { "name": "windows-mingw-release", "displayName": "Windows MinGW x86-64 Release", @@ -64,6 +73,10 @@ "name": "windows-ninja-msvc", "configurePreset": "windows-ninja-msvc" }, + { + "name": "windows-clang-release", + "configurePreset": "windows-clang-release" + }, { "name": "windows-mingw-release", "configurePreset": "windows-mingw-release" diff --git a/README.md b/README.md index d16ace9..67e843c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ opening, configuring, reading from, and writing to serial ports. - CMake 3.30 or newer - Git -- A compiler with sufficient C++26 support +- A compiler with C++23 support - One of: - Windows with Visual Studio 2022 and the C++ workload - Linux with an x86-64 MinGW-w64 toolchain for cross-compilation diff --git a/integration_tests/ffi_bindings.ts b/integration_tests/ffi_bindings.ts index dc26431..bc49897 100644 --- a/integration_tests/ffi_bindings.ts +++ b/integration_tests/ffi_bindings.ts @@ -37,6 +37,7 @@ export async function loadSerialLib( const possiblePaths = [ libraryPath, + "../build/cpp_bindings_windows.dll", "../build/Release/cpp_bindings_windows.dll", "../build/cpp_bindings_windows/Release/cpp_bindings_windows.dll", "../build/**/Release/cpp_bindings_windows.dll", @@ -69,5 +70,3 @@ export async function loadSerialLib( return lib; } - - From e11e9ac449cd7373c13a16d600f8f313b5b2cea6 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 26 Aug 2026 21:47:16 +0200 Subject: [PATCH 04/18] ci: install LLVM 22 for Windows builds --- .github/workflows/build_binary.yml | 16 ++++++++++++++++ .github/workflows/deno_tests.yml | 8 ++++++++ .github/workflows/test_unit_cpp.yml | 8 ++++++++ CMakeLists.txt | 18 ++++++++++-------- README.md | 2 +- 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index bffbe35..4d262fa 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -29,6 +29,14 @@ jobs: with: cmake-version: '3.31.x' + - name: 'Install LLVM 22.1.8' + uses: KyleMayes/install-llvm-action@v2 + with: + version: '22.1.8' + arch: x64 + directory: ${{ runner.temp }}/llvm + force-url: 'https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/LLVM-22.1.8-win64.exe' + - name: 'Download ASTrein' shell: pwsh run: | @@ -106,6 +114,14 @@ jobs: with: cmake-version: '3.31.x' + - name: 'Install LLVM 22.1.8' + uses: KyleMayes/install-llvm-action@v2 + with: + version: '22.1.8' + arch: x64 + directory: ${{ runner.temp }}/llvm + force-url: 'https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/LLVM-22.1.8-win64.exe' + - name: 'Configure release' run: | cmake --preset windows-clang-release ` diff --git a/.github/workflows/deno_tests.yml b/.github/workflows/deno_tests.yml index 22f8b25..f8037c3 100644 --- a/.github/workflows/deno_tests.yml +++ b/.github/workflows/deno_tests.yml @@ -23,6 +23,14 @@ jobs: with: cmake-version: "3.31.x" + - name: Install LLVM 22.1.8 + uses: KyleMayes/install-llvm-action@v2 + with: + version: "22.1.8" + arch: x64 + directory: ${{ runner.temp }}/llvm + force-url: "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/LLVM-22.1.8-win64.exe" + - name: Setup Deno uses: denoland/setup-deno@v2 with: diff --git a/.github/workflows/test_unit_cpp.yml b/.github/workflows/test_unit_cpp.yml index 8449001..a84769f 100644 --- a/.github/workflows/test_unit_cpp.yml +++ b/.github/workflows/test_unit_cpp.yml @@ -35,6 +35,14 @@ jobs: with: cmake-version: '3.31.x' + - name: 'Install LLVM 22.1.8' + uses: KyleMayes/install-llvm-action@v2 + with: + version: '22.1.8' + arch: x64 + directory: ${{ runner.temp }}/llvm + force-url: 'https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/LLVM-22.1.8-win64.exe' + - name: 'Configure CMake' run: | cmake --preset windows-clang-release diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fda84c..50c1e36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,14 +30,15 @@ endif() file(WRITE "${CMAKE_BINARY_DIR}/env.bat" "set PACKAGE_VERSION=${GIT_DESCRIBE_NO_V}\n") -# The Windows bindings use the non-reflection cpp-core headers only. Keeping the -# implementation on C++23 lets the MSVC ABI build use the compiler shipped on -# GitHub's Windows image; cpp-core's reflection facilities require C++26 and a -# newer experimental compiler. -set(CMAKE_CXX_STANDARD 23) +# Set C++ standard +set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +# Enable C++26 module support +set(CMAKE_CXX_MODULE_STD 26) +set(CMAKE_CXX_MODULE_EXTENSIONS OFF) + option( CPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT "Enable ASTrein JSON export for the cpp-core FFI headers" @@ -119,7 +120,7 @@ if(CPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT) PRIVATE cpp_bindings_windows_EXPORTS ) - target_compile_features(cpp_bindings_windows_ffi_ast_context PRIVATE cxx_std_23) + target_compile_features(cpp_bindings_windows_ffi_ast_context PRIVATE cxx_std_26) add_custom_command( OUTPUT "${CPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT}" @@ -197,10 +198,11 @@ target_include_directories( target_link_libraries( cpp_bindings_windows PRIVATE + cpp_core::cpp_core setupapi ) -target_compile_features(cpp_bindings_windows PRIVATE cxx_std_23) +target_compile_features(cpp_bindings_windows PRIVATE cxx_std_26) if(MSVC AND CPP_BINDINGS_WINDOWS_STATIC_MSVC_RUNTIME) set_property( @@ -234,7 +236,7 @@ if(TEST_SOURCES) GTest::gtest_main ) - target_compile_features(cpp_bindings_windows_tests PRIVATE cxx_std_23) + target_compile_features(cpp_bindings_windows_tests PRIVATE cxx_std_26) include(GoogleTest) if(CMAKE_CROSSCOMPILING) diff --git a/README.md b/README.md index 67e843c..d16ace9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ opening, configuring, reading from, and writing to serial ports. - CMake 3.30 or newer - Git -- A compiler with C++23 support +- A compiler with sufficient C++26 support - One of: - Windows with Visual Studio 2022 and the C++ workload - Linux with an x86-64 MinGW-w64 toolchain for cross-compilation From a05a629bf1ecbf6aaa45993130e618a3b3e10722 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 26 Aug 2026 21:50:50 +0200 Subject: [PATCH 05/18] ci: use CMake 4.3 with clang-cl --- .github/workflows/build_binary.yml | 4 ++-- .github/workflows/deno_tests.yml | 2 +- .github/workflows/test_unit_cpp.yml | 2 +- README.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index 4d262fa..48b461a 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -27,7 +27,7 @@ jobs: - name: 'Setup CMake' uses: jwlawson/actions-setup-cmake@v2 with: - cmake-version: '3.31.x' + cmake-version: '4.3.x' - name: 'Install LLVM 22.1.8' uses: KyleMayes/install-llvm-action@v2 @@ -112,7 +112,7 @@ jobs: - name: 'Setup CMake' uses: jwlawson/actions-setup-cmake@v2 with: - cmake-version: '3.31.x' + cmake-version: '4.3.x' - name: 'Install LLVM 22.1.8' uses: KyleMayes/install-llvm-action@v2 diff --git a/.github/workflows/deno_tests.yml b/.github/workflows/deno_tests.yml index f8037c3..962f039 100644 --- a/.github/workflows/deno_tests.yml +++ b/.github/workflows/deno_tests.yml @@ -21,7 +21,7 @@ jobs: - name: Setup CMake >= 3.30 uses: jwlawson/actions-setup-cmake@v2 with: - cmake-version: "3.31.x" + cmake-version: "4.3.x" - name: Install LLVM 22.1.8 uses: KyleMayes/install-llvm-action@v2 diff --git a/.github/workflows/test_unit_cpp.yml b/.github/workflows/test_unit_cpp.yml index a84769f..61859fa 100644 --- a/.github/workflows/test_unit_cpp.yml +++ b/.github/workflows/test_unit_cpp.yml @@ -33,7 +33,7 @@ jobs: - name: 'Setup CMake' uses: jwlawson/actions-setup-cmake@v2 with: - cmake-version: '3.31.x' + cmake-version: '4.3.x' - name: 'Install LLVM 22.1.8' uses: KyleMayes/install-llvm-action@v2 diff --git a/README.md b/README.md index d16ace9..c59f514 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ opening, configuring, reading from, and writing to serial ports. ## Requirements -- CMake 3.30 or newer +- CMake 3.30 or newer (4.3 or newer when building with clang-cl) - Git - A compiler with sufficient C++26 support - One of: From 36e4d2d28d95f1fa6fe8a6b82b8d6485e0b7b321 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 26 Aug 2026 21:54:56 +0200 Subject: [PATCH 06/18] build: support Clang 22 frontend flags --- CMakeLists.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 50c1e36..6f649ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,14 @@ CPMAddPackage( "CMAKE_EXPORT_COMPILE_COMMANDS OFF" ) +# clang-cl only forwards Clang frontend flags through /clang:. +if( + CMAKE_CXX_COMPILER_ID STREQUAL "Clang" + AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC" +) + set_property(TARGET cpp_core PROPERTY INTERFACE_COMPILE_OPTIONS "/clang:-freflection") +endif() + if(CPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT) if(CPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE) set(_cpp_bindings_windows_astrein "${CPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE}") @@ -168,6 +176,16 @@ CPMAddPackage( "BUILD_GMOCK OFF" ) +# GoogleTest 1.14 enables /WX internally and triggers this Clang 22 warning in +# its char8_t printer. Keep dependency warnings from breaking our test build. +if( + CMAKE_CXX_COMPILER_ID STREQUAL "Clang" + AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC" +) + target_compile_options(gtest PRIVATE "/clang:-Wno-character-conversion") + target_compile_options(gtest_main PRIVATE "/clang:-Wno-character-conversion") +endif() + include(CTest) enable_testing() From d0d6d497afab684d47214438720d910513c5bb63 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 26 Aug 2026 22:21:33 +0200 Subject: [PATCH 07/18] ci: write unit test report to build root --- .github/workflows/test_unit_cpp.yml | 4 ++-- CMakeLists.txt | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_unit_cpp.yml b/.github/workflows/test_unit_cpp.yml index 61859fa..9fb4f45 100644 --- a/.github/workflows/test_unit_cpp.yml +++ b/.github/workflows/test_unit_cpp.yml @@ -62,10 +62,10 @@ jobs: run: | $testExe = Get-ChildItem -Recurse -Path build -Filter cpp_bindings_windows_tests.exe -File | Select-Object -First 1 if (-not $testExe) { throw "cpp_bindings_windows_tests.exe not found" } + $reportPath = Join-Path (Resolve-Path build).Path $env:TEST_REPORT_NAME Push-Location $testExe.DirectoryName - & $testExe.FullName --gtest_color=yes --gtest_output=xml:$env:TEST_REPORT_NAME + & $testExe.FullName --gtest_color=yes "--gtest_output=xml:$reportPath" Pop-Location - Copy-Item (Join-Path $testExe.DirectoryName $env:TEST_REPORT_NAME) build/ - name: 'Upload test report' if: always() diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f649ef..655abc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,12 +70,14 @@ CPMAddPackage( "CMAKE_EXPORT_COMPILE_COMMANDS OFF" ) -# clang-cl only forwards Clang frontend flags through /clang:. +# The Windows bindings consume cpp-core's C API and non-reflection helpers. +# Released clang-cl 22 does not expose -freflection, so do not propagate that +# experimental option into these targets. if( CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC" ) - set_property(TARGET cpp_core PROPERTY INTERFACE_COMPILE_OPTIONS "/clang:-freflection") + set_property(TARGET cpp_core PROPERTY INTERFACE_COMPILE_OPTIONS "") endif() if(CPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT) From 74a4b4a637e7d862188d4cf6f7f3c5c95a12b817 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Thu, 27 Aug 2026 15:08:14 +0200 Subject: [PATCH 08/18] ci: update ASTrein to v1.2.1 --- .github/workflows/build_binary.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index 48b461a..b999090 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -12,8 +12,8 @@ jobs: name: 'Generate FFI metadata (x86_64-windows-msvc)' runs-on: windows-2025 env: - ASTREIN_VERSION: '1.2.0' - ASTREIN_SHA256: 'd8a4984dca05175a6523530bef5756ef9bf87d0e4e6d58981bee3b9980544149' + ASTREIN_VERSION: '1.2.1' + ASTREIN_SHA256: 'c90eb0e3a24dbdd8775289b30e60d0ee2dfa1c0395c8687aa9a2fabeada2d2df' outputs: package_version: ${{ steps.version.outputs.PACKAGE_VERSION }} is_valid_package_version: ${{ steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION }} @@ -71,6 +71,26 @@ jobs: run: | cmake --build build/ffi --target cpp_bindings_windows_ffi_json + - name: 'Verify FFI metadata' + shell: pwsh + run: | + $metadataPath = 'dist/ffi/x86_64.ffi.json' + $metadata = Get-Content -Raw $metadataPath | + ConvertFrom-Json -ErrorAction Stop + + if ($metadata.schema -ne 'astrein_ffi_api') { + throw "Unexpected FFI metadata schema: $($metadata.schema)" + } + if ($metadata.schemaVersion -ne 1) { + throw "Unexpected FFI metadata schema version: $($metadata.schemaVersion)" + } + + $functionCount = @($metadata.functions).Count + if ($functionCount -eq 0) { + throw 'FFI metadata contains no functions' + } + Write-Host "Verified FFI metadata with $functionCount functions" + - name: 'Set package version' id: version shell: pwsh From 7fd7638e4e87c2f3cf01d34cfba6bf136347eb3a Mon Sep 17 00:00:00 2001 From: Katze719 Date: Fri, 28 Aug 2026 23:48:02 +0200 Subject: [PATCH 09/18] Implement detailed handle management and I/O operations for Windows serial communication - Added functions for registering and removing handle states. - Implemented abort request handling for pending operations. - Created utilities for starting and waiting on pending I/O operations. - Added conversion functions between UTF-8 and wide strings. - Developed a validation function for Win32 handles. - Introduced error handling utilities for Win32 API calls. - Refactored existing serial communication functions to utilize new handle management and I/O utilities. - Removed deprecated win32_helpers.hpp and consolidated functionality into new headers. - Updated serial communication interface to improve error handling and state management. --- src/detail/abort_flag.hpp | 11 + src/detail/acquire_handle_context.hpp | 23 ++ src/detail/apply_line_settings.hpp | 78 +++++++ src/detail/bytes_waiting.hpp | 27 +++ src/detail/consume_abort.hpp | 11 + src/detail/effective_error_callback.hpp | 11 + src/detail/ensure_handle_state.hpp | 17 ++ src/detail/fail_win32.hpp | 18 ++ src/detail/finish_pending_io.hpp | 19 ++ src/detail/handle_key.hpp | 11 + src/detail/handle_state.hpp | 229 -------------------- src/detail/handle_types.hpp | 70 ++++++ src/detail/io_impl.hpp | 277 ------------------------ src/detail/io_types.hpp | 21 ++ src/detail/matches_suffix.hpp | 14 ++ src/detail/multiplier_timeout.hpp | 20 ++ src/detail/normalize_port_path.hpp | 20 ++ src/detail/note_bytes_transferred.hpp | 26 +++ src/detail/pending_operation.hpp | 11 + src/detail/read_chunk.hpp | 40 ++++ src/detail/read_impl.hpp | 86 ++++++++ src/detail/register_opened_handle.hpp | 11 + src/detail/remove_handle_state.hpp | 12 + src/detail/request_abort.hpp | 18 ++ src/detail/start_pending_io.hpp | 24 ++ src/detail/utf8_to_wide.hpp | 30 +++ src/detail/validate_win32_handle.hpp | 41 ++++ src/detail/wait_for_pending_io.hpp | 51 +++++ src/detail/wide_to_utf8.hpp | 32 +++ src/detail/win32_error_to_string.hpp | 29 +++ src/detail/win32_helpers.hpp | 127 ----------- src/detail/windows.hpp | 27 +++ src/detail/write_chunk.hpp | 41 ++++ src/detail/write_impl.hpp | 62 ++++++ src/serial_abort_read.cpp | 3 +- src/serial_abort_write.cpp | 3 +- src/serial_clear_buffer_in.cpp | 4 +- src/serial_clear_buffer_out.cpp | 4 +- src/serial_close.cpp | 4 +- src/serial_drain.cpp | 4 +- src/serial_get_baudrate.cpp | 3 +- src/serial_get_cts.cpp | 3 +- src/serial_get_data_bits.cpp | 3 +- src/serial_get_dcd.cpp | 3 +- src/serial_get_dsr.cpp | 3 +- src/serial_get_flow_control.cpp | 3 +- src/serial_get_parity.cpp | 3 +- src/serial_get_ri.cpp | 3 +- src/serial_get_stop_bits.cpp | 3 +- src/serial_in_bytes_total.cpp | 2 +- src/serial_in_bytes_waiting.cpp | 5 +- src/serial_list_ports.cpp | 5 +- src/serial_monitor_ports.cpp | 4 +- src/serial_open.cpp | 89 +------- src/serial_open.test.cpp | 5 +- src/serial_out_bytes_total.cpp | 2 +- src/serial_out_bytes_waiting.cpp | 4 +- src/serial_read.cpp | 2 +- src/serial_read_line.cpp | 2 +- src/serial_read_until.cpp | 2 +- src/serial_read_until_sequence.cpp | 2 +- src/serial_send_break.cpp | 3 +- src/serial_set_baudrate.cpp | 3 +- src/serial_set_data_bits.cpp | 3 +- src/serial_set_dtr.cpp | 3 +- src/serial_set_flow_control.cpp | 3 +- src/serial_set_parity.cpp | 3 +- src/serial_set_read_callback.cpp | 2 +- src/serial_set_rts.cpp | 3 +- src/serial_set_stop_bits.cpp | 3 +- src/serial_set_write_callback.cpp | 2 +- src/serial_write.cpp | 2 +- 72 files changed, 988 insertions(+), 760 deletions(-) create mode 100644 src/detail/abort_flag.hpp create mode 100644 src/detail/acquire_handle_context.hpp create mode 100644 src/detail/apply_line_settings.hpp create mode 100644 src/detail/bytes_waiting.hpp create mode 100644 src/detail/consume_abort.hpp create mode 100644 src/detail/effective_error_callback.hpp create mode 100644 src/detail/ensure_handle_state.hpp create mode 100644 src/detail/fail_win32.hpp create mode 100644 src/detail/finish_pending_io.hpp create mode 100644 src/detail/handle_key.hpp delete mode 100644 src/detail/handle_state.hpp create mode 100644 src/detail/handle_types.hpp delete mode 100644 src/detail/io_impl.hpp create mode 100644 src/detail/io_types.hpp create mode 100644 src/detail/matches_suffix.hpp create mode 100644 src/detail/multiplier_timeout.hpp create mode 100644 src/detail/normalize_port_path.hpp create mode 100644 src/detail/note_bytes_transferred.hpp create mode 100644 src/detail/pending_operation.hpp create mode 100644 src/detail/read_chunk.hpp create mode 100644 src/detail/read_impl.hpp create mode 100644 src/detail/register_opened_handle.hpp create mode 100644 src/detail/remove_handle_state.hpp create mode 100644 src/detail/request_abort.hpp create mode 100644 src/detail/start_pending_io.hpp create mode 100644 src/detail/utf8_to_wide.hpp create mode 100644 src/detail/validate_win32_handle.hpp create mode 100644 src/detail/wait_for_pending_io.hpp create mode 100644 src/detail/wide_to_utf8.hpp create mode 100644 src/detail/win32_error_to_string.hpp delete mode 100644 src/detail/win32_helpers.hpp create mode 100644 src/detail/windows.hpp create mode 100644 src/detail/write_chunk.hpp create mode 100644 src/detail/write_impl.hpp diff --git a/src/detail/abort_flag.hpp b/src/detail/abort_flag.hpp new file mode 100644 index 0000000..ddaabdc --- /dev/null +++ b/src/detail/abort_flag.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "handle_types.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto abortFlag(const std::shared_ptr &state, Operation operation) -> std::atomic & +{ + return operation == Operation::kRead ? state->abort_read : state->abort_write; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/acquire_handle_context.hpp b/src/detail/acquire_handle_context.hpp new file mode 100644 index 0000000..1b7f0fd --- /dev/null +++ b/src/detail/acquire_handle_context.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "ensure_handle_state.hpp" +#include "validate_win32_handle.hpp" + +namespace cpp_bindings_windows::detail +{ +template +inline auto acquireHandleContext(int64_t handle, ErrorCallbackT error_callback, HandleContext *out_context) + -> ReturnType +{ + HANDLE native_handle = nullptr; + const auto status = validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + out_context->handle = native_handle; + out_context->state = ensureHandleState(native_handle); + return static_cast(StatusCode::kSuccess); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/apply_line_settings.hpp b/src/detail/apply_line_settings.hpp new file mode 100644 index 0000000..de90818 --- /dev/null +++ b/src/detail/apply_line_settings.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include "win32_error_to_string.hpp" + +#include +#include + +namespace cpp_bindings_windows::detail +{ +inline auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Parity parity_value, + cpp_core::StopBits stop_bits_value) -> cpp_core::Status +{ + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + + if (GetCommState(handle, &dcb) == 0) + { + const DWORD error = GetLastError(); + return cpp_core::fail(cpp_core::StatusCode::Control::kGetStateError, + "GetCommState failed: " + win32ErrorToString(error)); + } + + dcb.BaudRate = static_cast(baudrate); + dcb.ByteSize = static_cast(data_bits); + + dcb.fBinary = TRUE; + dcb.fParity = (parity_value != cpp_core::Parity::kNone) ? TRUE : FALSE; + dcb.fOutxCtsFlow = FALSE; + dcb.fOutxDsrFlow = FALSE; + dcb.fDtrControl = DTR_CONTROL_ENABLE; + dcb.fDsrSensitivity = FALSE; + dcb.fTXContinueOnXoff = TRUE; + dcb.fOutX = FALSE; + dcb.fInX = FALSE; + dcb.fRtsControl = RTS_CONTROL_ENABLE; + + switch (parity_value) + { + case cpp_core::Parity::kNone: + dcb.Parity = NOPARITY; + break; + case cpp_core::Parity::kEven: + dcb.Parity = EVENPARITY; + break; + case cpp_core::Parity::kOdd: + dcb.Parity = ODDPARITY; + break; + default: + return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, "Invalid parity"); + } + + if (stop_bits_value == cpp_core::StopBits::kOne) + { + dcb.StopBits = ONESTOPBIT; + } + else if (stop_bits_value == cpp_core::StopBits::kTwo) + { + dcb.StopBits = TWOSTOPBITS; + } + + if (SetCommState(handle, &dcb) == 0) + { + const DWORD error = GetLastError(); + return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, + "SetCommState failed: " + win32ErrorToString(error)); + } + + COMMTIMEOUTS timeouts = {}; + if (SetCommTimeouts(handle, &timeouts) == 0) + { + const DWORD error = GetLastError(); + return cpp_core::fail(cpp_core::StatusCode::Configuration::kSetTimeoutError, + "SetCommTimeouts failed: " + win32ErrorToString(error)); + } + + return cpp_core::ok(); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/bytes_waiting.hpp b/src/detail/bytes_waiting.hpp new file mode 100644 index 0000000..a80c33f --- /dev/null +++ b/src/detail/bytes_waiting.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "windows.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto bytesWaiting(HANDLE handle, int *out_bytes) -> bool +{ + if (out_bytes == nullptr) + { + return false; + } + *out_bytes = 0; + + DWORD errors = 0; + COMSTAT status = {}; + if (ClearCommError(handle, &errors, &status) == 0) + { + return false; + } + + *out_bytes = status.cbInQue > static_cast(INT_MAX) ? INT_MAX : static_cast(status.cbInQue); + return true; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/consume_abort.hpp b/src/detail/consume_abort.hpp new file mode 100644 index 0000000..a3d2ce3 --- /dev/null +++ b/src/detail/consume_abort.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "abort_flag.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto consumeAbort(const std::shared_ptr &state, Operation operation) -> bool +{ + return abortFlag(state, operation).exchange(false, std::memory_order_acq_rel); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/effective_error_callback.hpp b/src/detail/effective_error_callback.hpp new file mode 100644 index 0000000..e0f7da8 --- /dev/null +++ b/src/detail/effective_error_callback.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "common_types.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto effectiveErrorCallback(ErrorCallbackT error_callback) -> ErrorCallbackT +{ + return error_callback != nullptr ? error_callback : g_error_callback.load(std::memory_order_acquire); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/ensure_handle_state.hpp b/src/detail/ensure_handle_state.hpp new file mode 100644 index 0000000..3bbbbc5 --- /dev/null +++ b/src/detail/ensure_handle_state.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "handle_key.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto ensureHandleState(HANDLE handle) -> std::shared_ptr +{ + std::lock_guard lock(g_handle_states_mutex); + auto &state = g_handle_states[handleKey(handle)]; + if (!state) + { + state = std::make_shared(); + } + return state; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/fail_win32.hpp b/src/detail/fail_win32.hpp new file mode 100644 index 0000000..a0c4899 --- /dev/null +++ b/src/detail/fail_win32.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "effective_error_callback.hpp" +#include "win32_error_to_string.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +template +inline auto failWin32(ErrorCallbackT error_callback, StatusCodeValue code) -> ReturnType +{ + const DWORD error = GetLastError(); + const std::string message = win32ErrorToString(error); + cpp_core::invokeError(effectiveErrorCallback(error_callback), code, message); + return static_cast(code); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/finish_pending_io.hpp b/src/detail/finish_pending_io.hpp new file mode 100644 index 0000000..d8d2ad0 --- /dev/null +++ b/src/detail/finish_pending_io.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "consume_abort.hpp" +#include "pending_operation.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto finishPendingIo(const std::shared_ptr &state, Operation operation, OVERLAPPED *overlapped) + -> bool +{ + std::lock_guard lock(state->pending_io_mutex); + auto &pending = pendingOperation(state, operation); + if (pending == overlapped) + { + pending = nullptr; + } + return consumeAbort(state, operation); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/handle_key.hpp b/src/detail/handle_key.hpp new file mode 100644 index 0000000..dc6f8df --- /dev/null +++ b/src/detail/handle_key.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "handle_types.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto handleKey(HANDLE handle) -> std::uintptr_t +{ + return reinterpret_cast(handle); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/handle_state.hpp b/src/detail/handle_state.hpp deleted file mode 100644 index d27ee59..0000000 --- a/src/detail/handle_state.hpp +++ /dev/null @@ -1,229 +0,0 @@ -#pragma once - -#include "common_types.hpp" - -#include -#include - -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace cpp_bindings_windows::detail -{ -enum class Operation -{ - kRead, - kWrite, -}; - -struct Win32HandleTraits -{ - using handle_type = HANDLE; // NOLINT(readability-identifier-naming) - - static constexpr auto invalid() noexcept -> handle_type - { - return nullptr; - } - - static auto close(handle_type handle) noexcept -> void - { - if (handle != nullptr && handle != INVALID_HANDLE_VALUE) - { - CloseHandle(handle); - } - } -}; - -using UniqueHandle = cpp_core::UniqueResource; - -struct HandleState -{ - std::atomic bytes_read_total{0}; - std::atomic bytes_written_total{0}; - std::atomic abort_read{false}; - std::atomic abort_write{false}; - std::mutex pending_io_mutex; - OVERLAPPED *pending_read = nullptr; - OVERLAPPED *pending_write = nullptr; -}; - -struct HandleContext -{ - HANDLE handle = nullptr; - std::shared_ptr state; -}; - -struct PendingIoStart -{ - BOOL completed = FALSE; - DWORD error = ERROR_SUCCESS; - bool aborted = false; -}; - -inline std::mutex g_handle_states_mutex; -inline std::unordered_map> g_handle_states; -inline std::atomic g_read_callback{nullptr}; -inline std::atomic g_write_callback{nullptr}; - -inline auto handleKey(HANDLE handle) -> std::uintptr_t -{ - return reinterpret_cast(handle); -} - -inline auto effectiveErrorCallback(ErrorCallbackT error_callback) -> ErrorCallbackT -{ - return error_callback != nullptr ? error_callback : g_error_callback.load(std::memory_order_acquire); -} - -inline auto ensureHandleState(HANDLE handle) -> std::shared_ptr -{ - std::lock_guard lock(g_handle_states_mutex); - auto &state = g_handle_states[handleKey(handle)]; - if (!state) - { - state = std::make_shared(); - } - return state; -} - -inline auto registerOpenedHandle(HANDLE handle) -> void -{ - (void)ensureHandleState(handle); -} - -inline auto removeHandleState(HANDLE handle) -> void -{ - std::lock_guard lock(g_handle_states_mutex); - g_handle_states.erase(handleKey(handle)); -} - -template -inline auto validateWin32Handle(int64_t handle, ErrorCallbackT error_callback, HANDLE *out_handle) -> ReturnType -{ - const auto callback = effectiveErrorCallback(error_callback); - if (handle <= 0) - { - return cpp_core::failMsg( - callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); - } - - if constexpr (sizeof(intptr_t) < sizeof(int64_t)) - { - if (handle > static_cast(std::numeric_limits::max())) - { - return cpp_core::failMsg( - callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); - } - } - - const auto native_handle = reinterpret_cast(static_cast(handle)); - if (native_handle == nullptr || native_handle == INVALID_HANDLE_VALUE) - { - return cpp_core::failMsg( - callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); - } - - *out_handle = native_handle; - return static_cast(StatusCode::kSuccess); -} - -template -inline auto acquireHandleContext(int64_t handle, ErrorCallbackT error_callback, HandleContext *out_context) - -> ReturnType -{ - HANDLE native_handle = nullptr; - const auto status = validateWin32Handle(handle, error_callback, &native_handle); - if (status < 0) - { - return status; - } - - out_context->handle = native_handle; - out_context->state = ensureHandleState(native_handle); - return static_cast(StatusCode::kSuccess); -} - -inline auto abortFlag(const std::shared_ptr &state, Operation operation) -> std::atomic & -{ - return operation == Operation::kRead ? state->abort_read : state->abort_write; -} - -inline auto pendingOperation(const std::shared_ptr &state, Operation operation) -> OVERLAPPED *& -{ - return operation == Operation::kRead ? state->pending_read : state->pending_write; -} - -inline auto requestAbort(HANDLE handle, const std::shared_ptr &state, Operation operation) -> void -{ - abortFlag(state, operation).store(true, std::memory_order_release); - - std::lock_guard lock(state->pending_io_mutex); - if (auto *pending = pendingOperation(state, operation); pending != nullptr) - { - (void)CancelIoEx(handle, pending); - } -} - -inline auto consumeAbort(const std::shared_ptr &state, Operation operation) -> bool -{ - return abortFlag(state, operation).exchange(false, std::memory_order_acq_rel); -} - -template -inline auto startPendingIo(const std::shared_ptr &state, Operation operation, OVERLAPPED *overlapped, - StartOperation &&start_operation) -> PendingIoStart -{ - std::lock_guard lock(state->pending_io_mutex); - if (consumeAbort(state, operation)) - { - return {.aborted = true}; - } - - pendingOperation(state, operation) = overlapped; - const BOOL completed = std::forward(start_operation)(); - return {.completed = completed, .error = completed != FALSE ? ERROR_SUCCESS : GetLastError()}; -} - -inline auto finishPendingIo(const std::shared_ptr &state, Operation operation, OVERLAPPED *overlapped) - -> bool -{ - std::lock_guard lock(state->pending_io_mutex); - auto &pending = pendingOperation(state, operation); - if (pending == overlapped) - { - pending = nullptr; - } - return consumeAbort(state, operation); -} - -inline auto noteBytesTransferred(const std::shared_ptr &state, Operation operation, int transferred_bytes) - -> void -{ - if (operation == Operation::kRead) - { - state->bytes_read_total.fetch_add(transferred_bytes, std::memory_order_relaxed); - if (const auto callback = g_read_callback.load(std::memory_order_acquire); callback != nullptr) - { - callback(transferred_bytes); - } - return; - } - - state->bytes_written_total.fetch_add(transferred_bytes, std::memory_order_relaxed); - if (const auto callback = g_write_callback.load(std::memory_order_acquire); callback != nullptr) - { - callback(transferred_bytes); - } -} - -} // namespace cpp_bindings_windows::detail diff --git a/src/detail/handle_types.hpp b/src/detail/handle_types.hpp new file mode 100644 index 0000000..fa5ede4 --- /dev/null +++ b/src/detail/handle_types.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include "common_types.hpp" +#include "windows.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace cpp_bindings_windows::detail +{ +enum class Operation +{ + kRead, + kWrite, +}; + +struct Win32HandleTraits +{ + using handle_type = HANDLE; // NOLINT(readability-identifier-naming) + + static constexpr auto invalid() noexcept -> handle_type + { + return nullptr; + } + + static auto close(handle_type handle) noexcept -> void + { + if (handle != nullptr && handle != INVALID_HANDLE_VALUE) + { + CloseHandle(handle); + } + } +}; + +using UniqueHandle = cpp_core::UniqueResource; + +struct HandleState +{ + std::atomic bytes_read_total{0}; + std::atomic bytes_written_total{0}; + std::atomic abort_read{false}; + std::atomic abort_write{false}; + std::mutex pending_io_mutex; + OVERLAPPED *pending_read = nullptr; + OVERLAPPED *pending_write = nullptr; +}; + +struct HandleContext +{ + HANDLE handle = nullptr; + std::shared_ptr state; +}; + +struct PendingIoStart +{ + BOOL completed = FALSE; + DWORD error = ERROR_SUCCESS; + bool aborted = false; +}; + +inline std::mutex g_handle_states_mutex; +inline std::unordered_map> g_handle_states; +inline std::atomic g_read_callback{nullptr}; +inline std::atomic g_write_callback{nullptr}; +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/io_impl.hpp b/src/detail/io_impl.hpp deleted file mode 100644 index 8aa11bb..0000000 --- a/src/detail/io_impl.hpp +++ /dev/null @@ -1,277 +0,0 @@ -#pragma once - -#include "handle_state.hpp" -#include "win32_helpers.hpp" - -#include - -#include -#include -#include - -namespace cpp_bindings_windows::detail -{ -enum class IoOutcome -{ - kCompleted, - kTimedOut, - kAborted, - kError, -}; - -struct IoResult -{ - IoOutcome outcome = IoOutcome::kError; - int bytes_transferred = 0; - DWORD error = ERROR_SUCCESS; -}; - -inline auto multiplierTimeout(int timeout_ms, int multiplier) -> int -{ - if (multiplier <= 0) - { - return 0; - } - - const auto timeout = static_cast(cpp_core::clampTimeout(timeout_ms)) * multiplier; - return timeout > INT_MAX ? INT_MAX : static_cast(timeout); -} - -inline auto waitForPendingIo(HANDLE handle, const std::shared_ptr &state, Operation operation, - OVERLAPPED *overlapped, int timeout_ms) -> IoResult -{ - const DWORD wait_result = - WaitForSingleObject(overlapped->hEvent, static_cast(cpp_core::clampTimeout(timeout_ms))); - if (wait_result == WAIT_TIMEOUT) - { - (void)CancelIoEx(handle, overlapped); - DWORD ignored = 0; - (void)GetOverlappedResult(handle, overlapped, &ignored, TRUE); - if (finishPendingIo(state, operation, overlapped)) - { - return {.outcome = IoOutcome::kAborted}; - } - return {.outcome = IoOutcome::kTimedOut}; - } - - if (wait_result != WAIT_OBJECT_0) - { - const DWORD error = GetLastError(); - (void)CancelIoEx(handle, overlapped); - DWORD ignored = 0; - (void)GetOverlappedResult(handle, overlapped, &ignored, TRUE); - const bool aborted = finishPendingIo(state, operation, overlapped); - return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = error}; - } - - DWORD transferred = 0; - const BOOL completed = GetOverlappedResult(handle, overlapped, &transferred, FALSE); - const DWORD error = completed != FALSE ? ERROR_SUCCESS : GetLastError(); - const bool aborted = finishPendingIo(state, operation, overlapped); - if (aborted || error == ERROR_OPERATION_ABORTED) - { - return {.outcome = IoOutcome::kAborted}; - } - if (completed == FALSE) - { - return {.outcome = IoOutcome::kError, .error = error}; - } - return {.outcome = IoOutcome::kCompleted, .bytes_transferred = static_cast(transferred)}; -} - -inline auto readChunk(const HandleContext &context, unsigned char *buffer, int buffer_size, int timeout_ms) -> IoResult -{ - UniqueHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); - if (!event) - { - return {.outcome = IoOutcome::kError, .error = GetLastError()}; - } - - OVERLAPPED overlapped = {}; - overlapped.hEvent = event.get(); - DWORD transferred = 0; - const auto start = startPendingIo(context.state, Operation::kRead, &overlapped, [&] { - return ReadFile(context.handle, buffer, static_cast(buffer_size), &transferred, &overlapped); - }); - if (start.aborted) - { - return {.outcome = IoOutcome::kAborted}; - } - if (start.completed != FALSE) - { - const bool aborted = finishPendingIo(context.state, Operation::kRead, &overlapped); - return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kCompleted, - .bytes_transferred = static_cast(transferred)}; - } - if (start.error != ERROR_IO_PENDING) - { - const bool aborted = finishPendingIo(context.state, Operation::kRead, &overlapped); - return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = start.error}; - } - - return waitForPendingIo(context.handle, context.state, Operation::kRead, &overlapped, timeout_ms); -} - -inline auto writeChunk(const HandleContext &context, const unsigned char *buffer, int buffer_size, int timeout_ms) - -> IoResult -{ - UniqueHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); - if (!event) - { - return {.outcome = IoOutcome::kError, .error = GetLastError()}; - } - - OVERLAPPED overlapped = {}; - overlapped.hEvent = event.get(); - DWORD transferred = 0; - const auto start = startPendingIo(context.state, Operation::kWrite, &overlapped, [&] { - return WriteFile(context.handle, buffer, static_cast(buffer_size), &transferred, &overlapped); - }); - if (start.aborted) - { - return {.outcome = IoOutcome::kAborted}; - } - if (start.completed != FALSE) - { - const bool aborted = finishPendingIo(context.state, Operation::kWrite, &overlapped); - return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kCompleted, - .bytes_transferred = static_cast(transferred)}; - } - if (start.error != ERROR_IO_PENDING) - { - const bool aborted = finishPendingIo(context.state, Operation::kWrite, &overlapped); - return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = start.error}; - } - - return waitForPendingIo(context.handle, context.state, Operation::kWrite, &overlapped, timeout_ms); -} - -inline auto matchesSuffix(const unsigned char *buffer, int buffer_size, const unsigned char *terminator, - int terminator_size) -> bool -{ - return terminator_size > 0 && buffer_size >= terminator_size && - std::memcmp(buffer + buffer_size - terminator_size, terminator, static_cast(terminator_size)) == - 0; -} - -inline auto readImpl(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, - const unsigned char *terminator, int terminator_size, ErrorCallbackT error_callback) -> int -{ - const auto callback = effectiveErrorCallback(error_callback); - const auto buffer_status = cpp_core::validateBuffer(buffer, buffer_size, callback); - if (buffer_status < 0) - { - return buffer_status; - } - if (terminator_size > 0 && terminator == nullptr) - { - return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kBufferError), - "Invalid terminator"); - } - - HandleContext context; - const auto handle_status = acquireHandleContext(handle, callback, &context); - if (handle_status < 0) - { - return handle_status; - } - - auto *output = static_cast(buffer); - int total_read = 0; - while (total_read < buffer_size) - { - int chunk_size = 1; - if (terminator_size <= 0) - { - int waiting = 0; - if (!bytesWaiting(context.handle, &waiting)) - { - return failWin32(callback, static_cast(StatusCode::Control::kGetStateError)); - } - chunk_size = waiting > 0 ? std::min(waiting, buffer_size - total_read) : 1; - } - - const int current_timeout = - total_read == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); - const auto result = readChunk(context, output + total_read, chunk_size, current_timeout); - if (result.outcome == IoOutcome::kTimedOut) - { - return total_read; - } - if (result.outcome == IoOutcome::kAborted) - { - return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kAbortReadError), - "Read aborted"); - } - if (result.outcome == IoOutcome::kError) - { - SetLastError(result.error); - return failWin32(callback, static_cast(StatusCode::Io::kReadError)); - } - if (result.bytes_transferred <= 0) - { - return total_read; - } - - noteBytesTransferred(context.state, Operation::kRead, result.bytes_transferred); - total_read += result.bytes_transferred; - if (matchesSuffix(output, total_read, terminator, terminator_size)) - { - return total_read; - } - } - - return total_read; -} - -inline auto writeImpl(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int multiplier, - ErrorCallbackT error_callback) -> int -{ - const auto callback = effectiveErrorCallback(error_callback); - const auto buffer_status = cpp_core::validateBuffer(buffer, buffer_size, callback); - if (buffer_status < 0) - { - return buffer_status; - } - - HandleContext context; - const auto handle_status = acquireHandleContext(handle, callback, &context); - if (handle_status < 0) - { - return handle_status; - } - - const auto *input = static_cast(buffer); - int total_written = 0; - while (total_written < buffer_size) - { - const int current_timeout = - total_written == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); - const auto result = writeChunk(context, input + total_written, buffer_size - total_written, current_timeout); - if (result.outcome == IoOutcome::kTimedOut) - { - return total_written; - } - if (result.outcome == IoOutcome::kAborted) - { - return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kAbortWriteError), - "Write aborted"); - } - if (result.outcome == IoOutcome::kError) - { - SetLastError(result.error); - return failWin32(callback, static_cast(StatusCode::Io::kWriteError)); - } - if (result.bytes_transferred <= 0) - { - return total_written; - } - - noteBytesTransferred(context.state, Operation::kWrite, result.bytes_transferred); - total_written += result.bytes_transferred; - } - - return total_written; -} - -} // namespace cpp_bindings_windows::detail diff --git a/src/detail/io_types.hpp b/src/detail/io_types.hpp new file mode 100644 index 0000000..adc98c2 --- /dev/null +++ b/src/detail/io_types.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "windows.hpp" + +namespace cpp_bindings_windows::detail +{ +enum class IoOutcome +{ + kCompleted, + kTimedOut, + kAborted, + kError, +}; + +struct IoResult +{ + IoOutcome outcome = IoOutcome::kError; + int bytes_transferred = 0; + DWORD error = ERROR_SUCCESS; +}; +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/matches_suffix.hpp b/src/detail/matches_suffix.hpp new file mode 100644 index 0000000..21d1ba6 --- /dev/null +++ b/src/detail/matches_suffix.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto matchesSuffix(const unsigned char *buffer, int buffer_size, const unsigned char *terminator, + int terminator_size) -> bool +{ + return terminator_size > 0 && buffer_size >= terminator_size && + std::memcmp(buffer + buffer_size - terminator_size, terminator, static_cast(terminator_size)) == + 0; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/multiplier_timeout.hpp b/src/detail/multiplier_timeout.hpp new file mode 100644 index 0000000..5f2eb63 --- /dev/null +++ b/src/detail/multiplier_timeout.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include +#include + +namespace cpp_bindings_windows::detail +{ +inline auto multiplierTimeout(int timeout_ms, int multiplier) -> int +{ + if (multiplier <= 0) + { + return 0; + } + + const auto timeout = static_cast(cpp_core::clampTimeout(timeout_ms)) * multiplier; + return timeout > INT_MAX ? INT_MAX : static_cast(timeout); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/normalize_port_path.hpp b/src/detail/normalize_port_path.hpp new file mode 100644 index 0000000..8e4865d --- /dev/null +++ b/src/detail/normalize_port_path.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace cpp_bindings_windows::detail +{ +inline auto normalizePortPath(std::wstring_view port) -> std::wstring +{ + if (port.starts_with(L"\\\\.\\")) + { + return std::wstring(port); + } + if (port.starts_with(L"COM") || port.starts_with(L"com")) + { + return L"\\\\.\\" + std::wstring(port); + } + return std::wstring(port); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/note_bytes_transferred.hpp b/src/detail/note_bytes_transferred.hpp new file mode 100644 index 0000000..a046545 --- /dev/null +++ b/src/detail/note_bytes_transferred.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "handle_types.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto noteBytesTransferred(const std::shared_ptr &state, Operation operation, int transferred_bytes) + -> void +{ + if (operation == Operation::kRead) + { + state->bytes_read_total.fetch_add(transferred_bytes, std::memory_order_relaxed); + if (const auto callback = g_read_callback.load(std::memory_order_acquire); callback != nullptr) + { + callback(transferred_bytes); + } + return; + } + + state->bytes_written_total.fetch_add(transferred_bytes, std::memory_order_relaxed); + if (const auto callback = g_write_callback.load(std::memory_order_acquire); callback != nullptr) + { + callback(transferred_bytes); + } +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/pending_operation.hpp b/src/detail/pending_operation.hpp new file mode 100644 index 0000000..08528a2 --- /dev/null +++ b/src/detail/pending_operation.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "handle_types.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto pendingOperation(const std::shared_ptr &state, Operation operation) -> OVERLAPPED *& +{ + return operation == Operation::kRead ? state->pending_read : state->pending_write; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/read_chunk.hpp b/src/detail/read_chunk.hpp new file mode 100644 index 0000000..5054c5f --- /dev/null +++ b/src/detail/read_chunk.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "start_pending_io.hpp" +#include "wait_for_pending_io.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto readChunk(const HandleContext &context, unsigned char *buffer, int buffer_size, int timeout_ms) -> IoResult +{ + UniqueHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!event) + { + return {.outcome = IoOutcome::kError, .error = GetLastError()}; + } + + OVERLAPPED overlapped = {}; + overlapped.hEvent = event.get(); + DWORD transferred = 0; + const auto start = startPendingIo(context.state, Operation::kRead, &overlapped, [&] { + return ReadFile(context.handle, buffer, static_cast(buffer_size), &transferred, &overlapped); + }); + if (start.aborted) + { + return {.outcome = IoOutcome::kAborted}; + } + if (start.completed != FALSE) + { + const bool aborted = finishPendingIo(context.state, Operation::kRead, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kCompleted, + .bytes_transferred = static_cast(transferred)}; + } + if (start.error != ERROR_IO_PENDING) + { + const bool aborted = finishPendingIo(context.state, Operation::kRead, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = start.error}; + } + + return waitForPendingIo(context.handle, context.state, Operation::kRead, &overlapped, timeout_ms); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/read_impl.hpp b/src/detail/read_impl.hpp new file mode 100644 index 0000000..92ca7e9 --- /dev/null +++ b/src/detail/read_impl.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include "acquire_handle_context.hpp" +#include "bytes_waiting.hpp" +#include "fail_win32.hpp" +#include "matches_suffix.hpp" +#include "multiplier_timeout.hpp" +#include "note_bytes_transferred.hpp" +#include "read_chunk.hpp" + +#include + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto readImpl(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, + const unsigned char *terminator, int terminator_size, ErrorCallbackT error_callback) -> int +{ + const auto callback = effectiveErrorCallback(error_callback); + const auto buffer_status = cpp_core::validateBuffer(buffer, buffer_size, callback); + if (buffer_status < 0) + { + return buffer_status; + } + if (terminator_size > 0 && terminator == nullptr) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kBufferError), + "Invalid terminator"); + } + + HandleContext context; + const auto handle_status = acquireHandleContext(handle, callback, &context); + if (handle_status < 0) + { + return handle_status; + } + + auto *output = static_cast(buffer); + int total_read = 0; + while (total_read < buffer_size) + { + int chunk_size = 1; + if (terminator_size <= 0) + { + int waiting = 0; + if (!bytesWaiting(context.handle, &waiting)) + { + return failWin32(callback, static_cast(StatusCode::Control::kGetStateError)); + } + chunk_size = waiting > 0 ? std::min(waiting, buffer_size - total_read) : 1; + } + + const int current_timeout = + total_read == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); + const auto result = readChunk(context, output + total_read, chunk_size, current_timeout); + if (result.outcome == IoOutcome::kTimedOut) + { + return total_read; + } + if (result.outcome == IoOutcome::kAborted) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kAbortReadError), + "Read aborted"); + } + if (result.outcome == IoOutcome::kError) + { + SetLastError(result.error); + return failWin32(callback, static_cast(StatusCode::Io::kReadError)); + } + if (result.bytes_transferred <= 0) + { + return total_read; + } + + noteBytesTransferred(context.state, Operation::kRead, result.bytes_transferred); + total_read += result.bytes_transferred; + if (matchesSuffix(output, total_read, terminator, terminator_size)) + { + return total_read; + } + } + + return total_read; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/register_opened_handle.hpp b/src/detail/register_opened_handle.hpp new file mode 100644 index 0000000..912e5bf --- /dev/null +++ b/src/detail/register_opened_handle.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ensure_handle_state.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto registerOpenedHandle(HANDLE handle) -> void +{ + (void)ensureHandleState(handle); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/remove_handle_state.hpp b/src/detail/remove_handle_state.hpp new file mode 100644 index 0000000..66de817 --- /dev/null +++ b/src/detail/remove_handle_state.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "handle_key.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto removeHandleState(HANDLE handle) -> void +{ + std::lock_guard lock(g_handle_states_mutex); + g_handle_states.erase(handleKey(handle)); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/request_abort.hpp b/src/detail/request_abort.hpp new file mode 100644 index 0000000..dc990a3 --- /dev/null +++ b/src/detail/request_abort.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "abort_flag.hpp" +#include "pending_operation.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto requestAbort(HANDLE handle, const std::shared_ptr &state, Operation operation) -> void +{ + abortFlag(state, operation).store(true, std::memory_order_release); + + std::lock_guard lock(state->pending_io_mutex); + if (auto *pending = pendingOperation(state, operation); pending != nullptr) + { + (void)CancelIoEx(handle, pending); + } +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/start_pending_io.hpp b/src/detail/start_pending_io.hpp new file mode 100644 index 0000000..b46c6f9 --- /dev/null +++ b/src/detail/start_pending_io.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "consume_abort.hpp" +#include "pending_operation.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +template +inline auto startPendingIo(const std::shared_ptr &state, Operation operation, OVERLAPPED *overlapped, + StartOperation &&start_operation) -> PendingIoStart +{ + std::lock_guard lock(state->pending_io_mutex); + if (consumeAbort(state, operation)) + { + return {.aborted = true}; + } + + pendingOperation(state, operation) = overlapped; + const BOOL completed = std::forward(start_operation)(); + return {.completed = completed, .error = completed != FALSE ? ERROR_SUCCESS : GetLastError()}; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/utf8_to_wide.hpp b/src/detail/utf8_to_wide.hpp new file mode 100644 index 0000000..4f0bae3 --- /dev/null +++ b/src/detail/utf8_to_wide.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "windows.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto utf8ToWide(const char *utf8) -> std::wstring +{ + if (utf8 == nullptr || *utf8 == '\0') + { + return {}; + } + + const int required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, -1, nullptr, 0); + if (required <= 0) + { + return {}; + } + + std::wstring wide(static_cast(required), L'\0'); + if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, -1, wide.data(), required) <= 0) + { + return {}; + } + wide.pop_back(); + return wide; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/validate_win32_handle.hpp b/src/detail/validate_win32_handle.hpp new file mode 100644 index 0000000..e75e982 --- /dev/null +++ b/src/detail/validate_win32_handle.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "effective_error_callback.hpp" +#include "handle_types.hpp" + +#include + +#include + +namespace cpp_bindings_windows::detail +{ +template +inline auto validateWin32Handle(int64_t handle, ErrorCallbackT error_callback, HANDLE *out_handle) -> ReturnType +{ + const auto callback = effectiveErrorCallback(error_callback); + if (handle <= 0) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + + if constexpr (sizeof(intptr_t) < sizeof(int64_t)) + { + if (handle > static_cast(std::numeric_limits::max())) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + } + + const auto native_handle = reinterpret_cast(static_cast(handle)); + if (native_handle == nullptr || native_handle == INVALID_HANDLE_VALUE) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + + *out_handle = native_handle; + return static_cast(StatusCode::kSuccess); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/wait_for_pending_io.hpp b/src/detail/wait_for_pending_io.hpp new file mode 100644 index 0000000..839f726 --- /dev/null +++ b/src/detail/wait_for_pending_io.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "finish_pending_io.hpp" +#include "io_types.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto waitForPendingIo(HANDLE handle, const std::shared_ptr &state, Operation operation, + OVERLAPPED *overlapped, int timeout_ms) -> IoResult +{ + const DWORD wait_result = + WaitForSingleObject(overlapped->hEvent, static_cast(cpp_core::clampTimeout(timeout_ms))); + if (wait_result == WAIT_TIMEOUT) + { + (void)CancelIoEx(handle, overlapped); + DWORD ignored = 0; + (void)GetOverlappedResult(handle, overlapped, &ignored, TRUE); + if (finishPendingIo(state, operation, overlapped)) + { + return {.outcome = IoOutcome::kAborted}; + } + return {.outcome = IoOutcome::kTimedOut}; + } + + if (wait_result != WAIT_OBJECT_0) + { + const DWORD error = GetLastError(); + (void)CancelIoEx(handle, overlapped); + DWORD ignored = 0; + (void)GetOverlappedResult(handle, overlapped, &ignored, TRUE); + const bool aborted = finishPendingIo(state, operation, overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = error}; + } + + DWORD transferred = 0; + const BOOL completed = GetOverlappedResult(handle, overlapped, &transferred, FALSE); + const DWORD error = completed != FALSE ? ERROR_SUCCESS : GetLastError(); + const bool aborted = finishPendingIo(state, operation, overlapped); + if (aborted || error == ERROR_OPERATION_ABORTED) + { + return {.outcome = IoOutcome::kAborted}; + } + if (completed == FALSE) + { + return {.outcome = IoOutcome::kError, .error = error}; + } + return {.outcome = IoOutcome::kCompleted, .bytes_transferred = static_cast(transferred)}; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/wide_to_utf8.hpp b/src/detail/wide_to_utf8.hpp new file mode 100644 index 0000000..8d0cc1e --- /dev/null +++ b/src/detail/wide_to_utf8.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "windows.hpp" + +#include +#include + +namespace cpp_bindings_windows::detail +{ +inline auto wideToUtf8(std::wstring_view wide) -> std::string +{ + if (wide.empty()) + { + return {}; + } + + const int required = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), + nullptr, 0, nullptr, nullptr); + if (required <= 0) + { + return {}; + } + + std::string utf8(static_cast(required), '\0'); + if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), utf8.data(), + required, nullptr, nullptr) <= 0) + { + return {}; + } + return utf8; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/win32_error_to_string.hpp b/src/detail/win32_error_to_string.hpp new file mode 100644 index 0000000..af8433c --- /dev/null +++ b/src/detail/win32_error_to_string.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "windows.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto win32ErrorToString(DWORD error) -> std::string +{ + LPSTR buffer = nullptr; + const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; + const DWORD language_id = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); + const DWORD length = + FormatMessageA(flags, nullptr, error, language_id, reinterpret_cast(&buffer), 0, nullptr); + if (length == 0 || buffer == nullptr) + { + return "Unknown Win32 error (" + std::to_string(error) + ")"; + } + + std::string message(buffer, length); + LocalFree(buffer); + while (!message.empty() && (message.back() == '\r' || message.back() == '\n')) + { + message.pop_back(); + } + return message; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/win32_helpers.hpp b/src/detail/win32_helpers.hpp deleted file mode 100644 index 964c5f7..0000000 --- a/src/detail/win32_helpers.hpp +++ /dev/null @@ -1,127 +0,0 @@ -#pragma once - -#include "common_types.hpp" -#include "handle_state.hpp" - -#include - -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include - -#include -#include -#include -#include - -namespace cpp_bindings_windows::detail -{ -inline auto win32ErrorToString(DWORD error) -> std::string -{ - LPSTR buffer = nullptr; - const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; - const DWORD language_id = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); - const DWORD length = - FormatMessageA(flags, nullptr, error, language_id, reinterpret_cast(&buffer), 0, nullptr); - if (length == 0 || buffer == nullptr) - { - return "Unknown Win32 error (" + std::to_string(error) + ")"; - } - - std::string message(buffer, length); - LocalFree(buffer); - while (!message.empty() && (message.back() == '\r' || message.back() == '\n')) - { - message.pop_back(); - } - return message; -} - -inline auto utf8ToWide(const char *utf8) -> std::wstring -{ - if (utf8 == nullptr || *utf8 == '\0') - { - return {}; - } - - const int required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, -1, nullptr, 0); - if (required <= 0) - { - return {}; - } - - std::wstring wide(static_cast(required), L'\0'); - if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, -1, wide.data(), required) <= 0) - { - return {}; - } - wide.pop_back(); - return wide; -} - -inline auto normalizePortPath(std::wstring_view port) -> std::wstring -{ - if (port.starts_with(L"\\\\.\\")) - { - return std::wstring(port); - } - if (port.starts_with(L"COM") || port.starts_with(L"com")) - { - return L"\\\\.\\" + std::wstring(port); - } - return std::wstring(port); -} - -inline auto wideToUtf8(std::wstring_view wide) -> std::string -{ - if (wide.empty()) - { - return {}; - } - - const int required = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), - nullptr, 0, nullptr, nullptr); - if (required <= 0) - { - return {}; - } - - std::string utf8(static_cast(required), '\0'); - if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), utf8.data(), - required, nullptr, nullptr) <= 0) - { - return {}; - } - return utf8; -} - -template -inline auto failWin32(ErrorCallbackT error_callback, StatusCodeValue code) -> ReturnType -{ - const DWORD error = GetLastError(); - const std::string message = win32ErrorToString(error); - cpp_core::invokeError(effectiveErrorCallback(error_callback), code, message); - return static_cast(code); -} - -inline auto bytesWaiting(HANDLE handle, int *out_bytes) -> bool -{ - if (out_bytes == nullptr) - { - return false; - } - *out_bytes = 0; - - DWORD errors = 0; - COMSTAT status = {}; - if (ClearCommError(handle, &errors, &status) == 0) - { - return false; - } - - *out_bytes = status.cbInQue > static_cast(INT_MAX) ? INT_MAX : static_cast(status.cbInQue); - return true; -} - -} // namespace cpp_bindings_windows::detail diff --git a/src/detail/windows.hpp b/src/detail/windows.hpp new file mode 100644 index 0000000..26447cd --- /dev/null +++ b/src/detail/windows.hpp @@ -0,0 +1,27 @@ +#pragma once + +#ifdef _WIN32 + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#ifndef STRICT +#define STRICT +#endif + +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0601 +#endif + +#ifndef WINVER +#define WINVER _WIN32_WINNT +#endif + +#include + +#endif diff --git a/src/detail/write_chunk.hpp b/src/detail/write_chunk.hpp new file mode 100644 index 0000000..61ae060 --- /dev/null +++ b/src/detail/write_chunk.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "start_pending_io.hpp" +#include "wait_for_pending_io.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto writeChunk(const HandleContext &context, const unsigned char *buffer, int buffer_size, int timeout_ms) + -> IoResult +{ + UniqueHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!event) + { + return {.outcome = IoOutcome::kError, .error = GetLastError()}; + } + + OVERLAPPED overlapped = {}; + overlapped.hEvent = event.get(); + DWORD transferred = 0; + const auto start = startPendingIo(context.state, Operation::kWrite, &overlapped, [&] { + return WriteFile(context.handle, buffer, static_cast(buffer_size), &transferred, &overlapped); + }); + if (start.aborted) + { + return {.outcome = IoOutcome::kAborted}; + } + if (start.completed != FALSE) + { + const bool aborted = finishPendingIo(context.state, Operation::kWrite, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kCompleted, + .bytes_transferred = static_cast(transferred)}; + } + if (start.error != ERROR_IO_PENDING) + { + const bool aborted = finishPendingIo(context.state, Operation::kWrite, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = start.error}; + } + + return waitForPendingIo(context.handle, context.state, Operation::kWrite, &overlapped, timeout_ms); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/write_impl.hpp b/src/detail/write_impl.hpp new file mode 100644 index 0000000..443cff1 --- /dev/null +++ b/src/detail/write_impl.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "acquire_handle_context.hpp" +#include "fail_win32.hpp" +#include "multiplier_timeout.hpp" +#include "note_bytes_transferred.hpp" +#include "write_chunk.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto writeImpl(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int multiplier, + ErrorCallbackT error_callback) -> int +{ + const auto callback = effectiveErrorCallback(error_callback); + const auto buffer_status = cpp_core::validateBuffer(buffer, buffer_size, callback); + if (buffer_status < 0) + { + return buffer_status; + } + + HandleContext context; + const auto handle_status = acquireHandleContext(handle, callback, &context); + if (handle_status < 0) + { + return handle_status; + } + + const auto *input = static_cast(buffer); + int total_written = 0; + while (total_written < buffer_size) + { + const int current_timeout = + total_written == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); + const auto result = writeChunk(context, input + total_written, buffer_size - total_written, current_timeout); + if (result.outcome == IoOutcome::kTimedOut) + { + return total_written; + } + if (result.outcome == IoOutcome::kAborted) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kAbortWriteError), + "Write aborted"); + } + if (result.outcome == IoOutcome::kError) + { + SetLastError(result.error); + return failWin32(callback, static_cast(StatusCode::Io::kWriteError)); + } + if (result.bytes_transferred <= 0) + { + return total_written; + } + + noteBytesTransferred(context.state, Operation::kWrite, result.bytes_transferred); + total_written += result.bytes_transferred; + } + + return total_written; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/serial_abort_read.cpp b/src/serial_abort_read.cpp index e881bcb..66cbc9f 100644 --- a/src/serial_abort_read.cpp +++ b/src/serial_abort_read.cpp @@ -1,6 +1,7 @@ #include -#include "detail/handle_state.hpp" +#include "detail/acquire_handle_context.hpp" +#include "detail/request_abort.hpp" extern "C" { diff --git a/src/serial_abort_write.cpp b/src/serial_abort_write.cpp index 5cc7400..a4b3405 100644 --- a/src/serial_abort_write.cpp +++ b/src/serial_abort_write.cpp @@ -1,6 +1,7 @@ #include -#include "detail/handle_state.hpp" +#include "detail/acquire_handle_context.hpp" +#include "detail/request_abort.hpp" extern "C" { diff --git a/src/serial_clear_buffer_in.cpp b/src/serial_clear_buffer_in.cpp index f5c6327..b45afe0 100644 --- a/src/serial_clear_buffer_in.cpp +++ b/src/serial_clear_buffer_in.cpp @@ -1,7 +1,7 @@ #include -#include "detail/handle_state.hpp" -#include "detail/win32_helpers.hpp" +#include "detail/acquire_handle_context.hpp" +#include "detail/fail_win32.hpp" extern "C" { diff --git a/src/serial_clear_buffer_out.cpp b/src/serial_clear_buffer_out.cpp index 52b4373..6a3c4ca 100644 --- a/src/serial_clear_buffer_out.cpp +++ b/src/serial_clear_buffer_out.cpp @@ -1,7 +1,7 @@ #include -#include "detail/handle_state.hpp" -#include "detail/win32_helpers.hpp" +#include "detail/acquire_handle_context.hpp" +#include "detail/fail_win32.hpp" extern "C" { diff --git a/src/serial_close.cpp b/src/serial_close.cpp index f5e20cb..8b9c897 100644 --- a/src/serial_close.cpp +++ b/src/serial_close.cpp @@ -1,7 +1,9 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/remove_handle_state.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_drain.cpp b/src/serial_drain.cpp index 50dc524..372404e 100644 --- a/src/serial_drain.cpp +++ b/src/serial_drain.cpp @@ -1,7 +1,7 @@ #include -#include "detail/handle_state.hpp" -#include "detail/win32_helpers.hpp" +#include "detail/acquire_handle_context.hpp" +#include "detail/fail_win32.hpp" extern "C" { diff --git a/src/serial_get_baudrate.cpp b/src/serial_get_baudrate.cpp index a4eece3..581993c 100644 --- a/src/serial_get_baudrate.cpp +++ b/src/serial_get_baudrate.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_get_cts.cpp b/src/serial_get_cts.cpp index 09c6860..1eee698 100644 --- a/src/serial_get_cts.cpp +++ b/src/serial_get_cts.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_get_data_bits.cpp b/src/serial_get_data_bits.cpp index 458a4ad..a6adce8 100644 --- a/src/serial_get_data_bits.cpp +++ b/src/serial_get_data_bits.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_get_dcd.cpp b/src/serial_get_dcd.cpp index 0b904e5..27479b7 100644 --- a/src/serial_get_dcd.cpp +++ b/src/serial_get_dcd.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_get_dsr.cpp b/src/serial_get_dsr.cpp index a67ea62..3149aa9 100644 --- a/src/serial_get_dsr.cpp +++ b/src/serial_get_dsr.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_get_flow_control.cpp b/src/serial_get_flow_control.cpp index 1394971..28a74dd 100644 --- a/src/serial_get_flow_control.cpp +++ b/src/serial_get_flow_control.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_get_parity.cpp b/src/serial_get_parity.cpp index f5ff9a1..5616549 100644 --- a/src/serial_get_parity.cpp +++ b/src/serial_get_parity.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_get_ri.cpp b/src/serial_get_ri.cpp index 0bb6254..54c96e1 100644 --- a/src/serial_get_ri.cpp +++ b/src/serial_get_ri.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_get_stop_bits.cpp b/src/serial_get_stop_bits.cpp index ccb2c5b..b08c586 100644 --- a/src/serial_get_stop_bits.cpp +++ b/src/serial_get_stop_bits.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_in_bytes_total.cpp b/src/serial_in_bytes_total.cpp index 50b867a..9292db9 100644 --- a/src/serial_in_bytes_total.cpp +++ b/src/serial_in_bytes_total.cpp @@ -1,6 +1,6 @@ #include -#include "detail/handle_state.hpp" +#include "detail/acquire_handle_context.hpp" extern "C" { diff --git a/src/serial_in_bytes_waiting.cpp b/src/serial_in_bytes_waiting.cpp index 291aeef..fd9f063 100644 --- a/src/serial_in_bytes_waiting.cpp +++ b/src/serial_in_bytes_waiting.cpp @@ -1,7 +1,8 @@ #include -#include "detail/handle_state.hpp" -#include "detail/win32_helpers.hpp" +#include "detail/acquire_handle_context.hpp" +#include "detail/bytes_waiting.hpp" +#include "detail/fail_win32.hpp" extern "C" { diff --git a/src/serial_list_ports.cpp b/src/serial_list_ports.cpp index 12cbbfb..bd0cb7e 100644 --- a/src/serial_list_ports.cpp +++ b/src/serial_list_ports.cpp @@ -1,8 +1,9 @@ #include #include -#include "detail/handle_state.hpp" -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/wide_to_utf8.hpp" +#include "detail/windows.hpp" #include #include diff --git a/src/serial_monitor_ports.cpp b/src/serial_monitor_ports.cpp index 90eb811..7f55fa1 100644 --- a/src/serial_monitor_ports.cpp +++ b/src/serial_monitor_ports.cpp @@ -1,7 +1,7 @@ #include -#include "detail/handle_state.hpp" -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/win32_error_to_string.hpp" #include #include diff --git a/src/serial_open.cpp b/src/serial_open.cpp index 73b3920..5b3f782 100644 --- a/src/serial_open.cpp +++ b/src/serial_open.cpp @@ -3,88 +3,16 @@ #include #include -#include "detail/win32_helpers.hpp" - -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include +#include "detail/apply_line_settings.hpp" +#include "detail/effective_error_callback.hpp" +#include "detail/fail_win32.hpp" +#include "detail/handle_types.hpp" +#include "detail/normalize_port_path.hpp" +#include "detail/register_opened_handle.hpp" +#include "detail/utf8_to_wide.hpp" #include -namespace -{ -auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Parity parity_value, - cpp_core::StopBits stop_bits_value) -> cpp_core::Status -{ - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - - if (GetCommState(handle, &dcb) == 0) - { - const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCode::Control::kGetStateError, - "GetCommState failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); - } - - dcb.BaudRate = static_cast(baudrate); - dcb.ByteSize = static_cast(data_bits); - - dcb.fBinary = TRUE; - dcb.fParity = (parity_value != cpp_core::Parity::kNone) ? TRUE : FALSE; - - dcb.fOutxCtsFlow = FALSE; - dcb.fOutxDsrFlow = FALSE; - dcb.fDtrControl = DTR_CONTROL_ENABLE; - dcb.fDsrSensitivity = FALSE; - dcb.fTXContinueOnXoff = TRUE; - dcb.fOutX = FALSE; - dcb.fInX = FALSE; - dcb.fRtsControl = RTS_CONTROL_ENABLE; - - switch (parity_value) - { - case cpp_core::Parity::kNone: - dcb.Parity = NOPARITY; - break; - case cpp_core::Parity::kEven: - dcb.Parity = EVENPARITY; - break; - case cpp_core::Parity::kOdd: - dcb.Parity = ODDPARITY; - break; - default: - return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, "Invalid parity"); - } - - if (stop_bits_value == cpp_core::StopBits::kOne) - { - dcb.StopBits = ONESTOPBIT; - } - else if (stop_bits_value == cpp_core::StopBits::kTwo) - { - dcb.StopBits = TWOSTOPBITS; - } - - if (SetCommState(handle, &dcb) == 0) - { - const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, - "SetCommState failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); - } - - COMMTIMEOUTS timeouts = {}; - if (SetCommTimeouts(handle, &timeouts) == 0) - { - const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCode::Configuration::kSetTimeoutError, - "SetCommTimeouts failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); - } - - return cpp_core::ok(); -} -} // namespace - extern "C" { MODULE_API auto serialOpen(void *port, int baudrate, int data_bits, int parity, int stop_bits, @@ -137,7 +65,8 @@ extern "C" cpp_core::StatusCode::Connection::kNotFoundError); } - const auto settings = applyLineSettings(handle.get(), baudrate, data_bits, parity_value, stop_bits_value); + const auto settings = cpp_bindings_windows::detail::applyLineSettings(handle.get(), baudrate, data_bits, + parity_value, stop_bits_value); if (!settings.has_value()) { return static_cast(cpp_core::toCStatus(settings, callback)); diff --git a/src/serial_open.test.cpp b/src/serial_open.test.cpp index 1c387c0..f6b378b 100644 --- a/src/serial_open.test.cpp +++ b/src/serial_open.test.cpp @@ -4,10 +4,7 @@ #include #include -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include +#include "detail/windows.hpp" #include diff --git a/src/serial_out_bytes_total.cpp b/src/serial_out_bytes_total.cpp index d479cb5..920792d 100644 --- a/src/serial_out_bytes_total.cpp +++ b/src/serial_out_bytes_total.cpp @@ -1,6 +1,6 @@ #include -#include "detail/handle_state.hpp" +#include "detail/acquire_handle_context.hpp" extern "C" { diff --git a/src/serial_out_bytes_waiting.cpp b/src/serial_out_bytes_waiting.cpp index fb762dc..49a38a5 100644 --- a/src/serial_out_bytes_waiting.cpp +++ b/src/serial_out_bytes_waiting.cpp @@ -1,7 +1,7 @@ #include -#include "detail/handle_state.hpp" -#include "detail/win32_helpers.hpp" +#include "detail/acquire_handle_context.hpp" +#include "detail/fail_win32.hpp" #include diff --git a/src/serial_read.cpp b/src/serial_read.cpp index 6892c52..b5da300 100644 --- a/src/serial_read.cpp +++ b/src/serial_read.cpp @@ -1,6 +1,6 @@ #include -#include "detail/io_impl.hpp" +#include "detail/read_impl.hpp" extern "C" { diff --git a/src/serial_read_line.cpp b/src/serial_read_line.cpp index c1c7092..3f5186b 100644 --- a/src/serial_read_line.cpp +++ b/src/serial_read_line.cpp @@ -1,6 +1,6 @@ #include -#include "detail/io_impl.hpp" +#include "detail/read_impl.hpp" extern "C" { diff --git a/src/serial_read_until.cpp b/src/serial_read_until.cpp index 7a2ccc5..9c1d3f5 100644 --- a/src/serial_read_until.cpp +++ b/src/serial_read_until.cpp @@ -1,6 +1,6 @@ #include -#include "detail/io_impl.hpp" +#include "detail/read_impl.hpp" extern "C" { diff --git a/src/serial_read_until_sequence.cpp b/src/serial_read_until_sequence.cpp index 32a8750..ce3a4ca 100644 --- a/src/serial_read_until_sequence.cpp +++ b/src/serial_read_until_sequence.cpp @@ -1,6 +1,6 @@ #include -#include "detail/io_impl.hpp" +#include "detail/read_impl.hpp" #include diff --git a/src/serial_send_break.cpp b/src/serial_send_break.cpp index c5dca34..df1f7ef 100644 --- a/src/serial_send_break.cpp +++ b/src/serial_send_break.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_set_baudrate.cpp b/src/serial_set_baudrate.cpp index 25243ca..6882aac 100644 --- a/src/serial_set_baudrate.cpp +++ b/src/serial_set_baudrate.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_set_data_bits.cpp b/src/serial_set_data_bits.cpp index a703604..916eb7f 100644 --- a/src/serial_set_data_bits.cpp +++ b/src/serial_set_data_bits.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_set_dtr.cpp b/src/serial_set_dtr.cpp index f53d55e..792b27a 100644 --- a/src/serial_set_dtr.cpp +++ b/src/serial_set_dtr.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_set_flow_control.cpp b/src/serial_set_flow_control.cpp index f493d5d..6f91422 100644 --- a/src/serial_set_flow_control.cpp +++ b/src/serial_set_flow_control.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_set_parity.cpp b/src/serial_set_parity.cpp index 2b444ca..e3b692c 100644 --- a/src/serial_set_parity.cpp +++ b/src/serial_set_parity.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_set_read_callback.cpp b/src/serial_set_read_callback.cpp index 4f094aa..0fc444e 100644 --- a/src/serial_set_read_callback.cpp +++ b/src/serial_set_read_callback.cpp @@ -1,6 +1,6 @@ #include -#include "detail/handle_state.hpp" +#include "detail/handle_types.hpp" extern "C" { diff --git a/src/serial_set_rts.cpp b/src/serial_set_rts.cpp index fac86df..2182e6e 100644 --- a/src/serial_set_rts.cpp +++ b/src/serial_set_rts.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_set_stop_bits.cpp b/src/serial_set_stop_bits.cpp index 5415780..7b7100b 100644 --- a/src/serial_set_stop_bits.cpp +++ b/src/serial_set_stop_bits.cpp @@ -1,7 +1,8 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { diff --git a/src/serial_set_write_callback.cpp b/src/serial_set_write_callback.cpp index 660becb..709c5de 100644 --- a/src/serial_set_write_callback.cpp +++ b/src/serial_set_write_callback.cpp @@ -1,6 +1,6 @@ #include -#include "detail/handle_state.hpp" +#include "detail/handle_types.hpp" extern "C" { diff --git a/src/serial_write.cpp b/src/serial_write.cpp index 910a402..3dab29f 100644 --- a/src/serial_write.cpp +++ b/src/serial_write.cpp @@ -1,6 +1,6 @@ #include -#include "detail/io_impl.hpp" +#include "detail/write_impl.hpp" extern "C" { From eb4b68ca104ebe3a9fac3ce47ecf22c199056103 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sun, 30 Aug 2026 18:53:00 +0200 Subject: [PATCH 10/18] ci: update ASTrein to 2.0.0 --- .github/workflows/build_binary.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index b999090..c008780 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -12,8 +12,8 @@ jobs: name: 'Generate FFI metadata (x86_64-windows-msvc)' runs-on: windows-2025 env: - ASTREIN_VERSION: '1.2.1' - ASTREIN_SHA256: 'c90eb0e3a24dbdd8775289b30e60d0ee2dfa1c0395c8687aa9a2fabeada2d2df' + ASTREIN_VERSION: '2.0.0' + ASTREIN_SHA256: '8ee3dd151ce685993420fbdb4f6fd032365b62dace1568e48c992add733410d5' outputs: package_version: ${{ steps.version.outputs.PACKAGE_VERSION }} is_valid_package_version: ${{ steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION }} @@ -81,7 +81,7 @@ jobs: if ($metadata.schema -ne 'astrein_ffi_api') { throw "Unexpected FFI metadata schema: $($metadata.schema)" } - if ($metadata.schemaVersion -ne 1) { + if ($metadata.schemaVersion -ne 2) { throw "Unexpected FFI metadata schema version: $($metadata.schemaVersion)" } From fc5afd8ac4827334d1dd6764d37bd15d2a6f6072 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sun, 30 Aug 2026 22:56:41 +0200 Subject: [PATCH 11/18] ci: refresh ASTrein 2.0.0 checksum --- .github/workflows/build_binary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index c008780..477b057 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -13,7 +13,7 @@ jobs: runs-on: windows-2025 env: ASTREIN_VERSION: '2.0.0' - ASTREIN_SHA256: '8ee3dd151ce685993420fbdb4f6fd032365b62dace1568e48c992add733410d5' + ASTREIN_SHA256: '9e80e6ab52fa874e523f604e6e2e26edbd0a326cff6651ad53dd3d0e6045b125' outputs: package_version: ${{ steps.version.outputs.PACKAGE_VERSION }} is_valid_package_version: ${{ steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION }} From 905bd8d6d2a2ab0ccc6eda026ae0e69e4cab1c72 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 31 Aug 2026 20:54:14 +0200 Subject: [PATCH 12/18] Refactor serial communication functions to use consistent variable naming and improve error handling - Updated variable names from 'h' to 'native_handle' for clarity across multiple serial functions. - Changed return values to use 'status' for error handling consistency. - Refactored DCB structure usage to 'serial_settings' for better readability. - Enhanced readability in the serial port listing and monitoring functions by renaming variables and improving callback function names. - Improved test code for better clarity and consistency in variable naming. --- .clang-tidy | 10 ++-- src/detail/apply_line_settings.hpp | 46 +++++++-------- src/serial_close.cpp | 13 +++-- src/serial_get_baudrate.cpp | 17 +++--- src/serial_get_cts.cpp | 11 ++-- src/serial_get_data_bits.cpp | 17 +++--- src/serial_get_dcd.cpp | 11 ++-- src/serial_get_dsr.cpp | 11 ++-- src/serial_get_flow_control.cpp | 19 ++++--- src/serial_get_parity.cpp | 17 +++--- src/serial_get_ri.cpp | 11 ++-- src/serial_get_stop_bits.cpp | 17 +++--- src/serial_list_ports.cpp | 89 ++++++++++++++++-------------- src/serial_monitor_ports.cpp | 8 +-- src/serial_open.cpp | 10 ++-- src/serial_out_bytes_waiting.cpp | 8 ++- src/serial_send_break.cpp | 13 +++-- src/serial_set_baudrate.cpp | 19 ++++--- src/serial_set_data_bits.cpp | 19 ++++--- src/serial_set_dtr.cpp | 13 +++-- src/serial_set_flow_control.cpp | 41 +++++++------- src/serial_set_parity.cpp | 29 +++++----- src/serial_set_read_callback.cpp | 4 +- src/serial_set_rts.cpp | 13 +++-- src/serial_set_stop_bits.cpp | 19 ++++--- src/serial_set_write_callback.cpp | 4 +- tests/serial_arduino.test.cpp | 68 ++++++++++++----------- 27 files changed, 293 insertions(+), 264 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 554459d..e8161b6 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -587,10 +587,12 @@ CheckOptions: value: '' - key: zircon-temporary-objects.Names value: '' -# allow x,y,z single letter names, to be used for coordinates -# allow fd (file descriptor) as it's a common POSIX convention + - key: readability-identifier-length.MinimumVariableNameLength + value: '3' + - key: readability-identifier-length.MinimumParameterNameLength + value: '3' - key: readability-identifier-length.IgnoredVariableNames - value: '^(x|y|z|m0|m1|fd|_)$' + value: '^_$' - key: readability-identifier-length.IgnoredParameterNames - value: '^(x|y|z|m0|m1|fd|_)$' + value: '^_$' ... diff --git a/src/detail/apply_line_settings.hpp b/src/detail/apply_line_settings.hpp index de90818..6cc8331 100644 --- a/src/detail/apply_line_settings.hpp +++ b/src/detail/apply_line_settings.hpp @@ -10,40 +10,40 @@ namespace cpp_bindings_windows::detail inline auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Parity parity_value, cpp_core::StopBits stop_bits_value) -> cpp_core::Status { - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); - if (GetCommState(handle, &dcb) == 0) + if (GetCommState(handle, &serial_settings) == 0) { const DWORD error = GetLastError(); return cpp_core::fail(cpp_core::StatusCode::Control::kGetStateError, "GetCommState failed: " + win32ErrorToString(error)); } - dcb.BaudRate = static_cast(baudrate); - dcb.ByteSize = static_cast(data_bits); + serial_settings.BaudRate = static_cast(baudrate); + serial_settings.ByteSize = static_cast(data_bits); - dcb.fBinary = TRUE; - dcb.fParity = (parity_value != cpp_core::Parity::kNone) ? TRUE : FALSE; - dcb.fOutxCtsFlow = FALSE; - dcb.fOutxDsrFlow = FALSE; - dcb.fDtrControl = DTR_CONTROL_ENABLE; - dcb.fDsrSensitivity = FALSE; - dcb.fTXContinueOnXoff = TRUE; - dcb.fOutX = FALSE; - dcb.fInX = FALSE; - dcb.fRtsControl = RTS_CONTROL_ENABLE; + serial_settings.fBinary = TRUE; + serial_settings.fParity = (parity_value != cpp_core::Parity::kNone) ? TRUE : FALSE; + serial_settings.fOutxCtsFlow = FALSE; + serial_settings.fOutxDsrFlow = FALSE; + serial_settings.fDtrControl = DTR_CONTROL_ENABLE; + serial_settings.fDsrSensitivity = FALSE; + serial_settings.fTXContinueOnXoff = TRUE; + serial_settings.fOutX = FALSE; + serial_settings.fInX = FALSE; + serial_settings.fRtsControl = RTS_CONTROL_ENABLE; switch (parity_value) { case cpp_core::Parity::kNone: - dcb.Parity = NOPARITY; + serial_settings.Parity = NOPARITY; break; case cpp_core::Parity::kEven: - dcb.Parity = EVENPARITY; + serial_settings.Parity = EVENPARITY; break; case cpp_core::Parity::kOdd: - dcb.Parity = ODDPARITY; + serial_settings.Parity = ODDPARITY; break; default: return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, "Invalid parity"); @@ -51,22 +51,22 @@ inline auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_co if (stop_bits_value == cpp_core::StopBits::kOne) { - dcb.StopBits = ONESTOPBIT; + serial_settings.StopBits = ONESTOPBIT; } else if (stop_bits_value == cpp_core::StopBits::kTwo) { - dcb.StopBits = TWOSTOPBITS; + serial_settings.StopBits = TWOSTOPBITS; } - if (SetCommState(handle, &dcb) == 0) + if (SetCommState(handle, &serial_settings) == 0) { const DWORD error = GetLastError(); return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, "SetCommState failed: " + win32ErrorToString(error)); } - COMMTIMEOUTS timeouts = {}; - if (SetCommTimeouts(handle, &timeouts) == 0) + COMMTIMEOUTS communication_timeouts = {}; + if (SetCommTimeouts(handle, &communication_timeouts) == 0) { const DWORD error = GetLastError(); return cpp_core::fail(cpp_core::StatusCode::Configuration::kSetTimeoutError, diff --git a/src/serial_close.cpp b/src/serial_close.cpp index 8b9c897..ab186e5 100644 --- a/src/serial_close.cpp +++ b/src/serial_close.cpp @@ -15,21 +15,22 @@ extern "C" return 0; } - HANDLE h = nullptr; - const auto handle_ok = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (handle_ok < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return handle_ok; + return status; } - if (CloseHandle(h) == 0) + if (CloseHandle(native_handle) == 0) { return cpp_bindings_windows::detail::failWin32( cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), cpp_core::StatusCode::Connection::kCloseHandleError); } - cpp_bindings_windows::detail::removeHandleState(h); + cpp_bindings_windows::detail::removeHandleState(native_handle); return static_cast(cpp_core::StatusCode::kSuccess); } diff --git a/src/serial_get_baudrate.cpp b/src/serial_get_baudrate.cpp index 581993c..14538f0 100644 --- a/src/serial_get_baudrate.cpp +++ b/src/serial_get_baudrate.cpp @@ -9,22 +9,23 @@ extern "C" MODULE_API auto serialGetBaudrate(int64_t handle, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - if (GetCommState(h, &dcb) == 0) + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetStateError); } - return static_cast(dcb.BaudRate); + return static_cast(serial_settings.BaudRate); } } // extern "C" diff --git a/src/serial_get_cts.cpp b/src/serial_get_cts.cpp index 1eee698..c93706f 100644 --- a/src/serial_get_cts.cpp +++ b/src/serial_get_cts.cpp @@ -9,15 +9,16 @@ extern "C" MODULE_API auto serialGetCts(int64_t handle, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } DWORD modem_status = 0; - if (GetCommModemStatus(h, &modem_status) == 0) + if (GetCommModemStatus(native_handle, &modem_status) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetModemStatusError); diff --git a/src/serial_get_data_bits.cpp b/src/serial_get_data_bits.cpp index a6adce8..4081acc 100644 --- a/src/serial_get_data_bits.cpp +++ b/src/serial_get_data_bits.cpp @@ -9,22 +9,23 @@ extern "C" MODULE_API auto serialGetDataBits(int64_t handle, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - if (GetCommState(h, &dcb) == 0) + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetStateError); } - return static_cast(dcb.ByteSize); + return static_cast(serial_settings.ByteSize); } } // extern "C" diff --git a/src/serial_get_dcd.cpp b/src/serial_get_dcd.cpp index 27479b7..1e884cc 100644 --- a/src/serial_get_dcd.cpp +++ b/src/serial_get_dcd.cpp @@ -9,15 +9,16 @@ extern "C" MODULE_API auto serialGetDcd(int64_t handle, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } DWORD modem_status = 0; - if (GetCommModemStatus(h, &modem_status) == 0) + if (GetCommModemStatus(native_handle, &modem_status) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetModemStatusError); diff --git a/src/serial_get_dsr.cpp b/src/serial_get_dsr.cpp index 3149aa9..b04111d 100644 --- a/src/serial_get_dsr.cpp +++ b/src/serial_get_dsr.cpp @@ -9,15 +9,16 @@ extern "C" MODULE_API auto serialGetDsr(int64_t handle, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } DWORD modem_status = 0; - if (GetCommModemStatus(h, &modem_status) == 0) + if (GetCommModemStatus(native_handle, &modem_status) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetModemStatusError); diff --git a/src/serial_get_flow_control.cpp b/src/serial_get_flow_control.cpp index 28a74dd..694f102 100644 --- a/src/serial_get_flow_control.cpp +++ b/src/serial_get_flow_control.cpp @@ -9,26 +9,27 @@ extern "C" MODULE_API auto serialGetFlowControl(int64_t handle, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - if (GetCommState(h, &dcb) == 0) + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetStateError); } - if (dcb.fOutxCtsFlow != 0 && dcb.fRtsControl == RTS_CONTROL_HANDSHAKE) + if (serial_settings.fOutxCtsFlow != 0 && serial_settings.fRtsControl == RTS_CONTROL_HANDSHAKE) { return 1; } - if (dcb.fOutX != 0 && dcb.fInX != 0) + if (serial_settings.fOutX != 0 && serial_settings.fInX != 0) { return 2; } diff --git a/src/serial_get_parity.cpp b/src/serial_get_parity.cpp index 5616549..76ea6bd 100644 --- a/src/serial_get_parity.cpp +++ b/src/serial_get_parity.cpp @@ -9,22 +9,23 @@ extern "C" MODULE_API auto serialGetParity(int64_t handle, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - if (GetCommState(h, &dcb) == 0) + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetStateError); } - switch (dcb.Parity) + switch (serial_settings.Parity) { case EVENPARITY: return 1; diff --git a/src/serial_get_ri.cpp b/src/serial_get_ri.cpp index 54c96e1..f68dc71 100644 --- a/src/serial_get_ri.cpp +++ b/src/serial_get_ri.cpp @@ -9,15 +9,16 @@ extern "C" MODULE_API auto serialGetRi(int64_t handle, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } DWORD modem_status = 0; - if (GetCommModemStatus(h, &modem_status) == 0) + if (GetCommModemStatus(native_handle, &modem_status) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetModemStatusError); diff --git a/src/serial_get_stop_bits.cpp b/src/serial_get_stop_bits.cpp index b08c586..b849db3 100644 --- a/src/serial_get_stop_bits.cpp +++ b/src/serial_get_stop_bits.cpp @@ -9,22 +9,23 @@ extern "C" MODULE_API auto serialGetStopBits(int64_t handle, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - if (GetCommState(h, &dcb) == 0) + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetStateError); } - return (dcb.StopBits == TWOSTOPBITS) ? 2 : 0; + return (serial_settings.StopBits == TWOSTOPBITS) ? 2 : 0; } } // extern "C" diff --git a/src/serial_list_ports.cpp b/src/serial_list_ports.cpp index bd0cb7e..5ec4238 100644 --- a/src/serial_list_ports.cpp +++ b/src/serial_list_ports.cpp @@ -17,7 +17,7 @@ namespace { -struct PortInfo +struct PortInformation { std::string port; std::string path; @@ -48,9 +48,10 @@ auto registryString(HKEY key, const wchar_t *value_name) -> std::optional std::optional +auto portName(HDEVINFO device_information_set, SP_DEVINFO_DATA *device_information) -> std::optional { - HKEY key = SetupDiOpenDevRegKey(device_info_set, device_info, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_QUERY_VALUE); + HKEY key = SetupDiOpenDevRegKey(device_information_set, device_information, DICS_FLAG_GLOBAL, 0, DIREG_DEV, + KEY_QUERY_VALUE); if (key == INVALID_HANDLE_VALUE) { return std::nullopt; @@ -60,37 +61,38 @@ auto portName(HDEVINFO device_info_set, SP_DEVINFO_DATA *device_info) -> std::op return value; } -auto deviceProperty(HDEVINFO device_info_set, SP_DEVINFO_DATA *device_info, DWORD property) +auto deviceProperty(HDEVINFO device_information_set, SP_DEVINFO_DATA *device_information, DWORD property) -> std::optional { DWORD type = 0; DWORD size = 0; - (void)SetupDiGetDeviceRegistryPropertyW(device_info_set, device_info, property, &type, nullptr, 0, &size); + (void)SetupDiGetDeviceRegistryPropertyW(device_information_set, device_information, property, &type, nullptr, 0, + &size); if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || size < sizeof(wchar_t)) { return std::nullopt; } std::vector buffer(size); - if (SetupDiGetDeviceRegistryPropertyW(device_info_set, device_info, property, &type, buffer.data(), size, - nullptr) == 0) + if (SetupDiGetDeviceRegistryPropertyW(device_information_set, device_information, property, &type, buffer.data(), + size, nullptr) == 0) { return std::nullopt; } return std::wstring(reinterpret_cast(buffer.data())); } -auto instanceId(HDEVINFO device_info_set, SP_DEVINFO_DATA *device_info) -> std::optional +auto instanceId(HDEVINFO device_information_set, SP_DEVINFO_DATA *device_information) -> std::optional { DWORD required = 0; - (void)SetupDiGetDeviceInstanceIdW(device_info_set, device_info, nullptr, 0, &required); + (void)SetupDiGetDeviceInstanceIdW(device_information_set, device_information, nullptr, 0, &required); if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || required == 0) { return std::nullopt; } std::vector buffer(required); - if (SetupDiGetDeviceInstanceIdW(device_info_set, device_info, buffer.data(), required, nullptr) == 0) + if (SetupDiGetDeviceInstanceIdW(device_information_set, device_information, buffer.data(), required, nullptr) == 0) { return std::nullopt; } @@ -136,34 +138,35 @@ auto optionalCString(const std::string &value) -> const char * extern "C" { - MODULE_API auto serialListPorts(void (*callback_fn)(const char *port, const char *path, const char *manufacturer, - const char *serial_number, const char *pnp_id, - const char *location_id, const char *product_id, - const char *vendor_id), + MODULE_API auto serialListPorts(void (*callback_function)(const char *port, const char *path, + const char *manufacturer, const char *serial_number, + const char *pnp_id, const char *location_id, + const char *product_id, const char *vendor_id), ErrorCallbackT error_callback) -> int { const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); - if (callback_fn == nullptr) + if (callback_function == nullptr) { return cpp_core::failMsg( callback, static_cast(cpp_core::StatusCode::Io::kBufferError), "Port callback must not be null"); } - const HDEVINFO device_info_set = SetupDiGetClassDevsW(&GUID_DEVCLASS_PORTS, nullptr, nullptr, DIGCF_PRESENT); - if (device_info_set == INVALID_HANDLE_VALUE) + const HDEVINFO device_information_set = + SetupDiGetClassDevsW(&GUID_DEVCLASS_PORTS, nullptr, nullptr, DIGCF_PRESENT); + if (device_information_set == INVALID_HANDLE_VALUE) { return cpp_bindings_windows::detail::failWin32( callback, static_cast(cpp_core::StatusCode::Monitor::kMonitorError)); } - const auto cleanup = cpp_core::defer([&] { SetupDiDestroyDeviceInfoList(device_info_set); }); + const auto cleanup = cpp_core::defer([&] { SetupDiDestroyDeviceInfoList(device_information_set); }); - std::vector ports; + std::vector ports; for (DWORD index = 0;; ++index) { - SP_DEVINFO_DATA device_info = {}; - device_info.cbSize = sizeof(device_info); - if (SetupDiEnumDeviceInfo(device_info_set, index, &device_info) == 0) + SP_DEVINFO_DATA device_information = {}; + device_information.cbSize = sizeof(device_information); + if (SetupDiEnumDeviceInfo(device_information_set, index, &device_information) == 0) { if (GetLastError() == ERROR_NO_MORE_ITEMS) { @@ -173,41 +176,43 @@ extern "C" callback, static_cast(cpp_core::StatusCode::Monitor::kMonitorError)); } - const auto port_name = portName(device_info_set, &device_info); + const auto port_name = portName(device_information_set, &device_information); if (!port_name || port_name->size() < 4 || (!port_name->starts_with(L"COM") && !port_name->starts_with(L"com"))) { continue; } - PortInfo info; - info.port = cpp_bindings_windows::detail::wideToUtf8(*port_name); - info.path = "\\\\.\\" + info.port; - if (const auto value = deviceProperty(device_info_set, &device_info, SPDRP_MFG)) + PortInformation port_information; + port_information.port = cpp_bindings_windows::detail::wideToUtf8(*port_name); + port_information.path = "\\\\.\\" + port_information.port; + if (const auto value = deviceProperty(device_information_set, &device_information, SPDRP_MFG)) { - info.manufacturer = cpp_bindings_windows::detail::wideToUtf8(*value); + port_information.manufacturer = cpp_bindings_windows::detail::wideToUtf8(*value); } - if (const auto value = deviceProperty(device_info_set, &device_info, SPDRP_LOCATION_INFORMATION)) + if (const auto value = + deviceProperty(device_information_set, &device_information, SPDRP_LOCATION_INFORMATION)) { - info.location_id = cpp_bindings_windows::detail::wideToUtf8(*value); + port_information.location_id = cpp_bindings_windows::detail::wideToUtf8(*value); } - if (const auto value = instanceId(device_info_set, &device_info)) + if (const auto value = instanceId(device_information_set, &device_information)) { - info.pnp_id = cpp_bindings_windows::detail::wideToUtf8(*value); - info.serial_number = serialNumber(info.pnp_id); - info.vendor_id = hardwareId(info.pnp_id, "VID_"); - info.product_id = hardwareId(info.pnp_id, "PID_"); + port_information.pnp_id = cpp_bindings_windows::detail::wideToUtf8(*value); + port_information.serial_number = serialNumber(port_information.pnp_id); + port_information.vendor_id = hardwareId(port_information.pnp_id, "VID_"); + port_information.product_id = hardwareId(port_information.pnp_id, "PID_"); } - ports.push_back(std::move(info)); + ports.push_back(std::move(port_information)); } - std::ranges::sort(ports, {}, &PortInfo::port); - for (const auto &info : ports) + std::ranges::sort(ports, {}, &PortInformation::port); + for (const auto &port_information : ports) { - callback_fn(optionalCString(info.port), optionalCString(info.path), optionalCString(info.manufacturer), - optionalCString(info.serial_number), optionalCString(info.pnp_id), - optionalCString(info.location_id), optionalCString(info.product_id), - optionalCString(info.vendor_id)); + callback_function( + optionalCString(port_information.port), optionalCString(port_information.path), + optionalCString(port_information.manufacturer), optionalCString(port_information.serial_number), + optionalCString(port_information.pnp_id), optionalCString(port_information.location_id), + optionalCString(port_information.product_id), optionalCString(port_information.vendor_id)); } return static_cast(ports.size()); } diff --git a/src/serial_monitor_ports.cpp b/src/serial_monitor_ports.cpp index 7f55fa1..7d3873b 100644 --- a/src/serial_monitor_ports.cpp +++ b/src/serial_monitor_ports.cpp @@ -104,12 +104,12 @@ auto monitorLoop(std::stop_token stop_token, std::set previous, extern "C" { - MODULE_API auto serialMonitorPorts(void (*callback_fn)(int event, const char *port), ErrorCallbackT error_callback) - -> int + MODULE_API auto serialMonitorPorts(void (*callback_function)(int event, const char *port), + ErrorCallbackT error_callback) -> int { std::lock_guard lock(g_monitor_mutex); stopMonitor(); - if (callback_fn == nullptr) + if (callback_function == nullptr) { return static_cast(cpp_core::StatusCode::kSuccess); } @@ -122,7 +122,7 @@ extern "C" callback, static_cast(cpp_core::StatusCode::Monitor::kMonitorError)); } - g_monitor_thread = std::jthread(monitorLoop, std::move(*initial_ports), callback_fn, callback); + g_monitor_thread = std::jthread(monitorLoop, std::move(*initial_ports), callback_function, callback); return static_cast(cpp_core::StatusCode::kSuccess); } diff --git a/src/serial_open.cpp b/src/serial_open.cpp index 5b3f782..8b81668 100644 --- a/src/serial_open.cpp +++ b/src/serial_open.cpp @@ -19,10 +19,10 @@ extern "C" ErrorCallbackT error_callback) -> intptr_t { const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); - const auto params_ok = cpp_core::validateOpenParams(port, baudrate, data_bits, callback); - if (params_ok < 0) + const auto parameter_status = cpp_core::validateOpenParams(port, baudrate, data_bits, callback); + if (parameter_status < 0) { - return params_ok; + return parameter_status; } if (parity < static_cast(cpp_core::Parity::kNone) || parity > static_cast(cpp_core::Parity::kOdd)) @@ -74,8 +74,8 @@ extern "C" PurgeComm(handle.get(), PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT); - const intptr_t out = reinterpret_cast(handle.get()); - if (out <= 0) + const intptr_t serial_handle = reinterpret_cast(handle.get()); + if (serial_handle <= 0) { return cpp_core::failMsg(callback, cpp_core::StatusCode::Connection::kInvalidHandleError, "Invalid handle generated"); diff --git a/src/serial_out_bytes_waiting.cpp b/src/serial_out_bytes_waiting.cpp index 49a38a5..ac2805d 100644 --- a/src/serial_out_bytes_waiting.cpp +++ b/src/serial_out_bytes_waiting.cpp @@ -18,14 +18,16 @@ extern "C" } DWORD errors = 0; - COMSTAT comm_status = {}; - if (ClearCommError(context.handle, &errors, &comm_status) == 0) + COMSTAT communication_status = {}; + if (ClearCommError(context.handle, &errors, &communication_status) == 0) { return cpp_bindings_windows::detail::failWin32( cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), static_cast(cpp_core::StatusCode::Control::kGetStateError)); } - return comm_status.cbOutQue > static_cast(INT_MAX) ? INT_MAX : static_cast(comm_status.cbOutQue); + return communication_status.cbOutQue > static_cast(INT_MAX) + ? INT_MAX + : static_cast(communication_status.cbOutQue); } } // extern "C" diff --git a/src/serial_send_break.cpp b/src/serial_send_break.cpp index df1f7ef..8d4d601 100644 --- a/src/serial_send_break.cpp +++ b/src/serial_send_break.cpp @@ -9,11 +9,12 @@ extern "C" MODULE_API auto serialSendBreak(int64_t handle, int duration_ms, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } if (duration_ms <= 0) @@ -22,7 +23,7 @@ extern "C" cpp_core::StatusCode::Control::kSendBreakError, "Break duration must be > 0"); } - if (SetCommBreak(h) == 0) + if (SetCommBreak(native_handle) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kSendBreakError); @@ -30,7 +31,7 @@ extern "C" Sleep(static_cast(duration_ms)); - if (ClearCommBreak(h) == 0) + if (ClearCommBreak(native_handle) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kSendBreakError); diff --git a/src/serial_set_baudrate.cpp b/src/serial_set_baudrate.cpp index 6882aac..208d3d1 100644 --- a/src/serial_set_baudrate.cpp +++ b/src/serial_set_baudrate.cpp @@ -9,11 +9,12 @@ extern "C" MODULE_API auto serialSetBaudrate(int64_t handle, int baudrate, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } if (baudrate < 300) @@ -23,17 +24,17 @@ extern "C" "Invalid baudrate: must be >= 300"); } - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - if (GetCommState(h, &dcb) == 0) + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetStateError); } - dcb.BaudRate = static_cast(baudrate); + serial_settings.BaudRate = static_cast(baudrate); - if (SetCommState(h, &dcb) == 0) + if (SetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Configuration::kSetBaudrateError); diff --git a/src/serial_set_data_bits.cpp b/src/serial_set_data_bits.cpp index 916eb7f..b3a1f52 100644 --- a/src/serial_set_data_bits.cpp +++ b/src/serial_set_data_bits.cpp @@ -9,11 +9,12 @@ extern "C" MODULE_API auto serialSetDataBits(int64_t handle, int data_bits, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } if (data_bits < 5 || data_bits > 8) @@ -23,17 +24,17 @@ extern "C" "Invalid data bits: must be 5-8"); } - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - if (GetCommState(h, &dcb) == 0) + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetStateError); } - dcb.ByteSize = static_cast(data_bits); + serial_settings.ByteSize = static_cast(data_bits); - if (SetCommState(h, &dcb) == 0) + if (SetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Configuration::kSetDataBitsError); diff --git a/src/serial_set_dtr.cpp b/src/serial_set_dtr.cpp index 792b27a..0469857 100644 --- a/src/serial_set_dtr.cpp +++ b/src/serial_set_dtr.cpp @@ -9,15 +9,16 @@ extern "C" MODULE_API auto serialSetDtr(int64_t handle, int state, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } - const DWORD func = state ? SETDTR : CLRDTR; - if (EscapeCommFunction(h, func) == 0) + const DWORD communication_function = state ? SETDTR : CLRDTR; + if (EscapeCommFunction(native_handle, communication_function) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kSetDtrError); diff --git a/src/serial_set_flow_control.cpp b/src/serial_set_flow_control.cpp index 6f91422..ade9918 100644 --- a/src/serial_set_flow_control.cpp +++ b/src/serial_set_flow_control.cpp @@ -9,11 +9,12 @@ extern "C" MODULE_API auto serialSetFlowControl(int64_t handle, int mode, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } if (mode < 0 || mode > 2) @@ -23,38 +24,38 @@ extern "C" "Invalid flow control mode: must be 0, 1, or 2"); } - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - if (GetCommState(h, &dcb) == 0) + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetStateError); } - dcb.fOutxCtsFlow = FALSE; - dcb.fRtsControl = RTS_CONTROL_ENABLE; - dcb.fOutX = FALSE; - dcb.fInX = FALSE; + serial_settings.fOutxCtsFlow = FALSE; + serial_settings.fRtsControl = RTS_CONTROL_ENABLE; + serial_settings.fOutX = FALSE; + serial_settings.fInX = FALSE; switch (mode) { case 1: - dcb.fOutxCtsFlow = TRUE; - dcb.fRtsControl = RTS_CONTROL_HANDSHAKE; + serial_settings.fOutxCtsFlow = TRUE; + serial_settings.fRtsControl = RTS_CONTROL_HANDSHAKE; break; case 2: - dcb.fOutX = TRUE; - dcb.fInX = TRUE; - dcb.XonChar = 0x11; - dcb.XoffChar = 0x13; - dcb.XonLim = 2048; - dcb.XoffLim = 512; + serial_settings.fOutX = TRUE; + serial_settings.fInX = TRUE; + serial_settings.XonChar = 0x11; + serial_settings.XoffChar = 0x13; + serial_settings.XonLim = 2048; + serial_settings.XoffLim = 512; break; default: break; } - if (SetCommState(h, &dcb) == 0) + if (SetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32( error_callback, cpp_core::StatusCode::Configuration::kSetFlowControlError); diff --git a/src/serial_set_parity.cpp b/src/serial_set_parity.cpp index e3b692c..a98128d 100644 --- a/src/serial_set_parity.cpp +++ b/src/serial_set_parity.cpp @@ -9,24 +9,25 @@ extern "C" MODULE_API auto serialSetParity(int64_t handle, int parity, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } - BYTE win_parity = NOPARITY; + BYTE windows_parity = NOPARITY; switch (parity) { case 0: - win_parity = NOPARITY; + windows_parity = NOPARITY; break; case 1: - win_parity = EVENPARITY; + windows_parity = EVENPARITY; break; case 2: - win_parity = ODDPARITY; + windows_parity = ODDPARITY; break; default: return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), @@ -34,18 +35,18 @@ extern "C" "Invalid parity: must be 0, 1, or 2"); } - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - if (GetCommState(h, &dcb) == 0) + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetStateError); } - dcb.Parity = win_parity; - dcb.fParity = (parity != 0) ? TRUE : FALSE; + serial_settings.Parity = windows_parity; + serial_settings.fParity = (parity != 0) ? TRUE : FALSE; - if (SetCommState(h, &dcb) == 0) + if (SetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Configuration::kSetParityError); diff --git a/src/serial_set_read_callback.cpp b/src/serial_set_read_callback.cpp index 0fc444e..73cfa95 100644 --- a/src/serial_set_read_callback.cpp +++ b/src/serial_set_read_callback.cpp @@ -5,9 +5,9 @@ extern "C" { - MODULE_API void serialSetReadCallback(void (*callback_fn)(int bytes_read)) + MODULE_API void serialSetReadCallback(void (*callback_function)(int bytes_read)) { - cpp_bindings_windows::detail::g_read_callback.store(callback_fn, std::memory_order_release); + cpp_bindings_windows::detail::g_read_callback.store(callback_function, std::memory_order_release); } } // extern "C" diff --git a/src/serial_set_rts.cpp b/src/serial_set_rts.cpp index 2182e6e..c4ed07e 100644 --- a/src/serial_set_rts.cpp +++ b/src/serial_set_rts.cpp @@ -9,15 +9,16 @@ extern "C" MODULE_API auto serialSetRts(int64_t handle, int state, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } - const DWORD func = state ? SETRTS : CLRRTS; - if (EscapeCommFunction(h, func) == 0) + const DWORD communication_function = state ? SETRTS : CLRRTS; + if (EscapeCommFunction(native_handle, communication_function) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kSetRtsError); diff --git a/src/serial_set_stop_bits.cpp b/src/serial_set_stop_bits.cpp index 7b7100b..9f55b21 100644 --- a/src/serial_set_stop_bits.cpp +++ b/src/serial_set_stop_bits.cpp @@ -9,11 +9,12 @@ extern "C" MODULE_API auto serialSetStopBits(int64_t handle, int stop_bits, ErrorCallbackT error_callback) -> int { - HANDLE h = nullptr; - const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (rc < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return rc; + return status; } if (stop_bits != 0 && stop_bits != 1 && stop_bits != 2) @@ -23,17 +24,17 @@ extern "C" "Invalid stop bits: must be 0, 1, or 2"); } - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - if (GetCommState(h, &dcb) == 0) + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Control::kGetStateError); } - dcb.StopBits = (stop_bits == 2) ? TWOSTOPBITS : ONESTOPBIT; + serial_settings.StopBits = (stop_bits == 2) ? TWOSTOPBITS : ONESTOPBIT; - if (SetCommState(h, &dcb) == 0) + if (SetCommState(native_handle, &serial_settings) == 0) { return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCode::Configuration::kSetStopBitsError); diff --git a/src/serial_set_write_callback.cpp b/src/serial_set_write_callback.cpp index 709c5de..f1ca4fd 100644 --- a/src/serial_set_write_callback.cpp +++ b/src/serial_set_write_callback.cpp @@ -5,9 +5,9 @@ extern "C" { - MODULE_API void serialSetWriteCallback(void (*callback_fn)(int bytes_written)) + MODULE_API void serialSetWriteCallback(void (*callback_function)(int bytes_written)) { - cpp_bindings_windows::detail::g_write_callback.store(callback_fn, std::memory_order_release); + cpp_bindings_windows::detail::g_write_callback.store(callback_function, std::memory_order_release); } } // extern "C" diff --git a/tests/serial_arduino.test.cpp b/tests/serial_arduino.test.cpp index f4e6535..2546c92 100644 --- a/tests/serial_arduino.test.cpp +++ b/tests/serial_arduino.test.cpp @@ -17,40 +17,41 @@ namespace { -auto readExact(intptr_t handle, char *dst, int want_bytes, int total_timeout_ms) -> int +auto readExact(intptr_t handle, char *destination, int requested_byte_count, int total_timeout_ms) -> int { - if (dst == nullptr || want_bytes <= 0) + if (destination == nullptr || requested_byte_count <= 0) { return 0; } const ULONGLONG start = GetTickCount64(); - int total = 0; - while (total < want_bytes) + int total_bytes_read = 0; + while (total_bytes_read < requested_byte_count) { const ULONGLONG now = GetTickCount64(); - const int elapsed = static_cast(now - start); - if (elapsed >= total_timeout_ms) + const int elapsed_milliseconds = static_cast(now - start); + if (elapsed_milliseconds >= total_timeout_ms) { break; } // Read remaining bytes with a small per-call timeout to make progress. - const int remaining = want_bytes - total; - const int chunk = serialRead(handle, dst + total, remaining, 200, 1, nullptr); - if (chunk < 0) + const int remaining_byte_count = requested_byte_count - total_bytes_read; + const int bytes_read = + serialRead(handle, destination + total_bytes_read, remaining_byte_count, 200, 1, nullptr); + if (bytes_read < 0) { - return chunk; + return bytes_read; } - if (chunk == 0) + if (bytes_read == 0) { Sleep(10); continue; } - total += chunk; + total_bytes_read += bytes_read; } - return total; + return total_bytes_read; } } // namespace @@ -59,13 +60,13 @@ class SerialArduinoTest : public ::testing::Test protected: void SetUp() override { - const char *env_port = std::getenv("SERIAL_TEST_PORT"); - const char *port = (env_port != nullptr && env_port[0] != '\0') ? env_port : "COM5"; + const char *environment_port = std::getenv("SERIAL_TEST_PORT"); + const char *port = (environment_port != nullptr && environment_port[0] != '\0') ? environment_port : "COM5"; handle_ = serialOpen(const_cast(static_cast(port)), 115200, 8, 0, 0, nullptr); if (handle_ <= 0) { - GTEST_SKIP() << "Could not open serial port '" << (env_port ? env_port : "COM5") + GTEST_SKIP() << "Could not open serial port '" << (environment_port ? environment_port : "COM5") << "'. Set SERIAL_TEST_PORT (e.g. COM5) or connect Arduino."; } @@ -93,43 +94,44 @@ TEST_F(SerialArduinoTest, OpenClose) TEST_F(SerialArduinoTest, WriteReadEcho) { const char *test_message = "Hello Arduino!\n"; - const int message_len = static_cast(strlen(test_message)); + const int message_length = static_cast(strlen(test_message)); - const int written = serialWrite(handle_, test_message, message_len, 1000, 1, nullptr); - EXPECT_EQ(written, message_len) << "Should write all bytes. Written: " << written << ", Expected: " << message_len; + const int bytes_written = serialWrite(handle_, test_message, message_length, 1000, 1, nullptr); + EXPECT_EQ(bytes_written, message_length) + << "Should write all bytes. Written: " << bytes_written << ", Expected: " << message_length; Sleep(500); char read_buffer[256] = {0}; - const int read_bytes = readExact(handle_, read_buffer, message_len, 3000); + const int read_bytes = readExact(handle_, read_buffer, message_length, 3000); EXPECT_GT(read_bytes, 0) << "Should read at least some bytes"; - EXPECT_EQ(read_bytes, message_len) << "Should read exactly the echoed message length"; - EXPECT_EQ(std::string_view(read_buffer, static_cast(message_len)), - std::string_view(test_message, static_cast(message_len))) + EXPECT_EQ(read_bytes, message_length) << "Should read exactly the echoed message length"; + EXPECT_EQ(std::string_view(read_buffer, static_cast(message_length)), + std::string_view(test_message, static_cast(message_length))) << "Echoed content should match what was sent"; } TEST_F(SerialArduinoTest, MultipleEchoCycles) { const char *messages[] = {"Test1\n", "Test2\n", "Test3\n"}; - const int num_messages = 3; + const int message_count = 3; - for (int i = 0; i < num_messages; ++i) + for (int message_index = 0; message_index < message_count; ++message_index) { - const int msg_len = static_cast(strlen(messages[i])); + const int message_length = static_cast(strlen(messages[message_index])); - const int written = serialWrite(handle_, messages[i], msg_len, 1000, 1, nullptr); - EXPECT_EQ(written, msg_len) << "Cycle " << i << ": write failed"; + const int bytes_written = serialWrite(handle_, messages[message_index], message_length, 1000, 1, nullptr); + EXPECT_EQ(bytes_written, message_length) << "Cycle " << message_index << ": write failed"; Sleep(500); char read_buffer[256] = {0}; - const int read_bytes = readExact(handle_, read_buffer, msg_len, 3000); - EXPECT_EQ(read_bytes, msg_len) << "Cycle " << i << ": read size mismatch"; - EXPECT_EQ(std::string_view(read_buffer, static_cast(msg_len)), - std::string_view(messages[i], static_cast(msg_len))) - << "Cycle " << i << ": echo content mismatch"; + const int read_bytes = readExact(handle_, read_buffer, message_length, 3000); + EXPECT_EQ(read_bytes, message_length) << "Cycle " << message_index << ": read size mismatch"; + EXPECT_EQ(std::string_view(read_buffer, static_cast(message_length)), + std::string_view(messages[message_index], static_cast(message_length))) + << "Cycle " << message_index << ": echo content mismatch"; } } From 8428e1bdc99931230ce62bd6b273b083a9c7f0ec Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 31 Aug 2026 22:12:54 +0200 Subject: [PATCH 13/18] refactor: replace manual ASTrein setup with action for improved reliability --- .github/workflows/build_binary.yml | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index 477b057..bf41e05 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -13,7 +13,6 @@ jobs: runs-on: windows-2025 env: ASTREIN_VERSION: '2.0.0' - ASTREIN_SHA256: '9e80e6ab52fa874e523f604e6e2e26edbd0a326cff6651ad53dd3d0e6045b125' outputs: package_version: ${{ steps.version.outputs.PACKAGE_VERSION }} is_valid_package_version: ${{ steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION }} @@ -37,24 +36,11 @@ jobs: directory: ${{ runner.temp }}/llvm force-url: 'https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/LLVM-22.1.8-win64.exe' - - name: 'Download ASTrein' - shell: pwsh - run: | - $archive = Join-Path $env:RUNNER_TEMP 'astrein-windows-x86_64.zip' - $destination = Join-Path $env:RUNNER_TEMP 'astrein-package' - Invoke-WebRequest ` - -Uri "https://github.com/Katze719/ASTrein/releases/download/v$env:ASTREIN_VERSION/astrein-windows-x86_64.zip" ` - -OutFile $archive - - $actualHash = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant() - if ($actualHash -ne $env:ASTREIN_SHA256) { - throw "ASTrein checksum mismatch: expected $env:ASTREIN_SHA256, got $actualHash" - } - - Expand-Archive -Path $archive -DestinationPath $destination - $astrein = Join-Path $destination 'astrein/bin/astrein.exe' - & $astrein --version - "ASTREIN_EXECUTABLE=$astrein" >> $env:GITHUB_ENV + - name: 'Setup ASTrein' + id: astrein + uses: Katze719/setup-astrein@v1 + with: + version: ${{ env.ASTREIN_VERSION }} - name: 'Configure metadata context' shell: pwsh @@ -64,7 +50,7 @@ jobs: -DCMAKE_C_COMPILER=clang-cl ` -DCMAKE_CXX_COMPILER=clang-cl ` -DCPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT=ON ` - "-DCPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE=$env:ASTREIN_EXECUTABLE" ` + "-DCPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE=${{ steps.astrein.outputs.path }}" ` "-DCPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT=$env:GITHUB_WORKSPACE/dist/ffi/x86_64.ffi.json" - name: 'Generate metadata' From 9d44f984ff0b83bbb63e9da016e55bd9287cf1f6 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 31 Aug 2026 22:14:12 +0200 Subject: [PATCH 14/18] fix: correct syntax for setting ASTrein executable and FFI JSON output in build configuration --- .github/workflows/build_binary.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index bf41e05..2be90ed 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -50,8 +50,8 @@ jobs: -DCMAKE_C_COMPILER=clang-cl ` -DCMAKE_CXX_COMPILER=clang-cl ` -DCPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT=ON ` - "-DCPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE=${{ steps.astrein.outputs.path }}" ` - "-DCPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT=$env:GITHUB_WORKSPACE/dist/ffi/x86_64.ffi.json" + -DCPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE=${{ steps.astrein.outputs.path }} ` + -DCPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT=$env:GITHUB_WORKSPACE/dist/ffi/x86_64.ffi.json` - name: 'Generate metadata' run: | From 536b17c1baf6f4cca4ced280de9804234df68825 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 31 Aug 2026 22:19:10 +0200 Subject: [PATCH 15/18] fix: update syntax for setting ASTrein executable and FFI JSON output in build configuration --- .github/workflows/build_binary.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index 2be90ed..fde6883 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -50,8 +50,8 @@ jobs: -DCMAKE_C_COMPILER=clang-cl ` -DCMAKE_CXX_COMPILER=clang-cl ` -DCPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT=ON ` - -DCPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE=${{ steps.astrein.outputs.path }} ` - -DCPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT=$env:GITHUB_WORKSPACE/dist/ffi/x86_64.ffi.json` + "-DCPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE=${{ steps.astrein.outputs.path }}" ` + "-DCPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT=${env:GITHUB_WORKSPACE}/dist/ffi/x86_64.ffi.json" - name: 'Generate metadata' run: | From 4c8b147eb7f45730df5c5f4ebb672e71e0bfca83 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 31 Aug 2026 22:49:52 +0200 Subject: [PATCH 16/18] chore: add description to the Build Binary workflow for clarity --- .github/workflows/build_binary.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index fde6883..2cc3acd 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -1,4 +1,7 @@ name: 'Build Binary' +description: | + This workflow builds the binary files for Windows. The build binaries are stored as artifact + and may be reused by other workflows. on: push: From 0c1052d31389c149ff1f36cc74adda537f0d6d05 Mon Sep 17 00:00:00 2001 From: Mqx <62719703+Mqxx@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:02:56 +0200 Subject: [PATCH 17/18] fix: format --- .github/workflows/build_binary.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index 2cc3acd..70186be 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -16,9 +16,6 @@ jobs: runs-on: windows-2025 env: ASTREIN_VERSION: '2.0.0' - outputs: - package_version: ${{ steps.version.outputs.PACKAGE_VERSION }} - is_valid_package_version: ${{ steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION }} steps: - name: 'Checkout repository' @@ -48,7 +45,9 @@ jobs: - name: 'Configure metadata context' shell: pwsh run: | - cmake -S . -B build/ffi -G Ninja ` + cmake -S . ` + -B build/ffi ` + -G Ninja ` -DCMAKE_BUILD_TYPE=Release ` -DCMAKE_C_COMPILER=clang-cl ` -DCMAKE_CXX_COMPILER=clang-cl ` @@ -108,6 +107,10 @@ jobs: name: cpp-bindings-windows-ffi path: dist/ffi/x86_64.ffi.json + outputs: + package_version: ${{ steps.version.outputs.PACKAGE_VERSION }} + is_valid_package_version: ${{ steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION }} + build-binary: name: 'Build x86_64-windows-msvc' runs-on: windows-2025 From 9d4b83085b17dc8865f2d5129719f68eb94aee78 Mon Sep 17 00:00:00 2001 From: Mqx <62719703+Mqxx@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:37:34 +0200 Subject: [PATCH 18/18] fix: README --- jsr/README.md | 149 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 137 insertions(+), 12 deletions(-) diff --git a/jsr/README.md b/jsr/README.md index 30909e7..92dd475 100644 --- a/jsr/README.md +++ b/jsr/README.md @@ -5,28 +5,153 @@ Binaries are provided as a [package on JSR](https://jsr.io/@serial/cpp-bindings-windows). They are serialized as a base64 string inside the JSON file. -The package currently contains the `x86_64-windows-msvc` DLL. The release DLL -statically includes the MSVC runtime. +This package targets server-side JavaScript runtimes that can write files and +load Windows dynamic libraries. Deno can consume it directly from JSR; Bun and +Node.js use JSR's npm compatibility layer. Browser and edge runtimes cannot use +the native library because they do not expose native FFI access. -It also includes cpp-core FFI API metadata generated with -[ASTrein](https://github.com/Katze719/ASTrein) at `bin/x86_64.ffi.json`. -It describes the exported C symbols, parameter and return types, callbacks, -default values, and API documentation used by downstream FFI adapter generators. +The package contains portable binaries for `x86_64`. The x86-64 artifact +uses the generic x86-64 baseline. + +## Binary compatibility + +Common release baselines are shown below for orientation: + +| Distribution | Release baseline | +| --- | --- | +| Windows | 10+ | + +## FFI metadata -This package is primarily intended as a dependency for [`@serial/serial`](https://jsr.io/@serial/serial). However, it can also be used independently. +It also includes cpp-core FFI API metadata generated with +[ASTrein](https://github.com/Katze719/ASTrein) at `bin/x86_64/ffi.json`. It describes the exported C symbols, parameter and +return types, callbacks, structs, default values, and API documentation used by +runtime-specific FFI adapter generators. +This package is primarily intended as a dependency for +[`@serial/serial`](https://jsr.io/@serial/serial). However, it can also be used +independently. ## Usage -Import the JSON and write the binary data to disk: +Select the export matching the host architecture. Each export contains the +base64-encoded shared library and its matching FFI metadata. The following +examples write the library to disk, load it, and release it again. + +### Deno + +Deno provides native JSR imports and the built-in `Deno.dlopen` FFI API. Save +this as `example.ts`: + +```ts +import { x86_64 } from "jsr:@serial/cpp-bindings-windows/bin"; + +const binary = x86_64; +const path = `./${binary.filename}`; + +Deno.writeFileSync(path, Uint8Array.fromBase64(binary.data)); + +const library = Deno.dlopen(path, { + serialOpen: { + parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"], + result: "i64", + }, +}); +library.close(); +``` + +Run it with write and FFI permissions: + +```sh +deno run --allow-write --allow-ffi example.ts +``` + +### Bun + +Add the package through JSR's npm compatibility layer: + +```sh +bunx jsr add @serial/cpp-bindings-windows +``` + +Then use Bun's built-in `bun:ffi` and `Bun.write` APIs: ```ts -import { x86_64 } from '@serial/cpp-bindings-windows/bin'; +import { dlopen } from "bun:ffi"; +import { resolve } from "node:path"; +import { x86_64 } from "@serial/cpp-bindings-windows/bin"; + +const binary = x86_64; + +const path = resolve(binary.filename); +await Bun.write(path, Buffer.from(binary.data, "base64")); + +const library = dlopen(path, { + serialOpen: { + args: ["ptr", "i32", "i32", "i32", "i32", "ptr"], + returns: "i64", + }, +}); +library.close(); +``` + +```sh +bun run example.ts +``` + +> [!WARNING] +> Bun currently marks its built-in +> [`bun:ffi` API](https://bun.sh/docs/runtime/ffi) as experimental. + +### Node.js -Deno.writeFileSync(`./${x86_64.filename}`, Uint8Array.fromBase64(x86_64.data)); +Node.js does not provide a general-purpose C FFI API. This example uses +[Koffi](https://koffi.dev/), together with JSR's npm compatibility layer: -// Now you can open the binary using for example `Deno.dlopen`... +```sh +npx jsr add @serial/cpp-bindings-windows +npm install koffi ``` +Save this as `example.mjs`: + +```js +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import koffi from "koffi"; +import { x86_64 } from "@serial/cpp-bindings-windows/bin"; + +const binary = x86_64; + +const path = resolve(binary.filename); +writeFileSync(path, Buffer.from(binary.data, "base64")); + +const library = koffi.load(path); +library.func("serialOpen", "int64_t", [ + "void *", + "int", + "int", + "int", + "int", + "void *", +]); +library.unload(); +``` + +```sh +node example.mjs +``` + +These examples verify that the native library can be loaded and that its +`serialOpen` symbol can be resolved. The matching `binary.ffi` value describes +the complete set of symbols and structs for generating or configuring +runtime-specific bindings. + +Non-JavaScript consumers can download the same architecture-specific `.so` and +`.ffi.json` files directly from the +[GitHub releases](https://github.com/Serial-IO/cpp-bindings-windows/releases). + > [!NOTE] -> For a more in depth guide, check out the [Wiki](https://github.com/Serial-IO/cpp-bindings-windows/wiki) section on how to use the C++ bindings for Windows. +> For a more in depth guide, check out the +> [Wiki](https://github.com/Serial-IO/cpp-bindings-windows/wiki) section on how to +> use the C++ bindings for Windows.