diff --git a/.github/workflows/compile_checks.yml b/.github/workflows/compile_checks.yml new file mode 100644 index 0000000..82ad73e --- /dev/null +++ b/.github/workflows/compile_checks.yml @@ -0,0 +1,47 @@ +name: 'Compile Checks' + +on: + push: + branches: + - '*' + pull_request: + workflow_dispatch: + +jobs: + compile_checks: + name: 'Compile checks' + runs-on: ubuntu-24.04 + + steps: + - name: 'Checkout repository' + uses: actions/checkout@v6 + + - name: 'Install GCC 16.1 toolchain' + run: | + sudo apt-get update -qq + sudo apt-get install -y software-properties-common + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update -qq + sudo apt-get install -y gcc-16 g++-16 ninja-build cmake + + - name: 'Show toolchain versions' + run: | + cmake --version + ninja --version + gcc-16 --version + g++-16 --version + + - name: 'Configure CMAKE' + run: | + cmake -S . -B build/ci -G Ninja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_COMPILER=gcc-16 \ + -DCMAKE_CXX_COMPILER=g++-16 + + - name: 'Build compile checks' + run: | + cmake --build build/ci --target cpp_core_compile_tests + + - name: 'Run CTest if registered tests exist' + run: | + ctest --test-dir build/ci --output-on-failure --no-tests=ignore diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index bf0be46..1c2da48 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -6,43 +6,48 @@ on: paths: - 'include/**' - 'Doxyfile' + - 'README.md' + - 'LICENSE' - '.github/workflows/doxygen.yml' workflow_dispatch: # Required for GitHub Pages deployment permissions: - contents: write - pages: write - id-token: write + contents: read + pages: write + id-token: write jobs: build-docs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: 'Build Doxygen HTML' steps: - - name: Checkout - uses: actions/checkout@v3 + - name: 'Checkout repository' + uses: actions/checkout@v6 - - name: Install Doxygen + Graphviz + - name: 'Install Doxygen + Graphviz' run: | sudo apt-get update -qq sudo apt-get install -y doxygen graphviz - - name: Generate documentation + - name: 'Generate documentation' run: | doxygen -v doxygen Doxyfile - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v3 + - name: 'Upload Pages artifact' + uses: actions/upload-pages-artifact@v5 with: path: docs/html deploy: needs: build-docs - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: 'Deploy to GitHub Pages' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} steps: - - name: Deploy + - name: 'Deploy' id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d831df..e570faf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,34 @@ cmake_minimum_required(VERSION 3.30) # Export compile commands to root directory set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +project(cpp-core LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 26) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_MODULE_STD 26) +set(CMAKE_CXX_MODULE_EXTENSIONS OFF) + +option(CPP_CORE_ENABLE_AST_EXPORT "Enable clang-based JSON AST export for the FFI headers" ON) +set( + CPP_CORE_AST_JSON_OUTPUT + "${CMAKE_BINARY_DIR}/ast/cpp_core_ffi_ast.json" + CACHE FILEPATH + "Output path for the generated FFI AST JSON file" +) +set( + CPP_CORE_AST_CLANGXX + "" + CACHE FILEPATH + "Path to the clang++ executable used for AST JSON export" +) +set( + CPP_CORE_AST_SLIM_JSON_OUTPUT + "${CMAKE_BINARY_DIR}/ast/cpp_core_ffi_api.json" + CACHE FILEPATH + "Output path for the reduced FFI API JSON metadata" +) + include(cmake/CPM.cmake) CPMAddPackage( @@ -18,17 +46,28 @@ include(${cmake-git-versioning_SOURCE_DIR}/cmake/cmake-git-versioning.cmake) get_git_version_info() generate_git_version(OUTPUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include/cpp_core) -project( - cpp-core - VERSION "${GIT_VERSION_MAJOR}.${GIT_VERSION_MINOR}.${GIT_VERSION_PATCH}" - DESCRIPTION "Cross-platform helper library shared by cpp-linux-bindings and cpp-windows-bindings" - LANGUAGES CXX -) +set(PROJECT_VERSION "${GIT_VERSION_MAJOR}.${GIT_VERSION_MINOR}.${GIT_VERSION_PATCH}") +set(PROJECT_DESCRIPTION "Cross-platform helper library shared by cpp-linux-bindings and cpp-windows-bindings") # Header-only library -------------------------------------------------------- add_library(cpp_core INTERFACE) add_library(cpp_core::cpp_core ALIAS cpp_core) +add_library(cpp_core_strict_warnings INTERFACE) + +target_compile_options( + cpp_core_strict_warnings + INTERFACE + -Wall + -Wextra + -Wpedantic + -Wconversion + -Wsign-conversion + -Wshadow + -Wundef + -Werror +) + # Public include directories target_include_directories( cpp_core @@ -37,23 +76,100 @@ target_include_directories( $ ) -# Set C++ standard -set(CMAKE_CXX_STANDARD 23) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -# Require C++23 -target_compile_features(cpp_core INTERFACE cxx_std_23) +target_compile_options(cpp_core INTERFACE -freflection) -# Enable C++23 module support -set(CMAKE_CXX_MODULE_STD 23) -set(CMAKE_CXX_MODULE_EXTENSIONS OFF) +target_compile_features(cpp_core INTERFACE cxx_std_26) include(CTest) if(BUILD_TESTING) - add_library(cpp_core_status_code_compile_test OBJECT tests/status_code_compile_test.cpp) - target_link_libraries(cpp_core_status_code_compile_test PRIVATE cpp_core::cpp_core) + file( + GLOB_RECURSE CPP_CORE_COMPILE_TEST_SOURCES + CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/include/*.test.cpp" + ) + add_library(cpp_core_compile_tests OBJECT ${CPP_CORE_COMPILE_TEST_SOURCES}) + target_link_libraries(cpp_core_compile_tests PRIVATE cpp_core::cpp_core) + target_link_libraries(cpp_core_compile_tests PRIVATE cpp_core_strict_warnings) +endif() + +if(CPP_CORE_ENABLE_AST_EXPORT) + find_package(Python3 COMPONENTS Interpreter REQUIRED) + + file( + GLOB _cpp_core_ast_interface_headers + CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp_core/interface/*.h" + ) + + set( + _cpp_core_ast_headers + "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp_core/serial.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp_core/error_callback.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp_core/module_api.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp_core/version.hpp" + ${_cpp_core_ast_interface_headers} + ) + + if(CPP_CORE_AST_CLANGXX) + set(_cpp_core_ast_clangxx "${CPP_CORE_AST_CLANGXX}") + else() + find_program(_cpp_core_ast_clangxx NAMES clang++) + endif() + + if(NOT _cpp_core_ast_clangxx) + message( + FATAL_ERROR + "CPP_CORE_ENABLE_AST_EXPORT=ON requires clang++. " + "Install clang++ or set CPP_CORE_AST_CLANGXX to an explicit path." + ) + endif() + + set(_cpp_core_ast_wrapper "${CMAKE_CURRENT_BINARY_DIR}/ast/cpp_core_ffi_ast.cpp") + get_filename_component(_cpp_core_ast_json_dir "${CPP_CORE_AST_JSON_OUTPUT}" DIRECTORY) + get_filename_component(_cpp_core_ast_slim_json_dir "${CPP_CORE_AST_SLIM_JSON_OUTPUT}" DIRECTORY) + + file( + GENERATE + OUTPUT "${_cpp_core_ast_wrapper}" + CONTENT "#include \n" + ) + + add_custom_command( + OUTPUT "${CPP_CORE_AST_JSON_OUTPUT}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${_cpp_core_ast_json_dir}" + COMMAND + ${CMAKE_COMMAND} + -DCPP_CORE_AST_CLANGXX=${_cpp_core_ast_clangxx} + -DCPP_CORE_AST_INCLUDE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/include + -DCPP_CORE_AST_OUTPUT=${CPP_CORE_AST_JSON_OUTPUT} + -DCPP_CORE_AST_SOURCE=${_cpp_core_ast_wrapper} + -DCPP_CORE_AST_STANDARD=${CMAKE_CXX_STANDARD} + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/export_ast_json.cmake + DEPENDS "${_cpp_core_ast_wrapper}" ${_cpp_core_ast_headers} + COMMENT "Exporting FFI header AST JSON with clang++" + VERBATIM + ) + + add_custom_command( + OUTPUT "${CPP_CORE_AST_SLIM_JSON_OUTPUT}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${_cpp_core_ast_slim_json_dir}" + COMMAND + "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/tools/clang_ast_to_ffi_json.py" + --input "${CPP_CORE_AST_JSON_OUTPUT}" + --output "${CPP_CORE_AST_SLIM_JSON_OUTPUT}" + --source-root "${CMAKE_CURRENT_SOURCE_DIR}" + --public-header "cpp_core/serial.h" + DEPENDS + "${CPP_CORE_AST_JSON_OUTPUT}" + "${CMAKE_CURRENT_SOURCE_DIR}/tools/clang_ast_to_ffi_json.py" + COMMENT "Reducing clang AST JSON to compact FFI API metadata" + VERBATIM + ) + + add_custom_target(cpp_core_ast_raw_json DEPENDS "${CPP_CORE_AST_JSON_OUTPUT}") + add_custom_target(cpp_core_ast_slim_json DEPENDS "${CPP_CORE_AST_SLIM_JSON_OUTPUT}") endif() # Install rules -------------------------------------------------------------- @@ -69,6 +185,14 @@ install( DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) +if(CPP_CORE_ENABLE_AST_EXPORT) + install( + FILES "${CPP_CORE_AST_SLIM_JSON_OUTPUT}" + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/cpp_core + OPTIONAL + ) +endif() + # CMake package configuration ------------------------------------------------ include(CMakePackageConfigHelpers) diff --git a/CMakePresets.json b/CMakePresets.json index 0c9fa5c..5c2c988 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -39,6 +39,17 @@ "value": "x64", "strategy": "external" } + }, + { + "name": "mingw", + "displayName": "MinGW-w64 (cross)", + "description": "Cross-compile for Windows using MinGW-w64", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/mingw", + "toolchainFile": "${sourceDir}/cmake/mingw-toolchain.cmake", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } } ], "buildPresets": [ @@ -77,6 +88,18 @@ "displayName": "MSVC Release", "configurePreset": "msvc", "configuration": "Release" + }, + { + "name": "mingw-debug", + "displayName": "MinGW Debug", + "configurePreset": "mingw", + "configuration": "Debug" + }, + { + "name": "mingw-release", + "displayName": "MinGW Release", + "configurePreset": "mingw", + "configuration": "Release" } ] } diff --git a/Doxyfile b/Doxyfile index 1a585ee..769b9ba 100644 --- a/Doxyfile +++ b/Doxyfile @@ -6,7 +6,7 @@ PROJECT_NAME = "cpp-core" PROJECT_BRIEF = "Header-only C++ helper library" PROJECT_NUMBER = "1.0" -OUTPUT_DIRECTORY = docs/html +OUTPUT_DIRECTORY = docs CREATE_SUBDIRS = NO # Build options ---------------------------------------------------------- @@ -22,7 +22,7 @@ RECURSIVE = YES # HTML output ------------------------------------------------------------ GENERATE_HTML = YES -HTML_OUTPUT = . +HTML_OUTPUT = html HTML_COLORSTYLE_HUE = 220 HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 diff --git a/README.md b/README.md index 8726f75..e6415f5 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,165 @@ # C++ Core -Header-only API definition library for cross-platform serial communication. Defines the **API contract** and **version information** shared by all platform-specific binding implementations. +`cpp-core` is the header-only **API and ABI contract** shared by the Serial-IO platform bindings. It defines the exported serial interface, the status-code model, and build-time version information consumed by the platform implementations. -> [!IMPORTANT] -> -> **API definitions only** (headers). For implementations and ready-to-use shared libraries: -> - [cpp-bindings-windows](https://github.com/Serial-IO/cpp-bindings-windows) - Windows (DLL) -> - [cpp-bindings-linux](https://github.com/Serial-IO/cpp-bindings-linux) - Linux (SO) -> - [cpp-bindings-macos](https://github.com/Serial-IO/cpp-bindings-macos) - macOS (dylib) +This repository does not provide a ready-to-load shared library by itself. It provides the contract consumed by the platform implementations: -## Features +- [cpp-bindings-linux](https://github.com/Serial-IO/cpp-bindings-linux) +- [cpp-bindings-windows](https://github.com/Serial-IO/cpp-bindings-windows) -* **C++23** header-only API definitions -* **Cross-platform serial I/O API** (POSIX/Windows compatible) -* **Centralized version information** from Git tags (`cpp_core::kVersion`) -* **Status codes** enum for error handling -* **C-compatible API** for FFI bindings (Rust, Python, Deno, etc.) +## Status -## Requirements +`cpp-core` is the canonical definition of the current API line. -* CMake >= 3.30 -* A C++23 compatible compiler (GCC 13+, Clang 16+, MSVC 2022+) -* Git (for automatic version detection) +- Current baseline compiler: GCC 16.1+ +- Reflection model: GCC `std::meta` with `-freflection` +- Supported Linux toolchain: GCC 16.1+ in C++26 mode +- Supported Windows toolchain: MinGW 16.1+ in C++26 mode +- macOS support: not ready yet +- Required language mode: C++26 +- Required build system: CMake 3.30+ -## Version Information +## What It Provides -The version is **automatically extracted from Git tags** during CMake configuration and cannot be overridden: +- Header-only C-compatible serial API definitions under `include/cpp_core` +- Modern C++26 helper surface for `std::expected`-based error propagation, strong typed config values, and compile-time reflection helpers +- A generated version surface used consistently across all platform bindings +- An installable CMake package target: `cpp_core::cpp_core` -* **With Git tag** (e.g., `v1.2.3`): Version is `1.2.3` (or `1.2.3-dirty` if working tree has uncommitted changes) -* **Without Git tag**: Version is `0.0.0` (or `0.0.0-dirty` if working tree has uncommitted changes) +## Repository Layout -All binding repositories that include this repository will use the same version information, ensuring consistency across platforms. +- `include/cpp_core/serial.h`: aggregated C ABI for serial operations +- `include/cpp_core/status_code.h`: shared status-code model +- `include/cpp_core/interface/get_version.h`: version struct and `getVersion` -```cpp -#include - -constexpr auto v = cpp_core::kVersion; // v.major, v.minor, v.patch -``` +## Quick Start -## Usage in Binding Repositories - -Binding repositories typically include this repository as a dependency: +Consume `cpp-core` as a CMake dependency from another project: ```cmake CPMAddPackage( NAME cpp_core GITHUB_REPOSITORY Serial-IO/cpp-core - GIT_TAG v1.2.3 # Version tag determines the API version + GIT_TAG vX.Y.Z ) -target_link_libraries(my_binding_library PRIVATE cpp_core::cpp_core) +target_link_libraries(my_binding PRIVATE cpp_core::cpp_core) ``` -The binding implementation then uses the API definitions: +Use the exported headers in your implementation: ```cpp #include -#include +#include -// Implement platform-specific functions matching the API -intptr_t serialOpen( +auto serialOpen( void *port, int baudrate, int data_bits, int parity, int stop_bits, ErrorCallbackT error_callback -) { - // Platform-specific implementation (Windows/Linux/macOS) -} +) -> intptr_t; +``` + +Read the version data baked into the checkout: + +```cpp +#include + +cpp_core::Version version{}; +getVersion(&version); ``` +## Building This Repository + +Native build: + +```sh +cmake -S . -B build -G Ninja +cmake --build build +ctest --test-dir build +``` + +The CMake project exports the package target and also builds these relevant targets: + +- `cpp_core::cpp_core`: header-only interface target +- `cpp_core_compile_tests`: compile-time validation target when testing is enabled + +Optional FFI AST export: + +```sh +cmake -S . -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/gcc-toolchain.cmake -DCPP_CORE_ENABLE_AST_EXPORT=ON +cmake --build build --target cpp_core_ast_slim_json +``` + +- The project still configures with GCC; the AST export itself invokes `clang++` separately +- The current FFI generation workflow is validated with `clang++ 22.1.4` (`Fedora 22.1.4-1.fc44`) +- `cpp_core_ast_raw_json` builds only the raw clang AST dump +- Requires `clang++` on `PATH` or `-DCPP_CORE_AST_CLANGXX=/path/to/clang++` +- Requires a Python 3 interpreter for the slim metadata reduction step +- Writes the combined FFI header AST JSON to `build/ast/cpp_core_ffi_ast.json` +- Writes compact, ship-friendly FFI metadata to `build/ast/cpp_core_ffi_api.json` +- Exports the `#include ` surface intended for downstream FFI adapter generation + +## ABI Surface + +The main aggregated interface lives in: + +```cpp +#include +``` + +The ABI is intentionally plain-C friendly: functions either return a status code, return a value-or-negative-status, or return an opaque handle-or-negative-status. + +Example: + +```cpp +MODULE_API auto serialOpen( + void *port, + int baudrate, + int data_bits, + int parity = 0, + int stop_bits = 0, + ErrorCallbackT error_callback = nullptr +) -> intptr_t; +``` + +This model keeps the ABI easy to consume from TypeScript hosts, Rust, Python, or other FFI hosts without requiring C++ runtime coupling. + +For C++ callers, the helper surface includes: + +- `include/cpp_core/result.hpp`: `Result`, `Status`, `forwardUnexpected(...)`, plus the native `std::expected` monadic operations +- `include/cpp_core/scope_guard.hpp`: `onScopeExit(...)`, `onScopeFail(...)`, `onScopeSuccess(...)`, `defer(...)` +- `include/cpp_core/strong_types.hpp`: arithmetic-preserving strong integral wrappers and enum conversion helpers +- `include/cpp_core/serial_config.hpp`: typed config construction with `Result` validation helpers +- `include/cpp_core/reflection.hpp`: GCC 16 / C++26 reflection helpers such as enum/member counts and names, plus public field counts and names + +## Versioning + +Version information is generated from Git during CMake configure and written into `include/cpp_core/version.hpp`. + +- Tagged checkout: uses the tag as the base version +- Additional commits after a tag: appends the commit distance and short hash +- Dirty working tree: appends `-dirty` +- No usable tag: falls back to `0.0.0` + +The version data is exposed through: + +- the `version` namespace in `include/cpp_core/version.hpp` +- the `cpp_core::Version` struct +- the `getVersion(cpp_core::Version *out)` ABI function + +## Relationship to Platform Repositories + +`cpp-core` defines the contract. The platform repositories implement it. + +- `cpp-bindings-linux` provides the Linux shared library implementation +- `cpp-bindings-windows` provides the Windows DLL implementation +- macOS bindings are not part of the supported line yet + +Keeping the contract and version surface here avoids ABI drift between platforms and keeps the shared API aligned with the actual exported implementation. + ## License -`Apache-2.0` - Check [LICENSE](LICENSE) for more details. +Licensed under `Apache-2.0`. See [LICENSE](LICENSE). diff --git a/cmake/export_ast_json.cmake b/cmake/export_ast_json.cmake new file mode 100644 index 0000000..0593d5a --- /dev/null +++ b/cmake/export_ast_json.cmake @@ -0,0 +1,50 @@ +cmake_minimum_required(VERSION 3.30) + +foreach(required_var + CPP_CORE_AST_CLANGXX + CPP_CORE_AST_INCLUDE_DIR + CPP_CORE_AST_OUTPUT + CPP_CORE_AST_SOURCE + CPP_CORE_AST_STANDARD) + if(NOT DEFINED ${required_var} OR "${${required_var}}" STREQUAL "") + message(FATAL_ERROR "Missing required variable: ${required_var}") + endif() +endforeach() + +get_filename_component(_cpp_core_ast_output_dir "${CPP_CORE_AST_OUTPUT}" DIRECTORY) +file(MAKE_DIRECTORY "${_cpp_core_ast_output_dir}") + +execute_process( + COMMAND + "${CMAKE_COMMAND}" -E env + CCACHE_DISABLE=1 + "${CPP_CORE_AST_CLANGXX}" + "-std=c++${CPP_CORE_AST_STANDARD}" + "-I${CPP_CORE_AST_INCLUDE_DIR}" + -fsyntax-only + -Xclang + -ast-dump=json + "${CPP_CORE_AST_SOURCE}" + OUTPUT_FILE "${CPP_CORE_AST_OUTPUT}" + ERROR_VARIABLE _cpp_core_ast_stderr + RESULT_VARIABLE _cpp_core_ast_result +) + +if(NOT _cpp_core_ast_result EQUAL 0) + file(REMOVE "${CPP_CORE_AST_OUTPUT}") + string(STRIP "${_cpp_core_ast_stderr}" _cpp_core_ast_stderr) + message( + FATAL_ERROR + "clang AST export failed with exit code ${_cpp_core_ast_result}.\n${_cpp_core_ast_stderr}" + ) +endif() + +if(NOT EXISTS "${CPP_CORE_AST_OUTPUT}") + message(FATAL_ERROR "clang AST export did not produce ${CPP_CORE_AST_OUTPUT}") +endif() + +file(SIZE "${CPP_CORE_AST_OUTPUT}" _cpp_core_ast_size) +if(_cpp_core_ast_size EQUAL 0) + file(REMOVE "${CPP_CORE_AST_OUTPUT}") + message(FATAL_ERROR "clang AST export produced an empty file: ${CPP_CORE_AST_OUTPUT}") +endif() diff --git a/cmake/mingw-toolchain.cmake b/cmake/mingw-toolchain.cmake new file mode 100644 index 0000000..3582dc0 --- /dev/null +++ b/cmake/mingw-toolchain.cmake @@ -0,0 +1,38 @@ +# MinGW-w64 Toolchain file for cpp-core +# Usage: +# cmake -B build-win -DCMAKE_TOOLCHAIN_FILE=cmake/mingw64-toolchain.cmake .. +# cmake --build build-win + +# Target system +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +# MinGW-w64 compilers +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) + +# Optional, useful for Windows resources +set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) + +# Search behavior for cross compiling +set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +# Compiler-specific flags +set(CMAKE_CXX_FLAGS_INIT "-Wall -Wextra -Wpedantic") +set(CMAKE_C_FLAGS_INIT "-Wall -Wextra -Wpedantic") + +set(CMAKE_CXX_FLAGS_DEBUG_INIT "-g -O0") +set(CMAKE_C_FLAGS_DEBUG_INIT "-g -O0") + +set(CMAKE_CXX_FLAGS_RELEASE_INIT "-O3 -DNDEBUG") +set(CMAKE_C_FLAGS_RELEASE_INIT "-O3 -DNDEBUG") + +# Recommended for FFI-loaded DLLs: +# statically link MinGW runtime parts, but keep Windows system DLLs dynamic. +set(CMAKE_SHARED_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++") +set(CMAKE_EXE_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++") diff --git a/include/cpp_core.h b/include/cpp_core.h index c198495..e25e632 100644 --- a/include/cpp_core.h +++ b/include/cpp_core.h @@ -1,18 +1,22 @@ #pragma once /* - * cpp_core.h - Umbrella header for the cpp_core library + * Umbrella header for the complete public cpp_core surface. * - * Include this single file to gain access to the complete public C/C++ API - * of the cpp_core runtime. Internally it forwards to the individual header - * units located in the cpp_core/ sub-directory so fine-grained includes continue - * to work as before. Prefer including the specific headers you actually need - * in translation units with strict compile-time requirements; however for - * quick prototyping and high-level application code, this umbrella header - * offers the most convenient entry point. + * Prefer narrower includes in translation units with strict compile-time + * requirements, but this header is the convenient entry point when a consumer + * wants the full API and helper layer in one include. */ #include "cpp_core/error_callback.h" +#include "cpp_core/error_handling.hpp" +#include "cpp_core/result.hpp" +#include "cpp_core/reflection.hpp" +#include "cpp_core/scope_guard.hpp" #include "cpp_core/serial.h" +#include "cpp_core/serial_config.hpp" #include "cpp_core/status_code.h" +#include "cpp_core/strong_types.hpp" +#include "cpp_core/unique_resource.hpp" +#include "cpp_core/validation.hpp" #include "cpp_core/version.hpp" diff --git a/include/cpp_core/error_handling.hpp b/include/cpp_core/error_handling.hpp index e3e52c6..998523d 100644 --- a/include/cpp_core/error_handling.hpp +++ b/include/cpp_core/error_handling.hpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace cpp_core { @@ -28,13 +29,13 @@ concept LegacyErrorCallback = std::is_convertible_v -concept StatusConvertible = std::is_arithmetic_v && requires(StatusCodes code) { static_cast(code); }; +concept StatusConvertible = std::is_arithmetic_v && requires(StatusCodeValue code) { static_cast(code); }; // Error invocation // Safely invoke an error callback, does nothing if callback is nullptr. template -constexpr auto invokeError(Callback &&callback, StatusCodes code, std::string_view message) noexcept -> void +constexpr auto invokeError(Callback &&callback, StatusCodeValue code, std::string_view message) noexcept -> void { if constexpr (std::is_null_pointer_v>) { @@ -57,7 +58,7 @@ constexpr auto invokeError(Callback &&callback, StatusCodes code, std::string_vi // Report failure through callback and return the status code cast to Ret. template -constexpr auto failMsg(Callback &&callback, StatusCodes code, std::string_view message) -> Ret +constexpr auto failMsg(Callback &&callback, StatusCodeValue code, std::string_view message) -> Ret { invokeError(std::forward(callback), code, message); return static_cast(code); @@ -65,7 +66,7 @@ constexpr auto failMsg(Callback &&callback, StatusCodes code, std::string_view m // Overload that builds the message by concatenating a prefix and a detail string. template -auto failMsg(Callback &&callback, StatusCodes code, std::string_view prefix, std::string_view detail) -> Ret +auto failMsg(Callback &&callback, StatusCodeValue code, std::string_view prefix, std::string_view detail) -> Ret { std::string full; full.reserve(prefix.size() + 2 + detail.size()); diff --git a/include/cpp_core/interface/serial_clear_buffer_in.h b/include/cpp_core/interface/serial_clear_buffer_in.h index 7a477eb..9870293 100644 --- a/include/cpp_core/interface/serial_clear_buffer_in.h +++ b/include/cpp_core/interface/serial_clear_buffer_in.h @@ -16,7 +16,7 @@ extern "C" * * @param handle Port handle. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialClearBufferIn(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_clear_buffer_out.h b/include/cpp_core/interface/serial_clear_buffer_out.h index d20beb2..eea1966 100644 --- a/include/cpp_core/interface/serial_clear_buffer_out.h +++ b/include/cpp_core/interface/serial_clear_buffer_out.h @@ -16,7 +16,7 @@ extern "C" * * @param handle Port handle. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialClearBufferOut(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_close.h b/include/cpp_core/interface/serial_close.h index 07175d3..0d3a7a5 100644 --- a/include/cpp_core/interface/serial_close.h +++ b/include/cpp_core/interface/serial_close.h @@ -16,7 +16,7 @@ extern "C" * * @param handle Handle obtained from serialOpen(). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialClose(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_drain.h b/include/cpp_core/interface/serial_drain.h index e6baf2b..16f3cf8 100644 --- a/include/cpp_core/interface/serial_drain.h +++ b/include/cpp_core/interface/serial_drain.h @@ -29,7 +29,7 @@ extern "C" * * @param handle Port handle. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialDrain(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_get_baudrate.h b/include/cpp_core/interface/serial_get_baudrate.h index 922bcee..157cf75 100644 --- a/include/cpp_core/interface/serial_get_baudrate.h +++ b/include/cpp_core/interface/serial_get_baudrate.h @@ -17,7 +17,7 @@ extern "C" * * @param handle Port handle obtained from serialOpen(). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Current baud rate in bit/s (>= 300) or a negative error code from ::cpp_core::StatusCodes. + * @return Current baud rate in bit/s (>= 300) or a negative error code from ::cpp_core::StatusCode. */ MODULE_API auto serialGetBaudrate(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_get_cts.h b/include/cpp_core/interface/serial_get_cts.h index 8e5df9f..8b2a695 100644 --- a/include/cpp_core/interface/serial_get_cts.h +++ b/include/cpp_core/interface/serial_get_cts.h @@ -17,7 +17,7 @@ extern "C" * * @param handle Port handle obtained from serialOpen(). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 1 if asserted (HIGH), 0 if de-asserted (LOW), or a negative error code from ::cpp_core::StatusCodes. + * @return 1 if asserted (HIGH), 0 if de-asserted (LOW), or a negative error code from ::cpp_core::StatusCode. */ MODULE_API auto serialGetCts(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_get_data_bits.h b/include/cpp_core/interface/serial_get_data_bits.h index 097faf4..ce537d5 100644 --- a/include/cpp_core/interface/serial_get_data_bits.h +++ b/include/cpp_core/interface/serial_get_data_bits.h @@ -13,7 +13,7 @@ extern "C" * * @param handle Port handle obtained from serialOpen(). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Current data bits (5-8) or a negative error code from ::cpp_core::StatusCodes. + * @return Current data bits (5-8) or a negative error code from ::cpp_core::StatusCode. */ MODULE_API auto serialGetDataBits(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_get_dcd.h b/include/cpp_core/interface/serial_get_dcd.h index 86b23ad..e34021c 100644 --- a/include/cpp_core/interface/serial_get_dcd.h +++ b/include/cpp_core/interface/serial_get_dcd.h @@ -17,7 +17,7 @@ extern "C" * * @param handle Port handle obtained from serialOpen(). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 1 if asserted (HIGH), 0 if de-asserted (LOW), or a negative error code from ::cpp_core::StatusCodes. + * @return 1 if asserted (HIGH), 0 if de-asserted (LOW), or a negative error code from ::cpp_core::StatusCode. */ MODULE_API auto serialGetDcd(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_get_dsr.h b/include/cpp_core/interface/serial_get_dsr.h index ff239f6..8408782 100644 --- a/include/cpp_core/interface/serial_get_dsr.h +++ b/include/cpp_core/interface/serial_get_dsr.h @@ -16,7 +16,7 @@ extern "C" * * @param handle Port handle obtained from serialOpen(). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 1 if asserted (HIGH), 0 if de-asserted (LOW), or a negative error code from ::cpp_core::StatusCodes. + * @return 1 if asserted (HIGH), 0 if de-asserted (LOW), or a negative error code from ::cpp_core::StatusCode. */ MODULE_API auto serialGetDsr(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_get_flow_control.h b/include/cpp_core/interface/serial_get_flow_control.h index 4e10d38..26f25ad 100644 --- a/include/cpp_core/interface/serial_get_flow_control.h +++ b/include/cpp_core/interface/serial_get_flow_control.h @@ -13,7 +13,7 @@ extern "C" * * @param handle Port handle obtained from serialOpen(). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 = none, 1 = RTS/CTS, 2 = XON/XOFF, or a negative error code from ::cpp_core::StatusCodes. + * @return 0 = none, 1 = RTS/CTS, 2 = XON/XOFF, or a negative error code from ::cpp_core::StatusCode. */ MODULE_API auto serialGetFlowControl(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_get_parity.h b/include/cpp_core/interface/serial_get_parity.h index ad9c82f..cf3106e 100644 --- a/include/cpp_core/interface/serial_get_parity.h +++ b/include/cpp_core/interface/serial_get_parity.h @@ -13,7 +13,7 @@ extern "C" * * @param handle Port handle obtained from serialOpen(). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 = none, 1 = even, 2 = odd, or a negative error code from ::cpp_core::StatusCodes. + * @return 0 = none, 1 = even, 2 = odd, or a negative error code from ::cpp_core::StatusCode. */ MODULE_API auto serialGetParity(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_get_ri.h b/include/cpp_core/interface/serial_get_ri.h index 13f1f9a..6cffea1 100644 --- a/include/cpp_core/interface/serial_get_ri.h +++ b/include/cpp_core/interface/serial_get_ri.h @@ -16,7 +16,7 @@ extern "C" * * @param handle Port handle obtained from serialOpen(). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 1 if asserted (HIGH), 0 if de-asserted (LOW), or a negative error code from ::cpp_core::StatusCodes. + * @return 1 if asserted (HIGH), 0 if de-asserted (LOW), or a negative error code from ::cpp_core::StatusCode. */ MODULE_API auto serialGetRi(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_get_stop_bits.h b/include/cpp_core/interface/serial_get_stop_bits.h index df40b61..90f39d8 100644 --- a/include/cpp_core/interface/serial_get_stop_bits.h +++ b/include/cpp_core/interface/serial_get_stop_bits.h @@ -13,7 +13,7 @@ extern "C" * * @param handle Port handle obtained from serialOpen(). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 = 1 stop bit, 2 = 2 stop bits, or a negative error code from ::cpp_core::StatusCodes. + * @return 0 = 1 stop bit, 2 = 2 stop bits, or a negative error code from ::cpp_core::StatusCode. */ MODULE_API auto serialGetStopBits(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_in_bytes_total.h b/include/cpp_core/interface/serial_in_bytes_total.h index 87a6776..01511bf 100644 --- a/include/cpp_core/interface/serial_in_bytes_total.h +++ b/include/cpp_core/interface/serial_in_bytes_total.h @@ -13,7 +13,7 @@ extern "C" * * @param handle Port handle. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Total number of bytes read or a negative error code from ::cpp_core::StatusCodes on error. + * @return Total number of bytes read or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialInBytesTotal(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int64_t; diff --git a/include/cpp_core/interface/serial_in_bytes_waiting.h b/include/cpp_core/interface/serial_in_bytes_waiting.h index ea5c0b9..0db50e1 100644 --- a/include/cpp_core/interface/serial_in_bytes_waiting.h +++ b/include/cpp_core/interface/serial_in_bytes_waiting.h @@ -24,7 +24,7 @@ extern "C" * * @param handle Port handle. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Bytes available for instant reading or a negative error code from ::cpp_core::StatusCodes on error. + * @return Bytes available for instant reading or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialInBytesWaiting(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_list_ports.h b/include/cpp_core/interface/serial_list_ports.h index 713f1eb..66b01e0 100644 --- a/include/cpp_core/interface/serial_list_ports.h +++ b/include/cpp_core/interface/serial_list_ports.h @@ -15,7 +15,7 @@ extern "C" * * @param callback_fn Callback receiving port information. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Number of ports found or a negative error code from ::cpp_core::StatusCodes on error. + * @return Number of ports found or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialListPorts(void (*callback_fn)(const char *port, const char *path, const char *manufacturer, const char *serial_number, const char *pnp_id, diff --git a/include/cpp_core/interface/serial_monitor_ports.h b/include/cpp_core/interface/serial_monitor_ports.h new file mode 100644 index 0000000..01e3b8c --- /dev/null +++ b/include/cpp_core/interface/serial_monitor_ports.h @@ -0,0 +1,26 @@ +#pragma once +#include "../error_callback.h" +#include "../module_api.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * @brief Start or stop port attach/detach notifications. + * + * Passing a non-null callback starts monitoring and invokes it with `event = 1` + * for attach and `event = 0` for detach notifications. Passing `nullptr` + * stops a previously running monitor. + * + * @param callback_fn Notification callback or `nullptr` to stop monitoring. + * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. + */ + MODULE_API auto serialMonitorPorts(void (*callback_fn)(int event, const char *port), + ErrorCallbackT error_callback = nullptr) -> int; + +#ifdef __cplusplus +} +#endif diff --git a/include/cpp_core/interface/serial_open.h b/include/cpp_core/interface/serial_open.h index cf61810..46ec7ec 100644 --- a/include/cpp_core/interface/serial_open.h +++ b/include/cpp_core/interface/serial_open.h @@ -22,7 +22,7 @@ extern "C" * @param parity 0 = none, 1 = even, 2 = odd. * @param stop_bits 0 = 1 stop bit, 2 = 2 stop bits. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return A positive opaque handle on success or a negative value from ::cpp_core::StatusCodes on failure. + * @return A positive opaque handle on success or a negative value from ::cpp_core::StatusCode on failure. */ MODULE_API auto serialOpen(void *port, int baudrate, int data_bits, int parity = 0, int stop_bits = 0, ErrorCallbackT error_callback = nullptr) -> intptr_t; diff --git a/include/cpp_core/interface/serial_out_bytes_total.h b/include/cpp_core/interface/serial_out_bytes_total.h index 9e5cd4d..d0c692f 100644 --- a/include/cpp_core/interface/serial_out_bytes_total.h +++ b/include/cpp_core/interface/serial_out_bytes_total.h @@ -13,7 +13,7 @@ extern "C" * * @param handle Port handle. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Total number of bytes written or a negative error code from ::cpp_core::StatusCodes on error. + * @return Total number of bytes written or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialOutBytesTotal(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int64_t; diff --git a/include/cpp_core/interface/serial_out_bytes_waiting.h b/include/cpp_core/interface/serial_out_bytes_waiting.h index f2f5db3..713946f 100644 --- a/include/cpp_core/interface/serial_out_bytes_waiting.h +++ b/include/cpp_core/interface/serial_out_bytes_waiting.h @@ -22,7 +22,7 @@ extern "C" * * @param handle Port handle. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Bytes still waiting in the TX FIFO or a negative error code from ::cpp_core::StatusCodes on error. + * @return Bytes still waiting in the TX FIFO or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialOutBytesWaiting(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_read.h b/include/cpp_core/interface/serial_read.h index 8ae136d..4403316 100644 --- a/include/cpp_core/interface/serial_read.h +++ b/include/cpp_core/interface/serial_read.h @@ -23,7 +23,7 @@ extern "C" * @param multiplier Factor applied to @p timeout_ms for every byte after the first. 0 -> return immediately after * the first byte. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Bytes read (0 on timeout) or a negative error code from ::cpp_core::StatusCodes on error. + * @return Bytes read (0 on timeout) or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_read_line.h b/include/cpp_core/interface/serial_read_line.h index b4a6db0..75a122c 100644 --- a/include/cpp_core/interface/serial_read_line.h +++ b/include/cpp_core/interface/serial_read_line.h @@ -21,7 +21,7 @@ extern "C" * `timeout_ms * multiplier`). * @param multiplier Factor applied to the timeout for subsequent bytes. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Bytes read (0 on timeout) or a negative error code from ::cpp_core::StatusCodes on error. + * @return Bytes read (0 on timeout) or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialReadLine(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_read_until.h b/include/cpp_core/interface/serial_read_until.h index 0e27217..5876b2c 100644 --- a/include/cpp_core/interface/serial_read_until.h +++ b/include/cpp_core/interface/serial_read_until.h @@ -23,7 +23,7 @@ extern "C" * @param multiplier Factor applied to the timeout for every additional byte. * @param until_char Pointer to the terminator character (must not be `nullptr`). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Bytes read (including the terminator), 0 on timeout or a negative error code from ::cpp_core::StatusCodes + * @return Bytes read (including the terminator), 0 on timeout or a negative error code from ::cpp_core::StatusCode * on error. */ MODULE_API auto serialReadUntil(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, diff --git a/include/cpp_core/interface/serial_read_until_sequence.h b/include/cpp_core/interface/serial_read_until_sequence.h index c56ebbd..f0ee7d7 100644 --- a/include/cpp_core/interface/serial_read_until_sequence.h +++ b/include/cpp_core/interface/serial_read_until_sequence.h @@ -22,7 +22,7 @@ extern "C" * @param multiplier Factor applied to the timeout for subsequent bytes. * @param sequence Pointer to the terminating byte sequence (must not be `nullptr`). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Bytes read (including the terminator) or a negative error code from ::cpp_core::StatusCodes on error. + * @return Bytes read (including the terminator) or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialReadUntilSequence(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, void *sequence, ErrorCallbackT error_callback = nullptr) diff --git a/include/cpp_core/interface/serial_send_break.h b/include/cpp_core/interface/serial_send_break.h index 046dd3d..d389f14 100644 --- a/include/cpp_core/interface/serial_send_break.h +++ b/include/cpp_core/interface/serial_send_break.h @@ -24,7 +24,7 @@ extern "C" * @param handle Port handle obtained from serialOpen(). * @param duration_ms Break duration in milliseconds (> 0). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialSendBreak(int64_t handle, int duration_ms, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_set_baudrate.h b/include/cpp_core/interface/serial_set_baudrate.h index 69f6c4c..fbd615b 100644 --- a/include/cpp_core/interface/serial_set_baudrate.h +++ b/include/cpp_core/interface/serial_set_baudrate.h @@ -21,7 +21,7 @@ extern "C" * @param handle Port handle obtained from serialOpen(). * @param baudrate New baud rate in bit/s (>= 300). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialSetBaudrate(int64_t handle, int baudrate, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_set_data_bits.h b/include/cpp_core/interface/serial_set_data_bits.h index d1550ce..5887397 100644 --- a/include/cpp_core/interface/serial_set_data_bits.h +++ b/include/cpp_core/interface/serial_set_data_bits.h @@ -16,7 +16,7 @@ extern "C" * @param handle Port handle obtained from serialOpen(). * @param data_bits Number of data bits (5-8). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialSetDataBits(int64_t handle, int data_bits, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_set_dtr.h b/include/cpp_core/interface/serial_set_dtr.h index 7a84021..97c4c5e 100644 --- a/include/cpp_core/interface/serial_set_dtr.h +++ b/include/cpp_core/interface/serial_set_dtr.h @@ -19,7 +19,7 @@ extern "C" * @param handle Port handle obtained from serialOpen(). * @param state Non-zero to assert (HIGH), zero to de-assert (LOW). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialSetDtr(int64_t handle, int state, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_set_flow_control.h b/include/cpp_core/interface/serial_set_flow_control.h index 39435c2..d5bf779 100644 --- a/include/cpp_core/interface/serial_set_flow_control.h +++ b/include/cpp_core/interface/serial_set_flow_control.h @@ -29,7 +29,7 @@ extern "C" * @param handle Port handle obtained from serialOpen(). * @param mode Flow-control mode: 0 = none, 1 = RTS/CTS, 2 = XON/XOFF. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialSetFlowControl(int64_t handle, int mode, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_set_parity.h b/include/cpp_core/interface/serial_set_parity.h index ef3edb9..d67193f 100644 --- a/include/cpp_core/interface/serial_set_parity.h +++ b/include/cpp_core/interface/serial_set_parity.h @@ -16,7 +16,7 @@ extern "C" * @param handle Port handle obtained from serialOpen(). * @param parity 0 = none, 1 = even, 2 = odd. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialSetParity(int64_t handle, int parity, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_set_rts.h b/include/cpp_core/interface/serial_set_rts.h index cda45c9..e6f2bf9 100644 --- a/include/cpp_core/interface/serial_set_rts.h +++ b/include/cpp_core/interface/serial_set_rts.h @@ -19,7 +19,7 @@ extern "C" * @param handle Port handle obtained from serialOpen(). * @param state Non-zero to assert (HIGH), zero to de-assert (LOW). * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialSetRts(int64_t handle, int state, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_set_stop_bits.h b/include/cpp_core/interface/serial_set_stop_bits.h index 0dbf2ce..fe9fe98 100644 --- a/include/cpp_core/interface/serial_set_stop_bits.h +++ b/include/cpp_core/interface/serial_set_stop_bits.h @@ -16,7 +16,7 @@ extern "C" * @param handle Port handle obtained from serialOpen(). * @param stop_bits 0 = 1 stop bit, 2 = 2 stop bits. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialSetStopBits(int64_t handle, int stop_bits, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_write.h b/include/cpp_core/interface/serial_write.h index 375b830..be6734f 100644 --- a/include/cpp_core/interface/serial_write.h +++ b/include/cpp_core/interface/serial_write.h @@ -21,7 +21,7 @@ extern "C" * `timeout_ms * multiplier`). * @param multiplier Factor applied to the timeout for subsequent bytes. * @param error_callback [optional] Callback to invoke on error. Defined in error_callback.h. Default is `nullptr`. - * @return Bytes written (may be 0 on timeout) or a negative error code from ::cpp_core::StatusCodes on error. + * @return Bytes written (may be 0 on timeout) or a negative error code from ::cpp_core::StatusCode on error. */ MODULE_API auto serialWrite(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int multiplier, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/reflection.hpp b/include/cpp_core/reflection.hpp new file mode 100644 index 0000000..767746e --- /dev/null +++ b/include/cpp_core/reflection.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace cpp_core::reflection +{ + +// Small std::meta helpers for downstream bindings and compile-time validation. +template +concept ReflectableRecord = std::is_class_v || std::is_union_v; + +namespace detail +{ + +template consteval auto publicAccessContext() -> std::meta::access_context +{ + return std::meta::access_context::unprivileged().via(^^T); +} + +} // namespace detail + +template [[nodiscard]] consteval auto hasPubliclyReflectableFields() -> bool +{ + constexpr auto access_context = detail::publicAccessContext(); + return !std::meta::has_inaccessible_nonstatic_data_members(^^T, access_context); +} + +template [[nodiscard]] consteval auto publicFieldCount() -> std::size_t +{ + constexpr auto access_context = detail::publicAccessContext(); + static_assert(!std::meta::has_inaccessible_nonstatic_data_members(^^T, access_context), + "cpp_core::reflection::publicFieldCount requires publicly reflectable non-static data members"); + return std::meta::nonstatic_data_members_of(^^T, access_context).size(); +} + +template +requires std::is_enum_v +[[nodiscard]] consteval auto enumeratorCount() -> std::size_t +{ + return std::meta::enumerators_of(^^E).size(); +} + +template +requires std::is_enum_v +[[nodiscard]] consteval auto enumeratorName() -> std::string_view +{ + static_assert(Index < enumeratorCount(), "Enumerator index out of range"); + return std::meta::identifier_of(std::meta::enumerators_of(^^E)[Index]); +} + +template +[[nodiscard]] consteval auto publicFieldName() -> std::string_view +{ + constexpr auto access_context = detail::publicAccessContext(); + static_assert(!std::meta::has_inaccessible_nonstatic_data_members(^^T, access_context), + "cpp_core::reflection::publicFieldName requires publicly reflectable non-static data members"); + static_assert(Index < publicFieldCount(), "Field index out of range"); + return std::meta::identifier_of(std::meta::nonstatic_data_members_of(^^T, access_context)[Index]); +} + +template inline constexpr std::size_t public_field_count_v = publicFieldCount(); +template +requires std::is_enum_v +inline constexpr std::size_t enumerator_count_v = enumeratorCount(); +template +requires std::is_enum_v +inline constexpr std::string_view enumerator_name_v = enumeratorName(); +template +inline constexpr std::string_view public_field_name_v = publicFieldName(); + +} // namespace cpp_core::reflection diff --git a/include/cpp_core/reflection.test.cpp b/include/cpp_core/reflection.test.cpp new file mode 100644 index 0000000..1967f84 --- /dev/null +++ b/include/cpp_core/reflection.test.cpp @@ -0,0 +1,28 @@ +#include "cpp_core/reflection.hpp" +#include "cpp_core/result.hpp" +#include "cpp_core/serial_config.hpp" +#include "cpp_core/strong_types.hpp" + +namespace cpp_core::tests::reflection +{ + +struct PrivateMemberAggregate +{ + private: + int value; +}; + +static_assert(cpp_core::reflection::enumeratorCount() == 3); +static_assert(cpp_core::reflection::enumeratorCount() == 2); +static_assert(cpp_core::reflection::enumeratorCount() == 3); +static_assert(cpp_core::reflection::enumeratorName() == "kNone"); +static_assert(cpp_core::reflection::enumeratorName() == "kEven"); +static_assert(cpp_core::reflection::enumerator_name_v == "kXonXoff"); +static_assert(cpp_core::reflection::hasPubliclyReflectableFields()); +static_assert(cpp_core::reflection::publicFieldCount() == 4); +static_assert(cpp_core::reflection::publicFieldName() == "parity"); +static_assert(cpp_core::reflection::public_field_name_v == "message"); +static_assert(cpp_core::reflection::public_field_count_v == 2); +static_assert(!cpp_core::reflection::hasPubliclyReflectableFields()); + +} // namespace cpp_core::tests::reflection diff --git a/include/cpp_core/result.hpp b/include/cpp_core/result.hpp index d00012f..eba0a72 100644 --- a/include/cpp_core/result.hpp +++ b/include/cpp_core/result.hpp @@ -4,7 +4,6 @@ #include #include -#include #include #include #include @@ -15,14 +14,14 @@ namespace cpp_core // Enriched error type carrying a StatusCode and an optional message. struct Error { - StatusCodes code; + StatusCodeValue code; std::string message; - constexpr explicit Error(StatusCodes code_in) noexcept : code(code_in) + constexpr explicit Error(StatusCodeValue code_in) noexcept : code(code_in) { } - Error(StatusCodes code_in, std::string msg) : code(code_in), message(std::move(msg)) + constexpr Error(StatusCodeValue code_in, std::string msg) : code(code_in), message(std::move(msg)) { } @@ -36,7 +35,7 @@ struct Error return code == other.code; } - [[nodiscard]] constexpr auto operator==(StatusCodes code_in) const noexcept -> bool + [[nodiscard]] constexpr auto operator==(StatusCodeValue code_in) const noexcept -> bool { return code == code_in; } @@ -66,12 +65,12 @@ template [[nodiscard]] constexpr auto ok(T &&value) -> Result [[nodiscard]] constexpr auto fail(StatusCodes code) -> Result +template [[nodiscard]] constexpr auto fail(StatusCodeValue code) -> Result { return std::unexpected(Error{code}); } -template [[nodiscard]] auto fail(StatusCodes code, std::string message) -> Result +template [[nodiscard]] constexpr auto fail(StatusCodeValue code, std::string message) -> Result { return std::unexpected(Error{code, std::move(message)}); } @@ -86,29 +85,32 @@ concept IsResult = requires { } && std::same_as; // clang-format on +namespace detail +{ + +template [[nodiscard]] constexpr auto propagateUnexpected(R &&result) -> std::unexpected +{ + return std::unexpected(std::forward(result).error()); +} + +} // namespace detail + /** - * TRY macro - * Usage: auto value = CPP_CORE_TRY(someResultReturningCall()); - * Propagates the error automatically if the result holds an error. + * Convert a failed Result/Status into the matching std::unexpected payload for + * an immediate `return`. + * + * Usage: + * auto opened = openPort(); + * if (!opened) + * { + * return forwardUnexpected(std::move(opened)); + * } + * auto handle = std::move(opened).value(); */ - -// NOLINTBEGIN(cppcoreguidelines-macro-usage) -#define CPP_CORE_TRY(expr) \ - ({ \ - auto _cpp_core_result_ = (expr); \ - if (!_cpp_core_result_.has_value()) \ - return std::unexpected(std::move(_cpp_core_result_).error()); \ - std::move(_cpp_core_result_).value(); \ - }) - -#define CPP_CORE_TRY_VOID(expr) \ - do \ - { \ - auto _cpp_core_result_ = (expr); \ - if (!_cpp_core_result_.has_value()) \ - return std::unexpected(std::move(_cpp_core_result_).error()); \ - } while (false) -// NOLINTEND(cppcoreguidelines-macro-usage) +template [[nodiscard]] constexpr auto forwardUnexpected(R &&result) -> std::unexpected +{ + return detail::propagateUnexpected(std::forward(result)); +} /** * Result -> C return code bridge diff --git a/include/cpp_core/result.test.cpp b/include/cpp_core/result.test.cpp new file mode 100644 index 0000000..ed17539 --- /dev/null +++ b/include/cpp_core/result.test.cpp @@ -0,0 +1,54 @@ +#include "cpp_core/result.hpp" + +namespace cpp_core::tests::result +{ + +constexpr auto leafValue() -> Result +{ + return ok(41); +} + +constexpr auto leafFailure() -> Result +{ + return fail(StatusCode::Connection::kInvalidHandleError); +} + +constexpr auto addOne() -> Result +{ + return leafValue().transform([](int value) { return value + 1; }); +} + +constexpr auto propagateLeafFailure() -> Result +{ + auto value = leafFailure(); + if (!value) + { + return forwardUnexpected(std::move(value)); + } + return ok(std::move(value).value() + 1); +} + +constexpr auto okStatus() -> Status +{ + return ok(); +} + +constexpr auto failedStatus() -> Status +{ + return fail(StatusCode::Io::kReadError); +} + +constexpr auto propagateStatusFailure() -> Status +{ + return failedStatus().and_then([]() -> Status { return ok(); }); +} + +static_assert(addOne().has_value()); +static_assert(addOne().value() == 42); +static_assert(okStatus().has_value()); +static_assert(!propagateLeafFailure().has_value()); +static_assert(propagateLeafFailure().error() == StatusCode::Connection::kInvalidHandleError); +static_assert(!propagateStatusFailure().has_value()); +static_assert(propagateStatusFailure().error() == StatusCode::Io::kReadError); + +} // namespace cpp_core::tests::result diff --git a/include/cpp_core/scope_guard.hpp b/include/cpp_core/scope_guard.hpp index dbcc434..449cbf1 100644 --- a/include/cpp_core/scope_guard.hpp +++ b/include/cpp_core/scope_guard.hpp @@ -135,24 +135,9 @@ template [[nodiscard]] constexpr auto onScopeSuccess(Fn &&fu return ScopeSuccess>(std::forward(func)); } -// DEFER macro (Go-style) -// Usage: DEFER { cleanup(); }; - -// NOLINTBEGIN(cppcoreguidelines-macro-usage) -namespace detail -{ -struct DeferHelper +template [[nodiscard]] constexpr auto defer(Fn &&func) { - template constexpr auto operator<<(Fn &&func) const - { - return ScopeGuard>(std::forward(func)); - } -}; -} // namespace detail - -#define CPP_CORE_DEFER_CONCAT_(a, b) a##b -#define CPP_CORE_DEFER_CONCAT(a, b) CPP_CORE_DEFER_CONCAT_(a, b) -#define DEFER auto CPP_CORE_DEFER_CONCAT(_cpp_core_defer_, __COUNTER__) = ::cpp_core::detail::DeferHelper{} << [&]() -// NOLINTEND(cppcoreguidelines-macro-usage) + return onScopeExit(std::forward(func)); +} } // namespace cpp_core diff --git a/include/cpp_core/serial.h b/include/cpp_core/serial.h index 06e9c58..6218a2e 100644 --- a/include/cpp_core/serial.h +++ b/include/cpp_core/serial.h @@ -1,8 +1,5 @@ #pragma once -#include "module_api.h" -#include "version.hpp" - // Aggregated interface headers #include "interface/get_version.h" #include "interface/serial_abort_read.h" @@ -14,6 +11,7 @@ #include "interface/serial_in_bytes_total.h" #include "interface/serial_in_bytes_waiting.h" #include "interface/serial_list_ports.h" +#include "interface/serial_monitor_ports.h" #include "interface/serial_open.h" #include "interface/serial_out_bytes_total.h" #include "interface/serial_out_bytes_waiting.h" diff --git a/include/cpp_core/serial_config.hpp b/include/cpp_core/serial_config.hpp index 4ebe0b6..b695c9c 100644 --- a/include/cpp_core/serial_config.hpp +++ b/include/cpp_core/serial_config.hpp @@ -1,8 +1,11 @@ #pragma once +#include "result.hpp" #include "strong_types.hpp" +#include #include +#include #include namespace cpp_core @@ -13,16 +16,26 @@ namespace cpp_core namespace detail { -consteval auto validateBaudrate(int baud) -> bool +constexpr auto validateBaudrate(int baud) -> bool { return baud >= 300; } -consteval auto validateDataBits(int bits) -> bool +constexpr auto validateDataBits(int bits) -> bool { return bits >= 5 && bits <= 8; } +constexpr auto validateParity(Parity parity) -> bool +{ + return parity == Parity::kNone || parity == Parity::kEven || parity == Parity::kOdd; +} + +constexpr auto validateStopBits(StopBits stop_bits) -> bool +{ + return stop_bits == StopBits::kOne || stop_bits == StopBits::kTwo; +} + } // namespace detail /** @@ -51,30 +64,73 @@ struct SerialConfig }; } + [[nodiscard]] static constexpr auto tryMake(Baudrate baud, DataBits data_bits, Parity parity = Parity::kNone, + StopBits stop_bits = StopBits::kOne) -> Result + { + return tryMake(baud.get(), data_bits.get(), parity, stop_bits); + } + [[nodiscard]] static constexpr auto tryMake(int baud, int data_bits_val, Parity parity = Parity::kNone, - StopBits stop_bits = StopBits::kOne) -> SerialConfig + StopBits stop_bits = StopBits::kOne) -> Result { - return SerialConfig{ + if (!detail::validateBaudrate(baud)) + { + return fail(StatusCode::Configuration::kSetBaudrateError); + } + if (!detail::validateDataBits(data_bits_val)) + { + return fail(StatusCode::Configuration::kSetDataBitsError); + } + if (!detail::validateParity(parity)) + { + return fail(StatusCode::Configuration::kSetParityError); + } + if (!detail::validateStopBits(stop_bits)) + { + return fail(StatusCode::Configuration::kSetStopBitsError); + } + return ok(SerialConfig{ .baudrate = baud, .data_bits = data_bits_val, .parity = parity, .stop_bits = stop_bits, - }; + }); } [[nodiscard]] constexpr auto isValid() const noexcept -> bool { - return baudrate >= 300 && data_bits >= 5 && data_bits <= 8; + return detail::validateBaudrate(baudrate) && detail::validateDataBits(data_bits) + && detail::validateParity(parity) && detail::validateStopBits(stop_bits); + } + + [[nodiscard]] constexpr auto baudrateValue() const noexcept -> Baudrate + { + return Baudrate{baudrate}; + } + + [[nodiscard]] constexpr auto dataBitsValue() const noexcept -> DataBits + { + return DataBits{data_bits}; } [[nodiscard]] constexpr auto parityInt() const noexcept -> int { - return static_cast(parity); + return toInt(parity); } [[nodiscard]] constexpr auto stopBitsInt() const noexcept -> int { - return static_cast(stop_bits); + return toInt(stop_bits); + } + + [[nodiscard]] constexpr auto withBaudrate(Baudrate baud) const -> Result + { + return tryMake(baud, dataBitsValue(), parity, stop_bits); + } + + [[nodiscard]] constexpr auto withDataBits(DataBits bits) const -> Result + { + return tryMake(baudrateValue(), bits, parity, stop_bits); } [[nodiscard]] constexpr auto operator<=>(const SerialConfig &) const noexcept = default; diff --git a/include/cpp_core/serial_config.test.cpp b/include/cpp_core/serial_config.test.cpp new file mode 100644 index 0000000..fe86b4d --- /dev/null +++ b/include/cpp_core/serial_config.test.cpp @@ -0,0 +1,45 @@ +#include "cpp_core/reflection.hpp" +#include "cpp_core/serial_config.hpp" + +namespace cpp_core::tests::serial_config +{ + +constexpr auto kCompileTimeConfig = SerialConfig::make<115'200, 8, Parity::kEven, StopBits::kTwo>(); +static_assert(kCompileTimeConfig.isValid()); +static_assert(kCompileTimeConfig.baudrateValue() == Baudrate{115'200}); +static_assert(kCompileTimeConfig.dataBitsValue() == DataBits{8}); +static_assert(kCompileTimeConfig.parityInt() == 1); +static_assert(kCompileTimeConfig.stopBitsInt() == 2); + +constexpr auto kRuntimeLikeConfig = SerialConfig::tryMake(Baudrate{57'600}, DataBits{7}, Parity::kOdd, StopBits::kOne); +static_assert(kRuntimeLikeConfig.has_value()); +static_assert(kRuntimeLikeConfig->baudrateValue() == Baudrate{57'600}); +static_assert(kRuntimeLikeConfig->dataBitsValue() == DataBits{7}); + +consteval auto rejectsBadBaudrate() -> bool +{ + return !SerialConfig::tryMake(Baudrate{299}, DataBits{8}).has_value(); +} + +consteval auto rejectsBadDataBits() -> bool +{ + return !SerialConfig::tryMake(9'600, 4).has_value(); +} + +consteval auto rejectsBadParity() -> bool +{ + return !SerialConfig::tryMake(9'600, 8, static_cast(77)).has_value(); +} + +static_assert(rejectsBadBaudrate()); +static_assert(rejectsBadDataBits()); +static_assert(rejectsBadParity()); + +constexpr auto kRetunedConfig = kCompileTimeConfig.withBaudrate(Baudrate{230'400}); +static_assert(kRetunedConfig.has_value()); +static_assert(kRetunedConfig->baudrateValue() == Baudrate{230'400}); + +static_assert(cpp_core::reflection::publicFieldName() == "baudrate"); +static_assert(cpp_core::reflection::publicFieldName() == "data_bits"); + +} // namespace cpp_core::tests::serial_config diff --git a/include/cpp_core/status_code.h b/include/cpp_core/status_code.h index cade93c..9b447bb 100644 --- a/include/cpp_core/status_code.h +++ b/include/cpp_core/status_code.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include namespace cpp_core::status_codes @@ -23,18 +24,26 @@ template struct Code { return kValue; } + [[nodiscard]] constexpr auto value() const noexcept -> ValueType { return kValue; } + [[nodiscard]] constexpr auto name() const noexcept -> std::string_view { return kName; } + [[nodiscard]] constexpr auto category() const noexcept -> std::string_view { return Category::kCategoryName; } + + [[nodiscard]] static constexpr auto categoryCode() noexcept -> ValueType + { + return Category::kCategoryCode; + } }; template struct CategoryBase @@ -116,14 +125,24 @@ struct StatusCode static constexpr Code<5> kSetStateError{"SetStateError"}; }; + struct Monitor : detail::CategoryBase + { + static constexpr ValueType kCategoryCode = 5; + static constexpr std::string_view kCategoryName{"Monitor"}; + + static constexpr Code<0> kMonitorError{"MonitorError"}; + }; + [[nodiscard]] static constexpr auto isError(ValueType code) noexcept -> bool { return code < 0; } + [[nodiscard]] static constexpr auto isSuccess(ValueType code) noexcept -> bool { return code >= 0; } + template [[nodiscard]] static constexpr auto belongsTo(ValueType code) noexcept -> bool { return code < 0 && (-code) / detail::kCategoryMultiplier == Category::kCategoryCode; @@ -134,6 +153,8 @@ struct StatusCode namespace cpp_core { -using StatusCodes = ::cpp_core::status_codes::detail::ValueType; + +using StatusCodeValue = ::cpp_core::status_codes::detail::ValueType; using ::cpp_core::status_codes::StatusCode; + } // namespace cpp_core diff --git a/tests/status_code_compile_test.cpp b/include/cpp_core/status_code.test.cpp similarity index 80% rename from tests/status_code_compile_test.cpp rename to include/cpp_core/status_code.test.cpp index d19906a..5333a50 100644 --- a/tests/status_code_compile_test.cpp +++ b/include/cpp_core/status_code.test.cpp @@ -21,6 +21,7 @@ static_assert(cpp_core::StatusCode::isSuccess(cpp_core::StatusCode::kSuccess)); static_assert(!cpp_core::StatusCode::isError(cpp_core::StatusCode::kSuccess)); static_assert(cpp_core::StatusCode::isError(cpp_core::StatusCode::Configuration::kSetBaudrateError)); +static_assert(cpp_core::StatusCode::Configuration::kSetBaudrateError.category() == "Configuration"); static_assert(cpp_core::StatusCode::Configuration::kSetBaudrateError == -100); static_assert(cpp_core::StatusCode::Configuration::kSetDataBitsError == -101); static_assert(cpp_core::StatusCode::Configuration::kSetParityError == -102); @@ -28,10 +29,12 @@ static_assert(cpp_core::StatusCode::Configuration::kSetStopBitsError == -103); static_assert(cpp_core::StatusCode::Configuration::kSetFlowControlError == -104); static_assert(cpp_core::StatusCode::Configuration::kSetTimeoutError == -105); +static_assert(cpp_core::StatusCode::Connection::kNotFoundError.category() == "Connection"); static_assert(cpp_core::StatusCode::Connection::kNotFoundError == -200); static_assert(cpp_core::StatusCode::Connection::kInvalidHandleError == -201); static_assert(cpp_core::StatusCode::Connection::kCloseHandleError == -202); +static_assert(cpp_core::StatusCode::Io::kReadError.category() == "Io"); static_assert(cpp_core::StatusCode::Io::kReadError == -300); static_assert(cpp_core::StatusCode::Io::kWriteError == -301); static_assert(cpp_core::StatusCode::Io::kAbortReadError == -302); @@ -40,6 +43,7 @@ static_assert(cpp_core::StatusCode::Io::kBufferError == -304); static_assert(cpp_core::StatusCode::Io::kClearBufferInError == -305); static_assert(cpp_core::StatusCode::Io::kClearBufferOutError == -306); +static_assert(cpp_core::StatusCode::Control::kSetDtrError.category() == "Control"); static_assert(cpp_core::StatusCode::Control::kSetDtrError == -400); static_assert(cpp_core::StatusCode::Control::kSetRtsError == -401); static_assert(cpp_core::StatusCode::Control::kGetModemStatusError == -402); @@ -47,25 +51,23 @@ static_assert(cpp_core::StatusCode::Control::kSendBreakError == -403); static_assert(cpp_core::StatusCode::Control::kGetStateError == -404); static_assert(cpp_core::StatusCode::Control::kSetStateError == -405); -// Formula: result == -(kCategoryCode * 100 + LocalCode) +static_assert(cpp_core::StatusCode::Monitor::kMonitorError.category() == "Monitor"); +static_assert(cpp_core::StatusCode::Monitor::kMonitorError == -500); + +static_assert(cpp_core::StatusCode::belongsTo( + cpp_core::StatusCode::Configuration::kSetBaudrateError)); +static_assert(!cpp_core::StatusCode::belongsTo( + cpp_core::StatusCode::Configuration::kSetBaudrateError)); + static_assert(FakeCategory<1>::call<0>() == -100); static_assert(FakeCategory<1>::call<42>() == -142); static_assert(FakeCategory<3>::call<7>() == -307); - -// Edge: kCategoryCode == 0 -> call<0>() produces 0 (not negative) static_assert(FakeCategory<0>::call<0>() == 0); static_assert(FakeCategory<0>::call<1>() == -1); - -// Edge: LocalCode == 99 (max before overflow guard) static_assert(FakeCategory<2>::call<99>() == -299); - -// Consecutive codes differ by exactly -1 static_assert(FakeCategory<1>::call<1>() - FakeCategory<1>::call<0>() == -1); - -// Adjacent category ranges don't overlap (last of cat N > first of cat N+1) static_assert(FakeCategory<1>::call<99>() > FakeCategory<2>::call<0>()); -// Overflow: largest safe category still produces correct results inline constexpr ValueType kMaxSafeCat = (std::numeric_limits::max() - kCategoryMultiplier + 1) / kCategoryMultiplier; static_assert(kMaxSafeCat > 1'000'000); diff --git a/include/cpp_core/strong_types.hpp b/include/cpp_core/strong_types.hpp index 330f571..f2f2b81 100644 --- a/include/cpp_core/strong_types.hpp +++ b/include/cpp_core/strong_types.hpp @@ -2,6 +2,7 @@ #include #include +#include namespace cpp_core { @@ -12,12 +13,43 @@ template struct StrongInt using TagType = Tag; using ValueType = Underlying; - Underlying value; + Underlying value{}; + + constexpr StrongInt() noexcept = default; constexpr explicit StrongInt(Underlying val) noexcept : value(val) { } + [[nodiscard]] constexpr auto get() const noexcept -> Underlying + { + return value; + } + + constexpr auto operator+=(StrongInt other) noexcept -> StrongInt & + { + value += other.value; + return *this; + } + + constexpr auto operator-=(StrongInt other) noexcept -> StrongInt & + { + value -= other.value; + return *this; + } + + [[nodiscard]] friend constexpr auto operator+(StrongInt lhs, StrongInt rhs) noexcept -> StrongInt + { + lhs += rhs; + return lhs; + } + + [[nodiscard]] friend constexpr auto operator-(StrongInt lhs, StrongInt rhs) noexcept -> StrongInt + { + lhs -= rhs; + return lhs; + } + [[nodiscard]] constexpr auto operator<=>(const StrongInt &) const noexcept = default; [[nodiscard]] constexpr explicit operator Underlying() const noexcept @@ -66,4 +98,11 @@ enum class FlowControl : int kXonXoff = 2, }; +template +requires std::is_enum_v +[[nodiscard]] constexpr auto toInt(Enum value) noexcept -> int +{ + return std::to_underlying(value); +} + } // namespace cpp_core diff --git a/include/cpp_core/strong_types.test.cpp b/include/cpp_core/strong_types.test.cpp new file mode 100644 index 0000000..0ca321d --- /dev/null +++ b/include/cpp_core/strong_types.test.cpp @@ -0,0 +1,13 @@ +#include "cpp_core/strong_types.hpp" + +namespace cpp_core::tests::strong_types +{ + +static_assert(Baudrate{}.get() == 0); +static_assert((Baudrate{9'600} + Baudrate{115'200}).get() == 124'800); +static_assert((DataBits{8} - DataBits{3}).get() == 5); +static_assert(toInt(Parity::kOdd) == 2); +static_assert(toInt(StopBits::kTwo) == 2); +static_assert(toInt(FlowControl::kXonXoff) == 2); + +} // namespace cpp_core::tests::strong_types diff --git a/include/cpp_core/unique_resource.hpp b/include/cpp_core/unique_resource.hpp index fff9287..2087514 100644 --- a/include/cpp_core/unique_resource.hpp +++ b/include/cpp_core/unique_resource.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include diff --git a/include/cpp_core/validation.hpp b/include/cpp_core/validation.hpp index fef67de..2ff5c80 100644 --- a/include/cpp_core/validation.hpp +++ b/include/cpp_core/validation.hpp @@ -20,7 +20,8 @@ constexpr auto validateHandle(int64_t handle, Callback &&error_callback) -> Ret if (handle <= 0 || handle > std::numeric_limits::max()) { return failMsg(std::forward(error_callback), - static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); + static_cast(StatusCode::Connection::kInvalidHandleError), + "Invalid handle"); } return static_cast(StatusCode::kSuccess); } @@ -35,19 +36,19 @@ constexpr auto validateOpenParams(void *port, int baudrate, int data_bits, Callb if (port == nullptr) { return failMsg(std::forward(error_callback), - static_cast(StatusCode::Connection::kNotFoundError), + static_cast(StatusCode::Connection::kNotFoundError), "Port parameter is nullptr"); } if (baudrate < 300) { return failMsg(std::forward(error_callback), - static_cast(StatusCode::Control::kSetStateError), + static_cast(StatusCode::Control::kSetStateError), "Invalid baudrate: must be >= 300"); } if (data_bits < 5 || data_bits > 8) { return failMsg(std::forward(error_callback), - static_cast(StatusCode::Control::kSetStateError), + static_cast(StatusCode::Control::kSetStateError), "Invalid data bits: must be 5-8"); } return static_cast(StatusCode::kSuccess); @@ -60,7 +61,7 @@ constexpr auto validateBuffer(const void *buffer, int buffer_size, Callback &&er if (buffer == nullptr || buffer_size <= 0) { return failMsg(std::forward(error_callback), - static_cast(StatusCode::Io::kBufferError), + static_cast(StatusCode::Io::kBufferError), "Invalid buffer or buffer_size"); } return static_cast(StatusCode::kSuccess); diff --git a/tools/clang_ast_to_ffi_json.py b/tools/clang_ast_to_ffi_json.py new file mode 100644 index 0000000..699e155 --- /dev/null +++ b/tools/clang_ast_to_ffi_json.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import re +from typing import Any + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Reduce a clang AST JSON dump to compact FFI API metadata.") + parser.add_argument("--input", required=True, help="Path to the full clang AST JSON file.") + parser.add_argument("--output", required=True, help="Path to the reduced output JSON file.") + parser.add_argument("--source-root", required=True, help="Repository root used for path normalization.") + parser.add_argument("--public-header", required=True, help="Stable public header exposed to consumers.") + return parser.parse_args() + + +def walk(node: Any): + stack = [node] + while stack: + current = stack.pop() + if isinstance(current, dict): + yield current + for value in reversed(list(current.values())): + if isinstance(value, (dict, list)): + stack.append(value) + elif isinstance(current, list): + for item in reversed(current): + if isinstance(item, (dict, list)): + stack.append(item) + + +def first_file(*candidates: Any) -> str | None: + for candidate in candidates: + if isinstance(candidate, dict): + file_value = candidate.get("file") + if file_value: + return file_value + return None + + +def declaration_file(node: dict[str, Any]) -> str | None: + loc = node.get("loc", {}) + range_begin = node.get("range", {}).get("begin", {}) + range_end = node.get("range", {}).get("end", {}) + return first_file( + range_begin.get("expansionLoc"), + range_begin.get("spellingLoc"), + loc, + range_begin, + range_end, + loc.get("includedFrom"), + range_begin.get("includedFrom"), + range_end.get("includedFrom"), + ) + + +def normalize_path(path: str | None, source_root: Path) -> str | None: + if not path: + return None + path_obj = Path(path).resolve() + try: + return path_obj.relative_to(source_root).as_posix() + except ValueError: + return path_obj.as_posix() + + +def normalize_header_path(path: str | None, source_root: Path) -> str | None: + normalized = normalize_path(path, source_root) + if normalized and normalized.startswith("include/"): + return normalized.removeprefix("include/") + return normalized + + +def has_default_visibility(node: dict[str, Any]) -> bool: + return any( + isinstance(child, dict) + and child.get("kind") == "VisibilityAttr" + and child.get("visibility") == "default" + for child in node.get("inner", []) + ) + + +def is_exported_function(node: dict[str, Any], source_root: Path) -> bool: + if node.get("kind") != "FunctionDecl" or not node.get("name"): + return False + if not has_default_visibility(node): + return False + + mangled_name = node.get("mangledName") + if mangled_name and mangled_name != node.get("name"): + return False + + path = normalize_path(declaration_file(node), source_root) + return bool(path and path.startswith("include/cpp_core/")) + + +def split_top_level(text: str) -> list[str]: + parts: list[str] = [] + current: list[str] = [] + angle_depth = 0 + paren_depth = 0 + square_depth = 0 + + for char in text: + if char == "<": + angle_depth += 1 + elif char == ">": + angle_depth = max(0, angle_depth - 1) + elif char == "(": + paren_depth += 1 + elif char == ")": + paren_depth = max(0, paren_depth - 1) + elif char == "[": + square_depth += 1 + elif char == "]": + square_depth = max(0, square_depth - 1) + + if char == "," and angle_depth == 0 and paren_depth == 0 and square_depth == 0: + part = "".join(current).strip() + if part: + parts.append(part) + current = [] + continue + + current.append(char) + + tail = "".join(current).strip() + if tail: + parts.append(tail) + return parts + + +def parse_function_pointer(type_name: str) -> dict[str, Any] | None: + signature_match = re.match(r"^(?P.+?)\s*\(\*\)\((?P.*)\)$", type_name) + if not signature_match: + return None + + params_text = signature_match.group("params").strip() + params = [] if not params_text or params_text == "void" else split_top_level(params_text) + return { + "returnType": signature_match.group("return_type").strip(), + "parameters": params, + } + + +def extract_default_expr(node: dict[str, Any] | None) -> Any: + if not node: + return None + + kind = node.get("kind") + if kind in {"ImplicitCastExpr", "ConstantExpr", "ExprWithCleanups"}: + for child in node.get("inner", []): + if isinstance(child, dict): + return extract_default_expr(child) + return None + if kind == "IntegerLiteral": + return node.get("value") + if kind == "FloatingLiteral": + return node.get("value") + if kind == "StringLiteral": + return node.get("value") + if kind == "CXXBoolLiteralExpr": + return node.get("value") + if kind == "CXXNullPtrLiteralExpr": + return "nullptr" + if kind == "DeclRefExpr": + referenced = node.get("referencedDecl", {}) + return referenced.get("name") or node.get("name") + if kind == "UnaryOperator": + inner = next((child for child in node.get("inner", []) if isinstance(child, dict)), None) + inner_value = extract_default_expr(inner) + if inner_value is None: + return None + return f"{node.get('opcode', '')}{inner_value}" + return None + + +def text_from_comment(node: dict[str, Any]) -> str: + kind = node.get("kind") + if kind == "TextComment": + return node.get("text", "") + if kind == "InlineCommandComment": + args = node.get("args", []) + if node.get("renderKind") == "monospaced": + return " ".join(f"`{arg}`" for arg in args) + return " ".join(args) + + chunks = [] + for child in node.get("inner", []): + if isinstance(child, dict): + text = text_from_comment(child) + if text: + chunks.append(text) + joined = " ".join(chunks) + joined = re.sub(r"\s+", " ", joined).strip() + joined = re.sub(r"`([^`]+?)([.,;:])`", r"`\1`\2", joined) + joined = re.sub(r"\s+([.,;])", r"\1", joined) + return joined + + +def extract_comment(node: dict[str, Any]) -> dict[str, Any]: + full_comment = next( + (child for child in node.get("inner", []) if isinstance(child, dict) and child.get("kind") == "FullComment"), + None, + ) + if not full_comment: + return {} + + doc: dict[str, Any] = {"params": {}} + details: list[str] = [] + + for child in full_comment.get("inner", []): + if not isinstance(child, dict): + continue + + kind = child.get("kind") + if kind == "BlockCommandComment" and child.get("name") == "brief": + brief = text_from_comment(child) + if brief: + doc["brief"] = brief + continue + + if kind == "BlockCommandComment" and child.get("name") in {"return", "returns"}: + returns = text_from_comment(child) + if returns: + doc["returns"] = returns + continue + + if kind == "ParamCommandComment": + name = child.get("param") + text = text_from_comment(child) + if name and text: + param_entry: dict[str, Any] = {"description": text} + direction = child.get("direction") + if direction: + param_entry["direction"] = direction + doc["params"][name] = param_entry + continue + + if kind == "ParagraphComment": + paragraph = text_from_comment(child) + if paragraph: + details.append(paragraph) + + if details: + doc["details"] = details + if not doc["params"]: + doc.pop("params") + return doc + + +def extract_return_type(function_type: str) -> str: + if "->" in function_type: + return function_type.rsplit("->", 1)[1].strip() + match = re.match(r"^(?P.+?)\s*\(", function_type) + return match.group("return_type").strip() if match else function_type + + +def build_parameter(node: dict[str, Any], doc_params: dict[str, Any]) -> dict[str, Any]: + type_info = node.get("type", {}) + public_type = type_info.get("qualType") + callback_source_type = type_info.get("desugaredQualType") or public_type + parameter: dict[str, Any] = { + "name": node.get("name", ""), + "type": public_type, + } + + default_expr = next((child for child in node.get("inner", []) if isinstance(child, dict)), None) + default_value = extract_default_expr(default_expr) + if default_value is not None: + parameter["default"] = default_value + + callback = parse_function_pointer(callback_source_type) + if callback: + parameter["callback"] = callback + + doc_entry = doc_params.get(parameter["name"]) + if doc_entry: + parameter["doc"] = doc_entry + + return parameter + + +def build_function(node: dict[str, Any], source_root: Path) -> dict[str, Any]: + type_name = node.get("type", {}).get("qualType", "") + doc = extract_comment(node) + doc_params = doc.pop("params", {}) + + function: dict[str, Any] = { + "name": node["name"], + "symbol": node.get("mangledName", node["name"]), + "declaredIn": normalize_header_path(declaration_file(node), source_root), + "returnType": extract_return_type(type_name), + "parameters": [ + build_parameter(child, doc_params) + for child in node.get("inner", []) + if isinstance(child, dict) and child.get("kind") == "ParmVarDecl" + ], + } + + if node.get("inline"): + function["inline"] = True + if doc: + function["doc"] = doc + + return function + + +def main() -> int: + args = parse_args() + source_root = Path(args.source_root).resolve() + input_path = Path(args.input) + output_path = Path(args.output) + + with input_path.open(encoding="utf-8") as input_file: + ast = json.load(input_file) + + functions = [ + build_function(node, source_root) + for node in walk(ast) + if is_exported_function(node, source_root) + ] + functions.sort(key=lambda item: item["name"]) + + metadata = { + "schema": "cpp_core_ffi_api", + "schemaVersion": 1, + "publicHeader": args.public_header, + "functions": functions, + } + + output_path.parent.mkdir(parents=True, exist_ok=True) + with output_path.open("w", encoding="utf-8") as output_file: + json.dump(metadata, output_file, indent=2) + output_file.write("\n") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())