diff --git a/.clang-tidy b/.clang-tidy index 554459d..e8161b6 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -587,10 +587,12 @@ CheckOptions: value: '' - key: zircon-temporary-objects.Names value: '' -# allow x,y,z single letter names, to be used for coordinates -# allow fd (file descriptor) as it's a common POSIX convention + - key: readability-identifier-length.MinimumVariableNameLength + value: '3' + - key: readability-identifier-length.MinimumParameterNameLength + value: '3' - key: readability-identifier-length.IgnoredVariableNames - value: '^(x|y|z|m0|m1|fd|_)$' + value: '^_$' - key: readability-identifier-length.IgnoredParameterNames - value: '^(x|y|z|m0|m1|fd|_)$' + value: '^_$' ... diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index 6f70fc2..70186be 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -5,18 +5,17 @@ description: | 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: '2.0.0' steps: - name: 'Checkout repository' @@ -27,89 +26,216 @@ jobs: - name: 'Setup CMake' uses: jwlawson/actions-setup-cmake@v2 with: - cmake-version: '3.31.x' + cmake-version: '4.3.x' - - name: 'Configure CMake' - run: | - cmake --preset windows-vs-release + - 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 ASTrein' + id: astrein + uses: Katze719/setup-astrein@v1 + with: + version: ${{ env.ASTREIN_VERSION }} - - name: 'Build' - id: build + - 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_C_COMPILER=clang-cl ` + -DCMAKE_CXX_COMPILER=clang-cl ` + -DCPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT=ON ` + "-DCPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE=${{ steps.astrein.outputs.path }}" ` + "-DCPP_BINDINGS_WINDOWS_FFI_JSON_OUTPUT=${env:GITHUB_WORKSPACE}/dist/ffi/x86_64.ffi.json" + + - name: 'Generate metadata' + run: | + cmake --build build/ffi --target cpp_bindings_windows_ffi_json - - name: 'Set PACKAGE_VERSION from env.bat' - id: version + - name: 'Verify FFI metadata' 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 + $metadataPath = 'dist/ffi/x86_64.ffi.json' + $metadata = Get-Content -Raw $metadataPath | + ConvertFrom-Json -ErrorAction Stop - - name: 'Copy DLL to stable path and upload artifact' + if ($metadata.schema -ne 'astrein_ffi_api') { + throw "Unexpected FFI metadata schema: $($metadata.schema)" + } + if ($metadata.schemaVersion -ne 2) { + 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 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 + $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: 'Upload artifacts' - uses: actions/upload-artifact@v4 - with: - if-no-files-found: error - name: cpp_bindings_windows - path: build/out/cpp_bindings_windows.dll - - - name: 'Check tag' + - name: 'Check package version' id: check-tag shell: pwsh env: PACKAGE_VERSION: ${{ steps.version.outputs.PACKAGE_VERSION }} - 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 + 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 { - echo "IS_VALID_PACKAGE_VERSION=false" >> $env:GITHUB_OUTPUT + "IS_VALID_PACKAGE_VERSION=false" >> $env:GITHUB_OUTPUT } - - name: 'Create GitHub Release' - if: github.ref_type == 'tag' && steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION == 'true' - uses: softprops/action-gh-release@v2 + - name: 'Upload FFI metadata' + 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: error + name: cpp-bindings-windows-ffi + path: dist/ffi/x86_64.ffi.json outputs: package_version: ${{ steps.version.outputs.PACKAGE_VERSION }} is_valid_package_version: ${{ steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION }} + build-binary: + name: 'Build x86_64-windows-msvc' + runs-on: windows-2025 + + steps: + - name: 'Checkout repository' + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 'Setup CMake' + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: '4.3.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 ` + -DCPP_BINDINGS_WINDOWS_STATIC_MSVC_RUNTIME=ON + + - name: 'Build and test' + run: | + cmake --build --preset windows-clang-release ` + --target cpp_bindings_windows cpp_bindings_windows_tests ` + --parallel 4 + ctest --test-dir build ` + --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/' + } + + 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: + if-no-files-found: warn + name: test-report-x86_64-windows-msvc + path: build/test-report.xml + + - 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..962f039 100644 --- a/.github/workflows/deno_tests.yml +++ b/.github/workflows/deno_tests.yml @@ -21,7 +21,15 @@ 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 + 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 @@ -30,11 +38,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 + cmake --build --preset windows-clang-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..9fb4f45 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 @@ -31,15 +33,23 @@ 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 + 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-vs-release + cmake --preset windows-clang-release - name: 'Build tests' run: | - cmake --build --preset windows-vs-release --config Release + cmake --build --preset windows-clang-release --target cpp_bindings_windows_tests - name: 'Copy library artifact next to test exe' shell: pwsh @@ -52,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 672c1ca..655abc2 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,145 @@ 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" ) +# 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 "") +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}") + 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 @@ -63,11 +178,21 @@ 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() # 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 +209,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 +245,7 @@ if(TEST_SOURCES) PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_BINARY_DIR}/generated + ${cpp_core_SOURCE_DIR}/include ) target_link_libraries( @@ -125,10 +256,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 +294,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..2c2f306 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -35,6 +35,29 @@ "CMAKE_C_COMPILER": "cl", "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", + "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 +72,14 @@ { "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 0bd5b40..c59f514 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 (4.3 or newer when building with clang-cl) +- 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/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; } - - diff --git a/jsr/README.md b/jsr/README.md index e4b7be4..92dd475 100644 --- a/jsr/README.md +++ b/jsr/README.md @@ -5,20 +5,153 @@ Binaries are provided as a [package on JSR](https://jsr.io/@serial/cpp-bindings-windows). They are serialized as a base64 string inside the JSON file. -This package is primarily intended as a dependency for [`@serial/serial`](https://jsr.io/@serial/serial). However, it can also be used independently. +This package targets server-side JavaScript runtimes that can write files and +load Windows dynamic libraries. Deno can consume it directly from JSR; Bun and +Node.js use JSR's npm compatibility layer. Browser and edge runtimes cannot use +the native library because they do not expose native FFI access. +The package contains portable binaries for `x86_64`. The x86-64 artifact +uses the generic x86-64 baseline. + +## Binary compatibility + +Common release baselines are shown below for orientation: + +| Distribution | Release baseline | +| --- | --- | +| Windows | 10+ | + +## FFI metadata + +It also includes cpp-core FFI API metadata generated with +[ASTrein](https://github.com/Katze719/ASTrein) at `bin/x86_64/ffi.json`. It describes the exported C symbols, parameter and +return types, callbacks, structs, default values, and API documentation used by +runtime-specific FFI adapter generators. + +This package is primarily intended as a dependency for +[`@serial/serial`](https://jsr.io/@serial/serial). However, it can also be used +independently. ## Usage -Import the JSON and write the binary data to disk: +Select the export matching the host architecture. Each export contains the +base64-encoded shared library and its matching FFI metadata. The following +examples write the library to disk, load it, and release it again. + +### Deno + +Deno provides native JSR imports and the built-in `Deno.dlopen` FFI API. Save +this as `example.ts`: + +```ts +import { x86_64 } from "jsr:@serial/cpp-bindings-windows/bin"; + +const binary = x86_64; +const path = `./${binary.filename}`; + +Deno.writeFileSync(path, Uint8Array.fromBase64(binary.data)); + +const library = Deno.dlopen(path, { + serialOpen: { + parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"], + result: "i64", + }, +}); +library.close(); +``` + +Run it with write and FFI permissions: + +```sh +deno run --allow-write --allow-ffi example.ts +``` + +### Bun + +Add the package through JSR's npm compatibility layer: + +```sh +bunx jsr add @serial/cpp-bindings-windows +``` + +Then use Bun's built-in `bun:ffi` and `Bun.write` APIs: ```ts -import { x86_64 } from '@serial/cpp-bindings-windows/bin'; +import { dlopen } from "bun:ffi"; +import { resolve } from "node:path"; +import { x86_64 } from "@serial/cpp-bindings-windows/bin"; + +const binary = x86_64; + +const path = resolve(binary.filename); +await Bun.write(path, Buffer.from(binary.data, "base64")); + +const library = dlopen(path, { + serialOpen: { + args: ["ptr", "i32", "i32", "i32", "i32", "ptr"], + returns: "i64", + }, +}); +library.close(); +``` + +```sh +bun run example.ts +``` + +> [!WARNING] +> Bun currently marks its built-in +> [`bun:ffi` API](https://bun.sh/docs/runtime/ffi) as experimental. + +### Node.js -Deno.writeFileSync(`./${x86_64.filename}`, Uint8Array.fromBase64(x86_64.data)); +Node.js does not provide a general-purpose C FFI API. This example uses +[Koffi](https://koffi.dev/), together with JSR's npm compatibility layer: -// Now you can open the binary using for example `Deno.dlopen`... +```sh +npx jsr add @serial/cpp-bindings-windows +npm install koffi ``` +Save this as `example.mjs`: + +```js +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import koffi from "koffi"; +import { x86_64 } from "@serial/cpp-bindings-windows/bin"; + +const binary = x86_64; + +const path = resolve(binary.filename); +writeFileSync(path, Buffer.from(binary.data, "base64")); + +const library = koffi.load(path); +library.func("serialOpen", "int64_t", [ + "void *", + "int", + "int", + "int", + "int", + "void *", +]); +library.unload(); +``` + +```sh +node example.mjs +``` + +These examples verify that the native library can be loaded and that its +`serialOpen` symbol can be resolved. The matching `binary.ffi` value describes +the complete set of symbols and structs for generating or configuring +runtime-specific bindings. + +Non-JavaScript consumers can download the same architecture-specific `.so` and +`.ffi.json` files directly from the +[GitHub releases](https://github.com/Serial-IO/cpp-bindings-windows/releases). + > [!NOTE] -> For a more in depth guide, check out the [Wiki](https://github.com/Serial-IO/cpp-bindings-windows/wiki) section on how to use the C++ bindings for Windows. +> For a more in depth guide, check out the +> [Wiki](https://github.com/Serial-IO/cpp-bindings-windows/wiki) section on how to +> use the C++ bindings for Windows. 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/abort_flag.hpp b/src/detail/abort_flag.hpp new file mode 100644 index 0000000..ddaabdc --- /dev/null +++ b/src/detail/abort_flag.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "handle_types.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto abortFlag(const std::shared_ptr &state, Operation operation) -> std::atomic & +{ + return operation == Operation::kRead ? state->abort_read : state->abort_write; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/acquire_handle_context.hpp b/src/detail/acquire_handle_context.hpp new file mode 100644 index 0000000..1b7f0fd --- /dev/null +++ b/src/detail/acquire_handle_context.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "ensure_handle_state.hpp" +#include "validate_win32_handle.hpp" + +namespace cpp_bindings_windows::detail +{ +template +inline auto acquireHandleContext(int64_t handle, ErrorCallbackT error_callback, HandleContext *out_context) + -> ReturnType +{ + HANDLE native_handle = nullptr; + const auto status = validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + out_context->handle = native_handle; + out_context->state = ensureHandleState(native_handle); + return static_cast(StatusCode::kSuccess); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/apply_line_settings.hpp b/src/detail/apply_line_settings.hpp new file mode 100644 index 0000000..6cc8331 --- /dev/null +++ b/src/detail/apply_line_settings.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include "win32_error_to_string.hpp" + +#include +#include + +namespace cpp_bindings_windows::detail +{ +inline auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Parity parity_value, + cpp_core::StopBits stop_bits_value) -> cpp_core::Status +{ + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + + if (GetCommState(handle, &serial_settings) == 0) + { + const DWORD error = GetLastError(); + return cpp_core::fail(cpp_core::StatusCode::Control::kGetStateError, + "GetCommState failed: " + win32ErrorToString(error)); + } + + serial_settings.BaudRate = static_cast(baudrate); + serial_settings.ByteSize = static_cast(data_bits); + + serial_settings.fBinary = TRUE; + serial_settings.fParity = (parity_value != cpp_core::Parity::kNone) ? TRUE : FALSE; + serial_settings.fOutxCtsFlow = FALSE; + serial_settings.fOutxDsrFlow = FALSE; + serial_settings.fDtrControl = DTR_CONTROL_ENABLE; + serial_settings.fDsrSensitivity = FALSE; + serial_settings.fTXContinueOnXoff = TRUE; + serial_settings.fOutX = FALSE; + serial_settings.fInX = FALSE; + serial_settings.fRtsControl = RTS_CONTROL_ENABLE; + + switch (parity_value) + { + case cpp_core::Parity::kNone: + serial_settings.Parity = NOPARITY; + break; + case cpp_core::Parity::kEven: + serial_settings.Parity = EVENPARITY; + break; + case cpp_core::Parity::kOdd: + serial_settings.Parity = ODDPARITY; + break; + default: + return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, "Invalid parity"); + } + + if (stop_bits_value == cpp_core::StopBits::kOne) + { + serial_settings.StopBits = ONESTOPBIT; + } + else if (stop_bits_value == cpp_core::StopBits::kTwo) + { + serial_settings.StopBits = TWOSTOPBITS; + } + + if (SetCommState(handle, &serial_settings) == 0) + { + const DWORD error = GetLastError(); + return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, + "SetCommState failed: " + win32ErrorToString(error)); + } + + COMMTIMEOUTS communication_timeouts = {}; + if (SetCommTimeouts(handle, &communication_timeouts) == 0) + { + const DWORD error = GetLastError(); + return cpp_core::fail(cpp_core::StatusCode::Configuration::kSetTimeoutError, + "SetCommTimeouts failed: " + win32ErrorToString(error)); + } + + return cpp_core::ok(); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/bytes_waiting.hpp b/src/detail/bytes_waiting.hpp new file mode 100644 index 0000000..a80c33f --- /dev/null +++ b/src/detail/bytes_waiting.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "windows.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto bytesWaiting(HANDLE handle, int *out_bytes) -> bool +{ + if (out_bytes == nullptr) + { + return false; + } + *out_bytes = 0; + + DWORD errors = 0; + COMSTAT status = {}; + if (ClearCommError(handle, &errors, &status) == 0) + { + return false; + } + + *out_bytes = status.cbInQue > static_cast(INT_MAX) ? INT_MAX : static_cast(status.cbInQue); + return true; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/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/consume_abort.hpp b/src/detail/consume_abort.hpp new file mode 100644 index 0000000..a3d2ce3 --- /dev/null +++ b/src/detail/consume_abort.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "abort_flag.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto consumeAbort(const std::shared_ptr &state, Operation operation) -> bool +{ + return abortFlag(state, operation).exchange(false, std::memory_order_acq_rel); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/effective_error_callback.hpp b/src/detail/effective_error_callback.hpp new file mode 100644 index 0000000..e0f7da8 --- /dev/null +++ b/src/detail/effective_error_callback.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "common_types.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto effectiveErrorCallback(ErrorCallbackT error_callback) -> ErrorCallbackT +{ + return error_callback != nullptr ? error_callback : g_error_callback.load(std::memory_order_acquire); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/ensure_handle_state.hpp b/src/detail/ensure_handle_state.hpp new file mode 100644 index 0000000..3bbbbc5 --- /dev/null +++ b/src/detail/ensure_handle_state.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "handle_key.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto ensureHandleState(HANDLE handle) -> std::shared_ptr +{ + std::lock_guard lock(g_handle_states_mutex); + auto &state = g_handle_states[handleKey(handle)]; + if (!state) + { + state = std::make_shared(); + } + return state; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/fail_win32.hpp b/src/detail/fail_win32.hpp new file mode 100644 index 0000000..a0c4899 --- /dev/null +++ b/src/detail/fail_win32.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "effective_error_callback.hpp" +#include "win32_error_to_string.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +template +inline auto failWin32(ErrorCallbackT error_callback, StatusCodeValue code) -> ReturnType +{ + const DWORD error = GetLastError(); + const std::string message = win32ErrorToString(error); + cpp_core::invokeError(effectiveErrorCallback(error_callback), code, message); + return static_cast(code); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/finish_pending_io.hpp b/src/detail/finish_pending_io.hpp new file mode 100644 index 0000000..d8d2ad0 --- /dev/null +++ b/src/detail/finish_pending_io.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "consume_abort.hpp" +#include "pending_operation.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto finishPendingIo(const std::shared_ptr &state, Operation operation, OVERLAPPED *overlapped) + -> bool +{ + std::lock_guard lock(state->pending_io_mutex); + auto &pending = pendingOperation(state, operation); + if (pending == overlapped) + { + pending = nullptr; + } + return consumeAbort(state, operation); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/handle_key.hpp b/src/detail/handle_key.hpp new file mode 100644 index 0000000..dc6f8df --- /dev/null +++ b/src/detail/handle_key.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "handle_types.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto handleKey(HANDLE handle) -> std::uintptr_t +{ + return reinterpret_cast(handle); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/handle_types.hpp b/src/detail/handle_types.hpp new file mode 100644 index 0000000..fa5ede4 --- /dev/null +++ b/src/detail/handle_types.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include "common_types.hpp" +#include "windows.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace cpp_bindings_windows::detail +{ +enum class Operation +{ + kRead, + kWrite, +}; + +struct Win32HandleTraits +{ + using handle_type = HANDLE; // NOLINT(readability-identifier-naming) + + static constexpr auto invalid() noexcept -> handle_type + { + return nullptr; + } + + static auto close(handle_type handle) noexcept -> void + { + if (handle != nullptr && handle != INVALID_HANDLE_VALUE) + { + CloseHandle(handle); + } + } +}; + +using UniqueHandle = cpp_core::UniqueResource; + +struct HandleState +{ + std::atomic bytes_read_total{0}; + std::atomic bytes_written_total{0}; + std::atomic abort_read{false}; + std::atomic abort_write{false}; + std::mutex pending_io_mutex; + OVERLAPPED *pending_read = nullptr; + OVERLAPPED *pending_write = nullptr; +}; + +struct HandleContext +{ + HANDLE handle = nullptr; + std::shared_ptr state; +}; + +struct PendingIoStart +{ + BOOL completed = FALSE; + DWORD error = ERROR_SUCCESS; + bool aborted = false; +}; + +inline std::mutex g_handle_states_mutex; +inline std::unordered_map> g_handle_states; +inline std::atomic g_read_callback{nullptr}; +inline std::atomic g_write_callback{nullptr}; +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/io_types.hpp b/src/detail/io_types.hpp new file mode 100644 index 0000000..adc98c2 --- /dev/null +++ b/src/detail/io_types.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "windows.hpp" + +namespace cpp_bindings_windows::detail +{ +enum class IoOutcome +{ + kCompleted, + kTimedOut, + kAborted, + kError, +}; + +struct IoResult +{ + IoOutcome outcome = IoOutcome::kError; + int bytes_transferred = 0; + DWORD error = ERROR_SUCCESS; +}; +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/matches_suffix.hpp b/src/detail/matches_suffix.hpp new file mode 100644 index 0000000..21d1ba6 --- /dev/null +++ b/src/detail/matches_suffix.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto matchesSuffix(const unsigned char *buffer, int buffer_size, const unsigned char *terminator, + int terminator_size) -> bool +{ + return terminator_size > 0 && buffer_size >= terminator_size && + std::memcmp(buffer + buffer_size - terminator_size, terminator, static_cast(terminator_size)) == + 0; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/multiplier_timeout.hpp b/src/detail/multiplier_timeout.hpp new file mode 100644 index 0000000..5f2eb63 --- /dev/null +++ b/src/detail/multiplier_timeout.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include +#include + +namespace cpp_bindings_windows::detail +{ +inline auto multiplierTimeout(int timeout_ms, int multiplier) -> int +{ + if (multiplier <= 0) + { + return 0; + } + + const auto timeout = static_cast(cpp_core::clampTimeout(timeout_ms)) * multiplier; + return timeout > INT_MAX ? INT_MAX : static_cast(timeout); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/normalize_port_path.hpp b/src/detail/normalize_port_path.hpp new file mode 100644 index 0000000..8e4865d --- /dev/null +++ b/src/detail/normalize_port_path.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace cpp_bindings_windows::detail +{ +inline auto normalizePortPath(std::wstring_view port) -> std::wstring +{ + if (port.starts_with(L"\\\\.\\")) + { + return std::wstring(port); + } + if (port.starts_with(L"COM") || port.starts_with(L"com")) + { + return L"\\\\.\\" + std::wstring(port); + } + return std::wstring(port); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/note_bytes_transferred.hpp b/src/detail/note_bytes_transferred.hpp new file mode 100644 index 0000000..a046545 --- /dev/null +++ b/src/detail/note_bytes_transferred.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "handle_types.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto noteBytesTransferred(const std::shared_ptr &state, Operation operation, int transferred_bytes) + -> void +{ + if (operation == Operation::kRead) + { + state->bytes_read_total.fetch_add(transferred_bytes, std::memory_order_relaxed); + if (const auto callback = g_read_callback.load(std::memory_order_acquire); callback != nullptr) + { + callback(transferred_bytes); + } + return; + } + + state->bytes_written_total.fetch_add(transferred_bytes, std::memory_order_relaxed); + if (const auto callback = g_write_callback.load(std::memory_order_acquire); callback != nullptr) + { + callback(transferred_bytes); + } +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/pending_operation.hpp b/src/detail/pending_operation.hpp new file mode 100644 index 0000000..08528a2 --- /dev/null +++ b/src/detail/pending_operation.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "handle_types.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto pendingOperation(const std::shared_ptr &state, Operation operation) -> OVERLAPPED *& +{ + return operation == Operation::kRead ? state->pending_read : state->pending_write; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/read_chunk.hpp b/src/detail/read_chunk.hpp new file mode 100644 index 0000000..5054c5f --- /dev/null +++ b/src/detail/read_chunk.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "start_pending_io.hpp" +#include "wait_for_pending_io.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto readChunk(const HandleContext &context, unsigned char *buffer, int buffer_size, int timeout_ms) -> IoResult +{ + UniqueHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!event) + { + return {.outcome = IoOutcome::kError, .error = GetLastError()}; + } + + OVERLAPPED overlapped = {}; + overlapped.hEvent = event.get(); + DWORD transferred = 0; + const auto start = startPendingIo(context.state, Operation::kRead, &overlapped, [&] { + return ReadFile(context.handle, buffer, static_cast(buffer_size), &transferred, &overlapped); + }); + if (start.aborted) + { + return {.outcome = IoOutcome::kAborted}; + } + if (start.completed != FALSE) + { + const bool aborted = finishPendingIo(context.state, Operation::kRead, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kCompleted, + .bytes_transferred = static_cast(transferred)}; + } + if (start.error != ERROR_IO_PENDING) + { + const bool aborted = finishPendingIo(context.state, Operation::kRead, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = start.error}; + } + + return waitForPendingIo(context.handle, context.state, Operation::kRead, &overlapped, timeout_ms); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/read_impl.hpp b/src/detail/read_impl.hpp new file mode 100644 index 0000000..92ca7e9 --- /dev/null +++ b/src/detail/read_impl.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include "acquire_handle_context.hpp" +#include "bytes_waiting.hpp" +#include "fail_win32.hpp" +#include "matches_suffix.hpp" +#include "multiplier_timeout.hpp" +#include "note_bytes_transferred.hpp" +#include "read_chunk.hpp" + +#include + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto readImpl(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, + const unsigned char *terminator, int terminator_size, ErrorCallbackT error_callback) -> int +{ + const auto callback = effectiveErrorCallback(error_callback); + const auto buffer_status = cpp_core::validateBuffer(buffer, buffer_size, callback); + if (buffer_status < 0) + { + return buffer_status; + } + if (terminator_size > 0 && terminator == nullptr) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kBufferError), + "Invalid terminator"); + } + + HandleContext context; + const auto handle_status = acquireHandleContext(handle, callback, &context); + if (handle_status < 0) + { + return handle_status; + } + + auto *output = static_cast(buffer); + int total_read = 0; + while (total_read < buffer_size) + { + int chunk_size = 1; + if (terminator_size <= 0) + { + int waiting = 0; + if (!bytesWaiting(context.handle, &waiting)) + { + return failWin32(callback, static_cast(StatusCode::Control::kGetStateError)); + } + chunk_size = waiting > 0 ? std::min(waiting, buffer_size - total_read) : 1; + } + + const int current_timeout = + total_read == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); + const auto result = readChunk(context, output + total_read, chunk_size, current_timeout); + if (result.outcome == IoOutcome::kTimedOut) + { + return total_read; + } + if (result.outcome == IoOutcome::kAborted) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kAbortReadError), + "Read aborted"); + } + if (result.outcome == IoOutcome::kError) + { + SetLastError(result.error); + return failWin32(callback, static_cast(StatusCode::Io::kReadError)); + } + if (result.bytes_transferred <= 0) + { + return total_read; + } + + noteBytesTransferred(context.state, Operation::kRead, result.bytes_transferred); + total_read += result.bytes_transferred; + if (matchesSuffix(output, total_read, terminator, terminator_size)) + { + return total_read; + } + } + + return total_read; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/register_opened_handle.hpp b/src/detail/register_opened_handle.hpp new file mode 100644 index 0000000..912e5bf --- /dev/null +++ b/src/detail/register_opened_handle.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ensure_handle_state.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto registerOpenedHandle(HANDLE handle) -> void +{ + (void)ensureHandleState(handle); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/remove_handle_state.hpp b/src/detail/remove_handle_state.hpp new file mode 100644 index 0000000..66de817 --- /dev/null +++ b/src/detail/remove_handle_state.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "handle_key.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto removeHandleState(HANDLE handle) -> void +{ + std::lock_guard lock(g_handle_states_mutex); + g_handle_states.erase(handleKey(handle)); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/request_abort.hpp b/src/detail/request_abort.hpp new file mode 100644 index 0000000..dc990a3 --- /dev/null +++ b/src/detail/request_abort.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "abort_flag.hpp" +#include "pending_operation.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto requestAbort(HANDLE handle, const std::shared_ptr &state, Operation operation) -> void +{ + abortFlag(state, operation).store(true, std::memory_order_release); + + std::lock_guard lock(state->pending_io_mutex); + if (auto *pending = pendingOperation(state, operation); pending != nullptr) + { + (void)CancelIoEx(handle, pending); + } +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/start_pending_io.hpp b/src/detail/start_pending_io.hpp new file mode 100644 index 0000000..b46c6f9 --- /dev/null +++ b/src/detail/start_pending_io.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "consume_abort.hpp" +#include "pending_operation.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +template +inline auto startPendingIo(const std::shared_ptr &state, Operation operation, OVERLAPPED *overlapped, + StartOperation &&start_operation) -> PendingIoStart +{ + std::lock_guard lock(state->pending_io_mutex); + if (consumeAbort(state, operation)) + { + return {.aborted = true}; + } + + pendingOperation(state, operation) = overlapped; + const BOOL completed = std::forward(start_operation)(); + return {.completed = completed, .error = completed != FALSE ? ERROR_SUCCESS : GetLastError()}; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/utf8_to_wide.hpp b/src/detail/utf8_to_wide.hpp new file mode 100644 index 0000000..4f0bae3 --- /dev/null +++ b/src/detail/utf8_to_wide.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "windows.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto utf8ToWide(const char *utf8) -> std::wstring +{ + if (utf8 == nullptr || *utf8 == '\0') + { + return {}; + } + + const int required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, -1, nullptr, 0); + if (required <= 0) + { + return {}; + } + + std::wstring wide(static_cast(required), L'\0'); + if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, -1, wide.data(), required) <= 0) + { + return {}; + } + wide.pop_back(); + return wide; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/validate_win32_handle.hpp b/src/detail/validate_win32_handle.hpp new file mode 100644 index 0000000..e75e982 --- /dev/null +++ b/src/detail/validate_win32_handle.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "effective_error_callback.hpp" +#include "handle_types.hpp" + +#include + +#include + +namespace cpp_bindings_windows::detail +{ +template +inline auto validateWin32Handle(int64_t handle, ErrorCallbackT error_callback, HANDLE *out_handle) -> ReturnType +{ + const auto callback = effectiveErrorCallback(error_callback); + if (handle <= 0) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + + if constexpr (sizeof(intptr_t) < sizeof(int64_t)) + { + if (handle > static_cast(std::numeric_limits::max())) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + } + + const auto native_handle = reinterpret_cast(static_cast(handle)); + if (native_handle == nullptr || native_handle == INVALID_HANDLE_VALUE) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + + *out_handle = native_handle; + return static_cast(StatusCode::kSuccess); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/wait_for_pending_io.hpp b/src/detail/wait_for_pending_io.hpp new file mode 100644 index 0000000..839f726 --- /dev/null +++ b/src/detail/wait_for_pending_io.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "finish_pending_io.hpp" +#include "io_types.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto waitForPendingIo(HANDLE handle, const std::shared_ptr &state, Operation operation, + OVERLAPPED *overlapped, int timeout_ms) -> IoResult +{ + const DWORD wait_result = + WaitForSingleObject(overlapped->hEvent, static_cast(cpp_core::clampTimeout(timeout_ms))); + if (wait_result == WAIT_TIMEOUT) + { + (void)CancelIoEx(handle, overlapped); + DWORD ignored = 0; + (void)GetOverlappedResult(handle, overlapped, &ignored, TRUE); + if (finishPendingIo(state, operation, overlapped)) + { + return {.outcome = IoOutcome::kAborted}; + } + return {.outcome = IoOutcome::kTimedOut}; + } + + if (wait_result != WAIT_OBJECT_0) + { + const DWORD error = GetLastError(); + (void)CancelIoEx(handle, overlapped); + DWORD ignored = 0; + (void)GetOverlappedResult(handle, overlapped, &ignored, TRUE); + const bool aborted = finishPendingIo(state, operation, overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = error}; + } + + DWORD transferred = 0; + const BOOL completed = GetOverlappedResult(handle, overlapped, &transferred, FALSE); + const DWORD error = completed != FALSE ? ERROR_SUCCESS : GetLastError(); + const bool aborted = finishPendingIo(state, operation, overlapped); + if (aborted || error == ERROR_OPERATION_ABORTED) + { + return {.outcome = IoOutcome::kAborted}; + } + if (completed == FALSE) + { + return {.outcome = IoOutcome::kError, .error = error}; + } + return {.outcome = IoOutcome::kCompleted, .bytes_transferred = static_cast(transferred)}; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/wide_to_utf8.hpp b/src/detail/wide_to_utf8.hpp new file mode 100644 index 0000000..8d0cc1e --- /dev/null +++ b/src/detail/wide_to_utf8.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "windows.hpp" + +#include +#include + +namespace cpp_bindings_windows::detail +{ +inline auto wideToUtf8(std::wstring_view wide) -> std::string +{ + if (wide.empty()) + { + return {}; + } + + const int required = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), + nullptr, 0, nullptr, nullptr); + if (required <= 0) + { + return {}; + } + + std::string utf8(static_cast(required), '\0'); + if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), utf8.data(), + required, nullptr, nullptr) <= 0) + { + return {}; + } + return utf8; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/win32_error_to_string.hpp b/src/detail/win32_error_to_string.hpp new file mode 100644 index 0000000..af8433c --- /dev/null +++ b/src/detail/win32_error_to_string.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "windows.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto win32ErrorToString(DWORD error) -> std::string +{ + LPSTR buffer = nullptr; + const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; + const DWORD language_id = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); + const DWORD length = + FormatMessageA(flags, nullptr, error, language_id, reinterpret_cast(&buffer), 0, nullptr); + if (length == 0 || buffer == nullptr) + { + return "Unknown Win32 error (" + std::to_string(error) + ")"; + } + + std::string message(buffer, length); + LocalFree(buffer); + while (!message.empty() && (message.back() == '\r' || message.back() == '\n')) + { + message.pop_back(); + } + return message; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/win32_helpers.hpp b/src/detail/win32_helpers.hpp deleted file mode 100644 index 6f404e6..0000000 --- a/src/detail/win32_helpers.hpp +++ /dev/null @@ -1,120 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include - -#include -#include -#include -#include - -namespace cpp_bindings_windows::detail -{ - -// Win32 HANDLE traits for UniqueResource -struct Win32HandleTraits -{ - using handle_type = HANDLE; - - static constexpr auto invalid() noexcept -> handle_type - { - return nullptr; - } - - static auto close(handle_type h) noexcept -> void - { - if (h != INVALID_HANDLE_VALUE) - { - CloseHandle(h); - } - } -}; - -using UniqueHandle = cpp_core::UniqueResource; - -// Win32-specific error helpers -inline auto win32ErrorToString(DWORD err) -> std::string -{ - 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); - - const DWORD len = FormatMessageA(flags, nullptr, err, lang_id, reinterpret_cast(&buffer), 0, nullptr); - if (len == 0 || buffer == nullptr) - { - return "Unknown Win32 error"; - } - - std::string msg(buffer, len); - LocalFree(buffer); - - while (!msg.empty() && (msg.back() == '\r' || msg.back() == '\n')) - { - msg.pop_back(); - } - return msg; -} - -template -inline auto failWin32(Callback &&error_callback, cpp_core::StatusCodes code) -> Ret -{ - const DWORD err = GetLastError(); - const std::string msg = win32ErrorToString(err); - cpp_core::invokeError(std::forward(error_callback), code, msg); - return static_cast(code); -} - -inline auto bytesWaiting(HANDLE handle, int *out_bytes) -> bool -{ - if (out_bytes == nullptr) - { - return false; - } - *out_bytes = 0; - - DWORD errors = 0; - COMSTAT stat = {}; - if (ClearCommError(handle, &errors, &stat) == 0) - { - return false; - } - - if (stat.cbInQue > static_cast(INT_MAX)) - { - *out_bytes = INT_MAX; - } - else - { - *out_bytes = static_cast(stat.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/detail/windows.hpp b/src/detail/windows.hpp new file mode 100644 index 0000000..26447cd --- /dev/null +++ b/src/detail/windows.hpp @@ -0,0 +1,27 @@ +#pragma once + +#ifdef _WIN32 + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#ifndef STRICT +#define STRICT +#endif + +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0601 +#endif + +#ifndef WINVER +#define WINVER _WIN32_WINNT +#endif + +#include + +#endif diff --git a/src/detail/write_chunk.hpp b/src/detail/write_chunk.hpp new file mode 100644 index 0000000..61ae060 --- /dev/null +++ b/src/detail/write_chunk.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "start_pending_io.hpp" +#include "wait_for_pending_io.hpp" + +namespace cpp_bindings_windows::detail +{ +inline auto writeChunk(const HandleContext &context, const unsigned char *buffer, int buffer_size, int timeout_ms) + -> IoResult +{ + UniqueHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!event) + { + return {.outcome = IoOutcome::kError, .error = GetLastError()}; + } + + OVERLAPPED overlapped = {}; + overlapped.hEvent = event.get(); + DWORD transferred = 0; + const auto start = startPendingIo(context.state, Operation::kWrite, &overlapped, [&] { + return WriteFile(context.handle, buffer, static_cast(buffer_size), &transferred, &overlapped); + }); + if (start.aborted) + { + return {.outcome = IoOutcome::kAborted}; + } + if (start.completed != FALSE) + { + const bool aborted = finishPendingIo(context.state, Operation::kWrite, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kCompleted, + .bytes_transferred = static_cast(transferred)}; + } + if (start.error != ERROR_IO_PENDING) + { + const bool aborted = finishPendingIo(context.state, Operation::kWrite, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = start.error}; + } + + return waitForPendingIo(context.handle, context.state, Operation::kWrite, &overlapped, timeout_ms); +} +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/write_impl.hpp b/src/detail/write_impl.hpp new file mode 100644 index 0000000..443cff1 --- /dev/null +++ b/src/detail/write_impl.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "acquire_handle_context.hpp" +#include "fail_win32.hpp" +#include "multiplier_timeout.hpp" +#include "note_bytes_transferred.hpp" +#include "write_chunk.hpp" + +#include + +namespace cpp_bindings_windows::detail +{ +inline auto writeImpl(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int multiplier, + ErrorCallbackT error_callback) -> int +{ + const auto callback = effectiveErrorCallback(error_callback); + const auto buffer_status = cpp_core::validateBuffer(buffer, buffer_size, callback); + if (buffer_status < 0) + { + return buffer_status; + } + + HandleContext context; + const auto handle_status = acquireHandleContext(handle, callback, &context); + if (handle_status < 0) + { + return handle_status; + } + + const auto *input = static_cast(buffer); + int total_written = 0; + while (total_written < buffer_size) + { + const int current_timeout = + total_written == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); + const auto result = writeChunk(context, input + total_written, buffer_size - total_written, current_timeout); + if (result.outcome == IoOutcome::kTimedOut) + { + return total_written; + } + if (result.outcome == IoOutcome::kAborted) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kAbortWriteError), + "Write aborted"); + } + if (result.outcome == IoOutcome::kError) + { + SetLastError(result.error); + return failWin32(callback, static_cast(StatusCode::Io::kWriteError)); + } + if (result.bytes_transferred <= 0) + { + return total_written; + } + + noteBytesTransferred(context.state, Operation::kWrite, result.bytes_transferred); + total_written += result.bytes_transferred; + } + + return total_written; +} +} // namespace cpp_bindings_windows::detail diff --git a/src/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..66cbc9f --- /dev/null +++ b/src/serial_abort_read.cpp @@ -0,0 +1,23 @@ +#include + +#include "detail/acquire_handle_context.hpp" +#include "detail/request_abort.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..a4b3405 --- /dev/null +++ b/src/serial_abort_write.cpp @@ -0,0 +1,23 @@ +#include + +#include "detail/acquire_handle_context.hpp" +#include "detail/request_abort.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..b45afe0 --- /dev/null +++ b/src/serial_clear_buffer_in.cpp @@ -0,0 +1,27 @@ +#include + +#include "detail/acquire_handle_context.hpp" +#include "detail/fail_win32.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..6a3c4ca --- /dev/null +++ b/src/serial_clear_buffer_out.cpp @@ -0,0 +1,27 @@ +#include + +#include "detail/acquire_handle_context.hpp" +#include "detail/fail_win32.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..ab186e5 100644 --- a/src/serial_close.cpp +++ b/src/serial_close.cpp @@ -1,7 +1,9 @@ #include #include -#include "detail/win32_helpers.hpp" +#include "detail/fail_win32.hpp" +#include "detail/remove_handle_state.hpp" +#include "detail/validate_win32_handle.hpp" extern "C" { @@ -13,21 +15,23 @@ extern "C" return 0; } - HANDLE h = nullptr; - const auto handle_ok = - cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (handle_ok < 0) + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) { - return handle_ok; + return status; } - if (CloseHandle(h) == 0) + if (CloseHandle(native_handle) == 0) { - return cpp_bindings_windows::detail::failWin32(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(native_handle); + 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..372404e --- /dev/null +++ b/src/serial_drain.cpp @@ -0,0 +1,27 @@ +#include + +#include "detail/acquire_handle_context.hpp" +#include "detail/fail_win32.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 new file mode 100644 index 0000000..14538f0 --- /dev/null +++ b/src/serial_get_baudrate.cpp @@ -0,0 +1,31 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialGetBaudrate(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + return static_cast(serial_settings.BaudRate); + } + +} // extern "C" diff --git a/src/serial_get_cts.cpp b/src/serial_get_cts.cpp new file mode 100644 index 0000000..c93706f --- /dev/null +++ b/src/serial_get_cts.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialGetCts(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(native_handle, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::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..4081acc --- /dev/null +++ b/src/serial_get_data_bits.cpp @@ -0,0 +1,31 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialGetDataBits(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + return static_cast(serial_settings.ByteSize); + } + +} // extern "C" diff --git a/src/serial_get_dcd.cpp b/src/serial_get_dcd.cpp new file mode 100644 index 0000000..1e884cc --- /dev/null +++ b/src/serial_get_dcd.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialGetDcd(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(native_handle, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::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..b04111d --- /dev/null +++ b/src/serial_get_dsr.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialGetDsr(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(native_handle, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::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..694f102 --- /dev/null +++ b/src/serial_get_flow_control.cpp @@ -0,0 +1,39 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialGetFlowControl(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + if (serial_settings.fOutxCtsFlow != 0 && serial_settings.fRtsControl == RTS_CONTROL_HANDSHAKE) + { + return 1; + } + if (serial_settings.fOutX != 0 && serial_settings.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..76ea6bd --- /dev/null +++ b/src/serial_get_parity.cpp @@ -0,0 +1,39 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialGetParity(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + switch (serial_settings.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..f68dc71 --- /dev/null +++ b/src/serial_get_ri.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialGetRi(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(native_handle, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::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..b849db3 --- /dev/null +++ b/src/serial_get_stop_bits.cpp @@ -0,0 +1,31 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialGetStopBits(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + return (serial_settings.StopBits == TWOSTOPBITS) ? 2 : 0; + } + +} // extern "C" diff --git a/src/serial_in_bytes_total.cpp b/src/serial_in_bytes_total.cpp new file mode 100644 index 0000000..9292db9 --- /dev/null +++ b/src/serial_in_bytes_total.cpp @@ -0,0 +1,20 @@ +#include + +#include "detail/acquire_handle_context.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..fd9f063 --- /dev/null +++ b/src/serial_in_bytes_waiting.cpp @@ -0,0 +1,29 @@ +#include + +#include "detail/acquire_handle_context.hpp" +#include "detail/bytes_waiting.hpp" +#include "detail/fail_win32.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..5ec4238 --- /dev/null +++ b/src/serial_list_ports.cpp @@ -0,0 +1,220 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/wide_to_utf8.hpp" +#include "detail/windows.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ +struct PortInformation +{ + 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_information_set, SP_DEVINFO_DATA *device_information) -> std::optional +{ + HKEY key = SetupDiOpenDevRegKey(device_information_set, device_information, DICS_FLAG_GLOBAL, 0, DIREG_DEV, + KEY_QUERY_VALUE); + if (key == INVALID_HANDLE_VALUE) + { + return std::nullopt; + } + const auto value = registryString(key, L"PortName"); + RegCloseKey(key); + return value; +} + +auto deviceProperty(HDEVINFO device_information_set, SP_DEVINFO_DATA *device_information, DWORD property) + -> std::optional +{ + DWORD type = 0; + DWORD size = 0; + (void)SetupDiGetDeviceRegistryPropertyW(device_information_set, device_information, property, &type, nullptr, 0, + &size); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || size < sizeof(wchar_t)) + { + return std::nullopt; + } + + std::vector buffer(size); + if (SetupDiGetDeviceRegistryPropertyW(device_information_set, device_information, property, &type, buffer.data(), + size, nullptr) == 0) + { + return std::nullopt; + } + return std::wstring(reinterpret_cast(buffer.data())); +} + +auto instanceId(HDEVINFO device_information_set, SP_DEVINFO_DATA *device_information) -> std::optional +{ + DWORD required = 0; + (void)SetupDiGetDeviceInstanceIdW(device_information_set, device_information, nullptr, 0, &required); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || required == 0) + { + return std::nullopt; + } + + std::vector buffer(required); + if (SetupDiGetDeviceInstanceIdW(device_information_set, device_information, 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_function)(const char *port, const char *path, + const char *manufacturer, const char *serial_number, + const char *pnp_id, const char *location_id, + const char *product_id, const char *vendor_id), + ErrorCallbackT error_callback) -> int + { + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + if (callback_function == nullptr) + { + return cpp_core::failMsg( + callback, static_cast(cpp_core::StatusCode::Io::kBufferError), + "Port callback must not be null"); + } + + const HDEVINFO device_information_set = + SetupDiGetClassDevsW(&GUID_DEVCLASS_PORTS, nullptr, nullptr, DIGCF_PRESENT); + if (device_information_set == INVALID_HANDLE_VALUE) + { + return cpp_bindings_windows::detail::failWin32( + callback, static_cast(cpp_core::StatusCode::Monitor::kMonitorError)); + } + const auto cleanup = cpp_core::defer([&] { SetupDiDestroyDeviceInfoList(device_information_set); }); + + std::vector ports; + for (DWORD index = 0;; ++index) + { + SP_DEVINFO_DATA device_information = {}; + device_information.cbSize = sizeof(device_information); + if (SetupDiEnumDeviceInfo(device_information_set, index, &device_information) == 0) + { + if (GetLastError() == ERROR_NO_MORE_ITEMS) + { + break; + } + return cpp_bindings_windows::detail::failWin32( + callback, static_cast(cpp_core::StatusCode::Monitor::kMonitorError)); + } + + const auto port_name = portName(device_information_set, &device_information); + if (!port_name || port_name->size() < 4 || + (!port_name->starts_with(L"COM") && !port_name->starts_with(L"com"))) + { + continue; + } + + PortInformation port_information; + port_information.port = cpp_bindings_windows::detail::wideToUtf8(*port_name); + port_information.path = "\\\\.\\" + port_information.port; + if (const auto value = deviceProperty(device_information_set, &device_information, SPDRP_MFG)) + { + port_information.manufacturer = cpp_bindings_windows::detail::wideToUtf8(*value); + } + if (const auto value = + deviceProperty(device_information_set, &device_information, SPDRP_LOCATION_INFORMATION)) + { + port_information.location_id = cpp_bindings_windows::detail::wideToUtf8(*value); + } + if (const auto value = instanceId(device_information_set, &device_information)) + { + port_information.pnp_id = cpp_bindings_windows::detail::wideToUtf8(*value); + port_information.serial_number = serialNumber(port_information.pnp_id); + port_information.vendor_id = hardwareId(port_information.pnp_id, "VID_"); + port_information.product_id = hardwareId(port_information.pnp_id, "PID_"); + } + ports.push_back(std::move(port_information)); + } + + std::ranges::sort(ports, {}, &PortInformation::port); + for (const auto &port_information : ports) + { + callback_function( + optionalCString(port_information.port), optionalCString(port_information.path), + optionalCString(port_information.manufacturer), optionalCString(port_information.serial_number), + optionalCString(port_information.pnp_id), optionalCString(port_information.location_id), + optionalCString(port_information.product_id), optionalCString(port_information.vendor_id)); + } + return static_cast(ports.size()); + } + +} // extern "C" diff --git a/src/serial_monitor_ports.cpp b/src/serial_monitor_ports.cpp new file mode 100644 index 0000000..7d3873b --- /dev/null +++ b/src/serial_monitor_ports.cpp @@ -0,0 +1,129 @@ +#include + +#include "detail/fail_win32.hpp" +#include "detail/win32_error_to_string.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +std::mutex g_monitor_mutex; +std::mutex g_wait_mutex; +std::condition_variable_any g_wakeup; +std::jthread g_monitor_thread; + +auto enumerateComPorts() -> std::optional> +{ + std::vector buffer(65536); + const DWORD length = QueryDosDeviceA(nullptr, buffer.data(), static_cast(buffer.size())); + if (length == 0) + { + return std::nullopt; + } + + std::set ports; + const char *current = buffer.data(); + while (*current != '\0') + { + std::string name(current); + if (name.size() >= 4 && (name.starts_with("COM") || name.starts_with("com"))) + { + ports.insert(std::move(name)); + } + current += std::char_traits::length(current) + 1; + } + return ports; +} + +auto stopMonitor() -> void +{ + 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(); +} + +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()) + { + (void)g_wakeup.wait_for(wait_lock, stop_token, std::chrono::milliseconds(500), [] { return false; }); + if (stop_token.stop_requested()) + { + break; + } + + 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 &port : *current) + { + if (!previous.contains(port)) + { + callback(1, port.c_str()); + } + } + for (const auto &port : previous) + { + if (!current->contains(port)) + { + callback(0, port.c_str()); + } + } + previous = std::move(*current); + wait_lock.lock(); + } +} +} // namespace + +extern "C" +{ + + MODULE_API auto serialMonitorPorts(void (*callback_function)(int event, const char *port), + ErrorCallbackT error_callback) -> int + { + std::lock_guard lock(g_monitor_mutex); + stopMonitor(); + if (callback_function == nullptr) + { + return static_cast(cpp_core::StatusCode::kSuccess); + } + + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + auto initial_ports = enumerateComPorts(); + if (!initial_ports) + { + return cpp_bindings_windows::detail::failWin32( + callback, static_cast(cpp_core::StatusCode::Monitor::kMonitorError)); + } + + g_monitor_thread = std::jthread(monitorLoop, std::move(*initial_ports), callback_function, callback); + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_open.cpp b/src/serial_open.cpp index 84f353c..8b81668 100644 --- a/src/serial_open.cpp +++ b/src/serial_open.cpp @@ -3,188 +3,86 @@ #include #include -#include "detail/win32_helpers.hpp" - -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include +#include "detail/apply_line_settings.hpp" +#include "detail/effective_error_callback.hpp" +#include "detail/fail_win32.hpp" +#include "detail/handle_types.hpp" +#include "detail/normalize_port_path.hpp" +#include "detail/register_opened_handle.hpp" +#include "detail/utf8_to_wide.hpp" #include -namespace -{ -auto 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 -{ - DCB dcb = {}; - dcb.DCBlength = sizeof(DCB); - - if (GetCommState(handle, &dcb) == 0) - { - const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCodes::kGetStateError, - "GetCommState failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); - } - - dcb.BaudRate = static_cast(baudrate); - dcb.ByteSize = static_cast(data_bits); - - dcb.fBinary = TRUE; - dcb.fParity = (par != cpp_core::Parity::kNone) ? TRUE : FALSE; - - dcb.fOutxCtsFlow = FALSE; - dcb.fOutxDsrFlow = FALSE; - dcb.fDtrControl = DTR_CONTROL_ENABLE; - dcb.fDsrSensitivity = FALSE; - dcb.fTXContinueOnXoff = TRUE; - dcb.fOutX = FALSE; - dcb.fInX = FALSE; - dcb.fRtsControl = RTS_CONTROL_ENABLE; - - switch (par) - { - case cpp_core::Parity::kNone: - dcb.Parity = NOPARITY; - break; - case cpp_core::Parity::kEven: - dcb.Parity = EVENPARITY; - break; - case cpp_core::Parity::kOdd: - dcb.Parity = ODDPARITY; - break; - default: - return cpp_core::fail(cpp_core::StatusCodes::kSetStateError, "Invalid parity"); - } - - if (sb == cpp_core::StopBits::kOne) - { - dcb.StopBits = ONESTOPBIT; - } - else if (sb == cpp_core::StopBits::kTwo) - { - dcb.StopBits = TWOSTOPBITS; - } - - if (SetCommState(handle, &dcb) == 0) - { - const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCodes::kSetStateError, - "SetCommState failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); - } - - COMMTIMEOUTS timeouts = {}; - if (SetCommTimeouts(handle, &timeouts) == 0) - { - const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCodes::kSetTimeoutError, - "SetCommTimeouts failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); - } - - return cpp_core::ok(); -} -} // namespace - extern "C" { MODULE_API auto serialOpen(void *port, int baudrate, int data_bits, int parity, int stop_bits, ErrorCallbackT error_callback) -> intptr_t { - const auto params_ok = cpp_core::validateOpenParams(port, baudrate, data_bits, error_callback); - if (params_ok < 0) + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + const auto parameter_status = cpp_core::validateOpenParams(port, baudrate, data_bits, callback); + if (parameter_status < 0) { - return params_ok; + return parameter_status; } - 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 = cpp_bindings_windows::detail::applyLineSettings(handle.get(), baudrate, data_bits, + parity_value, stop_bits_value); if (!settings.has_value()) { - return static_cast(cpp_core::toCStatus(settings, error_callback)); + return static_cast(cpp_core::toCStatus(settings, callback)); } PurgeComm(handle.get(), PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT); - const intptr_t out = reinterpret_cast(handle.get()); - if (out <= 0) + const intptr_t serial_handle = reinterpret_cast(handle.get()); + if (serial_handle <= 0) { - return cpp_core::failMsg(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..f6b378b 100644 --- a/src/serial_open.test.cpp +++ b/src/serial_open.test.cpp @@ -1,13 +1,10 @@ #include -#include +#include #include #include -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include +#include "detail/windows.hpp" #include @@ -40,155 +37,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 +194,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 +205,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..920792d --- /dev/null +++ b/src/serial_out_bytes_total.cpp @@ -0,0 +1,20 @@ +#include + +#include "detail/acquire_handle_context.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..ac2805d --- /dev/null +++ b/src/serial_out_bytes_waiting.cpp @@ -0,0 +1,33 @@ +#include + +#include "detail/acquire_handle_context.hpp" +#include "detail/fail_win32.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 communication_status = {}; + if (ClearCommError(context.handle, &errors, &communication_status) == 0) + { + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + static_cast(cpp_core::StatusCode::Control::kGetStateError)); + } + return communication_status.cbOutQue > static_cast(INT_MAX) + ? INT_MAX + : static_cast(communication_status.cbOutQue); + } + +} // extern "C" diff --git a/src/serial_read.cpp b/src/serial_read.cpp index d7e36d1..b5da300 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/read_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..3f5186b --- /dev/null +++ b/src/serial_read_line.cpp @@ -0,0 +1,16 @@ +#include + +#include "detail/read_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..9c1d3f5 --- /dev/null +++ b/src/serial_read_until.cpp @@ -0,0 +1,23 @@ +#include + +#include "detail/read_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..ce3a4ca --- /dev/null +++ b/src/serial_read_until_sequence.cpp @@ -0,0 +1,34 @@ +#include + +#include "detail/read_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 new file mode 100644 index 0000000..8d4d601 --- /dev/null +++ b/src/serial_send_break.cpp @@ -0,0 +1,43 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialSendBreak(int64_t handle, int duration_ms, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + if (duration_ms <= 0) + { + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Control::kSendBreakError, "Break duration must be > 0"); + } + + if (SetCommBreak(native_handle) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kSendBreakError); + } + + Sleep(static_cast(duration_ms)); + + if (ClearCommBreak(native_handle) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::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..208d3d1 --- /dev/null +++ b/src/serial_set_baudrate.cpp @@ -0,0 +1,46 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialSetBaudrate(int64_t handle, int baudrate, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + if (baudrate < 300) + { + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetBaudrateError, + "Invalid baudrate: must be >= 300"); + } + + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + serial_settings.BaudRate = static_cast(baudrate); + + if (SetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Configuration::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..b3a1f52 --- /dev/null +++ b/src/serial_set_data_bits.cpp @@ -0,0 +1,46 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialSetDataBits(int64_t handle, int data_bits, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + if (data_bits < 5 || data_bits > 8) + { + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetDataBitsError, + "Invalid data bits: must be 5-8"); + } + + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + serial_settings.ByteSize = static_cast(data_bits); + + if (SetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Configuration::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..0469857 --- /dev/null +++ b/src/serial_set_dtr.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialSetDtr(int64_t handle, int state, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + const DWORD communication_function = state ? SETDTR : CLRDTR; + if (EscapeCommFunction(native_handle, communication_function) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kSetDtrError); + } + + return 0; + } + +} // extern "C" 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 new file mode 100644 index 0000000..ade9918 --- /dev/null +++ b/src/serial_set_flow_control.cpp @@ -0,0 +1,67 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialSetFlowControl(int64_t handle, int mode, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + if (mode < 0 || mode > 2) + { + 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"); + } + + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + serial_settings.fOutxCtsFlow = FALSE; + serial_settings.fRtsControl = RTS_CONTROL_ENABLE; + serial_settings.fOutX = FALSE; + serial_settings.fInX = FALSE; + + switch (mode) + { + case 1: + serial_settings.fOutxCtsFlow = TRUE; + serial_settings.fRtsControl = RTS_CONTROL_HANDSHAKE; + break; + case 2: + serial_settings.fOutX = TRUE; + serial_settings.fInX = TRUE; + serial_settings.XonChar = 0x11; + serial_settings.XoffChar = 0x13; + serial_settings.XonLim = 2048; + serial_settings.XoffLim = 512; + break; + default: + break; + } + + if (SetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32( + error_callback, cpp_core::StatusCode::Configuration::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..a98128d --- /dev/null +++ b/src/serial_set_parity.cpp @@ -0,0 +1,58 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialSetParity(int64_t handle, int parity, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + BYTE windows_parity = NOPARITY; + switch (parity) + { + case 0: + windows_parity = NOPARITY; + break; + case 1: + windows_parity = EVENPARITY; + break; + case 2: + windows_parity = ODDPARITY; + break; + default: + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetParityError, + "Invalid parity: must be 0, 1, or 2"); + } + + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + serial_settings.Parity = windows_parity; + serial_settings.fParity = (parity != 0) ? TRUE : FALSE; + + if (SetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Configuration::kSetParityError); + } + + return 0; + } + +} // extern "C" diff --git a/src/serial_set_read_callback.cpp b/src/serial_set_read_callback.cpp new file mode 100644 index 0000000..73cfa95 --- /dev/null +++ b/src/serial_set_read_callback.cpp @@ -0,0 +1,13 @@ +#include + +#include "detail/handle_types.hpp" + +extern "C" +{ + + MODULE_API void serialSetReadCallback(void (*callback_function)(int bytes_read)) + { + cpp_bindings_windows::detail::g_read_callback.store(callback_function, std::memory_order_release); + } + +} // extern "C" diff --git a/src/serial_set_rts.cpp b/src/serial_set_rts.cpp new file mode 100644 index 0000000..c4ed07e --- /dev/null +++ b/src/serial_set_rts.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialSetRts(int64_t handle, int state, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + const DWORD communication_function = state ? SETRTS : CLRRTS; + if (EscapeCommFunction(native_handle, communication_function) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kSetRtsError); + } + + 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..9f55b21 --- /dev/null +++ b/src/serial_set_stop_bits.cpp @@ -0,0 +1,46 @@ +#include +#include + +#include "detail/fail_win32.hpp" +#include "detail/validate_win32_handle.hpp" + +extern "C" +{ + + MODULE_API auto serialSetStopBits(int64_t handle, int stop_bits, ErrorCallbackT error_callback) -> int + { + HANDLE native_handle = nullptr; + const auto status = + cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + if (stop_bits != 0 && stop_bits != 1 && stop_bits != 2) + { + 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"); + } + + DCB serial_settings = {}; + serial_settings.DCBlength = sizeof(DCB); + if (GetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + serial_settings.StopBits = (stop_bits == 2) ? TWOSTOPBITS : ONESTOPBIT; + + if (SetCommState(native_handle, &serial_settings) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Configuration::kSetStopBitsError); + } + + return 0; + } + +} // extern "C" diff --git a/src/serial_set_write_callback.cpp b/src/serial_set_write_callback.cpp new file mode 100644 index 0000000..f1ca4fd --- /dev/null +++ b/src/serial_set_write_callback.cpp @@ -0,0 +1,13 @@ +#include + +#include "detail/handle_types.hpp" + +extern "C" +{ + + MODULE_API void serialSetWriteCallback(void (*callback_function)(int bytes_written)) + { + cpp_bindings_windows::detail::g_write_callback.store(callback_function, std::memory_order_release); + } + +} // extern "C" diff --git a/src/serial_write.cpp b/src/serial_write.cpp index 62b2fdc..3dab29f 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/write_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..2546c92 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 @@ -17,40 +17,41 @@ namespace { -auto readExact(intptr_t handle, char *dst, int want_bytes, int total_timeout_ms) -> int +auto readExact(intptr_t handle, char *destination, int requested_byte_count, int total_timeout_ms) -> int { - if (dst == nullptr || want_bytes <= 0) + if (destination == nullptr || requested_byte_count <= 0) { return 0; } const ULONGLONG start = GetTickCount64(); - int total = 0; - while (total < want_bytes) + int total_bytes_read = 0; + while (total_bytes_read < requested_byte_count) { const ULONGLONG now = GetTickCount64(); - const int elapsed = static_cast(now - start); - if (elapsed >= total_timeout_ms) + const int elapsed_milliseconds = static_cast(now - start); + if (elapsed_milliseconds >= total_timeout_ms) { break; } // Read remaining bytes with a small per-call timeout to make progress. - const int remaining = want_bytes - total; - const int chunk = serialRead(handle, dst + total, remaining, 200, 1, nullptr); - if (chunk < 0) + const int remaining_byte_count = requested_byte_count - total_bytes_read; + const int bytes_read = + serialRead(handle, destination + total_bytes_read, remaining_byte_count, 200, 1, nullptr); + if (bytes_read < 0) { - return chunk; + return bytes_read; } - if (chunk == 0) + if (bytes_read == 0) { Sleep(10); continue; } - total += chunk; + total_bytes_read += bytes_read; } - return total; + return total_bytes_read; } } // namespace @@ -59,13 +60,13 @@ class SerialArduinoTest : public ::testing::Test protected: void SetUp() override { - const char *env_port = std::getenv("SERIAL_TEST_PORT"); - const char *port = (env_port != nullptr && env_port[0] != '\0') ? env_port : "COM5"; + const char *environment_port = std::getenv("SERIAL_TEST_PORT"); + const char *port = (environment_port != nullptr && environment_port[0] != '\0') ? environment_port : "COM5"; handle_ = serialOpen(const_cast(static_cast(port)), 115200, 8, 0, 0, nullptr); if (handle_ <= 0) { - GTEST_SKIP() << "Could not open serial port '" << (env_port ? env_port : "COM5") + GTEST_SKIP() << "Could not open serial port '" << (environment_port ? environment_port : "COM5") << "'. Set SERIAL_TEST_PORT (e.g. COM5) or connect Arduino."; } @@ -93,43 +94,44 @@ TEST_F(SerialArduinoTest, OpenClose) TEST_F(SerialArduinoTest, WriteReadEcho) { const char *test_message = "Hello Arduino!\n"; - const int message_len = static_cast(strlen(test_message)); + const int message_length = static_cast(strlen(test_message)); - const int written = serialWrite(handle_, test_message, message_len, 1000, 1, nullptr); - EXPECT_EQ(written, message_len) << "Should write all bytes. Written: " << written << ", Expected: " << message_len; + const int bytes_written = serialWrite(handle_, test_message, message_length, 1000, 1, nullptr); + EXPECT_EQ(bytes_written, message_length) + << "Should write all bytes. Written: " << bytes_written << ", Expected: " << message_length; Sleep(500); char read_buffer[256] = {0}; - const int read_bytes = readExact(handle_, read_buffer, message_len, 3000); + const int read_bytes = readExact(handle_, read_buffer, message_length, 3000); EXPECT_GT(read_bytes, 0) << "Should read at least some bytes"; - EXPECT_EQ(read_bytes, message_len) << "Should read exactly the echoed message length"; - EXPECT_EQ(std::string_view(read_buffer, static_cast(message_len)), - std::string_view(test_message, static_cast(message_len))) + EXPECT_EQ(read_bytes, message_length) << "Should read exactly the echoed message length"; + EXPECT_EQ(std::string_view(read_buffer, static_cast(message_length)), + std::string_view(test_message, static_cast(message_length))) << "Echoed content should match what was sent"; } TEST_F(SerialArduinoTest, MultipleEchoCycles) { const char *messages[] = {"Test1\n", "Test2\n", "Test3\n"}; - const int num_messages = 3; + const int message_count = 3; - for (int i = 0; i < num_messages; ++i) + for (int message_index = 0; message_index < message_count; ++message_index) { - const int msg_len = static_cast(strlen(messages[i])); + const int message_length = static_cast(strlen(messages[message_index])); - const int written = serialWrite(handle_, messages[i], msg_len, 1000, 1, nullptr); - EXPECT_EQ(written, msg_len) << "Cycle " << i << ": write failed"; + const int bytes_written = serialWrite(handle_, messages[message_index], message_length, 1000, 1, nullptr); + EXPECT_EQ(bytes_written, message_length) << "Cycle " << message_index << ": write failed"; Sleep(500); char read_buffer[256] = {0}; - const int read_bytes = readExact(handle_, read_buffer, msg_len, 3000); - EXPECT_EQ(read_bytes, msg_len) << "Cycle " << i << ": read size mismatch"; - EXPECT_EQ(std::string_view(read_buffer, static_cast(msg_len)), - std::string_view(messages[i], static_cast(msg_len))) - << "Cycle " << i << ": echo content mismatch"; + const int read_bytes = readExact(handle_, read_buffer, message_length, 3000); + EXPECT_EQ(read_bytes, message_length) << "Cycle " << message_index << ": read size mismatch"; + EXPECT_EQ(std::string_view(read_buffer, static_cast(message_length)), + std::string_view(messages[message_index], static_cast(message_length))) + << "Cycle " << message_index << ": echo content mismatch"; } } @@ -144,7 +146,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 +154,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)); }