From b1296b315d1eea10315812a6cbdacacee6c6e90c Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sat, 14 Mar 2026 16:41:11 +0100 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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