diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index 6f70fc2..b999090 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -1,22 +1,22 @@ name: 'Build Binary' -description: | - This workflow builds the binary files for Windows. The build binaries are stored as artifact - and may be reused by other workflows. on: push: - branches: [ 'main' ] - tags: [ '*' ] - + branches: ['main'] + tags: ['*'] pull_request: - branches: [ '*' ] + branches: ['*'] jobs: - build-binary: - name: 'Build binary' - runs-on: windows-latest - permissions: - contents: write + generate-metadata: + name: 'Generate FFI metadata (x86_64-windows-msvc)' + runs-on: windows-2025 + env: + ASTREIN_VERSION: '1.2.1' + ASTREIN_SHA256: 'c90eb0e3a24dbdd8775289b30e60d0ee2dfa1c0395c8687aa9a2fabeada2d2df' + outputs: + package_version: ${{ steps.version.outputs.PACKAGE_VERSION }} + is_valid_package_version: ${{ steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION }} steps: - name: 'Checkout repository' @@ -27,89 +27,223 @@ 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: 'Download ASTrein' + shell: pwsh + run: | + $archive = Join-Path $env:RUNNER_TEMP 'astrein-windows-x86_64.zip' + $destination = Join-Path $env:RUNNER_TEMP 'astrein-package' + Invoke-WebRequest ` + -Uri "https://github.com/Katze719/ASTrein/releases/download/v$env:ASTREIN_VERSION/astrein-windows-x86_64.zip" ` + -OutFile $archive + + $actualHash = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant() + if ($actualHash -ne $env:ASTREIN_SHA256) { + throw "ASTrein checksum mismatch: expected $env:ASTREIN_SHA256, got $actualHash" + } - - name: 'Configure CMake' + Expand-Archive -Path $archive -DestinationPath $destination + $astrein = Join-Path $destination 'astrein/bin/astrein.exe' + & $astrein --version + "ASTREIN_EXECUTABLE=$astrein" >> $env:GITHUB_ENV + + - name: 'Configure metadata context' + shell: pwsh run: | - cmake --preset windows-vs-release + cmake -S . -B build/ffi -G Ninja ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_C_COMPILER=clang-cl ` + -DCMAKE_CXX_COMPILER=clang-cl ` + -DCPP_BINDINGS_WINDOWS_ENABLE_FFI_JSON_EXPORT=ON ` + "-DCPP_BINDINGS_WINDOWS_ASTREIN_EXECUTABLE=$env:ASTREIN_EXECUTABLE" ` + "-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: 'Build' - id: build + - name: 'Verify FFI metadata' + shell: pwsh run: | - cmake --build --preset windows-vs-release --config Release --target cpp_bindings_windows + $metadataPath = 'dist/ffi/x86_64.ffi.json' + $metadata = Get-Content -Raw $metadataPath | + ConvertFrom-Json -ErrorAction Stop + + if ($metadata.schema -ne 'astrein_ffi_api') { + throw "Unexpected FFI metadata schema: $($metadata.schema)" + } + if ($metadata.schemaVersion -ne 1) { + throw "Unexpected FFI metadata schema version: $($metadata.schemaVersion)" + } - - name: 'Set PACKAGE_VERSION from env.bat' + $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: | - $content = Get-Content -Raw build/env.bat - echo "$content" - $m = [regex]::Match($content, 'PACKAGE_VERSION=(.+)') - $v = if ($m.Success) { $m.Groups[1].Value.Trim() } else { '0.0.0' } - echo "PACKAGE_VERSION=$v" >> $env:GITHUB_OUTPUT + $content = Get-Content -Raw build/ffi/env.bat + $match = [regex]::Match($content, 'PACKAGE_VERSION=(.+)') + $version = if ($match.Success) { $match.Groups[1].Value.Trim() } else { '0.0.0' } + "PACKAGE_VERSION=$version" >> $env:GITHUB_OUTPUT - - name: 'Copy DLL to stable path and upload artifact' + - name: 'Check package version' + id: check-tag shell: pwsh + env: + PACKAGE_VERSION: ${{ steps.version.outputs.PACKAGE_VERSION }} run: | - $dll = Get-ChildItem -Recurse -Path build -Filter cpp_bindings_windows.dll -File | Select-Object -First 1 - if (-not $dll) { throw "cpp_bindings_windows.dll not found under build/" } - New-Item -ItemType Directory -Force -Path build/out | Out-Null - Copy-Item -Force $dll.FullName -Destination build/out/cpp_bindings_windows.dll + if ($env:PACKAGE_VERSION -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(alpha|rc|beta|experimental)\.[1-9]\d*)?$') { + "IS_VALID_PACKAGE_VERSION=true" >> $env:GITHUB_OUTPUT + } else { + "IS_VALID_PACKAGE_VERSION=false" >> $env:GITHUB_OUTPUT + } - - name: 'Upload artifacts' + - name: 'Upload FFI metadata' uses: actions/upload-artifact@v4 with: if-no-files-found: error - name: cpp_bindings_windows - path: build/out/cpp_bindings_windows.dll + name: cpp-bindings-windows-ffi + path: dist/ffi/x86_64.ffi.json - - name: 'Check tag' - id: check-tag - shell: pwsh - env: - PACKAGE_VERSION: ${{ steps.version.outputs.PACKAGE_VERSION }} + build-binary: + name: 'Build x86_64-windows-msvc' + runs-on: windows-2025 + + steps: + - name: 'Checkout repository' + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 'Setup CMake' + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: '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: | - $v = $env:PACKAGE_VERSION - if ($v -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(alpha|rc|beta|experimental)\.[1-9]\d*)?$') { - echo "IS_VALID_PACKAGE_VERSION=true" >> $env:GITHUB_OUTPUT - } else { - echo "IS_VALID_PACKAGE_VERSION=false" >> $env:GITHUB_OUTPUT + cmake --preset windows-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/' } - - name: 'Create GitHub Release' - if: github.ref_type == 'tag' && steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION == 'true' - uses: softprops/action-gh-release@v2 + New-Item -ItemType Directory -Force -Path dist/x86_64-windows-msvc | Out-Null + Copy-Item -Force $dll.FullName dist/x86_64-windows-msvc/cpp_bindings_windows.dll + ./scripts/verify_release_binary.ps1 ` + dist/x86_64-windows-msvc/cpp_bindings_windows.dll ` + x86_64-windows-msvc + + - name: 'Upload test report' + if: always() + uses: actions/upload-artifact@v4 with: - name: 'v${{ steps.version.outputs.PACKAGE_VERSION }}' - tag_name: ${{ github.ref_name }} - generate_release_notes: true - files: build/out/cpp_bindings_windows.dll + if-no-files-found: warn + name: test-report-x86_64-windows-msvc + path: build/test-report.xml - outputs: - package_version: ${{ steps.version.outputs.PACKAGE_VERSION }} - is_valid_package_version: ${{ steps.check-tag.outputs.IS_VALID_PACKAGE_VERSION }} + - name: 'Upload binary' + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: cpp-bindings-windows-x86_64-windows-msvc + path: dist/x86_64-windows-msvc/cpp_bindings_windows.dll test-unit-cpp: name: 'Run: Test Unit C++' - needs: [ 'build-binary' ] + needs: ['build-binary'] uses: './.github/workflows/test_unit_cpp.yml' with: - artifact-name: cpp_bindings_windows - + artifact-name: cpp-bindings-windows-x86_64-windows-msvc permissions: contents: read checks: write + create-release: + name: 'Create GitHub release' + needs: ['generate-metadata', 'build-binary', 'test-unit-cpp'] + if: github.ref_type == 'tag' && needs.generate-metadata.outputs.is_valid_package_version == 'true' + runs-on: windows-2025 + permissions: + contents: write + + steps: + - name: 'Download binary' + uses: actions/download-artifact@v4 + with: + name: cpp-bindings-windows-x86_64-windows-msvc + path: release/binary + + - name: 'Download FFI metadata' + uses: actions/download-artifact@v4 + with: + name: cpp-bindings-windows-ffi + path: release/ffi + + - name: 'Name release assets' + shell: pwsh + run: | + Move-Item release/binary/cpp_bindings_windows.dll ` + release/cpp_bindings_windows-x86_64-windows-msvc.dll + Move-Item release/ffi/x86_64.ffi.json ` + release/cpp_bindings_windows-x86_64-windows-msvc.ffi.json + + - name: 'Create GitHub release' + uses: softprops/action-gh-release@v2 + with: + name: 'v${{ needs.generate-metadata.outputs.package_version }}' + tag_name: ${{ github.ref_name }} + generate_release_notes: true + files: | + release/cpp_bindings_windows-x86_64-windows-msvc.dll + release/cpp_bindings_windows-x86_64-windows-msvc.ffi.json + publish-jsr: name: 'Run: Publish JSR' - needs: [ 'build-binary', 'test-unit-cpp' ] + needs: ['generate-metadata', 'build-binary', 'test-unit-cpp'] uses: './.github/workflows/publish_jsr.yml' with: - publish: ${{ needs.build-binary.outputs.is_valid_package_version == 'true' }} - version: ${{ needs.build-binary.outputs.package_version }} - artifact-name: cpp_bindings_windows - + publish: ${{ needs.generate-metadata.outputs.is_valid_package_version == 'true' }} + version: ${{ needs.generate-metadata.outputs.package_version }} + artifact-name: cpp-bindings-windows-x86_64-windows-msvc + ffi-artifact-name: cpp-bindings-windows-ffi permissions: contents: read id-token: write diff --git a/.github/workflows/deno_tests.yml b/.github/workflows/deno_tests.yml index 471e293..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..30909e7 100644 --- a/jsr/README.md +++ b/jsr/README.md @@ -5,6 +5,14 @@ Binaries are provided as a [package on JSR](https://jsr.io/@serial/cpp-bindings-windows). They are serialized as a base64 string inside the JSON file. +The package currently contains the `x86_64-windows-msvc` DLL. The release DLL +statically includes the MSVC runtime. + +It also includes cpp-core FFI API metadata generated with +[ASTrein](https://github.com/Katze719/ASTrein) at `bin/x86_64.ffi.json`. +It describes the exported C symbols, parameter and return types, callbacks, +default values, and API documentation used by downstream FFI adapter generators. + This package is primarily intended as a dependency for [`@serial/serial`](https://jsr.io/@serial/serial). However, it can also be used independently. diff --git a/jsr/jsr.json b/jsr/jsr.json index 1d0d7ab..05df6b7 100644 --- a/jsr/jsr.json +++ b/jsr/jsr.json @@ -9,6 +9,7 @@ "publish": { "include": [ "README.md", + "LICENSE", "jsr.json", "src/**", "bin/**" diff --git a/scripts/verify_release_binary.ps1 b/scripts/verify_release_binary.ps1 new file mode 100644 index 0000000..6ba93e9 --- /dev/null +++ b/scripts/verify_release_binary.ps1 @@ -0,0 +1,95 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Binary, + + [Parameter(Mandatory = $true)] + [ValidateSet("x86_64-windows-msvc")] + [string]$Target +) + +$ErrorActionPreference = "Stop" + +$binaryPath = (Resolve-Path $Binary).Path +$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio/Installer/vswhere.exe" +if (-not (Test-Path $vswhere)) { + throw "vswhere.exe was not found" +} + +$dumpbin = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` + -find "VC/Tools/MSVC/*/bin/Hostx64/x64/dumpbin.exe" | Select-Object -First 1 +if (-not $dumpbin) { + throw "dumpbin.exe was not found in the Visual Studio installation" +} + +$headers = (& $dumpbin /headers $binaryPath 2>&1) -join "`n" +if ($LASTEXITCODE -ne 0) { + throw "dumpbin /headers failed`n$headers" +} +if ($headers -notmatch "(?im)^\s*8664 machine \(x64\)") { + throw "Expected an x86-64 PE DLL for $Target" +} +if ($headers -notmatch "(?im)^\s*DLL\s*$") { + throw "Expected a PE DLL, not an executable" +} + +$dependents = (& $dumpbin /dependents $binaryPath 2>&1) -join "`n" +if ($LASTEXITCODE -ne 0) { + throw "dumpbin /dependents failed`n$dependents" +} +if ($dependents -match "(?i)(msvcp[^\s]*|vcruntime[^\s]*|ucrtbased)\.dll") { + throw "Release DLL unexpectedly depends on a dynamic MSVC C/C++ runtime`n$dependents" +} + +$exports = (& $dumpbin /exports $binaryPath 2>&1) -join "`n" +if ($LASTEXITCODE -ne 0) { + throw "dumpbin /exports failed`n$exports" +} + +$expectedExports = @( + "getVersion", + "serialAbortRead", + "serialAbortWrite", + "serialClearBufferIn", + "serialClearBufferOut", + "serialClose", + "serialDrain", + "serialGetBaudrate", + "serialGetCts", + "serialGetDataBits", + "serialGetDcd", + "serialGetDsr", + "serialGetFlowControl", + "serialGetParity", + "serialGetRi", + "serialGetStopBits", + "serialInBytesTotal", + "serialInBytesWaiting", + "serialListPorts", + "serialMonitorPorts", + "serialOpen", + "serialOutBytesTotal", + "serialOutBytesWaiting", + "serialRead", + "serialReadLine", + "serialReadUntil", + "serialReadUntilSequence", + "serialSendBreak", + "serialSetBaudrate", + "serialSetDataBits", + "serialSetDtr", + "serialSetErrorCallback", + "serialSetFlowControl", + "serialSetParity", + "serialSetReadCallback", + "serialSetRts", + "serialSetStopBits", + "serialSetWriteCallback", + "serialWrite" +) + +$missingExports = @($expectedExports | Where-Object { $exports -notmatch "(?m)\s$([regex]::Escape($_))\s*$" }) +if ($missingExports.Count -ne 0) { + throw "Release DLL is missing exports: $($missingExports -join ', ')" +} + +Write-Host "Verified $Target DLL: x86-64, static MSVC runtime, and $($expectedExports.Count) C API exports" diff --git a/src/detail/common_types.hpp b/src/detail/common_types.hpp new file mode 100644 index 0000000..e353a81 --- /dev/null +++ b/src/detail/common_types.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +#include + +namespace cpp_bindings_windows::detail +{ +using IoCallbackT = void (*)(int); +using StatusCodeValue = cpp_core::StatusCodeValue; +using cpp_core::StatusCode; + +inline std::atomic g_error_callback{nullptr}; +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/handle_state.hpp b/src/detail/handle_state.hpp new file mode 100644 index 0000000..d27ee59 --- /dev/null +++ b/src/detail/handle_state.hpp @@ -0,0 +1,229 @@ +#pragma once + +#include "common_types.hpp" + +#include +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace cpp_bindings_windows::detail +{ +enum class Operation +{ + kRead, + kWrite, +}; + +struct Win32HandleTraits +{ + using handle_type = HANDLE; // NOLINT(readability-identifier-naming) + + static constexpr auto invalid() noexcept -> handle_type + { + return nullptr; + } + + static auto close(handle_type handle) noexcept -> void + { + if (handle != nullptr && handle != INVALID_HANDLE_VALUE) + { + CloseHandle(handle); + } + } +}; + +using UniqueHandle = cpp_core::UniqueResource; + +struct HandleState +{ + std::atomic bytes_read_total{0}; + std::atomic bytes_written_total{0}; + std::atomic abort_read{false}; + std::atomic abort_write{false}; + std::mutex pending_io_mutex; + OVERLAPPED *pending_read = nullptr; + OVERLAPPED *pending_write = nullptr; +}; + +struct HandleContext +{ + HANDLE handle = nullptr; + std::shared_ptr state; +}; + +struct PendingIoStart +{ + BOOL completed = FALSE; + DWORD error = ERROR_SUCCESS; + bool aborted = false; +}; + +inline std::mutex g_handle_states_mutex; +inline std::unordered_map> g_handle_states; +inline std::atomic g_read_callback{nullptr}; +inline std::atomic g_write_callback{nullptr}; + +inline auto handleKey(HANDLE handle) -> std::uintptr_t +{ + return reinterpret_cast(handle); +} + +inline auto effectiveErrorCallback(ErrorCallbackT error_callback) -> ErrorCallbackT +{ + return error_callback != nullptr ? error_callback : g_error_callback.load(std::memory_order_acquire); +} + +inline auto ensureHandleState(HANDLE handle) -> std::shared_ptr +{ + std::lock_guard lock(g_handle_states_mutex); + auto &state = g_handle_states[handleKey(handle)]; + if (!state) + { + state = std::make_shared(); + } + return state; +} + +inline auto registerOpenedHandle(HANDLE handle) -> void +{ + (void)ensureHandleState(handle); +} + +inline auto removeHandleState(HANDLE handle) -> void +{ + std::lock_guard lock(g_handle_states_mutex); + g_handle_states.erase(handleKey(handle)); +} + +template +inline auto validateWin32Handle(int64_t handle, ErrorCallbackT error_callback, HANDLE *out_handle) -> ReturnType +{ + const auto callback = effectiveErrorCallback(error_callback); + if (handle <= 0) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + + if constexpr (sizeof(intptr_t) < sizeof(int64_t)) + { + if (handle > static_cast(std::numeric_limits::max())) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + } + + const auto native_handle = reinterpret_cast(static_cast(handle)); + if (native_handle == nullptr || native_handle == INVALID_HANDLE_VALUE) + { + return cpp_core::failMsg( + callback, static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + } + + *out_handle = native_handle; + return static_cast(StatusCode::kSuccess); +} + +template +inline auto acquireHandleContext(int64_t handle, ErrorCallbackT error_callback, HandleContext *out_context) + -> ReturnType +{ + HANDLE native_handle = nullptr; + const auto status = validateWin32Handle(handle, error_callback, &native_handle); + if (status < 0) + { + return status; + } + + out_context->handle = native_handle; + out_context->state = ensureHandleState(native_handle); + return static_cast(StatusCode::kSuccess); +} + +inline auto abortFlag(const std::shared_ptr &state, Operation operation) -> std::atomic & +{ + return operation == Operation::kRead ? state->abort_read : state->abort_write; +} + +inline auto pendingOperation(const std::shared_ptr &state, Operation operation) -> OVERLAPPED *& +{ + return operation == Operation::kRead ? state->pending_read : state->pending_write; +} + +inline auto requestAbort(HANDLE handle, const std::shared_ptr &state, Operation operation) -> void +{ + abortFlag(state, operation).store(true, std::memory_order_release); + + std::lock_guard lock(state->pending_io_mutex); + if (auto *pending = pendingOperation(state, operation); pending != nullptr) + { + (void)CancelIoEx(handle, pending); + } +} + +inline auto consumeAbort(const std::shared_ptr &state, Operation operation) -> bool +{ + return abortFlag(state, operation).exchange(false, std::memory_order_acq_rel); +} + +template +inline auto startPendingIo(const std::shared_ptr &state, Operation operation, OVERLAPPED *overlapped, + StartOperation &&start_operation) -> PendingIoStart +{ + std::lock_guard lock(state->pending_io_mutex); + if (consumeAbort(state, operation)) + { + return {.aborted = true}; + } + + pendingOperation(state, operation) = overlapped; + const BOOL completed = std::forward(start_operation)(); + return {.completed = completed, .error = completed != FALSE ? ERROR_SUCCESS : GetLastError()}; +} + +inline auto finishPendingIo(const std::shared_ptr &state, Operation operation, OVERLAPPED *overlapped) + -> bool +{ + std::lock_guard lock(state->pending_io_mutex); + auto &pending = pendingOperation(state, operation); + if (pending == overlapped) + { + pending = nullptr; + } + return consumeAbort(state, operation); +} + +inline auto noteBytesTransferred(const std::shared_ptr &state, Operation operation, int transferred_bytes) + -> void +{ + if (operation == Operation::kRead) + { + state->bytes_read_total.fetch_add(transferred_bytes, std::memory_order_relaxed); + if (const auto callback = g_read_callback.load(std::memory_order_acquire); callback != nullptr) + { + callback(transferred_bytes); + } + return; + } + + state->bytes_written_total.fetch_add(transferred_bytes, std::memory_order_relaxed); + if (const auto callback = g_write_callback.load(std::memory_order_acquire); callback != nullptr) + { + callback(transferred_bytes); + } +} + +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/io_impl.hpp b/src/detail/io_impl.hpp new file mode 100644 index 0000000..8aa11bb --- /dev/null +++ b/src/detail/io_impl.hpp @@ -0,0 +1,277 @@ +#pragma once + +#include "handle_state.hpp" +#include "win32_helpers.hpp" + +#include + +#include +#include +#include + +namespace cpp_bindings_windows::detail +{ +enum class IoOutcome +{ + kCompleted, + kTimedOut, + kAborted, + kError, +}; + +struct IoResult +{ + IoOutcome outcome = IoOutcome::kError; + int bytes_transferred = 0; + DWORD error = ERROR_SUCCESS; +}; + +inline auto multiplierTimeout(int timeout_ms, int multiplier) -> int +{ + if (multiplier <= 0) + { + return 0; + } + + const auto timeout = static_cast(cpp_core::clampTimeout(timeout_ms)) * multiplier; + return timeout > INT_MAX ? INT_MAX : static_cast(timeout); +} + +inline auto waitForPendingIo(HANDLE handle, const std::shared_ptr &state, Operation operation, + OVERLAPPED *overlapped, int timeout_ms) -> IoResult +{ + const DWORD wait_result = + WaitForSingleObject(overlapped->hEvent, static_cast(cpp_core::clampTimeout(timeout_ms))); + if (wait_result == WAIT_TIMEOUT) + { + (void)CancelIoEx(handle, overlapped); + DWORD ignored = 0; + (void)GetOverlappedResult(handle, overlapped, &ignored, TRUE); + if (finishPendingIo(state, operation, overlapped)) + { + return {.outcome = IoOutcome::kAborted}; + } + return {.outcome = IoOutcome::kTimedOut}; + } + + if (wait_result != WAIT_OBJECT_0) + { + const DWORD error = GetLastError(); + (void)CancelIoEx(handle, overlapped); + DWORD ignored = 0; + (void)GetOverlappedResult(handle, overlapped, &ignored, TRUE); + const bool aborted = finishPendingIo(state, operation, overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = error}; + } + + DWORD transferred = 0; + const BOOL completed = GetOverlappedResult(handle, overlapped, &transferred, FALSE); + const DWORD error = completed != FALSE ? ERROR_SUCCESS : GetLastError(); + const bool aborted = finishPendingIo(state, operation, overlapped); + if (aborted || error == ERROR_OPERATION_ABORTED) + { + return {.outcome = IoOutcome::kAborted}; + } + if (completed == FALSE) + { + return {.outcome = IoOutcome::kError, .error = error}; + } + return {.outcome = IoOutcome::kCompleted, .bytes_transferred = static_cast(transferred)}; +} + +inline auto readChunk(const HandleContext &context, unsigned char *buffer, int buffer_size, int timeout_ms) -> IoResult +{ + UniqueHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!event) + { + return {.outcome = IoOutcome::kError, .error = GetLastError()}; + } + + OVERLAPPED overlapped = {}; + overlapped.hEvent = event.get(); + DWORD transferred = 0; + const auto start = startPendingIo(context.state, Operation::kRead, &overlapped, [&] { + return ReadFile(context.handle, buffer, static_cast(buffer_size), &transferred, &overlapped); + }); + if (start.aborted) + { + return {.outcome = IoOutcome::kAborted}; + } + if (start.completed != FALSE) + { + const bool aborted = finishPendingIo(context.state, Operation::kRead, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kCompleted, + .bytes_transferred = static_cast(transferred)}; + } + if (start.error != ERROR_IO_PENDING) + { + const bool aborted = finishPendingIo(context.state, Operation::kRead, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = start.error}; + } + + return waitForPendingIo(context.handle, context.state, Operation::kRead, &overlapped, timeout_ms); +} + +inline auto writeChunk(const HandleContext &context, const unsigned char *buffer, int buffer_size, int timeout_ms) + -> IoResult +{ + UniqueHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!event) + { + return {.outcome = IoOutcome::kError, .error = GetLastError()}; + } + + OVERLAPPED overlapped = {}; + overlapped.hEvent = event.get(); + DWORD transferred = 0; + const auto start = startPendingIo(context.state, Operation::kWrite, &overlapped, [&] { + return WriteFile(context.handle, buffer, static_cast(buffer_size), &transferred, &overlapped); + }); + if (start.aborted) + { + return {.outcome = IoOutcome::kAborted}; + } + if (start.completed != FALSE) + { + const bool aborted = finishPendingIo(context.state, Operation::kWrite, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kCompleted, + .bytes_transferred = static_cast(transferred)}; + } + if (start.error != ERROR_IO_PENDING) + { + const bool aborted = finishPendingIo(context.state, Operation::kWrite, &overlapped); + return {.outcome = aborted ? IoOutcome::kAborted : IoOutcome::kError, .error = start.error}; + } + + return waitForPendingIo(context.handle, context.state, Operation::kWrite, &overlapped, timeout_ms); +} + +inline auto matchesSuffix(const unsigned char *buffer, int buffer_size, const unsigned char *terminator, + int terminator_size) -> bool +{ + return terminator_size > 0 && buffer_size >= terminator_size && + std::memcmp(buffer + buffer_size - terminator_size, terminator, static_cast(terminator_size)) == + 0; +} + +inline auto readImpl(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, + const unsigned char *terminator, int terminator_size, ErrorCallbackT error_callback) -> int +{ + const auto callback = effectiveErrorCallback(error_callback); + const auto buffer_status = cpp_core::validateBuffer(buffer, buffer_size, callback); + if (buffer_status < 0) + { + return buffer_status; + } + if (terminator_size > 0 && terminator == nullptr) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kBufferError), + "Invalid terminator"); + } + + HandleContext context; + const auto handle_status = acquireHandleContext(handle, callback, &context); + if (handle_status < 0) + { + return handle_status; + } + + auto *output = static_cast(buffer); + int total_read = 0; + while (total_read < buffer_size) + { + int chunk_size = 1; + if (terminator_size <= 0) + { + int waiting = 0; + if (!bytesWaiting(context.handle, &waiting)) + { + return failWin32(callback, static_cast(StatusCode::Control::kGetStateError)); + } + chunk_size = waiting > 0 ? std::min(waiting, buffer_size - total_read) : 1; + } + + const int current_timeout = + total_read == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); + const auto result = readChunk(context, output + total_read, chunk_size, current_timeout); + if (result.outcome == IoOutcome::kTimedOut) + { + return total_read; + } + if (result.outcome == IoOutcome::kAborted) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kAbortReadError), + "Read aborted"); + } + if (result.outcome == IoOutcome::kError) + { + SetLastError(result.error); + return failWin32(callback, static_cast(StatusCode::Io::kReadError)); + } + if (result.bytes_transferred <= 0) + { + return total_read; + } + + noteBytesTransferred(context.state, Operation::kRead, result.bytes_transferred); + total_read += result.bytes_transferred; + if (matchesSuffix(output, total_read, terminator, terminator_size)) + { + return total_read; + } + } + + return total_read; +} + +inline auto writeImpl(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int multiplier, + ErrorCallbackT error_callback) -> int +{ + const auto callback = effectiveErrorCallback(error_callback); + const auto buffer_status = cpp_core::validateBuffer(buffer, buffer_size, callback); + if (buffer_status < 0) + { + return buffer_status; + } + + HandleContext context; + const auto handle_status = acquireHandleContext(handle, callback, &context); + if (handle_status < 0) + { + return handle_status; + } + + const auto *input = static_cast(buffer); + int total_written = 0; + while (total_written < buffer_size) + { + const int current_timeout = + total_written == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); + const auto result = writeChunk(context, input + total_written, buffer_size - total_written, current_timeout); + if (result.outcome == IoOutcome::kTimedOut) + { + return total_written; + } + if (result.outcome == IoOutcome::kAborted) + { + return cpp_core::failMsg(callback, static_cast(StatusCode::Io::kAbortWriteError), + "Write aborted"); + } + if (result.outcome == IoOutcome::kError) + { + SetLastError(result.error); + return failWin32(callback, static_cast(StatusCode::Io::kWriteError)); + } + if (result.bytes_transferred <= 0) + { + return total_written; + } + + noteBytesTransferred(context.state, Operation::kWrite, result.bytes_transferred); + total_written += result.bytes_transferred; + } + + return total_written; +} + +} // namespace cpp_bindings_windows::detail diff --git a/src/detail/win32_helpers.hpp b/src/detail/win32_helpers.hpp index 6f404e6..964c5f7 100644 --- a/src/detail/win32_helpers.hpp +++ b/src/detail/win32_helpers.hpp @@ -1,9 +1,9 @@ #pragma once +#include "common_types.hpp" +#include "handle_state.hpp" + #include -#include -#include -#include #ifndef NOMINMAX #define NOMINMAX @@ -11,64 +11,98 @@ #include #include -#include -#include #include +#include +#include namespace cpp_bindings_windows::detail { - -// Win32 HANDLE traits for UniqueResource -struct Win32HandleTraits +inline auto win32ErrorToString(DWORD error) -> std::string { - using handle_type = HANDLE; + LPSTR buffer = nullptr; + const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; + const DWORD language_id = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); + const DWORD length = + FormatMessageA(flags, nullptr, error, language_id, reinterpret_cast(&buffer), 0, nullptr); + if (length == 0 || buffer == nullptr) + { + return "Unknown Win32 error (" + std::to_string(error) + ")"; + } - static constexpr auto invalid() noexcept -> handle_type + std::string message(buffer, length); + LocalFree(buffer); + while (!message.empty() && (message.back() == '\r' || message.back() == '\n')) { - return nullptr; + message.pop_back(); } + return message; +} - static auto close(handle_type h) noexcept -> void +inline auto utf8ToWide(const char *utf8) -> std::wstring +{ + if (utf8 == nullptr || *utf8 == '\0') { - if (h != INVALID_HANDLE_VALUE) - { - CloseHandle(h); - } + return {}; } -}; -using UniqueHandle = cpp_core::UniqueResource; + const int required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, -1, nullptr, 0); + if (required <= 0) + { + return {}; + } + + std::wstring wide(static_cast(required), L'\0'); + if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, -1, wide.data(), required) <= 0) + { + return {}; + } + wide.pop_back(); + return wide; +} -// Win32-specific error helpers -inline auto win32ErrorToString(DWORD err) -> std::string +inline auto normalizePortPath(std::wstring_view port) -> std::wstring { - LPSTR buffer = nullptr; - const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; - const DWORD lang_id = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); + if (port.starts_with(L"\\\\.\\")) + { + return std::wstring(port); + } + if (port.starts_with(L"COM") || port.starts_with(L"com")) + { + return L"\\\\.\\" + std::wstring(port); + } + return std::wstring(port); +} - const DWORD len = FormatMessageA(flags, nullptr, err, lang_id, reinterpret_cast(&buffer), 0, nullptr); - if (len == 0 || buffer == nullptr) +inline auto wideToUtf8(std::wstring_view wide) -> std::string +{ + if (wide.empty()) { - return "Unknown Win32 error"; + return {}; } - std::string msg(buffer, len); - LocalFree(buffer); + const int required = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), + nullptr, 0, nullptr, nullptr); + if (required <= 0) + { + return {}; + } - while (!msg.empty() && (msg.back() == '\r' || msg.back() == '\n')) + std::string utf8(static_cast(required), '\0'); + if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), utf8.data(), + required, nullptr, nullptr) <= 0) { - msg.pop_back(); + return {}; } - return msg; + return utf8; } -template -inline auto failWin32(Callback &&error_callback, cpp_core::StatusCodes code) -> Ret +template +inline auto failWin32(ErrorCallbackT error_callback, StatusCodeValue code) -> ReturnType { - const DWORD err = GetLastError(); - const std::string msg = win32ErrorToString(err); - cpp_core::invokeError(std::forward(error_callback), code, msg); - return static_cast(code); + const DWORD error = GetLastError(); + const std::string message = win32ErrorToString(error); + cpp_core::invokeError(effectiveErrorCallback(error_callback), code, message); + return static_cast(code); } inline auto bytesWaiting(HANDLE handle, int *out_bytes) -> bool @@ -80,41 +114,14 @@ inline auto bytesWaiting(HANDLE handle, int *out_bytes) -> bool *out_bytes = 0; DWORD errors = 0; - COMSTAT stat = {}; - if (ClearCommError(handle, &errors, &stat) == 0) + COMSTAT status = {}; + if (ClearCommError(handle, &errors, &status) == 0) { return false; } - if (stat.cbInQue > static_cast(INT_MAX)) - { - *out_bytes = INT_MAX; - } - else - { - *out_bytes = static_cast(stat.cbInQue); - } + *out_bytes = status.cbInQue > static_cast(INT_MAX) ? INT_MAX : static_cast(status.cbInQue); return true; } -// Combined int64_t -> HANDLE validation for the C API boundary. -// Checks numeric range, nullptr, and INVALID_HANDLE_VALUE. -template -inline auto validateWin32Handle(int64_t handle, Callback &&error_callback, HANDLE *out) -> Ret -{ - if (handle <= 0 || handle > std::numeric_limits::max() || handle > std::numeric_limits::max()) - { - return cpp_core::failMsg(std::forward(error_callback), - cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle"); - } - const HANDLE h = reinterpret_cast(static_cast(handle)); - if (h == nullptr || h == INVALID_HANDLE_VALUE) - { - return cpp_core::failMsg(std::forward(error_callback), - cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle"); - } - *out = h; - return static_cast(cpp_core::StatusCodes::kSuccess); -} - } // namespace cpp_bindings_windows::detail diff --git a/src/get_version.cpp b/src/get_version.cpp new file mode 100644 index 0000000..472a0b0 --- /dev/null +++ b/src/get_version.cpp @@ -0,0 +1,4 @@ +#include + +// Keep the inline C API definition in a library translation unit so Windows +// linkers emit the exported getVersion symbol because windows is a bit picky and stupid sometimes. diff --git a/src/serial_abort_read.cpp b/src/serial_abort_read.cpp new file mode 100644 index 0000000..e881bcb --- /dev/null +++ b/src/serial_abort_read.cpp @@ -0,0 +1,22 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API auto serialAbortRead(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + cpp_bindings_windows::detail::requestAbort(context.handle, context.state, + cpp_bindings_windows::detail::Operation::kRead); + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_abort_write.cpp b/src/serial_abort_write.cpp new file mode 100644 index 0000000..5cc7400 --- /dev/null +++ b/src/serial_abort_write.cpp @@ -0,0 +1,22 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API auto serialAbortWrite(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + cpp_bindings_windows::detail::requestAbort(context.handle, context.state, + cpp_bindings_windows::detail::Operation::kWrite); + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_clear_buffer_in.cpp b/src/serial_clear_buffer_in.cpp new file mode 100644 index 0000000..f5c6327 --- /dev/null +++ b/src/serial_clear_buffer_in.cpp @@ -0,0 +1,27 @@ +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialClearBufferIn(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + if (PurgeComm(context.handle, PURGE_RXCLEAR) == 0) + { + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + static_cast(cpp_core::StatusCode::Io::kClearBufferInError)); + } + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_clear_buffer_out.cpp b/src/serial_clear_buffer_out.cpp new file mode 100644 index 0000000..52b4373 --- /dev/null +++ b/src/serial_clear_buffer_out.cpp @@ -0,0 +1,27 @@ +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialClearBufferOut(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + if (FlushFileBuffers(context.handle) == 0 || PurgeComm(context.handle, PURGE_TXCLEAR) == 0) + { + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + static_cast(cpp_core::StatusCode::Io::kClearBufferOutError)); + } + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_close.cpp b/src/serial_close.cpp index 4ebf015..f5e20cb 100644 --- a/src/serial_close.cpp +++ b/src/serial_close.cpp @@ -14,8 +14,7 @@ extern "C" } HANDLE h = nullptr; - const auto handle_ok = - cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + const auto handle_ok = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); if (handle_ok < 0) { return handle_ok; @@ -23,11 +22,13 @@ extern "C" if (CloseHandle(h) == 0) { - return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kCloseHandleError); + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Connection::kCloseHandleError); } - return 0; + cpp_bindings_windows::detail::removeHandleState(h); + return static_cast(cpp_core::StatusCode::kSuccess); } } // extern "C" diff --git a/src/serial_close.test.cpp b/src/serial_close.test.cpp index af2d38b..aff951e 100644 --- a/src/serial_close.test.cpp +++ b/src/serial_close.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include @@ -29,29 +29,29 @@ TEST_F(SerialCloseTest, CloseInvalidHandleZero) { int result = serialClose(0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::kSuccess)); } TEST_F(SerialCloseTest, CloseInvalidHandleNegative) { int result = serialClose(-1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::kSuccess)); } TEST_F(SerialCloseTest, CloseInvalidHandleNegativeLarge) { int result = serialClose(-12345, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::kSuccess)); } -TEST_F(SerialCloseTest, CloseInvalidHandleTooLarge) +TEST_F(SerialCloseTest, HandleAboveIntMaxIsNotRejectedByRangeValidation) { auto too_large_handle = static_cast(std::numeric_limits::max()) + 1; int result = serialClose(too_large_handle, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialCloseTest, CloseInvalidHandleIntMaxBoundary) @@ -59,14 +59,14 @@ TEST_F(SerialCloseTest, CloseInvalidHandleIntMaxBoundary) auto handle = static_cast(std::numeric_limits::max()); int result = serialClose(handle, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialCloseTest, CloseNoErrorCallback) { int result = serialClose(0, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::kSuccess)); } TEST_F(SerialCloseTest, CloseInvalidHandle) @@ -74,5 +74,5 @@ TEST_F(SerialCloseTest, CloseInvalidHandle) // Closing a value that is not a valid HANDLE int result = serialClose(9999, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kCloseHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kCloseHandleError)); } diff --git a/src/serial_drain.cpp b/src/serial_drain.cpp new file mode 100644 index 0000000..50dc524 --- /dev/null +++ b/src/serial_drain.cpp @@ -0,0 +1,27 @@ +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialDrain(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + if (FlushFileBuffers(context.handle) == 0) + { + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + static_cast(cpp_core::StatusCode::Io::kWriteError)); + } + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_extended_api.test.cpp b/src/serial_extended_api.test.cpp new file mode 100644 index 0000000..83d09f5 --- /dev/null +++ b/src/serial_extended_api.test.cpp @@ -0,0 +1,101 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace +{ +std::atomic g_last_error_code{0}; +std::atomic g_port_callback_count{0}; + +void globalErrorCallback(int code, const char * /*message*/) +{ + g_last_error_code.store(code, std::memory_order_relaxed); +} + +void listPortsCallback(const char * /*port*/, const char * /*path*/, const char * /*manufacturer*/, + const char * /*serial_number*/, const char * /*pnp_id*/, const char * /*location_id*/, + const char * /*product_id*/, const char * /*vendor_id*/) +{ + g_port_callback_count.fetch_add(1, std::memory_order_relaxed); +} + +constexpr auto kBufferError = static_cast(cpp_core::StatusCode::Io::kBufferError); +constexpr auto kInvalidHandleError = static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError); +} // namespace + +class SerialExtendedApiTest : public ::testing::Test +{ + protected: + void SetUp() override + { + g_last_error_code.store(0, std::memory_order_relaxed); + g_port_callback_count.store(0, std::memory_order_relaxed); + serialSetErrorCallback(nullptr); + serialSetReadCallback(nullptr); + serialSetWriteCallback(nullptr); + ASSERT_EQ(serialMonitorPorts(nullptr, nullptr), 0); + } + + void TearDown() override + { + serialSetErrorCallback(nullptr); + serialSetReadCallback(nullptr); + serialSetWriteCallback(nullptr); + (void)serialMonitorPorts(nullptr, nullptr); + } +}; + +TEST_F(SerialExtendedApiTest, GlobalErrorCallbackActsAsFallback) +{ + serialSetErrorCallback(globalErrorCallback); + + std::array buffer{}; + EXPECT_EQ(serialRead(-1, buffer.data(), static_cast(buffer.size()), 10, 1, nullptr), kInvalidHandleError); + EXPECT_EQ(g_last_error_code.load(std::memory_order_relaxed), kInvalidHandleError); +} + +TEST_F(SerialExtendedApiTest, ReadHelpersValidateTerminators) +{ + std::array buffer{}; + EXPECT_EQ(serialReadUntil(1, buffer.data(), static_cast(buffer.size()), 10, 1, nullptr, nullptr), + kBufferError); + EXPECT_EQ(serialReadUntilSequence(1, buffer.data(), static_cast(buffer.size()), 10, 1, nullptr, nullptr), + kBufferError); + + char empty_sequence[] = ""; + EXPECT_EQ( + serialReadUntilSequence(1, buffer.data(), static_cast(buffer.size()), 10, 1, empty_sequence, nullptr), + kBufferError); +} + +TEST_F(SerialExtendedApiTest, HandleBasedExtensionsRejectInvalidHandles) +{ + EXPECT_EQ(serialAbortRead(-1, nullptr), kInvalidHandleError); + EXPECT_EQ(serialAbortWrite(-1, nullptr), kInvalidHandleError); + EXPECT_EQ(serialInBytesTotal(-1, nullptr), kInvalidHandleError); + EXPECT_EQ(serialOutBytesTotal(-1, nullptr), kInvalidHandleError); +} + +TEST_F(SerialExtendedApiTest, ListPortsValidatesAndEnumerates) +{ + EXPECT_EQ(serialListPorts(nullptr, nullptr), kBufferError); + + const int result = serialListPorts(listPortsCallback, nullptr); + EXPECT_GE(result, 0); + EXPECT_EQ(result, g_port_callback_count.load(std::memory_order_relaxed)); +} diff --git a/src/serial_get_baudrate.cpp b/src/serial_get_baudrate.cpp new file mode 100644 index 0000000..a4eece3 --- /dev/null +++ b/src/serial_get_baudrate.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetBaudrate(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + return static_cast(dcb.BaudRate); + } + +} // extern "C" diff --git a/src/serial_get_cts.cpp b/src/serial_get_cts.cpp new file mode 100644 index 0000000..09c6860 --- /dev/null +++ b/src/serial_get_cts.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetCts(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(h, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::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..458a4ad --- /dev/null +++ b/src/serial_get_data_bits.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetDataBits(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + return static_cast(dcb.ByteSize); + } + +} // extern "C" diff --git a/src/serial_get_dcd.cpp b/src/serial_get_dcd.cpp new file mode 100644 index 0000000..0b904e5 --- /dev/null +++ b/src/serial_get_dcd.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetDcd(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(h, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::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..a67ea62 --- /dev/null +++ b/src/serial_get_dsr.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetDsr(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(h, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::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..1394971 --- /dev/null +++ b/src/serial_get_flow_control.cpp @@ -0,0 +1,37 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetFlowControl(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + if (dcb.fOutxCtsFlow != 0 && dcb.fRtsControl == RTS_CONTROL_HANDSHAKE) + { + return 1; + } + if (dcb.fOutX != 0 && dcb.fInX != 0) + { + return 2; + } + return 0; + } + +} // extern "C" diff --git a/src/serial_get_parity.cpp b/src/serial_get_parity.cpp new file mode 100644 index 0000000..f5ff9a1 --- /dev/null +++ b/src/serial_get_parity.cpp @@ -0,0 +1,37 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetParity(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + switch (dcb.Parity) + { + case EVENPARITY: + return 1; + case ODDPARITY: + return 2; + default: + return 0; + } + } + +} // extern "C" diff --git a/src/serial_get_ri.cpp b/src/serial_get_ri.cpp new file mode 100644 index 0000000..0bb6254 --- /dev/null +++ b/src/serial_get_ri.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetRi(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DWORD modem_status = 0; + if (GetCommModemStatus(h, &modem_status) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::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..ccb2c5b --- /dev/null +++ b/src/serial_get_stop_bits.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialGetStopBits(int64_t handle, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + return (dcb.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..50b867a --- /dev/null +++ b/src/serial_in_bytes_total.cpp @@ -0,0 +1,20 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API auto serialInBytesTotal(int64_t handle, ErrorCallbackT error_callback) -> int64_t + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = + cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + return context.state->bytes_read_total.load(std::memory_order_relaxed); + } + +} // extern "C" diff --git a/src/serial_in_bytes_waiting.cpp b/src/serial_in_bytes_waiting.cpp new file mode 100644 index 0000000..291aeef --- /dev/null +++ b/src/serial_in_bytes_waiting.cpp @@ -0,0 +1,28 @@ +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialInBytesWaiting(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + int waiting = 0; + if (!cpp_bindings_windows::detail::bytesWaiting(context.handle, &waiting)) + { + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + static_cast(cpp_core::StatusCode::Control::kGetStateError)); + } + return waiting; + } + +} // extern "C" diff --git a/src/serial_list_ports.cpp b/src/serial_list_ports.cpp new file mode 100644 index 0000000..12cbbfb --- /dev/null +++ b/src/serial_list_ports.cpp @@ -0,0 +1,214 @@ +#include +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ +struct PortInfo +{ + std::string port; + std::string path; + std::string manufacturer; + std::string serial_number; + std::string pnp_id; + std::string location_id; + std::string product_id; + std::string vendor_id; +}; + +auto registryString(HKEY key, const wchar_t *value_name) -> std::optional +{ + DWORD type = 0; + DWORD size = 0; + if (RegQueryValueExW(key, value_name, nullptr, &type, nullptr, &size) != ERROR_SUCCESS || + (type != REG_SZ && type != REG_EXPAND_SZ) || size < sizeof(wchar_t)) + { + return std::nullopt; + } + + std::vector buffer(size / sizeof(wchar_t)); + if (RegQueryValueExW(key, value_name, nullptr, &type, reinterpret_cast(buffer.data()), &size) != + ERROR_SUCCESS) + { + return std::nullopt; + } + return std::wstring(buffer.data()); +} + +auto portName(HDEVINFO device_info_set, SP_DEVINFO_DATA *device_info) -> std::optional +{ + HKEY key = SetupDiOpenDevRegKey(device_info_set, device_info, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_QUERY_VALUE); + if (key == INVALID_HANDLE_VALUE) + { + return std::nullopt; + } + const auto value = registryString(key, L"PortName"); + RegCloseKey(key); + return value; +} + +auto deviceProperty(HDEVINFO device_info_set, SP_DEVINFO_DATA *device_info, DWORD property) + -> std::optional +{ + DWORD type = 0; + DWORD size = 0; + (void)SetupDiGetDeviceRegistryPropertyW(device_info_set, device_info, property, &type, nullptr, 0, &size); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || size < sizeof(wchar_t)) + { + return std::nullopt; + } + + std::vector buffer(size); + if (SetupDiGetDeviceRegistryPropertyW(device_info_set, device_info, property, &type, buffer.data(), size, + nullptr) == 0) + { + return std::nullopt; + } + return std::wstring(reinterpret_cast(buffer.data())); +} + +auto instanceId(HDEVINFO device_info_set, SP_DEVINFO_DATA *device_info) -> std::optional +{ + DWORD required = 0; + (void)SetupDiGetDeviceInstanceIdW(device_info_set, device_info, nullptr, 0, &required); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || required == 0) + { + return std::nullopt; + } + + std::vector buffer(required); + if (SetupDiGetDeviceInstanceIdW(device_info_set, device_info, buffer.data(), required, nullptr) == 0) + { + return std::nullopt; + } + return std::wstring(buffer.data()); +} + +auto asciiUpper(std::string value) -> std::string +{ + std::ranges::transform(value, value.begin(), + [](unsigned char character) { return static_cast(std::toupper(character)); }); + return value; +} + +auto hardwareId(std::string_view pnp_id, std::string_view prefix) -> std::string +{ + const std::string upper = asciiUpper(std::string(pnp_id)); + const auto position = upper.find(prefix); + if (position == std::string::npos || position + prefix.size() + 4 > upper.size()) + { + return {}; + } + return upper.substr(position + prefix.size(), 4); +} + +auto serialNumber(std::string_view pnp_id) -> std::string +{ + const auto separator = pnp_id.rfind('\\'); + if (separator == std::string_view::npos || separator + 1 >= pnp_id.size()) + { + return {}; + } + + std::string candidate(pnp_id.substr(separator + 1)); + return candidate.find('&') == std::string::npos ? candidate : std::string{}; +} + +auto optionalCString(const std::string &value) -> const char * +{ + return value.empty() ? nullptr : value.c_str(); +} +} // namespace + +extern "C" +{ + + MODULE_API auto serialListPorts(void (*callback_fn)(const char *port, const char *path, const char *manufacturer, + const char *serial_number, const char *pnp_id, + const char *location_id, const char *product_id, + const char *vendor_id), + ErrorCallbackT error_callback) -> int + { + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + if (callback_fn == nullptr) + { + return cpp_core::failMsg( + callback, static_cast(cpp_core::StatusCode::Io::kBufferError), + "Port callback must not be null"); + } + + const HDEVINFO device_info_set = SetupDiGetClassDevsW(&GUID_DEVCLASS_PORTS, nullptr, nullptr, DIGCF_PRESENT); + if (device_info_set == INVALID_HANDLE_VALUE) + { + return cpp_bindings_windows::detail::failWin32( + callback, static_cast(cpp_core::StatusCode::Monitor::kMonitorError)); + } + const auto cleanup = cpp_core::defer([&] { SetupDiDestroyDeviceInfoList(device_info_set); }); + + std::vector ports; + for (DWORD index = 0;; ++index) + { + SP_DEVINFO_DATA device_info = {}; + device_info.cbSize = sizeof(device_info); + if (SetupDiEnumDeviceInfo(device_info_set, index, &device_info) == 0) + { + if (GetLastError() == ERROR_NO_MORE_ITEMS) + { + break; + } + return cpp_bindings_windows::detail::failWin32( + callback, static_cast(cpp_core::StatusCode::Monitor::kMonitorError)); + } + + const auto port_name = portName(device_info_set, &device_info); + if (!port_name || port_name->size() < 4 || + (!port_name->starts_with(L"COM") && !port_name->starts_with(L"com"))) + { + continue; + } + + PortInfo info; + info.port = cpp_bindings_windows::detail::wideToUtf8(*port_name); + info.path = "\\\\.\\" + info.port; + if (const auto value = deviceProperty(device_info_set, &device_info, SPDRP_MFG)) + { + info.manufacturer = cpp_bindings_windows::detail::wideToUtf8(*value); + } + if (const auto value = deviceProperty(device_info_set, &device_info, SPDRP_LOCATION_INFORMATION)) + { + info.location_id = cpp_bindings_windows::detail::wideToUtf8(*value); + } + if (const auto value = instanceId(device_info_set, &device_info)) + { + info.pnp_id = cpp_bindings_windows::detail::wideToUtf8(*value); + info.serial_number = serialNumber(info.pnp_id); + info.vendor_id = hardwareId(info.pnp_id, "VID_"); + info.product_id = hardwareId(info.pnp_id, "PID_"); + } + ports.push_back(std::move(info)); + } + + std::ranges::sort(ports, {}, &PortInfo::port); + for (const auto &info : ports) + { + callback_fn(optionalCString(info.port), optionalCString(info.path), optionalCString(info.manufacturer), + optionalCString(info.serial_number), optionalCString(info.pnp_id), + optionalCString(info.location_id), optionalCString(info.product_id), + optionalCString(info.vendor_id)); + } + return static_cast(ports.size()); + } + +} // extern "C" diff --git a/src/serial_monitor_ports.cpp b/src/serial_monitor_ports.cpp new file mode 100644 index 0000000..90eb811 --- /dev/null +++ b/src/serial_monitor_ports.cpp @@ -0,0 +1,129 @@ +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.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_fn)(int event, const char *port), ErrorCallbackT error_callback) + -> int + { + std::lock_guard lock(g_monitor_mutex); + stopMonitor(); + if (callback_fn == 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_fn, callback); + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_open.cpp b/src/serial_open.cpp index 84f353c..73b3920 100644 --- a/src/serial_open.cpp +++ b/src/serial_open.cpp @@ -14,46 +14,8 @@ namespace { -auto utf8ToWide(const char *utf8) -> std::wstring -{ - if (utf8 == nullptr || utf8[0] == '\0') - { - return {}; - } - const int needed = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, nullptr, 0); - if (needed <= 0) - { - return {}; - } - std::wstring out(static_cast(needed), L'\0'); - const int written = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, out.data(), needed); - if (written <= 0) - { - return {}; - } - if (!out.empty() && out.back() == L'\0') - { - out.pop_back(); - } - return out; -} - -auto normalizePortPath(const wchar_t *port) -> std::wstring -{ - std::wstring p(port); - if (p.rfind(L"\\\\.\\", 0) == 0) - { - return p; - } - if (p.rfind(L"COM", 0) == 0 || p.rfind(L"com", 0) == 0) - { - return L"\\\\.\\" + p; - } - return p; -} - -auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Parity par, - cpp_core::StopBits sb) -> cpp_core::Status +auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Parity parity_value, + cpp_core::StopBits stop_bits_value) -> cpp_core::Status { DCB dcb = {}; dcb.DCBlength = sizeof(DCB); @@ -61,7 +23,7 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par if (GetCommState(handle, &dcb) == 0) { const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCodes::kGetStateError, + return cpp_core::fail(cpp_core::StatusCode::Control::kGetStateError, "GetCommState failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); } @@ -69,7 +31,7 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par dcb.ByteSize = static_cast(data_bits); dcb.fBinary = TRUE; - dcb.fParity = (par != cpp_core::Parity::kNone) ? TRUE : FALSE; + dcb.fParity = (parity_value != cpp_core::Parity::kNone) ? TRUE : FALSE; dcb.fOutxCtsFlow = FALSE; dcb.fOutxDsrFlow = FALSE; @@ -80,7 +42,7 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par dcb.fInX = FALSE; dcb.fRtsControl = RTS_CONTROL_ENABLE; - switch (par) + switch (parity_value) { case cpp_core::Parity::kNone: dcb.Parity = NOPARITY; @@ -92,14 +54,14 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par dcb.Parity = ODDPARITY; break; default: - return cpp_core::fail(cpp_core::StatusCodes::kSetStateError, "Invalid parity"); + return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, "Invalid parity"); } - if (sb == cpp_core::StopBits::kOne) + if (stop_bits_value == cpp_core::StopBits::kOne) { dcb.StopBits = ONESTOPBIT; } - else if (sb == cpp_core::StopBits::kTwo) + else if (stop_bits_value == cpp_core::StopBits::kTwo) { dcb.StopBits = TWOSTOPBITS; } @@ -107,7 +69,7 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par if (SetCommState(handle, &dcb) == 0) { const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCodes::kSetStateError, + return cpp_core::fail(cpp_core::StatusCode::Control::kSetStateError, "SetCommState failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); } @@ -115,7 +77,7 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, cpp_core::Par if (SetCommTimeouts(handle, &timeouts) == 0) { const DWORD err = GetLastError(); - return cpp_core::fail(cpp_core::StatusCodes::kSetTimeoutError, + return cpp_core::fail(cpp_core::StatusCode::Configuration::kSetTimeoutError, "SetCommTimeouts failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)); } @@ -128,52 +90,57 @@ extern "C" MODULE_API auto serialOpen(void *port, int baudrate, int data_bits, int parity, int stop_bits, ErrorCallbackT error_callback) -> intptr_t { - const auto params_ok = cpp_core::validateOpenParams(port, baudrate, data_bits, error_callback); + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + const auto params_ok = cpp_core::validateOpenParams(port, baudrate, data_bits, callback); if (params_ok < 0) { return params_ok; } - const auto par = static_cast(parity); + if (parity < static_cast(cpp_core::Parity::kNone) || parity > static_cast(cpp_core::Parity::kOdd)) + { + return cpp_core::failMsg(callback, cpp_core::StatusCode::Control::kSetStateError, + "Invalid parity: must be 0, 1, or 2"); + } + const auto parity_value = static_cast(parity); // stop_bits: 0 or 1 = one stop bit (0 kept for backward compat), 2 = two stop bits if (stop_bits != static_cast(cpp_core::StopBits::kOne) && stop_bits != 1 && stop_bits != static_cast(cpp_core::StopBits::kTwo)) { - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, + return cpp_core::failMsg(callback, cpp_core::StatusCode::Control::kSetStateError, "Invalid stop bits: must be 0, 1, or 2"); } - const auto sb = (stop_bits == static_cast(cpp_core::StopBits::kTwo)) ? cpp_core::StopBits::kTwo - : cpp_core::StopBits::kOne; + const auto stop_bits_value = (stop_bits == static_cast(cpp_core::StopBits::kTwo)) + ? cpp_core::StopBits::kTwo + : cpp_core::StopBits::kOne; const auto *port_utf8 = static_cast(port); - std::wstring port_wide = utf8ToWide(port_utf8); + std::wstring port_wide = cpp_bindings_windows::detail::utf8ToWide(port_utf8); if (port_wide.empty()) { - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kNotFoundError, + return cpp_core::failMsg(callback, cpp_core::StatusCode::Connection::kNotFoundError, "Port string is invalid or not valid UTF-8"); } - const std::wstring device_path = normalizePortPath(port_wide.c_str()); + const std::wstring device_path = cpp_bindings_windows::detail::normalizePortPath(port_wide); - const HANDLE raw_handle = - CreateFileW(device_path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr); + const HANDLE raw_handle = CreateFileW(device_path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr); // CreateFileW returns INVALID_HANDLE_VALUE on failure, normalize to nullptr // so UniqueHandle (sentinel = nullptr) treats it as invalid. - cpp_bindings_windows::detail::UniqueHandle handle( - (raw_handle == INVALID_HANDLE_VALUE) ? nullptr : raw_handle); + cpp_bindings_windows::detail::UniqueHandle handle((raw_handle == INVALID_HANDLE_VALUE) ? nullptr : raw_handle); if (!handle) { - return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kNotFoundError); + return cpp_bindings_windows::detail::failWin32(callback, + cpp_core::StatusCode::Connection::kNotFoundError); } - const auto settings = applyLineSettings(handle.get(), baudrate, data_bits, par, sb); + const auto settings = applyLineSettings(handle.get(), baudrate, data_bits, parity_value, stop_bits_value); if (!settings.has_value()) { - return static_cast(cpp_core::toCStatus(settings, error_callback)); + return static_cast(cpp_core::toCStatus(settings, callback)); } PurgeComm(handle.get(), PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT); @@ -181,10 +148,12 @@ extern "C" const intptr_t out = reinterpret_cast(handle.get()); if (out <= 0) { - return cpp_core::failMsg(error_callback, cpp_core::StatusCodes::kInvalidHandleError, + return cpp_core::failMsg(callback, cpp_core::StatusCode::Connection::kInvalidHandleError, "Invalid handle generated"); } - return reinterpret_cast(handle.release()); + const HANDLE opened_handle = handle.release(); + cpp_bindings_windows::detail::registerOpenedHandle(opened_handle); + return reinterpret_cast(opened_handle); } } // extern "C" diff --git a/src/serial_open.test.cpp b/src/serial_open.test.cpp index c43d563..1c387c0 100644 --- a/src/serial_open.test.cpp +++ b/src/serial_open.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -40,155 +40,155 @@ TEST_F(SerialOpenTest, NullPortParameter) { intptr_t result = serialOpen(nullptr, 9600, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kNotFoundError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kNotFoundError)); EXPECT_NE(error_capture.last_message.find("nullptr"), std::string::npos); } TEST_F(SerialOpenTest, BaudrateTooLow) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 100, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 100, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); EXPECT_NE(error_capture.last_message.find("baudrate"), std::string::npos); } TEST_F(SerialOpenTest, BaudrateTooLowBoundary) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 299, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 299, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, BaudrateBoundaryValid) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 300, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 300, 8, 0, 1, error_callback); // COM99999 does not exist, but should pass baudrate validation (kNotFoundError, not kSetStateError) - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, DataBitsTooLow) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 4, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 4, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); EXPECT_NE(error_capture.last_message.find("data bits"), std::string::npos); } TEST_F(SerialOpenTest, DataBitsTooHigh) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 9, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 9, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidDataBits5) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 5, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 5, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidDataBits6) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 6, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 6, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidDataBits7) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 7, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 7, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidDataBits8) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, InvalidParity) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 5, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 5, 1, error_callback); EXPECT_LT(result, 0); } TEST_F(SerialOpenTest, ValidParityNone) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidParityEven) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 1, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 1, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidParityOdd) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 2, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 2, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, InvalidStopBits) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 3, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 3, error_callback); EXPECT_LT(result, 0); } TEST_F(SerialOpenTest, ValidStopBits0) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 0, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 0, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidStopBits1) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, ValidStopBits2) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 2, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 2, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)); } TEST_F(SerialOpenTest, NonExistentPort) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, - error_callback); + intptr_t result = + serialOpen(const_cast(static_cast(kNonExistentPort)), 9600, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kNotFoundError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kNotFoundError)); } TEST_F(SerialOpenTest, VariousBaudrates) @@ -197,9 +197,9 @@ TEST_F(SerialOpenTest, VariousBaudrates) for (int baudrate : baudrates) { - intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), baudrate, 8, 0, - 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)) + intptr_t result = serialOpen(const_cast(static_cast(kNonExistentPort)), baudrate, 8, 0, 1, + error_callback); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Control::kSetStateError)) << "Baudrate " << baudrate << " should be valid"; } } @@ -208,5 +208,5 @@ TEST_F(SerialOpenTest, NoErrorCallbackNullPort) { intptr_t result = serialOpen(nullptr, 9600, 8, 0, 1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kNotFoundError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kNotFoundError)); } diff --git a/src/serial_out_bytes_total.cpp b/src/serial_out_bytes_total.cpp new file mode 100644 index 0000000..d479cb5 --- /dev/null +++ b/src/serial_out_bytes_total.cpp @@ -0,0 +1,20 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API auto serialOutBytesTotal(int64_t handle, ErrorCallbackT error_callback) -> int64_t + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = + cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + return context.state->bytes_written_total.load(std::memory_order_relaxed); + } + +} // extern "C" diff --git a/src/serial_out_bytes_waiting.cpp b/src/serial_out_bytes_waiting.cpp new file mode 100644 index 0000000..fb762dc --- /dev/null +++ b/src/serial_out_bytes_waiting.cpp @@ -0,0 +1,31 @@ +#include + +#include "detail/handle_state.hpp" +#include "detail/win32_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialOutBytesWaiting(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_windows::detail::HandleContext context; + const auto status = cpp_bindings_windows::detail::acquireHandleContext(handle, error_callback, &context); + if (status < 0) + { + return status; + } + + DWORD errors = 0; + COMSTAT comm_status = {}; + if (ClearCommError(context.handle, &errors, &comm_status) == 0) + { + return cpp_bindings_windows::detail::failWin32( + cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + static_cast(cpp_core::StatusCode::Control::kGetStateError)); + } + return comm_status.cbOutQue > static_cast(INT_MAX) ? INT_MAX : static_cast(comm_status.cbOutQue); + } + +} // extern "C" diff --git a/src/serial_read.cpp b/src/serial_read.cpp index d7e36d1..6892c52 100644 --- a/src/serial_read.cpp +++ b/src/serial_read.cpp @@ -1,221 +1,15 @@ #include -#include -#include -#include "detail/win32_helpers.hpp" - -#include - -namespace -{ -auto waitForRxChar(HANDLE handle, int timeout_ms) -> int -{ - timeout_ms = cpp_core::clampTimeout(timeout_ms); - - if (SetCommMask(handle, EV_RXCHAR) == 0) - { - return -1; - } - - OVERLAPPED ov = {}; - ov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (ov.hEvent == nullptr) - { - return -1; - } - DEFER - { - CloseHandle(ov.hEvent); - }; - - DWORD mask = 0; - const BOOL ok = WaitCommEvent(handle, &mask, &ov); - if (ok != 0) - { - return 1; - } - if (ok == 0) - { - const DWORD err = GetLastError(); - if (err != ERROR_IO_PENDING) - { - SetLastError(err); - return -1; - } - } - - const DWORD wait_rc = WaitForSingleObject(ov.hEvent, static_cast(timeout_ms)); - if (wait_rc == WAIT_TIMEOUT) - { - CancelIoEx(handle, &ov); - return 0; - } - if (wait_rc != WAIT_OBJECT_0) - { - CancelIoEx(handle, &ov); - SetLastError(ERROR_GEN_FAILURE); - return -1; - } - - DWORD bytes = 0; - if (GetOverlappedResult(handle, &ov, &bytes, FALSE) == 0) - { - return -1; - } - - return 1; -} - -auto readSome(HANDLE handle, unsigned char *dst, int size, int timeout_ms) -> int -{ - if (size <= 0) - { - return 0; - } - - OVERLAPPED ov = {}; - ov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (ov.hEvent == nullptr) - { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return -1; - } - DEFER - { - CloseHandle(ov.hEvent); - }; - - DWORD bytes_read = 0; - const BOOL ok = ReadFile(handle, dst, static_cast(size), &bytes_read, &ov); - if (ok != 0) - { - return static_cast(bytes_read); - } - - const DWORD err = GetLastError(); - if (err != ERROR_IO_PENDING) - { - SetLastError(err); - return -1; - } - - timeout_ms = cpp_core::clampTimeout(timeout_ms); - const DWORD wait_rc = WaitForSingleObject(ov.hEvent, static_cast(timeout_ms)); - if (wait_rc == WAIT_TIMEOUT) - { - CancelIoEx(handle, &ov); - return 0; - } - if (wait_rc != WAIT_OBJECT_0) - { - CancelIoEx(handle, &ov); - SetLastError(ERROR_GEN_FAILURE); - return -1; - } - - if (GetOverlappedResult(handle, &ov, &bytes_read, FALSE) == 0) - { - return -1; - } - - return static_cast(bytes_read); -} -} // namespace +#include "detail/io_impl.hpp" extern "C" { - MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, + + MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, ErrorCallbackT error_callback) -> int { - const auto buf_ok = cpp_core::validateBuffer(buffer, buffer_size, error_callback); - if (buf_ok < 0) - { - return buf_ok; - } - - HANDLE h = nullptr; - const auto handle_ok = - cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (handle_ok < 0) - { - return handle_ok; - } - - auto *buf = static_cast(buffer); - - int waiting = 0; - if (!cpp_bindings_windows::detail::bytesWaiting(h, &waiting)) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); - } - - if (waiting <= 0) - { - if (timeout_ms <= 0) - { - return 0; - } - const int ready = waitForRxChar(h, timeout_ms); - if (ready < 0) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kReadError); - } - if (ready == 0) - { - return 0; - } - } - - if (!cpp_bindings_windows::detail::bytesWaiting(h, &waiting)) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); - } - - if (waiting <= 0) - { - return 0; - } - - const int first_chunk = std::min(waiting, buffer_size); - int total = readSome(h, buf, first_chunk, timeout_ms); - if (total < 0) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kReadError); - } - if (total == 0) - { - total = readSome(h, buf, first_chunk, 10); - if (total < 0) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kReadError); - } - if (total == 0) - { - return 0; - } - } - - while (total < buffer_size) - { - if (!cpp_bindings_windows::detail::bytesWaiting(h, &waiting)) - { - return cpp_bindings_windows::detail::failWin32(error_callback, - cpp_core::StatusCodes::kGetStateError); - } - if (waiting <= 0) - { - break; - } - const int chunk = std::min(waiting, buffer_size - total); - const int got = readSome(h, buf + total, chunk, 0); - if (got <= 0) - { - break; - } - total += got; - } - - return total; + return cpp_bindings_windows::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, nullptr, 0, + error_callback); } } // extern "C" diff --git a/src/serial_read.test.cpp b/src/serial_read.test.cpp index 840a7d6..70e1d69 100644 --- a/src/serial_read.test.cpp +++ b/src/serial_read.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -30,7 +30,7 @@ TEST_F(SerialReadTest, ReadNullBuffer) { int result = serialRead(1, nullptr, 10, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); EXPECT_NE(error_capture.last_message.find("buffer"), std::string::npos); } @@ -39,7 +39,7 @@ TEST_F(SerialReadTest, ReadZeroBufferSize) std::array buffer{}; int result = serialRead(1, buffer.data(), 0, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); } TEST_F(SerialReadTest, ReadNegativeBufferSize) @@ -47,7 +47,7 @@ TEST_F(SerialReadTest, ReadNegativeBufferSize) std::array buffer{}; int result = serialRead(1, buffer.data(), -1, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); } TEST_F(SerialReadTest, ReadInvalidHandleZero) @@ -55,7 +55,7 @@ TEST_F(SerialReadTest, ReadInvalidHandleZero) std::array buffer{}; int result = serialRead(0, buffer.data(), static_cast(buffer.size()), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialReadTest, ReadInvalidHandleNegative) @@ -63,16 +63,16 @@ TEST_F(SerialReadTest, ReadInvalidHandleNegative) std::array buffer{}; int result = serialRead(-1, buffer.data(), static_cast(buffer.size()), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } -TEST_F(SerialReadTest, ReadInvalidHandleTooLarge) +TEST_F(SerialReadTest, ReadHandleAboveIntMaxIsNotRejectedByRangeValidation) { std::array buffer{}; auto too_large = static_cast(std::numeric_limits::max()) + 1; int result = serialRead(too_large, buffer.data(), static_cast(buffer.size()), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialReadTest, ReadNoErrorCallback) @@ -80,5 +80,5 @@ TEST_F(SerialReadTest, ReadNoErrorCallback) std::array buffer{}; int result = serialRead(0, buffer.data(), static_cast(buffer.size()), 100, 0, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } diff --git a/src/serial_read_line.cpp b/src/serial_read_line.cpp new file mode 100644 index 0000000..c1c7092 --- /dev/null +++ b/src/serial_read_line.cpp @@ -0,0 +1,16 @@ +#include + +#include "detail/io_impl.hpp" + +extern "C" +{ + + MODULE_API auto serialReadLine(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, + ErrorCallbackT error_callback) -> int + { + static constexpr unsigned char kNewline = '\n'; + return cpp_bindings_windows::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, &kNewline, 1, + error_callback); + } + +} // extern "C" diff --git a/src/serial_read_until.cpp b/src/serial_read_until.cpp new file mode 100644 index 0000000..7a2ccc5 --- /dev/null +++ b/src/serial_read_until.cpp @@ -0,0 +1,23 @@ +#include + +#include "detail/io_impl.hpp" + +extern "C" +{ + + MODULE_API auto serialReadUntil(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, + void *until_char, ErrorCallbackT error_callback) -> int + { + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + if (until_char == nullptr) + { + return cpp_core::failMsg( + callback, static_cast(cpp_core::StatusCode::Io::kBufferError), + "Terminator pointer must not be null"); + } + + return cpp_bindings_windows::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, + static_cast(until_char), 1, callback); + } + +} // extern "C" diff --git a/src/serial_read_until_sequence.cpp b/src/serial_read_until_sequence.cpp new file mode 100644 index 0000000..32a8750 --- /dev/null +++ b/src/serial_read_until_sequence.cpp @@ -0,0 +1,34 @@ +#include + +#include "detail/io_impl.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialReadUntilSequence(int64_t handle, void *buffer, int buffer_size, int timeout_ms, + int multiplier, void *sequence, ErrorCallbackT error_callback) -> int + { + const auto callback = cpp_bindings_windows::detail::effectiveErrorCallback(error_callback); + if (sequence == nullptr) + { + return cpp_core::failMsg( + callback, static_cast(cpp_core::StatusCode::Io::kBufferError), + "Sequence pointer must not be null"); + } + + const auto *sequence_bytes = static_cast(sequence); + const int sequence_size = static_cast(std::strlen(reinterpret_cast(sequence_bytes))); + if (sequence_size <= 0) + { + return cpp_core::failMsg( + callback, static_cast(cpp_core::StatusCode::Io::kBufferError), + "Sequence must not be empty"); + } + + return cpp_bindings_windows::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, + sequence_bytes, sequence_size, callback); + } + +} // extern "C" diff --git a/src/serial_send_break.cpp b/src/serial_send_break.cpp new file mode 100644 index 0000000..c5dca34 --- /dev/null +++ b/src/serial_send_break.cpp @@ -0,0 +1,41 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSendBreak(int64_t handle, int duration_ms, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + if (duration_ms <= 0) + { + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Control::kSendBreakError, "Break duration must be > 0"); + } + + if (SetCommBreak(h) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kSendBreakError); + } + + Sleep(static_cast(duration_ms)); + + if (ClearCommBreak(h) == 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..25243ca --- /dev/null +++ b/src/serial_set_baudrate.cpp @@ -0,0 +1,44 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetBaudrate(int64_t handle, int baudrate, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + if (baudrate < 300) + { + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetBaudrateError, + "Invalid baudrate: must be >= 300"); + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + dcb.BaudRate = static_cast(baudrate); + + if (SetCommState(h, &dcb) == 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..a703604 --- /dev/null +++ b/src/serial_set_data_bits.cpp @@ -0,0 +1,44 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetDataBits(int64_t handle, int data_bits, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + if (data_bits < 5 || data_bits > 8) + { + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetDataBitsError, + "Invalid data bits: must be 5-8"); + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + dcb.ByteSize = static_cast(data_bits); + + if (SetCommState(h, &dcb) == 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..f53d55e --- /dev/null +++ b/src/serial_set_dtr.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetDtr(int64_t handle, int state, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + const DWORD func = state ? SETDTR : CLRDTR; + if (EscapeCommFunction(h, func) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::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..f493d5d --- /dev/null +++ b/src/serial_set_flow_control.cpp @@ -0,0 +1,65 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetFlowControl(int64_t handle, int mode, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + if (mode < 0 || mode > 2) + { + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetFlowControlError, + "Invalid flow control mode: must be 0, 1, or 2"); + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + dcb.fOutxCtsFlow = FALSE; + dcb.fRtsControl = RTS_CONTROL_ENABLE; + dcb.fOutX = FALSE; + dcb.fInX = FALSE; + + switch (mode) + { + case 1: + dcb.fOutxCtsFlow = TRUE; + dcb.fRtsControl = RTS_CONTROL_HANDSHAKE; + break; + case 2: + dcb.fOutX = TRUE; + dcb.fInX = TRUE; + dcb.XonChar = 0x11; + dcb.XoffChar = 0x13; + dcb.XonLim = 2048; + dcb.XoffLim = 512; + break; + default: + break; + } + + if (SetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32( + error_callback, cpp_core::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..2b444ca --- /dev/null +++ b/src/serial_set_parity.cpp @@ -0,0 +1,56 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetParity(int64_t handle, int parity, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + BYTE win_parity = NOPARITY; + switch (parity) + { + case 0: + win_parity = NOPARITY; + break; + case 1: + win_parity = EVENPARITY; + break; + case 2: + win_parity = ODDPARITY; + break; + default: + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetParityError, + "Invalid parity: must be 0, 1, or 2"); + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + dcb.Parity = win_parity; + dcb.fParity = (parity != 0) ? TRUE : FALSE; + + if (SetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::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..4f094aa --- /dev/null +++ b/src/serial_set_read_callback.cpp @@ -0,0 +1,13 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API void serialSetReadCallback(void (*callback_fn)(int bytes_read)) + { + cpp_bindings_windows::detail::g_read_callback.store(callback_fn, std::memory_order_release); + } + +} // extern "C" diff --git a/src/serial_set_rts.cpp b/src/serial_set_rts.cpp new file mode 100644 index 0000000..fac86df --- /dev/null +++ b/src/serial_set_rts.cpp @@ -0,0 +1,28 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetRts(int64_t handle, int state, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + const DWORD func = state ? SETRTS : CLRRTS; + if (EscapeCommFunction(h, func) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::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..5415780 --- /dev/null +++ b/src/serial_set_stop_bits.cpp @@ -0,0 +1,44 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialSetStopBits(int64_t handle, int stop_bits, ErrorCallbackT error_callback) -> int + { + HANDLE h = nullptr; + const auto rc = cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); + if (rc < 0) + { + return rc; + } + + if (stop_bits != 0 && stop_bits != 1 && stop_bits != 2) + { + return cpp_core::failMsg(cpp_bindings_windows::detail::effectiveErrorCallback(error_callback), + cpp_core::StatusCode::Configuration::kSetStopBitsError, + "Invalid stop bits: must be 0, 1, or 2"); + } + + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + if (GetCommState(h, &dcb) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCode::Control::kGetStateError); + } + + dcb.StopBits = (stop_bits == 2) ? TWOSTOPBITS : ONESTOPBIT; + + if (SetCommState(h, &dcb) == 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..660becb --- /dev/null +++ b/src/serial_set_write_callback.cpp @@ -0,0 +1,13 @@ +#include + +#include "detail/handle_state.hpp" + +extern "C" +{ + + MODULE_API void serialSetWriteCallback(void (*callback_fn)(int bytes_written)) + { + cpp_bindings_windows::detail::g_write_callback.store(callback_fn, std::memory_order_release); + } + +} // extern "C" diff --git a/src/serial_write.cpp b/src/serial_write.cpp index 62b2fdc..910a402 100644 --- a/src/serial_write.cpp +++ b/src/serial_write.cpp @@ -1,95 +1,15 @@ #include -#include -#include -#include "detail/win32_helpers.hpp" - -namespace -{ -auto writeSome(HANDLE handle, const void *src, int size, int timeout_ms) -> int -{ - if (size <= 0) - { - return 0; - } - - OVERLAPPED ov = {}; - ov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (ov.hEvent == nullptr) - { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return -1; - } - DEFER - { - CloseHandle(ov.hEvent); - }; - - DWORD bytes_written = 0; - const BOOL ok = WriteFile(handle, src, static_cast(size), &bytes_written, &ov); - if (ok != 0) - { - return static_cast(bytes_written); - } - - const DWORD err = GetLastError(); - if (err != ERROR_IO_PENDING) - { - SetLastError(err); - return -1; - } - - timeout_ms = cpp_core::clampTimeout(timeout_ms); - const DWORD wait_rc = WaitForSingleObject(ov.hEvent, static_cast(timeout_ms)); - if (wait_rc == WAIT_TIMEOUT) - { - CancelIoEx(handle, &ov); - return 0; - } - if (wait_rc != WAIT_OBJECT_0) - { - CancelIoEx(handle, &ov); - SetLastError(ERROR_GEN_FAILURE); - return -1; - } - - if (GetOverlappedResult(handle, &ov, &bytes_written, FALSE) == 0) - { - return -1; - } - - return static_cast(bytes_written); -} -} // namespace +#include "detail/io_impl.hpp" extern "C" { - MODULE_API auto serialWrite(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, + + MODULE_API auto serialWrite(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int multiplier, ErrorCallbackT error_callback) -> int { - const auto buf_ok = cpp_core::validateBuffer(buffer, buffer_size, error_callback); - if (buf_ok < 0) - { - return buf_ok; - } - - HANDLE h = nullptr; - const auto handle_ok = - cpp_bindings_windows::detail::validateWin32Handle(handle, error_callback, &h); - if (handle_ok < 0) - { - return handle_ok; - } - - const int written = writeSome(h, buffer, buffer_size, timeout_ms); - if (written < 0) - { - return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kWriteError); - } - - FlushFileBuffers(h); - - return written; + return cpp_bindings_windows::detail::writeImpl(handle, buffer, buffer_size, timeout_ms, multiplier, + error_callback); } } // extern "C" diff --git a/src/serial_write.test.cpp b/src/serial_write.test.cpp index 7bb183c..dd246cd 100644 --- a/src/serial_write.test.cpp +++ b/src/serial_write.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -31,7 +31,7 @@ TEST_F(SerialWriteTest, WriteNullBuffer) { int result = serialWrite(1, nullptr, 10, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); EXPECT_NE(error_capture.last_message.find("buffer"), std::string::npos); } @@ -40,7 +40,7 @@ TEST_F(SerialWriteTest, WriteZeroBufferSize) std::array buffer{}; int result = serialWrite(1, buffer.data(), 0, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); } TEST_F(SerialWriteTest, WriteNegativeBufferSize) @@ -48,7 +48,7 @@ TEST_F(SerialWriteTest, WriteNegativeBufferSize) std::array buffer{}; int result = serialWrite(1, buffer.data(), -1, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); } TEST_F(SerialWriteTest, WriteInvalidHandleZero) @@ -56,7 +56,7 @@ TEST_F(SerialWriteTest, WriteInvalidHandleZero) const char *buffer = "test"; int result = serialWrite(0, buffer, static_cast(strlen(buffer)), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialWriteTest, WriteInvalidHandleNegative) @@ -64,16 +64,16 @@ TEST_F(SerialWriteTest, WriteInvalidHandleNegative) const char *buffer = "test"; int result = serialWrite(-1, buffer, static_cast(strlen(buffer)), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } -TEST_F(SerialWriteTest, WriteInvalidHandleTooLarge) +TEST_F(SerialWriteTest, WriteHandleAboveIntMaxIsNotRejectedByRangeValidation) { const char *buffer = "test"; auto too_large = static_cast(std::numeric_limits::max()) + 1; int result = serialWrite(too_large, buffer, static_cast(strlen(buffer)), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_NE(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } TEST_F(SerialWriteTest, WriteEmptyStringZeroSize) @@ -81,7 +81,7 @@ TEST_F(SerialWriteTest, WriteEmptyStringZeroSize) const char *empty = ""; int result = serialWrite(1, empty, 0, 0, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Io::kBufferError)); } TEST_F(SerialWriteTest, WriteNoErrorCallback) @@ -89,5 +89,5 @@ TEST_F(SerialWriteTest, WriteNoErrorCallback) std::array buffer{}; int result = serialWrite(0, buffer.data(), 1, 0, 0, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)); } diff --git a/src/test_helpers/error_capture.hpp b/src/test_helpers/error_capture.hpp index 9749538..987d0a9 100644 --- a/src/test_helpers/error_capture.hpp +++ b/src/test_helpers/error_capture.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include diff --git a/tests/serial_arduino.test.cpp b/tests/serial_arduino.test.cpp index 4ca19c3..f4e6535 100644 --- a/tests/serial_arduino.test.cpp +++ b/tests/serial_arduino.test.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include #include #ifndef NOMINMAX @@ -144,7 +144,7 @@ TEST(SerialInvalidHandleTest, InvalidHandleRead) { char buffer[256]; const int result = serialRead(-1, buffer, static_cast(sizeof(buffer)), 1000, 1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)) + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)) << "Should return error for invalid handle"; } @@ -152,12 +152,12 @@ TEST(SerialInvalidHandleTest, InvalidHandleWrite) { const char *data = "test"; const int result = serialWrite(-1, data, 4, 1000, 1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)) + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError)) << "Should return error for invalid handle"; } TEST(SerialInvalidHandleTest, InvalidHandleClose) { const int result = serialClose(-1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, static_cast(cpp_core::StatusCode::kSuccess)); }