From cd6b2ee8fa7ed88e486ded8b3e811235bbc9799b Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sat, 6 Jun 2026 20:38:15 +0200 Subject: [PATCH 01/28] feat(ffi): add C++26 reflection FFI metadata layer and Deno bindgen --- CMakeLists.txt | 97 +++- README.md | 126 +++- docs/ffi-metadata.md | 310 ++++++++++ include/cpp_core.h | 1 + include/cpp_core/error_handling.hpp | 8 +- include/cpp_core/ffi_metadata.hpp | 536 ++++++++++++++++++ .../cpp_core/interface/serial_monitor_ports.h | 26 + include/cpp_core/result.hpp | 12 +- include/cpp_core/serial.h | 1 + include/cpp_core/status_code.h | 44 +- include/cpp_core/status_codes.h | 3 + include/cpp_core/validation.hpp | 11 +- tests/ffi_metadata_compile_test.cpp | 50 ++ tools/check_generated_deno_symbols.cmake | 35 ++ tools/generate_deno_symbols.cpp | 528 +++++++++++++++++ 15 files changed, 1748 insertions(+), 40 deletions(-) create mode 100644 docs/ffi-metadata.md create mode 100644 include/cpp_core/ffi_metadata.hpp create mode 100644 include/cpp_core/interface/serial_monitor_ports.h create mode 100644 include/cpp_core/status_codes.h create mode 100644 tests/ffi_metadata_compile_test.cpp create mode 100644 tools/check_generated_deno_symbols.cmake create mode 100644 tools/generate_deno_symbols.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d831df..1ecbfb7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,50 @@ cmake_minimum_required(VERSION 3.30) # Export compile commands to root directory set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +project(cpp-core LANGUAGES CXX) + +if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + message(FATAL_ERROR "This experimental branch requires GCC 16+ with -std=c++26 and -freflection.") +endif() + +if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16) + message(FATAL_ERROR "This experimental branch requires GCC 16 or newer.") +endif() + +# This branch intentionally targets a single experimental toolchain. +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) + +include(CheckCXXCompilerFlag) +include(CheckCXXSourceCompiles) + +check_cxx_compiler_flag("-freflection" CPP_CORE_HAS_FREFLECTION_FLAG) +if(NOT CPP_CORE_HAS_FREFLECTION_FLAG) + message(FATAL_ERROR "GCC 16 was found, but -freflection is not supported by this compiler build.") +endif() + +set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -std=c++26 -freflection") +check_cxx_source_compiles( + " + #include + #ifndef __cpp_impl_reflection + #error Reflection feature macro missing + #endif + constexpr auto kReflectionProbe = ^^int; + static_assert(kReflectionProbe == ^^int); + int main() { return 0; } + " + CPP_CORE_HAS_WORKING_REFLECTION +) +unset(CMAKE_REQUIRED_FLAGS) + +if(NOT CPP_CORE_HAS_WORKING_REFLECTION) + message(FATAL_ERROR "Reflection support is required. Configure with GCC 16+ and a working -freflection implementation.") +endif() + include(cmake/CPM.cmake) CPMAddPackage( @@ -18,12 +62,8 @@ 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 "Experimental GCC 16 / C++26 reflection branch for shared serial FFI metadata") # Header-only library -------------------------------------------------------- add_library(cpp_core INTERFACE) @@ -37,23 +77,38 @@ 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) +# Require C++26 reflection on every consumer of the interface target. +target_compile_features(cpp_core INTERFACE cxx_std_26) +target_compile_options(cpp_core INTERFACE -freflection) -# Enable C++23 module support -set(CMAKE_CXX_MODULE_STD 23) -set(CMAKE_CXX_MODULE_EXTENSIONS OFF) +add_executable(cpp_core_bindgen EXCLUDE_FROM_ALL tools/generate_deno_symbols.cpp) +target_link_libraries(cpp_core_bindgen PRIVATE cpp_core::cpp_core) 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 CPP_CORE_COMPILE_TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/*_compile_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) + + set(CPP_CORE_DENO_BINDGEN_SMOKE_EXE "${CMAKE_CURRENT_BINARY_DIR}/cpp_core_bindgen_smoke_bin") + add_custom_target( + cpp_core_bindgen_smoke + COMMAND + ${CMAKE_COMMAND} + -E + copy + $ + ${CPP_CORE_DENO_BINDGEN_SMOKE_EXE} + COMMAND + ${CMAKE_COMMAND} + -DGENERATOR=${CPP_CORE_DENO_BINDGEN_SMOKE_EXE} + -P + ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_generated_deno_symbols.cmake + DEPENDS cpp_core_bindgen + COMMENT "Running smoke check for generated Deno bindgen output" + VERBATIM + ) endif() # Install rules -------------------------------------------------------------- @@ -64,6 +119,12 @@ install( EXPORT cpp_coreTargets ) +install( + TARGETS cpp_core_bindgen + EXPORT cpp_coreTargets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + install( DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} diff --git a/README.md b/README.md index 8726f75..e4c7770 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Header-only API definition library for cross-platform serial communication. Defi > [!IMPORTANT] > +> This branch is an **experimental GCC 16 / C++26 / reflection** branch. +> > **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) @@ -11,18 +13,70 @@ Header-only API definition library for cross-platform serial communication. Defi ## Features -* **C++23** header-only API definitions +* **C++26 + reflection** 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.) +* **Reflection-backed FFI metadata** for functions, status codes, and shared structs +* **C-compatible ABI** for FFI bindings (Rust, Python, Deno, etc.) ## Requirements * CMake >= 3.30 -* A C++23 compatible compiler (GCC 13+, Clang 16+, MSVC 2022+) +* GCC 16 or newer +* `-std=c++26` and `-freflection` * Git (for automatic version detection) +Clang and MSVC are intentionally not supported on this branch. + +## Experimental Direction + +This branch keeps the **C ABI** as the external contract while moving internal metadata to a reflection-backed C++26 model. + +That split is intentional: + +* the **runtime ABI** stays easy to consume from Deno FFI and other foreign runtimes +* the **C++ authoring model** gains a single metadata source for status codes, shared structs, and exported function signatures +* bind generation for Deno bindings can read `cpp_core::kFunctionDescriptors`, `cpp_core::kOperationDescriptors`, + and the reflected struct field descriptors instead of re-parsing handwritten headers + +The new metadata entrypoint is: + +```cpp +#include + +constexpr auto functions = cpp_core::functionDescriptors(); +constexpr auto operations = cpp_core::operationDescriptors(); +constexpr auto statuses = cpp_core::statusCodeDescriptors(); +constexpr auto config_fields = cpp_core::serialConfigFieldDescriptors(); +``` + +More practical usage examples and the intended Deno/FFI workflow are documented in [docs/ffi-metadata.md](docs/ffi-metadata.md). + +There is also a bindgen target now: + +```sh +cmake --build build/gcc --target cpp_core_bindgen +./build/gcc/cpp_core_bindgen --output deno_bindings.ts +``` + +The generated module now contains: + +* `symbols` for `Deno.dlopen` +* `operations` metadata +* `statusCodes` plus `StatusCodeError` +* `createBindings(dylib)` which returns named TypeScript wrapper functions + +## Why This Helps Already + +The new metadata layer is useful even before any full code generator exists: + +* **One source of truth** for exported function names, parameter names, return semantics, and status categories +* **No manual re-parsing of headers** when you want to derive Deno FFI symbol definitions or internal binding manifests +* **Generated wrapper functions** with named parameters, result decoding, and status-to-exception conversion +* **Safer refactors** because compile-time metadata checks fail when the exposed signatures drift +* **Shared understanding of the ABI** because buffers, callbacks, UTF-8 strings, opaque handles, and status-returning functions are classified centrally +* **Less version drift in consumers** because the same `cpp-core` checkout now provides both the headers and the generator used by Linux/Windows builds + ## Version Information The version is **automatically extracted from Git tags** during CMake configuration and cannot be overridden: @@ -33,7 +87,7 @@ The version is **automatically extracted from Git tags** during CMake configurat All binding repositories that include this repository will use the same version information, ensuring consistency across platforms. ```cpp -#include +#include constexpr auto v = cpp_core::kVersion; // v.major, v.minor, v.patch ``` @@ -56,7 +110,7 @@ The binding implementation then uses the API definitions: ```cpp #include -#include +#include // Implement platform-specific functions matching the API intptr_t serialOpen( @@ -71,6 +125,66 @@ intptr_t serialOpen( } ``` +## Error Handling Today and Later + +The current ABI remains `status + out-params` or `value-or-negative-status`, because that maps cleanly to plain C and Deno FFI. + +```c +intptr_t serialOpen( + const char *port, + int baudrate, + int data_bits, + int parity, + int stop_bits, + ErrorCallbackT error_callback +); +``` + +Internally the implementation can still use `std::expected`-style flows and translate them at the ABI edge into the plain C model. + +The corresponding Deno FFI shape would stay straightforward: + +```ts +const symbols = { + serialOpen: { + parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"], + result: "isize", + }, +} as const; + +const handle = dylib.symbols.serialOpen( + cstr("/dev/ttyUSB0"), + 115200, + 8, + 0, + 0, + null, +); +``` + +With the generated wrapper layer on top, the same call can be used more directly: + +```ts +const dylib = Deno.dlopen(path, symbols); +const serial = createBindings(dylib); + +const handle = serial.serialOpen({ + port: "/dev/ttyUSB0", + baudrate: 115200, + data_bits: 8, +}); +``` + +## Using The Metadata + +For concrete usage examples, see [docs/ffi-metadata.md](docs/ffi-metadata.md). The short version is: + +* use `cpp_core::functionDescriptors()` when you want to inspect the exported C ABI +* use `cpp_core::operationDescriptors()` when you want a stable per-operation view next to the raw function list +* use `cpp_core::statusCodeDescriptors()` when you want symbolic error information +* use `cpp_core::serialConfigFieldDescriptors()` and `cpp_core::versionFieldDescriptors()` when you want shared struct metadata +* use `cpp_core::findFunctionDescriptor()`, `cpp_core::findOperationDescriptor()`, and `cpp_core::findStatusCodeDescriptor()` for targeted lookups + ## License `Apache-2.0` - Check [LICENSE](LICENSE) for more details. diff --git a/docs/ffi-metadata.md b/docs/ffi-metadata.md new file mode 100644 index 0000000..a1d85b2 --- /dev/null +++ b/docs/ffi-metadata.md @@ -0,0 +1,310 @@ +# FFI Metadata Guide + +This branch adds a reflection-backed metadata layer in `cpp_core` for the public C ABI. + +The important distinction is: + +* the exported runtime interface is still plain C ABI +* the description of that interface is now available as typed compile-time metadata in C++ + +That gives you a place to inspect the ABI, build tooling around it, and later generate bindings from it without maintaining a second hand-written schema. + +## What Exists Today + +Include: + +```cpp +#include +``` + +Main entrypoints: + +* `cpp_core::functionDescriptors()` +* `cpp_core::operationDescriptors()` +* `cpp_core::statusCodeDescriptors()` +* `cpp_core::serialConfigFieldDescriptors()` +* `cpp_core::versionFieldDescriptors()` +* `cpp_core::findFunctionDescriptor(name)` +* `cpp_core::findOperationDescriptor(name)` +* `cpp_core::findStatusCodeDescriptor(code)` + +Generator tooling in this branch: + +* `cpp_core_bindgen` generates a Deno-oriented TypeScript module from the same metadata used by the headers +* the generated module includes `symbols`, metadata exports, `StatusCodeError`, and `createBindings(dylib)` + +Main descriptor types: + +* `cpp_core::FunctionDescriptor` +* `cpp_core::OperationDescriptor` +* `cpp_core::ParameterDescriptor` +* `cpp_core::StatusCodeDescriptor` +* `cpp_core::StructFieldDescriptor` +* `cpp_core::AbiValueKind` +* `cpp_core::ParameterDirection` +* `cpp_core::FunctionResultModel` + +## What Problem This Solves + +Before this layer, consumers had to learn ABI details from handwritten headers only. + +That caused three problems: + +* tools had to re-parse or duplicate knowledge about functions and parameters +* buffer/callback/string/handle semantics were implicit and spread across comments +* future Deno binding generation had no canonical machine-readable source inside `cpp-core` + +Now the ABI description is available in a typed form inside the library itself. + +## Practical Benefits Right Now + +### 1. You can inspect the exported ABI programmatically + +Example: + +```cpp +#include +#include + +auto main() -> int +{ + for (const auto &fn : cpp_core::functionDescriptors()) + { + std::cout << fn.name << " -> " << fn.return_cpp_type << '\n'; + + for (const auto ¶m : fn.parameters) + { + std::cout << " - " << param.name + << " : " << param.cpp_type + << " optional=" << param.optional + << '\n'; + } + } +} +``` + +This is useful for debugging the ABI contract, generating reports, or building glue code. + +### 2. You get a normalized ABI classification + +Each parameter is mapped into a semantic bucket via `AbiValueKind`. + +Examples: + +* `kUtf8CString` +* `kMutableBuffer` +* `kConstBuffer` +* `kOpaqueHandle` +* `kErrorCallback` +* `kNotificationCallback` + +This matters because foreign runtimes care about ABI semantics, not just raw C++ spellings. + +For example, `"void*"` by itself is not enough to know whether something is: + +* a writable byte buffer +* a UTF-8 C string +* an opaque pointer +* a callback-associated payload + +The metadata layer makes that distinction explicit. + +### 3. You can reason about result behavior + +Each function is classified with `FunctionResultModel`. + +Current models: + +* `kVoid` +* `kStatusCode` +* `kValueOrStatus` +* `kHandleOrStatus` + +That lets tooling distinguish: + +* functions that are pure commands +* functions that return status only +* functions that return a useful numeric value or a negative status +* functions like `serialOpen` that return a handle or a negative status + +This is especially useful when generating Deno wrappers that should not treat every `int` the same way. + +### 4. Status codes become machine-readable + +You can inspect names, categories, and numeric values: + +```cpp +#include +#include + +auto main() -> int +{ + for (const auto &status : cpp_core::statusCodeDescriptors()) + { + std::cout << status.category + << "::" << status.name + << " = " << static_cast(status.value) + << '\n'; + } +} +``` + +This is useful for: + +* generating TS enums or lookup tables +* mapping numeric failures back to symbolic names +* producing consistent docs for consumers + +### 5. Shared structs can be described centrally + +`SerialConfig` and `Version` expose field descriptors. + +That is the first step toward: + +* generated docs for shared data +* generated FFI struct definitions and byte-layout helpers +* compatibility checks between metadata and runtime-facing declarations + +## Example: Look Up One Function + +```cpp +#include +#include + +auto main() -> int +{ + const auto *fn = cpp_core::findFunctionDescriptor("serialRead"); + if (fn == nullptr) + { + return 1; + } + + std::cout << fn->name << '\n'; + std::cout << fn->signature << '\n'; + + for (const auto ¶m : fn->parameters) + { + std::cout << param.name << " -> " << param.cpp_type << '\n'; + } +} +``` + +Typical use case: + +* lookup by exported symbol name +* build FFI binding metadata for exactly that symbol +* emit wrapper code based on parameter and result classifications + +## Example: Derive Deno-Oriented Symbol Metadata + +This is not a full generator yet, but the metadata is now good enough to drive one. + +There is now a bindgen CLI tool for this in the repo: + +```sh +cmake --build build/gcc --target cpp_core_bindgen +./build/gcc/cpp_core_bindgen +``` + +To capture the output into a TypeScript file: + +```sh +./build/gcc/cpp_core_bindgen --output deno_bindings.ts +``` + +Example sketch: + +```cpp +#include + +constexpr auto toDenoType(cpp_core::AbiValueKind kind) -> const char * +{ + switch (kind) + { + case cpp_core::AbiValueKind::kInt32: + return "i32"; + case cpp_core::AbiValueKind::kInt64: + case cpp_core::AbiValueKind::kOpaqueHandle: + return "i64"; + case cpp_core::AbiValueKind::kUtf8CString: + case cpp_core::AbiValueKind::kMutableBuffer: + case cpp_core::AbiValueKind::kConstBuffer: + case cpp_core::AbiValueKind::kOpaquePointer: + case cpp_core::AbiValueKind::kVersionStructPointer: + case cpp_core::AbiValueKind::kErrorCallback: + case cpp_core::AbiValueKind::kNotificationCallback: + return "pointer"; + case cpp_core::AbiValueKind::kVoid: + return "void"; + } + + return "pointer"; +} +``` + +The current generator already emits an importable module and wraps the current legacy ABI: + +```ts +// Generated from cpp-core reflection metadata. +// Do not edit manually. +// Intended for use with Deno.dlopen. +// ABI mode: preferred. + +export const symbols = { + serialOpen: { + parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"], + result: "isize", + }, + // ... +} as const; + +export const operations = { + serialOpen: { + symbol: "serialOpen", + }, +} as const; + +export function createBindings(dylib: Deno.DynamicLibrary) { + return { + serialOpen(args: SerialOpenParams): bigint { + // marshals strings + // throws StatusCodeError on failure + }, + }; +} +``` + +## What This Does Not Do Yet + +Current limitations: + +* it does not encode every possible semantic rule for every parameter +* it does not expose `std::expected` itself across the external ABI + +That is deliberate. The branch first makes the ABI description explicit and compile-checked. + +## Relation To `std::expected` + +The metadata layer does not expose `std::expected` across the ABI boundary. + +That would be the wrong abstraction for Deno FFI and plain C consumers. + +Instead, the model is: + +* internally: implementations can use richer C++ result handling +* externally: the ABI stays plain C +* metadata: describes exported functions, parameters, shared structs, and error semantics + +That gives you most of the practical benefits of structured error handling without making the FFI surface harder to consume. + +## Recommended Current Usage + +Today, the best use of the new layer is: + +* inspect exported functions from tooling or tests +* generate a Deno metadata module with `cpp_core_bindgen` +* generate or validate Deno FFI symbol tables +* consume `createBindings(dylib)` as a higher-level TS wrapper layer with named params and automatic error conversion +* generate status-code lookup tables +* let Linux/Windows builds consume the same `cpp-core` revision for both headers and bindgen output +* keep ABI comments and generated docs consistent with the actual declarations diff --git a/include/cpp_core.h b/include/cpp_core.h index c198495..8d6f64a 100644 --- a/include/cpp_core.h +++ b/include/cpp_core.h @@ -13,6 +13,7 @@ */ #include "cpp_core/error_callback.h" +#include "cpp_core/ffi_metadata.hpp" #include "cpp_core/serial.h" #include "cpp_core/status_code.h" #include "cpp_core/version.hpp" diff --git a/include/cpp_core/error_handling.hpp b/include/cpp_core/error_handling.hpp index e3e52c6..c9fa654 100644 --- a/include/cpp_core/error_handling.hpp +++ b/include/cpp_core/error_handling.hpp @@ -28,13 +28,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 +57,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 +65,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/ffi_metadata.hpp b/include/cpp_core/ffi_metadata.hpp new file mode 100644 index 0000000..0b90b11 --- /dev/null +++ b/include/cpp_core/ffi_metadata.hpp @@ -0,0 +1,536 @@ +#pragma once + +#include "serial.h" +#include "serial_config.hpp" +#include "status_code.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef __cpp_impl_reflection +#error "cpp_core/ffi_metadata.hpp requires C++26 reflection support." +#endif + +namespace cpp_core +{ + +enum class AbiValueKind +{ + kVoid, + kInt32, + kInt64, + kOpaqueHandle, + kUtf8CString, + kMutableBuffer, + kConstBuffer, + kOpaquePointer, + kVersionStructPointer, + kErrorCallback, + kNotificationCallback, +}; + +enum class ParameterDirection +{ + kIn, + kOut, + kInOut, +}; + +enum class FunctionResultModel +{ + kVoid, + kStatusCode, + kValueOrStatus, + kHandleOrStatus, +}; + +struct ParameterDescriptor +{ + std::string_view name; + std::string_view cpp_type; + AbiValueKind abi_kind; + ParameterDirection direction; + bool optional; +}; + +struct FunctionDescriptor +{ + std::string_view name; + std::string_view return_cpp_type; + std::string_view signature; + AbiValueKind return_abi_kind; + FunctionResultModel result_model; + std::span parameters; +}; + +struct OperationDescriptor +{ + std::string_view name; + std::string_view function_name; +}; + +struct StatusCodeDescriptor +{ + std::string_view category; + std::string_view name; + StatusCodeValue value; +}; + +struct StructFieldDescriptor +{ + std::string_view name; + std::string_view cpp_type; + AbiValueKind abi_kind; + std::size_t offset; + std::size_t size; +}; + +namespace detail +{ + +[[nodiscard]] consteval auto isConstPointer(std::meta::info type_info) -> bool +{ + if (!std::meta::is_pointer_type(type_info)) + { + return false; + } + + return std::meta::is_const(std::meta::remove_pointer(type_info)); +} + +[[nodiscard]] consteval auto inferAbiValueKind(std::meta::info type_info, std::string_view parameter_name) -> AbiValueKind +{ + if (std::meta::is_pointer_type(type_info)) + { + if (std::meta::display_string_of(type_info) == "cpp_core::Version*") + { + return AbiValueKind::kVersionStructPointer; + } + + if (parameter_name == "error_callback") + { + return AbiValueKind::kErrorCallback; + } + + if (parameter_name.ends_with("_callback") || parameter_name == "callback_fn") + { + return AbiValueKind::kNotificationCallback; + } + + if (parameter_name == "port") + { + return AbiValueKind::kUtf8CString; + } + + if (parameter_name == "buffer") + { + return isConstPointer(type_info) ? AbiValueKind::kConstBuffer : AbiValueKind::kMutableBuffer; + } + + if (parameter_name == "sequence" || parameter_name == "until_char") + { + return AbiValueKind::kConstBuffer; + } + + if (std::meta::display_string_of(std::meta::remove_pointer(type_info)) == "char") + { + return AbiValueKind::kUtf8CString; + } + + return AbiValueKind::kOpaquePointer; + } + + if (std::meta::is_integral_type(type_info)) + { + if (parameter_name == "handle") + { + return AbiValueKind::kOpaqueHandle; + } + + return std::meta::size_of(type_info) > sizeof(std::int32_t) ? AbiValueKind::kInt64 : AbiValueKind::kInt32; + } + + if (std::meta::is_void_type(type_info)) + { + return AbiValueKind::kVoid; + } + + return AbiValueKind::kOpaquePointer; +} + +[[nodiscard]] consteval auto inferReturnAbiValueKind(std::string_view function_name, std::meta::info return_type_info) + -> AbiValueKind +{ + if (std::meta::is_void_type(return_type_info)) + { + return AbiValueKind::kVoid; + } + + if (function_name == "serialOpen") + { + return AbiValueKind::kOpaqueHandle; + } + + if (std::meta::is_integral_type(return_type_info)) + { + return std::meta::size_of(return_type_info) > sizeof(std::int32_t) ? AbiValueKind::kInt64 : AbiValueKind::kInt32; + } + + return AbiValueKind::kOpaquePointer; +} + +[[nodiscard]] consteval auto inferParameterDirection(std::string_view function_name, std::string_view parameter_name, + std::meta::info type_info) -> ParameterDirection +{ + if (parameter_name.ends_with("_callback") || parameter_name == "callback_fn" || parameter_name == "error_callback") + { + return ParameterDirection::kIn; + } + + if (parameter_name == "out") + { + return ParameterDirection::kOut; + } + + if (parameter_name == "buffer") + { + if (function_name.starts_with("serialRead")) + { + return ParameterDirection::kOut; + } + + if (function_name.starts_with("serialWrite")) + { + return ParameterDirection::kIn; + } + } + + if (parameter_name == "sequence" || parameter_name == "until_char" || parameter_name == "port") + { + return ParameterDirection::kIn; + } + + if (std::meta::is_pointer_type(type_info)) + { + return isConstPointer(type_info) ? ParameterDirection::kIn : ParameterDirection::kInOut; + } + + return ParameterDirection::kIn; +} + +[[nodiscard]] consteval auto inferResultModel(std::string_view function_name, std::meta::info return_type_info) + -> FunctionResultModel +{ + if (std::meta::is_void_type(return_type_info)) + { + return FunctionResultModel::kVoid; + } + + if (function_name == "serialOpen") + { + return FunctionResultModel::kHandleOrStatus; + } + + if (function_name.starts_with("serialRead") || function_name.starts_with("serialWrite") + || function_name.starts_with("serialGet") || function_name.starts_with("serialInBytes") + || function_name.starts_with("serialOutBytes") || function_name == "serialListPorts") + { + return FunctionResultModel::kValueOrStatus; + } + + return FunctionResultModel::kStatusCode; +} + +[[nodiscard]] consteval auto makeParameterDescriptor(std::string_view function_name, std::meta::info parameter_info) + -> ParameterDescriptor +{ + const auto type_info = std::meta::type_of(parameter_info); + const auto parameter_name = std::meta::identifier_of(parameter_info); + + return ParameterDescriptor{ + .name = parameter_name, + .cpp_type = std::meta::display_string_of(type_info), + .abi_kind = inferAbiValueKind(type_info, parameter_name), + .direction = inferParameterDirection(function_name, parameter_name, type_info), + .optional = std::meta::has_default_argument(parameter_info), + }; +} + +template +[[nodiscard]] consteval auto makeParameterDescriptorsImpl(std::string_view function_name, + const std::vector ¶ms, + std::index_sequence) +{ + return std::array{makeParameterDescriptor(function_name, params[Index])...}; +} + +template +[[nodiscard]] consteval auto makeFunctionDescriptor(std::meta::info function_info, + const std::array ¶ms) + -> FunctionDescriptor +{ + const auto function_name = std::meta::identifier_of(function_info); + const auto return_type = std::meta::return_type_of(function_info); + + return FunctionDescriptor{ + .name = function_name, + .return_cpp_type = std::meta::display_string_of(return_type), + .signature = std::meta::display_string_of(std::meta::type_of(function_info)), + .return_abi_kind = inferReturnAbiValueKind(function_name, return_type), + .result_model = inferResultModel(function_name, return_type), + .parameters = std::span(params), + }; +} + +template +[[nodiscard]] consteval auto makeFieldDescriptorsImpl(const std::vector &fields, + std::index_sequence) +{ + return std::array{ + StructFieldDescriptor{ + .name = std::meta::identifier_of(fields[Index]), + .cpp_type = std::meta::display_string_of(std::meta::type_of(fields[Index])), + .abi_kind = inferAbiValueKind(std::meta::type_of(fields[Index]), std::meta::identifier_of(fields[Index])), + .offset = static_cast(std::meta::offset_of(fields[Index]).bytes), + .size = std::meta::size_of(std::meta::type_of(fields[Index])), + }..., + }; +} + +template +[[nodiscard]] consteval auto makeStatusCodeDescriptor(const Code &code_value, std::string_view fallback_category = {}) + -> StatusCodeDescriptor +{ + const auto category = fallback_category.empty() ? code_value.category() : fallback_category; + return StatusCodeDescriptor{ + .category = category, + .name = code_value.name(), + .value = static_cast(code_value), + }; +} + +} // namespace detail + +#define CPP_CORE_FFI_FUNCTION_LIST(X) \ + X(getVersion) \ + X(serialAbortRead) \ + X(serialAbortWrite) \ + X(serialClearBufferIn) \ + X(serialClearBufferOut) \ + X(serialClose) \ + X(serialDrain) \ + X(serialGetBaudrate) \ + X(serialGetCts) \ + X(serialGetDataBits) \ + X(serialGetDcd) \ + X(serialGetDsr) \ + X(serialGetFlowControl) \ + X(serialGetParity) \ + X(serialGetRi) \ + X(serialGetStopBits) \ + X(serialInBytesTotal) \ + X(serialInBytesWaiting) \ + X(serialListPorts) \ + X(serialMonitorPorts) \ + X(serialOpen) \ + X(serialOutBytesTotal) \ + X(serialOutBytesWaiting) \ + X(serialRead) \ + X(serialReadLine) \ + X(serialReadUntil) \ + X(serialReadUntilSequence) \ + X(serialSendBreak) \ + X(serialSetBaudrate) \ + X(serialSetDataBits) \ + X(serialSetDtr) \ + X(serialSetErrorCallback) \ + X(serialSetFlowControl) \ + X(serialSetParity) \ + X(serialSetReadCallback) \ + X(serialSetRts) \ + X(serialSetStopBits) \ + X(serialSetWriteCallback) \ + X(serialWrite) + +#define CPP_CORE_DECLARE_FFI_METADATA(FunctionName) \ + namespace detail \ + { \ + inline constexpr auto kParameters_##FunctionName = []() consteval \ + { \ + auto params = std::meta::parameters_of(^^FunctionName); \ + return makeParameterDescriptorsImpl(std::meta::identifier_of(^^FunctionName), params, \ + std::make_index_sequence{}); \ + }(); \ + inline constexpr auto kDescriptor_##FunctionName = makeFunctionDescriptor(^^FunctionName, kParameters_##FunctionName); \ + } + +CPP_CORE_FFI_FUNCTION_LIST(CPP_CORE_DECLARE_FFI_METADATA) + +#undef CPP_CORE_DECLARE_FFI_METADATA + +inline constexpr auto kFunctionDescriptors = std::array{ +#define CPP_CORE_FFI_DESCRIPTOR_ENTRY(FunctionName) detail::kDescriptor_##FunctionName, + CPP_CORE_FFI_FUNCTION_LIST(CPP_CORE_FFI_DESCRIPTOR_ENTRY) +#undef CPP_CORE_FFI_DESCRIPTOR_ENTRY +}; + +inline constexpr auto kOperationDescriptors = std::array{ + OperationDescriptor{"getVersion", "getVersion"}, + OperationDescriptor{"serialAbortRead", "serialAbortRead"}, + OperationDescriptor{"serialAbortWrite", "serialAbortWrite"}, + OperationDescriptor{"serialClearBufferIn", "serialClearBufferIn"}, + OperationDescriptor{"serialClearBufferOut", "serialClearBufferOut"}, + OperationDescriptor{"serialClose", "serialClose"}, + OperationDescriptor{"serialDrain", "serialDrain"}, + OperationDescriptor{"serialGetBaudrate", "serialGetBaudrate"}, + OperationDescriptor{"serialGetCts", "serialGetCts"}, + OperationDescriptor{"serialGetDataBits", "serialGetDataBits"}, + OperationDescriptor{"serialGetDcd", "serialGetDcd"}, + OperationDescriptor{"serialGetDsr", "serialGetDsr"}, + OperationDescriptor{"serialGetFlowControl", "serialGetFlowControl"}, + OperationDescriptor{"serialGetParity", "serialGetParity"}, + OperationDescriptor{"serialGetRi", "serialGetRi"}, + OperationDescriptor{"serialGetStopBits", "serialGetStopBits"}, + OperationDescriptor{"serialInBytesTotal", "serialInBytesTotal"}, + OperationDescriptor{"serialInBytesWaiting", "serialInBytesWaiting"}, + OperationDescriptor{"serialListPorts", "serialListPorts"}, + OperationDescriptor{"serialMonitorPorts", "serialMonitorPorts"}, + OperationDescriptor{"serialOpen", "serialOpen"}, + OperationDescriptor{"serialOutBytesTotal", "serialOutBytesTotal"}, + OperationDescriptor{"serialOutBytesWaiting", "serialOutBytesWaiting"}, + OperationDescriptor{"serialRead", "serialRead"}, + OperationDescriptor{"serialReadLine", "serialReadLine"}, + OperationDescriptor{"serialReadUntil", "serialReadUntil"}, + OperationDescriptor{"serialReadUntilSequence", "serialReadUntilSequence"}, + OperationDescriptor{"serialSendBreak", "serialSendBreak"}, + OperationDescriptor{"serialSetBaudrate", "serialSetBaudrate"}, + OperationDescriptor{"serialSetDataBits", "serialSetDataBits"}, + OperationDescriptor{"serialSetDtr", "serialSetDtr"}, + OperationDescriptor{"serialSetErrorCallback", "serialSetErrorCallback"}, + OperationDescriptor{"serialSetFlowControl", "serialSetFlowControl"}, + OperationDescriptor{"serialSetParity", "serialSetParity"}, + OperationDescriptor{"serialSetReadCallback", "serialSetReadCallback"}, + OperationDescriptor{"serialSetRts", "serialSetRts"}, + OperationDescriptor{"serialSetStopBits", "serialSetStopBits"}, + OperationDescriptor{"serialSetWriteCallback", "serialSetWriteCallback"}, + OperationDescriptor{"serialWrite", "serialWrite"}, +}; + +inline constexpr auto kStatusCodeDescriptors = std::array{ + StatusCodeDescriptor{"General", "Success", StatusCode::kSuccess}, + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetBaudrateError), + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetDataBitsError), + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetParityError), + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetStopBitsError), + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetFlowControlError), + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetTimeoutError), + detail::makeStatusCodeDescriptor(StatusCode::Connection::kNotFoundError), + detail::makeStatusCodeDescriptor(StatusCode::Connection::kInvalidHandleError), + detail::makeStatusCodeDescriptor(StatusCode::Connection::kCloseHandleError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kReadError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kWriteError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kAbortReadError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kAbortWriteError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kBufferError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kClearBufferInError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kClearBufferOutError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kSetDtrError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kSetRtsError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kGetModemStatusError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kSendBreakError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kGetStateError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kSetStateError), + detail::makeStatusCodeDescriptor(StatusCode::Monitor::kMonitorError), +}; + +inline constexpr auto kSerialConfigFieldDescriptors = []() consteval +{ + auto fields = std::meta::nonstatic_data_members_of(^^SerialConfig, std::meta::access_context::unchecked()); + return detail::makeFieldDescriptorsImpl( + fields, std::make_index_sequence{}); +}(); + +inline constexpr auto kVersionFieldDescriptors = []() consteval +{ + auto fields = std::meta::nonstatic_data_members_of(^^Version, std::meta::access_context::unchecked()); + return detail::makeFieldDescriptorsImpl( + fields, + std::make_index_sequence{}); +}(); + +[[nodiscard]] constexpr auto functionDescriptors() noexcept -> std::span +{ + return kFunctionDescriptors; +} + +[[nodiscard]] constexpr auto operationDescriptors() noexcept -> std::span +{ + return kOperationDescriptors; +} + +[[nodiscard]] constexpr auto statusCodeDescriptors() noexcept -> std::span +{ + return kStatusCodeDescriptors; +} + +[[nodiscard]] constexpr auto serialConfigFieldDescriptors() noexcept -> std::span +{ + return kSerialConfigFieldDescriptors; +} + +[[nodiscard]] constexpr auto versionFieldDescriptors() noexcept -> std::span +{ + return kVersionFieldDescriptors; +} + +[[nodiscard]] constexpr auto findFunctionDescriptor(std::string_view function_name) noexcept -> const FunctionDescriptor * +{ + for (const auto &descriptor : kFunctionDescriptors) + { + if (descriptor.name == function_name) + { + return &descriptor; + } + } + + return nullptr; +} + +[[nodiscard]] constexpr auto findOperationDescriptor(std::string_view operation_name) noexcept -> const OperationDescriptor * +{ + for (const auto &descriptor : kOperationDescriptors) + { + if (descriptor.name == operation_name) + { + return &descriptor; + } + } + + return nullptr; +} + +[[nodiscard]] constexpr auto findStatusCodeDescriptor(StatusCodeValue code) noexcept -> const StatusCodeDescriptor * +{ + for (const auto &descriptor : kStatusCodeDescriptors) + { + if (descriptor.value == code) + { + return &descriptor; + } + } + + return nullptr; +} + +#undef CPP_CORE_FFI_FUNCTION_LIST + +} // namespace cpp_core 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..54214dd --- /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::StatusCodes 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/result.hpp b/include/cpp_core/result.hpp index d00012f..bd28683 100644 --- a/include/cpp_core/result.hpp +++ b/include/cpp_core/result.hpp @@ -15,14 +15,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)) + Error(StatusCodeValue code_in, std::string msg) : code(code_in), message(std::move(msg)) { } @@ -36,7 +36,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 +66,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]] auto fail(StatusCodeValue code, std::string message) -> Result { return std::unexpected(Error{code, std::move(message)}); } diff --git a/include/cpp_core/serial.h b/include/cpp_core/serial.h index 06e9c58..e92c6bf 100644 --- a/include/cpp_core/serial.h +++ b/include/cpp_core/serial.h @@ -14,6 +14,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/status_code.h b/include/cpp_core/status_code.h index cade93c..df07c8a 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 @@ -116,6 +117,14 @@ 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; @@ -134,6 +143,39 @@ 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; + +struct StatusCodes +{ + static constexpr StatusCodeValue kSuccess = StatusCode::kSuccess; + + static constexpr StatusCodeValue kSetBaudrateError = StatusCode::Configuration::kSetBaudrateError; + static constexpr StatusCodeValue kSetDataBitsError = StatusCode::Configuration::kSetDataBitsError; + static constexpr StatusCodeValue kSetParityError = StatusCode::Configuration::kSetParityError; + static constexpr StatusCodeValue kSetStopBitsError = StatusCode::Configuration::kSetStopBitsError; + static constexpr StatusCodeValue kSetFlowControlError = StatusCode::Configuration::kSetFlowControlError; + static constexpr StatusCodeValue kSetTimeoutError = StatusCode::Configuration::kSetTimeoutError; + + static constexpr StatusCodeValue kNotFoundError = StatusCode::Connection::kNotFoundError; + static constexpr StatusCodeValue kInvalidHandleError = StatusCode::Connection::kInvalidHandleError; + static constexpr StatusCodeValue kCloseHandleError = StatusCode::Connection::kCloseHandleError; + + static constexpr StatusCodeValue kReadError = StatusCode::Io::kReadError; + static constexpr StatusCodeValue kWriteError = StatusCode::Io::kWriteError; + static constexpr StatusCodeValue kAbortReadError = StatusCode::Io::kAbortReadError; + static constexpr StatusCodeValue kAbortWriteError = StatusCode::Io::kAbortWriteError; + static constexpr StatusCodeValue kBufferError = StatusCode::Io::kBufferError; + static constexpr StatusCodeValue kClearBufferInError = StatusCode::Io::kClearBufferInError; + static constexpr StatusCodeValue kClearBufferOutError = StatusCode::Io::kClearBufferOutError; + + static constexpr StatusCodeValue kSetDtrError = StatusCode::Control::kSetDtrError; + static constexpr StatusCodeValue kSetRtsError = StatusCode::Control::kSetRtsError; + static constexpr StatusCodeValue kGetModemStatusError = StatusCode::Control::kGetModemStatusError; + static constexpr StatusCodeValue kSendBreakError = StatusCode::Control::kSendBreakError; + static constexpr StatusCodeValue kGetStateError = StatusCode::Control::kGetStateError; + static constexpr StatusCodeValue kSetStateError = StatusCode::Control::kSetStateError; + + static constexpr StatusCodeValue kMonitorError = StatusCode::Monitor::kMonitorError; +}; } // namespace cpp_core diff --git a/include/cpp_core/status_codes.h b/include/cpp_core/status_codes.h new file mode 100644 index 0000000..62fc8f3 --- /dev/null +++ b/include/cpp_core/status_codes.h @@ -0,0 +1,3 @@ +#pragma once + +#include "status_code.h" 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/tests/ffi_metadata_compile_test.cpp b/tests/ffi_metadata_compile_test.cpp new file mode 100644 index 0000000..6e9957a --- /dev/null +++ b/tests/ffi_metadata_compile_test.cpp @@ -0,0 +1,50 @@ +#include "cpp_core/ffi_metadata.hpp" + +static_assert(__cpp_impl_reflection >= 202603L); + +static_assert(cpp_core::kFunctionDescriptors.size() == 39); +static_assert(cpp_core::kOperationDescriptors.size() == 39); +static_assert(cpp_core::kStatusCodeDescriptors.size() == 24); +static_assert(cpp_core::kSerialConfigFieldDescriptors.size() == 4); +static_assert(cpp_core::kVersionFieldDescriptors.size() == 8); + +constexpr auto kSerialOpen = cpp_core::detail::kDescriptor_serialOpen; +static_assert(kSerialOpen.result_model == cpp_core::FunctionResultModel::kHandleOrStatus); +static_assert(kSerialOpen.return_abi_kind == cpp_core::AbiValueKind::kOpaqueHandle); +static_assert(kSerialOpen.parameters.size() == 6); +static_assert(kSerialOpen.parameters[0].name == "port"); +static_assert(kSerialOpen.parameters[0].abi_kind == cpp_core::AbiValueKind::kUtf8CString); +static_assert(kSerialOpen.parameters[4].optional); +static_assert(kSerialOpen.parameters[5].name == "error_callback"); +static_assert(kSerialOpen.parameters[5].abi_kind == cpp_core::AbiValueKind::kErrorCallback); + +constexpr auto kSerialRead = cpp_core::detail::kDescriptor_serialRead; +static_assert(kSerialRead.result_model == cpp_core::FunctionResultModel::kValueOrStatus); +static_assert(kSerialRead.parameters[1].name == "buffer"); +static_assert(kSerialRead.parameters[1].abi_kind == cpp_core::AbiValueKind::kMutableBuffer); +static_assert(kSerialRead.parameters[1].direction == cpp_core::ParameterDirection::kOut); + +constexpr auto kSetErrorCallback = cpp_core::detail::kDescriptor_serialSetErrorCallback; +static_assert(kSetErrorCallback.result_model == cpp_core::FunctionResultModel::kVoid); +static_assert(kSetErrorCallback.parameters.size() == 1); +static_assert(kSetErrorCallback.parameters[0].abi_kind == cpp_core::AbiValueKind::kErrorCallback); + +static_assert(cpp_core::kStatusCodeDescriptors[10].category == "Io"); +static_assert(cpp_core::kStatusCodeDescriptors[10].name == "ReadError"); +static_assert(cpp_core::kStatusCodeDescriptors[23].category == "Monitor"); +static_assert(cpp_core::kStatusCodeDescriptors[23].name == "MonitorError"); + +static_assert(cpp_core::kSerialConfigFieldDescriptors[0].name == "baudrate"); +static_assert(cpp_core::kSerialConfigFieldDescriptors[1].name == "data_bits"); +static_assert(cpp_core::kSerialConfigFieldDescriptors[2].name == "parity"); +static_assert(cpp_core::kSerialConfigFieldDescriptors[3].name == "stop_bits"); + +static_assert(cpp_core::kVersionFieldDescriptors[0].name == "major"); +static_assert(cpp_core::kVersionFieldDescriptors[3].name == "commit_hash_short"); +static_assert(cpp_core::kVersionFieldDescriptors[7].name == "version_string"); + +static_assert(cpp_core::findFunctionDescriptor("serialOpen") != nullptr); +static_assert(cpp_core::findFunctionDescriptor("serialOpen")->name == "serialOpen"); +static_assert(cpp_core::findOperationDescriptor("serialOpen") != nullptr); +static_assert(cpp_core::findOperationDescriptor("serialOpen")->function_name == "serialOpen"); +static_assert(cpp_core::findStatusCodeDescriptor(cpp_core::StatusCodes::kMonitorError) != nullptr); diff --git a/tools/check_generated_deno_symbols.cmake b/tools/check_generated_deno_symbols.cmake new file mode 100644 index 0000000..f6f8085 --- /dev/null +++ b/tools/check_generated_deno_symbols.cmake @@ -0,0 +1,35 @@ +if(NOT DEFINED GENERATOR) + message(FATAL_ERROR "GENERATOR must point to the cpp_core_bindgen executable.") +endif() + +execute_process( + COMMAND "${GENERATOR}" + RESULT_VARIABLE generator_result + OUTPUT_VARIABLE generator_output + ERROR_VARIABLE generator_error +) + +if(NOT generator_result EQUAL 0) + message(FATAL_ERROR "Generator failed with code ${generator_result}: ${generator_error}") +endif() + +set(required_snippets + "export const symbols = {" + "serialOpen: {" + "result: \"isize\"" + "serialClose: {" + "result: \"i32\"" + "serialInBytesTotal: {" + "result: \"i64\"" + "export const operations = {" + "export class StatusCodeError extends Error" + "export function createBindings(dylib: BindgenLibrary)" + "serialOpen(args: SerialOpenParams): bigint" +) + +foreach(snippet IN LISTS required_snippets) + string(FIND "${generator_output}" "${snippet}" snippet_index) + if(snippet_index EQUAL -1) + message(FATAL_ERROR "Missing expected snippet in generated output: ${snippet}") + endif() +endforeach() diff --git a/tools/generate_deno_symbols.cpp b/tools/generate_deno_symbols.cpp new file mode 100644 index 0000000..b853b3e --- /dev/null +++ b/tools/generate_deno_symbols.cpp @@ -0,0 +1,528 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +struct CliOptions +{ + std::optional output_path; + bool show_help = false; +}; + +[[nodiscard]] constexpr auto toDenoParameterType(cpp_core::AbiValueKind kind) -> std::string_view +{ + switch (kind) + { + case cpp_core::AbiValueKind::kVoid: + return "void"; + case cpp_core::AbiValueKind::kInt32: + return "i32"; + case cpp_core::AbiValueKind::kInt64: + case cpp_core::AbiValueKind::kOpaqueHandle: + return "i64"; + case cpp_core::AbiValueKind::kUtf8CString: + case cpp_core::AbiValueKind::kMutableBuffer: + case cpp_core::AbiValueKind::kConstBuffer: + case cpp_core::AbiValueKind::kOpaquePointer: + case cpp_core::AbiValueKind::kVersionStructPointer: + case cpp_core::AbiValueKind::kErrorCallback: + case cpp_core::AbiValueKind::kNotificationCallback: + return "pointer"; + } + + return "pointer"; +} + +[[nodiscard]] constexpr auto toDenoResultType(const cpp_core::FunctionDescriptor &function) -> std::string_view +{ + switch (function.return_abi_kind) + { + case cpp_core::AbiValueKind::kVoid: + return "void"; + case cpp_core::AbiValueKind::kInt32: + return "i32"; + case cpp_core::AbiValueKind::kInt64: + return "i64"; + case cpp_core::AbiValueKind::kOpaqueHandle: + return "isize"; + case cpp_core::AbiValueKind::kUtf8CString: + case cpp_core::AbiValueKind::kMutableBuffer: + case cpp_core::AbiValueKind::kConstBuffer: + case cpp_core::AbiValueKind::kOpaquePointer: + case cpp_core::AbiValueKind::kVersionStructPointer: + case cpp_core::AbiValueKind::kErrorCallback: + case cpp_core::AbiValueKind::kNotificationCallback: + return "pointer"; + } + + return "void"; +} + +[[nodiscard]] auto toPascalCase(std::string_view text) -> std::string +{ + std::string out; + out.reserve(text.size()); + + bool upper_next = true; + for (const char ch : text) + { + if (ch == '_' || ch == '-' || ch == ' ') + { + upper_next = true; + continue; + } + + if (upper_next) + { + out.push_back(static_cast(std::toupper(static_cast(ch)))); + upper_next = false; + continue; + } + + out.push_back(ch); + } + + return out; +} + +[[nodiscard]] auto toTsParameterType(const cpp_core::ParameterDescriptor ¶meter) -> std::string_view +{ + switch (parameter.abi_kind) + { + case cpp_core::AbiValueKind::kInt32: + return "number"; + case cpp_core::AbiValueKind::kInt64: + case cpp_core::AbiValueKind::kOpaqueHandle: + return "number | bigint"; + case cpp_core::AbiValueKind::kUtf8CString: + return "string | Deno.PointerValue"; + case cpp_core::AbiValueKind::kMutableBuffer: + return "ArrayBufferView | Deno.PointerValue"; + case cpp_core::AbiValueKind::kConstBuffer: + return "string | ArrayBufferView | Deno.PointerValue"; + case cpp_core::AbiValueKind::kOpaquePointer: + case cpp_core::AbiValueKind::kVersionStructPointer: + return "ArrayBufferView | Deno.PointerValue"; + case cpp_core::AbiValueKind::kErrorCallback: + case cpp_core::AbiValueKind::kNotificationCallback: + return "Deno.PointerValue | null"; + case cpp_core::AbiValueKind::kVoid: + return "void"; + } + + return "unknown"; +} + +[[nodiscard]] auto toWrapperReturnType(const cpp_core::FunctionDescriptor &function) -> std::string_view +{ + switch (function.result_model) + { + case cpp_core::FunctionResultModel::kVoid: + case cpp_core::FunctionResultModel::kStatusCode: + return "void"; + case cpp_core::FunctionResultModel::kValueOrStatus: + return function.return_abi_kind == cpp_core::AbiValueKind::kInt64 ? "bigint" : "number"; + case cpp_core::FunctionResultModel::kHandleOrStatus: + return "bigint"; + } + + return "unknown"; +} + +[[nodiscard]] auto usesBufferMarshalling(const cpp_core::FunctionDescriptor &function) -> bool +{ + return std::ranges::any_of(function.parameters, [](const auto ¶meter) { + return parameter.abi_kind == cpp_core::AbiValueKind::kUtf8CString + || parameter.abi_kind == cpp_core::AbiValueKind::kConstBuffer + || parameter.abi_kind == cpp_core::AbiValueKind::kMutableBuffer + || parameter.abi_kind == cpp_core::AbiValueKind::kOpaquePointer + || parameter.abi_kind == cpp_core::AbiValueKind::kVersionStructPointer; + }); +} + +[[nodiscard]] auto sourceExpr(const cpp_core::ParameterDescriptor ¶meter) -> std::string +{ + if (parameter.optional) + { + return std::string(parameter.name); + } + + return "args." + std::string(parameter.name); +} + +auto printUsage(std::ostream &stream) -> void +{ + stream << "Usage: cpp_core_bindgen [--output PATH] [--help]\n"; + stream << "Generates a Deno.dlopen-compatible TypeScript module from cpp-core FFI metadata.\n"; +} + +[[nodiscard]] auto parseArgs(int argc, char **argv, CliOptions &options) -> bool +{ + for (int index = 1; index < argc; ++index) + { + const std::string_view arg = argv[index]; + if (arg == "--help" || arg == "-h") + { + options.show_help = true; + return true; + } + + if (arg == "--output") + { + if (index + 1 >= argc) + { + return false; + } + + options.output_path = std::string(argv[++index]); + continue; + } + + return false; + } + + return true; +} + +auto writeHeader(std::ostream &stream) -> void +{ + stream << "// Generated from cpp-core reflection metadata.\n"; + stream << "// Do not edit manually.\n"; + stream << "// Intended for use with Deno.dlopen.\n"; + stream << "// ABI mode: legacy.\n\n"; +} + +auto writeSymbols(std::ostream &stream) -> void +{ + stream << "export const symbols = {\n"; + + for (const auto &function : cpp_core::functionDescriptors()) + { + stream << " " << function.name << ": {\n"; + stream << " parameters: ["; + + for (std::size_t index = 0; index < function.parameters.size(); ++index) + { + if (index != 0) + { + stream << ", "; + } + + stream << '"' << toDenoParameterType(function.parameters[index].abi_kind) << '"'; + } + + stream << "],\n"; + stream << " result: \"" << toDenoResultType(function) << "\",\n"; + stream << " },\n"; + } + + stream << "} as const;\n\n"; +} + +auto writeOperations(std::ostream &stream) -> void +{ + stream << "export const operations = {\n"; + for (const auto &operation : cpp_core::operationDescriptors()) + { + stream << " " << operation.name << ": { symbol: \"" << operation.function_name << "\" },\n"; + } + stream << "} as const;\n\n"; +} + +auto writeStatusCodes(std::ostream &stream) -> void +{ + stream << "export const statusCodes = {\n"; + for (const auto &descriptor : cpp_core::statusCodeDescriptors()) + { + stream << " " << descriptor.name << ": " << descriptor.value << ",\n"; + } + stream << "} as const;\n\n"; + + stream << "export const statusCodeInfo = {\n"; + for (const auto &descriptor : cpp_core::statusCodeDescriptors()) + { + stream << " \"" << descriptor.value << "\": { category: \"" << descriptor.category << "\", name: \"" + << descriptor.name << "\" },\n"; + } + stream << "} as const;\n\n"; +} + +auto writeParamInterfaces(std::ostream &stream) -> void +{ + for (const auto &function : cpp_core::functionDescriptors()) + { + stream << "export interface " << toPascalCase(function.name) << "Params {\n"; + for (const auto ¶meter : function.parameters) + { + stream << " " << parameter.name << (parameter.optional ? "?: " : ": ") << toTsParameterType(parameter) + << ";\n"; + } + stream << "}\n\n"; + } +} + +auto writeHelpers(std::ostream &stream) -> void +{ + stream << "const textEncoder = new TextEncoder();\n\n"; + stream << "type KeepAlive = ArrayBufferView[];\n\n"; + stream << "function isArrayBufferView(value: unknown): value is ArrayBufferView {\n"; + stream << " return ArrayBuffer.isView(value);\n"; + stream << "}\n\n"; + stream << "function isPointerValue(value: unknown): value is Exclude {\n"; + stream << " return typeof value === \"number\" || typeof value === \"bigint\";\n"; + stream << "}\n\n"; + stream << "function requirePointer(\n"; + stream << " value: Deno.PointerValue | ArrayBufferView | null | undefined,\n"; + stream << " name: string,\n"; + stream << " keepAlive?: KeepAlive,\n"; + stream << "): Deno.PointerValue {\n"; + stream << " if (isPointerValue(value)) {\n"; + stream << " return value;\n"; + stream << " }\n"; + stream << " if (value == null || !isArrayBufferView(value)) {\n"; + stream << " throw new TypeError(`${name} must be a Deno.PointerValue or ArrayBufferView`);\n"; + stream << " }\n"; + stream << " keepAlive?.push(value);\n"; + stream << " const pointer = Deno.UnsafePointer.of(value as BufferSource);\n"; + stream << " if (pointer === null) {\n"; + stream << " throw new TypeError(`Failed to get pointer for ${name}`);\n"; + stream << " }\n"; + stream << " return pointer;\n"; + stream << "}\n\n"; + stream << "function marshalCString(value: string | Deno.PointerValue, name: string, keepAlive: KeepAlive): Deno.PointerValue {\n"; + stream << " if (typeof value === \"string\") {\n"; + stream << " const bytes = textEncoder.encode(`${value}\\0`);\n"; + stream << " keepAlive.push(bytes);\n"; + stream << " return requirePointer(bytes, name, keepAlive);\n"; + stream << " }\n"; + stream << " return requirePointer(value, name);\n"; + stream << "}\n\n"; + stream << "function marshalConstBuffer(\n"; + stream << " value: string | ArrayBufferView | Deno.PointerValue,\n"; + stream << " name: string,\n"; + stream << " keepAlive: KeepAlive,\n"; + stream << "): Deno.PointerValue {\n"; + stream << " if (typeof value === \"string\") {\n"; + stream << " const bytes = textEncoder.encode(value);\n"; + stream << " keepAlive.push(bytes);\n"; + stream << " return requirePointer(bytes, name, keepAlive);\n"; + stream << " }\n"; + stream << " return requirePointer(value, name, keepAlive);\n"; + stream << "}\n\n"; + stream << "function marshalMutableBuffer(\n"; + stream << " value: ArrayBufferView | Deno.PointerValue,\n"; + stream << " name: string,\n"; + stream << " keepAlive: KeepAlive,\n"; + stream << "): Deno.PointerValue {\n"; + stream << " return requirePointer(value, name, keepAlive);\n"; + stream << "}\n\n"; + stream << "function toInt64(value: number | bigint): bigint {\n"; + stream << " return typeof value === \"bigint\" ? value : BigInt(value);\n"; + stream << "}\n\n"; + stream << "export class StatusCodeError extends Error {\n"; + stream << " readonly code: number;\n"; + stream << " readonly category: string;\n"; + stream << " readonly statusName: string;\n\n"; + stream << " constructor(code: number | bigint) {\n"; + stream << " const normalized = typeof code === \"bigint\" ? Number(code) : code;\n"; + stream << " const info = statusCodeInfo[String(normalized) as keyof typeof statusCodeInfo];\n"; + stream << " const category = info?.category ?? \"Unknown\";\n"; + stream << " const statusName = info?.name ?? \"UnknownStatus\";\n"; + stream << " super(`${category}::${statusName} (${normalized})`);\n"; + stream << " this.name = \"StatusCodeError\";\n"; + stream << " this.code = normalized;\n"; + stream << " this.category = category;\n"; + stream << " this.statusName = statusName;\n"; + stream << " }\n"; + stream << "}\n\n"; + stream << "function assertStatus(status: number | bigint): void {\n"; + stream << " const normalized = typeof status === \"bigint\" ? Number(status) : status;\n"; + stream << " if (normalized < 0) {\n"; + stream << " throw new StatusCodeError(normalized);\n"; + stream << " }\n"; + stream << "}\n\n"; +} + +auto writeWrapperBody(std::ostream &stream, const cpp_core::FunctionDescriptor &function) -> void +{ + const bool use_keep_alive = usesBufferMarshalling(function); + if (use_keep_alive) + { + stream << " const keepAlive: KeepAlive = [];\n"; + } + + for (const auto ¶meter : function.parameters) + { + if (!parameter.optional) + { + continue; + } + + stream << " const " << parameter.name << " = args." << parameter.name; + if (parameter.abi_kind == cpp_core::AbiValueKind::kInt32) + { + stream << " ?? 0"; + } + else if (parameter.abi_kind == cpp_core::AbiValueKind::kErrorCallback + || parameter.abi_kind == cpp_core::AbiValueKind::kNotificationCallback) + { + stream << " ?? null"; + } + stream << ";\n"; + } + + for (const auto ¶meter : function.parameters) + { + const std::string source = sourceExpr(parameter); + switch (parameter.abi_kind) + { + case cpp_core::AbiValueKind::kUtf8CString: + stream << " const " << parameter.name << "Pointer = marshalCString(" << source << ", \"" + << parameter.name << "\", keepAlive);\n"; + break; + case cpp_core::AbiValueKind::kConstBuffer: + stream << " const " << parameter.name << "Pointer = marshalConstBuffer(" << source << ", \"" + << parameter.name << "\", keepAlive);\n"; + break; + case cpp_core::AbiValueKind::kMutableBuffer: + stream << " const " << parameter.name << "Pointer = marshalMutableBuffer(" << source << ", \"" + << parameter.name << "\", keepAlive);\n"; + break; + case cpp_core::AbiValueKind::kOpaquePointer: + case cpp_core::AbiValueKind::kVersionStructPointer: + stream << " const " << parameter.name << "Pointer = requirePointer(" << source << ", \"" << parameter.name + << "\", keepAlive);\n"; + break; + case cpp_core::AbiValueKind::kInt64: + case cpp_core::AbiValueKind::kOpaqueHandle: + stream << " const " << parameter.name << "Value = toInt64(" << source << ");\n"; + break; + default: + break; + } + } + + stream << " const rawResult = dylib.symbols." << function.name << "("; + for (std::size_t index = 0; index < function.parameters.size(); ++index) + { + if (index != 0) + { + stream << ", "; + } + + const auto ¶meter = function.parameters[index]; + switch (parameter.abi_kind) + { + case cpp_core::AbiValueKind::kUtf8CString: + case cpp_core::AbiValueKind::kConstBuffer: + case cpp_core::AbiValueKind::kMutableBuffer: + case cpp_core::AbiValueKind::kOpaquePointer: + case cpp_core::AbiValueKind::kVersionStructPointer: + stream << parameter.name << "Pointer"; + break; + case cpp_core::AbiValueKind::kInt64: + case cpp_core::AbiValueKind::kOpaqueHandle: + stream << parameter.name << "Value"; + break; + default: + stream << sourceExpr(parameter); + break; + } + } + stream << ");\n"; + + if (use_keep_alive) + { + stream << " void keepAlive;\n"; + } + + switch (function.result_model) + { + case cpp_core::FunctionResultModel::kVoid: + stream << " return rawResult;\n"; + break; + case cpp_core::FunctionResultModel::kStatusCode: + stream << " assertStatus(rawResult);\n"; + stream << " return;\n"; + break; + case cpp_core::FunctionResultModel::kValueOrStatus: + case cpp_core::FunctionResultModel::kHandleOrStatus: + stream << " assertStatus(rawResult);\n"; + stream << " return rawResult;\n"; + break; + } +} + +auto writeWrapperFunctions(std::ostream &stream) -> void +{ + stream << "export type BindgenLibrary = Deno.DynamicLibrary;\n"; + stream << "export type BindgenSymbols = BindgenLibrary[\"symbols\"];\n\n"; + stream << "export function createBindings(dylib: BindgenLibrary) {\n"; + stream << " return {\n"; + + for (const auto &function : cpp_core::functionDescriptors()) + { + stream << " " << function.name << "(args: " << toPascalCase(function.name) << "Params): " + << toWrapperReturnType(function) << " {\n"; + writeWrapperBody(stream, function); + stream << " },\n"; + } + + stream << " };\n"; + stream << "}\n\n"; + stream << "export type GeneratedBindings = ReturnType;\n"; +} + +auto writeModule(std::ostream &stream) -> void +{ + writeHeader(stream); + writeSymbols(stream); + writeOperations(stream); + writeStatusCodes(stream); + writeParamInterfaces(stream); + writeHelpers(stream); + writeWrapperFunctions(stream); +} + +} // namespace + +auto main(int argc, char **argv) -> int +{ + CliOptions options; + if (!parseArgs(argc, argv, options)) + { + printUsage(std::cerr); + return 1; + } + + if (options.show_help) + { + printUsage(std::cout); + return 0; + } + + if (options.output_path.has_value()) + { + std::ofstream output(*options.output_path); + if (!output) + { + std::cerr << "Failed to open output path: " << *options.output_path << '\n'; + return 1; + } + + writeModule(output); + return 0; + } + + writeModule(std::cout); + return 0; +} From 83e6c2439a6cdcc666ef95a0fbbc959a675df965 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sat, 6 Jun 2026 22:46:34 +0200 Subject: [PATCH 02/28] feat: add mingw toolchain and preset --- CMakeLists.txt | 8 +- CMakePresets.json | 23 ++++++ README.md | 128 ++++++++++++++++++++++++++------ cmake/mingw-toolchain.cmake | 38 ++++++++++ docs/ffi-metadata.md | 36 ++++----- tools/generate_deno_symbols.cpp | 27 +++---- 6 files changed, 202 insertions(+), 58 deletions(-) create mode 100644 cmake/mingw-toolchain.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ecbfb7..7126a50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,14 +6,14 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) project(cpp-core LANGUAGES CXX) if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - message(FATAL_ERROR "This experimental branch requires GCC 16+ with -std=c++26 and -freflection.") + message(FATAL_ERROR "cpp-core requires GCC 16+ with -std=c++26 and -freflection.") endif() if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16) - message(FATAL_ERROR "This experimental branch requires GCC 16 or newer.") + message(FATAL_ERROR "cpp-core requires GCC 16 or newer.") endif() -# This branch intentionally targets a single experimental toolchain. +# The project currently targets a single toolchain with C++26 reflection support. set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -63,7 +63,7 @@ get_git_version_info() generate_git_version(OUTPUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include/cpp_core) set(PROJECT_VERSION "${GIT_VERSION_MAJOR}.${GIT_VERSION_MINOR}.${GIT_VERSION_PATCH}") -set(PROJECT_DESCRIPTION "Experimental GCC 16 / C++26 reflection branch for shared serial FFI metadata") +set(PROJECT_DESCRIPTION "Shared serial API and FFI metadata for GCC 16 / C++26 reflection builds") # Header-only library -------------------------------------------------------- add_library(cpp_core INTERFACE) 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/README.md b/README.md index e4c7770..d35dd98 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Header-only API definition library for cross-platform serial communication. Defi > [!IMPORTANT] > -> This branch is an **experimental GCC 16 / C++26 / reflection** branch. +> This version of `cpp-core` requires **GCC 16**, **C++26**, and **reflection support**. > > **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) @@ -26,20 +26,14 @@ Header-only API definition library for cross-platform serial communication. Defi * `-std=c++26` and `-freflection` * Git (for automatic version detection) -Clang and MSVC are intentionally not supported on this branch. +Clang and MSVC are not supported. -## Experimental Direction +## C Interface -This branch keeps the **C ABI** as the external contract while moving internal metadata to a reflection-backed C++26 model. +`cpp-core` defines a C-compatible interface for the platform-specific binding libraries and exposes reflection-backed +metadata for that same interface. -That split is intentional: - -* the **runtime ABI** stays easy to consume from Deno FFI and other foreign runtimes -* the **C++ authoring model** gains a single metadata source for status codes, shared structs, and exported function signatures -* bind generation for Deno bindings can read `cpp_core::kFunctionDescriptors`, `cpp_core::kOperationDescriptors`, - and the reflected struct field descriptors instead of re-parsing handwritten headers - -The new metadata entrypoint is: +Metadata entrypoints: ```cpp #include @@ -50,32 +44,122 @@ constexpr auto statuses = cpp_core::statusCodeDescriptors(); constexpr auto config_fields = cpp_core::serialConfigFieldDescriptors(); ``` -More practical usage examples and the intended Deno/FFI workflow are documented in [docs/ffi-metadata.md](docs/ffi-metadata.md). +More practical usage examples are documented in [docs/ffi-metadata.md](docs/ffi-metadata.md). -There is also a bindgen target now: +Bindgen target: ```sh cmake --build build/gcc --target cpp_core_bindgen ./build/gcc/cpp_core_bindgen --output deno_bindings.ts ``` -The generated module now contains: +The generated module contains: * `symbols` for `Deno.dlopen` * `operations` metadata * `statusCodes` plus `StatusCodeError` * `createBindings(dylib)` which returns named TypeScript wrapper functions -## Why This Helps Already +Example: -The new metadata layer is useful even before any full code generator exists: +```ts +import { + createBindings, + operations, + StatusCodeError, + statusCodeInfo, + statusCodes, + symbols, +} from "./deno_bindings.ts"; + +const dylib = Deno.dlopen("./libcpp_bindings_linux.so", symbols); +const serial = createBindings(dylib); +const decoder = new TextDecoder(); + +let handle: bigint | undefined; + +function describeStatus(code: number) { + const info = statusCodeInfo[String(code) as keyof typeof statusCodeInfo]; + return info ? `${info.category}::${info.name}` : `UnknownStatus (${code})`; +} + +try { + console.log("opening via symbol", operations.serialOpen.symbol); + + handle = serial.serialOpen({ + port: "/dev/ttyUSB0", + baudrate: 115200, + data_bits: 8, + parity: 0, + stop_bits: 0, + }); + + const writeBuffer = new Uint8Array([0x41, 0x54, 0x49, 0x0d, 0x0a]); + const bytesWritten = serial.serialWrite({ + handle, + buffer: writeBuffer, + buffer_size: writeBuffer.byteLength, + timeout_ms: 500, + multiplier: 0, + }); + + const queued = serial.serialInBytesWaiting({ handle }); + const readBuffer = new Uint8Array(256); + const bytesRead = serial.serialRead({ + handle, + buffer: readBuffer, + buffer_size: readBuffer.byteLength, + timeout_ms: 500, + multiplier: 0, + }); + + const payload = decoder.decode(readBuffer.subarray(0, bytesRead)); + + console.log({ + handle, + bytesWritten, + queued, + bytesRead, + payload, + }); +} catch (error) { + if (error instanceof StatusCodeError) { + if (error.code === statusCodes.NotFoundError) { + console.error("port not found"); + } else if (error.code === statusCodes.ReadError) { + console.error("read failed"); + } else { + console.error(describeStatus(error.code)); + } + throw error; + } + throw error; +} finally { + if (handle !== undefined) { + try { + serial.serialClose({ handle }); + } catch (error) { + if (error instanceof StatusCodeError) { + console.error("close failed:", describeStatus(error.code)); + } else { + console.error("close failed:", error); + } + } + } + dylib.close(); +} +``` + +## Benefits + +The metadata layer provides: * **One source of truth** for exported function names, parameter names, return semantics, and status categories * **No manual re-parsing of headers** when you want to derive Deno FFI symbol definitions or internal binding manifests * **Generated wrapper functions** with named parameters, result decoding, and status-to-exception conversion * **Safer refactors** because compile-time metadata checks fail when the exposed signatures drift * **Shared understanding of the ABI** because buffers, callbacks, UTF-8 strings, opaque handles, and status-returning functions are classified centrally -* **Less version drift in consumers** because the same `cpp-core` checkout now provides both the headers and the generator used by Linux/Windows builds +* **Less version drift in consumers** because the same `cpp-core` checkout provides both the headers and the generator used by Linux/Windows builds ## Version Information @@ -125,9 +209,9 @@ intptr_t serialOpen( } ``` -## Error Handling Today and Later +## Error Handling -The current ABI remains `status + out-params` or `value-or-negative-status`, because that maps cleanly to plain C and Deno FFI. +The API uses `status + out-params` or `value-or-negative-status`, because that maps cleanly to plain C and Deno FFI. ```c intptr_t serialOpen( @@ -140,9 +224,7 @@ intptr_t serialOpen( ); ``` -Internally the implementation can still use `std::expected`-style flows and translate them at the ABI edge into the plain C model. - -The corresponding Deno FFI shape would stay straightforward: +The corresponding Deno FFI shape is straightforward: ```ts const symbols = { diff --git a/cmake/mingw-toolchain.cmake b/cmake/mingw-toolchain.cmake new file mode 100644 index 0000000..0acb898 --- /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 Deno-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/docs/ffi-metadata.md b/docs/ffi-metadata.md index a1d85b2..e043f1f 100644 --- a/docs/ffi-metadata.md +++ b/docs/ffi-metadata.md @@ -1,15 +1,15 @@ # FFI Metadata Guide -This branch adds a reflection-backed metadata layer in `cpp_core` for the public C ABI. +`cpp_core` provides a reflection-backed metadata layer for the public C ABI. The important distinction is: * the exported runtime interface is still plain C ABI -* the description of that interface is now available as typed compile-time metadata in C++ +* the description of that interface is available as typed compile-time metadata in C++ -That gives you a place to inspect the ABI, build tooling around it, and later generate bindings from it without maintaining a second hand-written schema. +That gives you a place to inspect the ABI and build tooling around it without maintaining a second hand-written schema. -## What Exists Today +## Available Metadata Include: @@ -28,7 +28,7 @@ Main entrypoints: * `cpp_core::findOperationDescriptor(name)` * `cpp_core::findStatusCodeDescriptor(code)` -Generator tooling in this branch: +Generator tooling: * `cpp_core_bindgen` generates a Deno-oriented TypeScript module from the same metadata used by the headers * the generated module includes `symbols`, metadata exports, `StatusCodeError`, and `createBindings(dylib)` @@ -52,11 +52,11 @@ That caused three problems: * tools had to re-parse or duplicate knowledge about functions and parameters * buffer/callback/string/handle semantics were implicit and spread across comments -* future Deno binding generation had no canonical machine-readable source inside `cpp-core` +* Deno binding generation had no canonical machine-readable source inside `cpp-core` Now the ABI description is available in a typed form inside the library itself. -## Practical Benefits Right Now +## Benefits ### 1. You can inspect the exported ABI programmatically @@ -113,7 +113,7 @@ The metadata layer makes that distinction explicit. Each function is classified with `FunctionResultModel`. -Current models: +Result models: * `kVoid` * `kStatusCode` @@ -159,7 +159,7 @@ This is useful for: `SerialConfig` and `Version` expose field descriptors. -That is the first step toward: +This is useful for: * generated docs for shared data * generated FFI struct definitions and byte-layout helpers @@ -197,9 +197,9 @@ Typical use case: ## Example: Derive Deno-Oriented Symbol Metadata -This is not a full generator yet, but the metadata is now good enough to drive one. +The metadata is sufficient to drive a generator directly. -There is now a bindgen CLI tool for this in the repo: +Bindgen CLI: ```sh cmake --build build/gcc --target cpp_core_bindgen @@ -242,13 +242,13 @@ constexpr auto toDenoType(cpp_core::AbiValueKind kind) -> const char * } ``` -The current generator already emits an importable module and wraps the current legacy ABI: +The generator emits an importable module and wraps the legacy ABI: ```ts // Generated from cpp-core reflection metadata. // Do not edit manually. // Intended for use with Deno.dlopen. -// ABI mode: preferred. +// ABI mode: legacy. export const symbols = { serialOpen: { @@ -274,14 +274,14 @@ export function createBindings(dylib: Deno.DynamicLibrary) { } ``` -## What This Does Not Do Yet +## Scope -Current limitations: +Limitations: * it does not encode every possible semantic rule for every parameter * it does not expose `std::expected` itself across the external ABI -That is deliberate. The branch first makes the ABI description explicit and compile-checked. +That is deliberate. The ABI description is explicit and compile-checked. ## Relation To `std::expected` @@ -297,9 +297,9 @@ Instead, the model is: That gives you most of the practical benefits of structured error handling without making the FFI surface harder to consume. -## Recommended Current Usage +## Usage -Today, the best use of the new layer is: +The metadata layer is useful for: * inspect exported functions from tooling or tests * generate a Deno metadata module with `cpp_core_bindgen` diff --git a/tools/generate_deno_symbols.cpp b/tools/generate_deno_symbols.cpp index b853b3e..9813456 100644 --- a/tools/generate_deno_symbols.cpp +++ b/tools/generate_deno_symbols.cpp @@ -141,11 +141,11 @@ struct CliOptions [[nodiscard]] auto usesBufferMarshalling(const cpp_core::FunctionDescriptor &function) -> bool { return std::ranges::any_of(function.parameters, [](const auto ¶meter) { - return parameter.abi_kind == cpp_core::AbiValueKind::kUtf8CString - || parameter.abi_kind == cpp_core::AbiValueKind::kConstBuffer - || parameter.abi_kind == cpp_core::AbiValueKind::kMutableBuffer - || parameter.abi_kind == cpp_core::AbiValueKind::kOpaquePointer - || parameter.abi_kind == cpp_core::AbiValueKind::kVersionStructPointer; + return parameter.abi_kind == cpp_core::AbiValueKind::kUtf8CString || + parameter.abi_kind == cpp_core::AbiValueKind::kConstBuffer || + parameter.abi_kind == cpp_core::AbiValueKind::kMutableBuffer || + parameter.abi_kind == cpp_core::AbiValueKind::kOpaquePointer || + parameter.abi_kind == cpp_core::AbiValueKind::kVersionStructPointer; }); } @@ -198,7 +198,7 @@ auto writeHeader(std::ostream &stream) -> void stream << "// Generated from cpp-core reflection metadata.\n"; stream << "// Do not edit manually.\n"; stream << "// Intended for use with Deno.dlopen.\n"; - stream << "// ABI mode: legacy.\n\n"; + stream << "// ABI mode: modern.\n\n"; } auto writeSymbols(std::ostream &stream) -> void @@ -298,7 +298,8 @@ auto writeHelpers(std::ostream &stream) -> void stream << " }\n"; stream << " return pointer;\n"; stream << "}\n\n"; - stream << "function marshalCString(value: string | Deno.PointerValue, name: string, keepAlive: KeepAlive): Deno.PointerValue {\n"; + stream << "function marshalCString(value: string | Deno.PointerValue, name: string, keepAlive: KeepAlive): " + "Deno.PointerValue {\n"; stream << " if (typeof value === \"string\") {\n"; stream << " const bytes = textEncoder.encode(`${value}\\0`);\n"; stream << " keepAlive.push(bytes);\n"; @@ -372,8 +373,8 @@ auto writeWrapperBody(std::ostream &stream, const cpp_core::FunctionDescriptor & { stream << " ?? 0"; } - else if (parameter.abi_kind == cpp_core::AbiValueKind::kErrorCallback - || parameter.abi_kind == cpp_core::AbiValueKind::kNotificationCallback) + else if (parameter.abi_kind == cpp_core::AbiValueKind::kErrorCallback || + parameter.abi_kind == cpp_core::AbiValueKind::kNotificationCallback) { stream << " ?? null"; } @@ -399,8 +400,8 @@ auto writeWrapperBody(std::ostream &stream, const cpp_core::FunctionDescriptor & break; case cpp_core::AbiValueKind::kOpaquePointer: case cpp_core::AbiValueKind::kVersionStructPointer: - stream << " const " << parameter.name << "Pointer = requirePointer(" << source << ", \"" << parameter.name - << "\", keepAlive);\n"; + stream << " const " << parameter.name << "Pointer = requirePointer(" << source << ", \"" + << parameter.name << "\", keepAlive);\n"; break; case cpp_core::AbiValueKind::kInt64: case cpp_core::AbiValueKind::kOpaqueHandle: @@ -471,8 +472,8 @@ auto writeWrapperFunctions(std::ostream &stream) -> void for (const auto &function : cpp_core::functionDescriptors()) { - stream << " " << function.name << "(args: " << toPascalCase(function.name) << "Params): " - << toWrapperReturnType(function) << " {\n"; + stream << " " << function.name << "(args: " << toPascalCase(function.name) + << "Params): " << toWrapperReturnType(function) << " {\n"; writeWrapperBody(stream, function); stream << " },\n"; } From 78fa350ce7547ef221ea2ee7a01ab3b2dd9d1fd0 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sun, 7 Jun 2026 16:05:31 +0200 Subject: [PATCH 03/28] feat: implement ABI registry and status code management for serial operations --- include/cpp_core/abi_registry.hpp | 42 +++++ include/cpp_core/ffi_metadata.hpp | 206 ++++++++++------------ include/cpp_core/status_code.h | 103 +++-------- include/cpp_core/status_code_registry.hpp | 41 +++++ tests/ffi_metadata_compile_test.cpp | 14 +- 5 files changed, 212 insertions(+), 194 deletions(-) create mode 100644 include/cpp_core/abi_registry.hpp create mode 100644 include/cpp_core/status_code_registry.hpp diff --git a/include/cpp_core/abi_registry.hpp b/include/cpp_core/abi_registry.hpp new file mode 100644 index 0000000..56b170a --- /dev/null +++ b/include/cpp_core/abi_registry.hpp @@ -0,0 +1,42 @@ +#pragma once + +#define CPP_CORE_ABI_FUNCTION_LIST(X) \ + X(getVersion) \ + X(serialAbortRead) \ + X(serialAbortWrite) \ + X(serialClearBufferIn) \ + X(serialClearBufferOut) \ + X(serialClose) \ + X(serialDrain) \ + X(serialGetBaudrate) \ + X(serialGetCts) \ + X(serialGetDataBits) \ + X(serialGetDcd) \ + X(serialGetDsr) \ + X(serialGetFlowControl) \ + X(serialGetParity) \ + X(serialGetRi) \ + X(serialGetStopBits) \ + X(serialInBytesTotal) \ + X(serialInBytesWaiting) \ + X(serialListPorts) \ + X(serialMonitorPorts) \ + X(serialOpen) \ + X(serialOutBytesTotal) \ + X(serialOutBytesWaiting) \ + X(serialRead) \ + X(serialReadLine) \ + X(serialReadUntil) \ + X(serialReadUntilSequence) \ + X(serialSendBreak) \ + X(serialSetBaudrate) \ + X(serialSetDataBits) \ + X(serialSetDtr) \ + X(serialSetErrorCallback) \ + X(serialSetFlowControl) \ + X(serialSetParity) \ + X(serialSetReadCallback) \ + X(serialSetRts) \ + X(serialSetStopBits) \ + X(serialSetWriteCallback) \ + X(serialWrite) diff --git a/include/cpp_core/ffi_metadata.hpp b/include/cpp_core/ffi_metadata.hpp index 0b90b11..eeeccc9 100644 --- a/include/cpp_core/ffi_metadata.hpp +++ b/include/cpp_core/ffi_metadata.hpp @@ -1,13 +1,16 @@ #pragma once +#include "abi_registry.hpp" #include "serial.h" #include "serial_config.hpp" #include "status_code.h" +#include "status_code_registry.hpp" #include #include #include #include +#include #include #include #include @@ -94,6 +97,22 @@ struct StructFieldDescriptor namespace detail { +[[nodiscard]] consteval auto overrideParameterAbiValueKind(std::string_view function_name, std::string_view parameter_name, + std::meta::info type_info) -> std::optional +{ + if (!std::meta::is_pointer_type(type_info)) + { + return std::nullopt; + } + + if (function_name == "getVersion" && parameter_name == "out") + { + return AbiValueKind::kVersionStructPointer; + } + + return std::nullopt; +} + [[nodiscard]] consteval auto isConstPointer(std::meta::info type_info) -> bool { if (!std::meta::is_pointer_type(type_info)) @@ -164,6 +183,28 @@ namespace detail return AbiValueKind::kOpaquePointer; } +[[nodiscard]] consteval auto inferParameterAbiValueKind(std::string_view function_name, std::string_view parameter_name, + std::meta::info type_info) -> AbiValueKind +{ + if (const auto override = overrideParameterAbiValueKind(function_name, parameter_name, type_info)) + { + return *override; + } + + return inferAbiValueKind(type_info, parameter_name); +} + +[[nodiscard]] consteval auto overrideReturnAbiValueKind(std::string_view function_name) + -> std::optional +{ + if (function_name == "serialOpen") + { + return AbiValueKind::kOpaqueHandle; + } + + return std::nullopt; +} + [[nodiscard]] consteval auto inferReturnAbiValueKind(std::string_view function_name, std::meta::info return_type_info) -> AbiValueKind { @@ -172,9 +213,9 @@ namespace detail return AbiValueKind::kVoid; } - if (function_name == "serialOpen") + if (const auto override = overrideReturnAbiValueKind(function_name)) { - return AbiValueKind::kOpaqueHandle; + return *override; } if (std::meta::is_integral_type(return_type_info)) @@ -185,9 +226,25 @@ namespace detail return AbiValueKind::kOpaquePointer; } +[[nodiscard]] consteval auto overrideParameterDirection(std::string_view function_name, std::string_view parameter_name) + -> std::optional +{ + if (function_name == "getVersion" && parameter_name == "out") + { + return ParameterDirection::kOut; + } + + return std::nullopt; +} + [[nodiscard]] consteval auto inferParameterDirection(std::string_view function_name, std::string_view parameter_name, std::meta::info type_info) -> ParameterDirection { + if (const auto override = overrideParameterDirection(function_name, parameter_name)) + { + return *override; + } + if (parameter_name.ends_with("_callback") || parameter_name == "callback_fn" || parameter_name == "error_callback") { return ParameterDirection::kIn; @@ -224,6 +281,17 @@ namespace detail return ParameterDirection::kIn; } +[[nodiscard]] consteval auto overrideResultModel(std::string_view function_name) + -> std::optional +{ + if (function_name == "serialOpen") + { + return FunctionResultModel::kHandleOrStatus; + } + + return std::nullopt; +} + [[nodiscard]] consteval auto inferResultModel(std::string_view function_name, std::meta::info return_type_info) -> FunctionResultModel { @@ -232,9 +300,9 @@ namespace detail return FunctionResultModel::kVoid; } - if (function_name == "serialOpen") + if (const auto override = overrideResultModel(function_name)) { - return FunctionResultModel::kHandleOrStatus; + return *override; } if (function_name.starts_with("serialRead") || function_name.starts_with("serialWrite") @@ -256,7 +324,7 @@ namespace detail return ParameterDescriptor{ .name = parameter_name, .cpp_type = std::meta::display_string_of(type_info), - .abi_kind = inferAbiValueKind(type_info, parameter_name), + .abi_kind = inferParameterAbiValueKind(function_name, parameter_name, type_info), .direction = inferParameterDirection(function_name, parameter_name, type_info), .optional = std::meta::has_default_argument(parameter_info), }; @@ -303,6 +371,15 @@ template }; } +[[nodiscard]] consteval auto makeOperationDescriptor(std::meta::info function_info) -> OperationDescriptor +{ + const auto function_name = std::meta::identifier_of(function_info); + return OperationDescriptor{ + .name = function_name, + .function_name = function_name, + }; +} + template [[nodiscard]] consteval auto makeStatusCodeDescriptor(const Code &code_value, std::string_view fallback_category = {}) -> StatusCodeDescriptor @@ -317,47 +394,6 @@ template } // namespace detail -#define CPP_CORE_FFI_FUNCTION_LIST(X) \ - X(getVersion) \ - X(serialAbortRead) \ - X(serialAbortWrite) \ - X(serialClearBufferIn) \ - X(serialClearBufferOut) \ - X(serialClose) \ - X(serialDrain) \ - X(serialGetBaudrate) \ - X(serialGetCts) \ - X(serialGetDataBits) \ - X(serialGetDcd) \ - X(serialGetDsr) \ - X(serialGetFlowControl) \ - X(serialGetParity) \ - X(serialGetRi) \ - X(serialGetStopBits) \ - X(serialInBytesTotal) \ - X(serialInBytesWaiting) \ - X(serialListPorts) \ - X(serialMonitorPorts) \ - X(serialOpen) \ - X(serialOutBytesTotal) \ - X(serialOutBytesWaiting) \ - X(serialRead) \ - X(serialReadLine) \ - X(serialReadUntil) \ - X(serialReadUntilSequence) \ - X(serialSendBreak) \ - X(serialSetBaudrate) \ - X(serialSetDataBits) \ - X(serialSetDtr) \ - X(serialSetErrorCallback) \ - X(serialSetFlowControl) \ - X(serialSetParity) \ - X(serialSetReadCallback) \ - X(serialSetRts) \ - X(serialSetStopBits) \ - X(serialSetWriteCallback) \ - X(serialWrite) - #define CPP_CORE_DECLARE_FFI_METADATA(FunctionName) \ namespace detail \ { \ @@ -370,83 +406,31 @@ template inline constexpr auto kDescriptor_##FunctionName = makeFunctionDescriptor(^^FunctionName, kParameters_##FunctionName); \ } -CPP_CORE_FFI_FUNCTION_LIST(CPP_CORE_DECLARE_FFI_METADATA) +CPP_CORE_ABI_FUNCTION_LIST(CPP_CORE_DECLARE_FFI_METADATA) #undef CPP_CORE_DECLARE_FFI_METADATA inline constexpr auto kFunctionDescriptors = std::array{ #define CPP_CORE_FFI_DESCRIPTOR_ENTRY(FunctionName) detail::kDescriptor_##FunctionName, - CPP_CORE_FFI_FUNCTION_LIST(CPP_CORE_FFI_DESCRIPTOR_ENTRY) + CPP_CORE_ABI_FUNCTION_LIST(CPP_CORE_FFI_DESCRIPTOR_ENTRY) #undef CPP_CORE_FFI_DESCRIPTOR_ENTRY }; inline constexpr auto kOperationDescriptors = std::array{ - OperationDescriptor{"getVersion", "getVersion"}, - OperationDescriptor{"serialAbortRead", "serialAbortRead"}, - OperationDescriptor{"serialAbortWrite", "serialAbortWrite"}, - OperationDescriptor{"serialClearBufferIn", "serialClearBufferIn"}, - OperationDescriptor{"serialClearBufferOut", "serialClearBufferOut"}, - OperationDescriptor{"serialClose", "serialClose"}, - OperationDescriptor{"serialDrain", "serialDrain"}, - OperationDescriptor{"serialGetBaudrate", "serialGetBaudrate"}, - OperationDescriptor{"serialGetCts", "serialGetCts"}, - OperationDescriptor{"serialGetDataBits", "serialGetDataBits"}, - OperationDescriptor{"serialGetDcd", "serialGetDcd"}, - OperationDescriptor{"serialGetDsr", "serialGetDsr"}, - OperationDescriptor{"serialGetFlowControl", "serialGetFlowControl"}, - OperationDescriptor{"serialGetParity", "serialGetParity"}, - OperationDescriptor{"serialGetRi", "serialGetRi"}, - OperationDescriptor{"serialGetStopBits", "serialGetStopBits"}, - OperationDescriptor{"serialInBytesTotal", "serialInBytesTotal"}, - OperationDescriptor{"serialInBytesWaiting", "serialInBytesWaiting"}, - OperationDescriptor{"serialListPorts", "serialListPorts"}, - OperationDescriptor{"serialMonitorPorts", "serialMonitorPorts"}, - OperationDescriptor{"serialOpen", "serialOpen"}, - OperationDescriptor{"serialOutBytesTotal", "serialOutBytesTotal"}, - OperationDescriptor{"serialOutBytesWaiting", "serialOutBytesWaiting"}, - OperationDescriptor{"serialRead", "serialRead"}, - OperationDescriptor{"serialReadLine", "serialReadLine"}, - OperationDescriptor{"serialReadUntil", "serialReadUntil"}, - OperationDescriptor{"serialReadUntilSequence", "serialReadUntilSequence"}, - OperationDescriptor{"serialSendBreak", "serialSendBreak"}, - OperationDescriptor{"serialSetBaudrate", "serialSetBaudrate"}, - OperationDescriptor{"serialSetDataBits", "serialSetDataBits"}, - OperationDescriptor{"serialSetDtr", "serialSetDtr"}, - OperationDescriptor{"serialSetErrorCallback", "serialSetErrorCallback"}, - OperationDescriptor{"serialSetFlowControl", "serialSetFlowControl"}, - OperationDescriptor{"serialSetParity", "serialSetParity"}, - OperationDescriptor{"serialSetReadCallback", "serialSetReadCallback"}, - OperationDescriptor{"serialSetRts", "serialSetRts"}, - OperationDescriptor{"serialSetStopBits", "serialSetStopBits"}, - OperationDescriptor{"serialSetWriteCallback", "serialSetWriteCallback"}, - OperationDescriptor{"serialWrite", "serialWrite"}, +#define CPP_CORE_FFI_OPERATION_ENTRY(FunctionName) detail::makeOperationDescriptor(^^FunctionName), + CPP_CORE_ABI_FUNCTION_LIST(CPP_CORE_FFI_OPERATION_ENTRY) +#undef CPP_CORE_FFI_OPERATION_ENTRY }; inline constexpr auto kStatusCodeDescriptors = std::array{ StatusCodeDescriptor{"General", "Success", StatusCode::kSuccess}, - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetBaudrateError), - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetDataBitsError), - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetParityError), - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetStopBitsError), - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetFlowControlError), - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetTimeoutError), - detail::makeStatusCodeDescriptor(StatusCode::Connection::kNotFoundError), - detail::makeStatusCodeDescriptor(StatusCode::Connection::kInvalidHandleError), - detail::makeStatusCodeDescriptor(StatusCode::Connection::kCloseHandleError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kReadError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kWriteError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kAbortReadError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kAbortWriteError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kBufferError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kClearBufferInError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kClearBufferOutError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kSetDtrError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kSetRtsError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kGetModemStatusError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kSendBreakError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kGetStateError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kSetStateError), - detail::makeStatusCodeDescriptor(StatusCode::Monitor::kMonitorError), +#define CPP_CORE_STATUS_CODE_DESCRIPTOR_ENTRY(CategoryName, LocalCode, CodeName) \ + detail::makeStatusCodeDescriptor(StatusCode::CategoryName::k##CodeName), +#define CPP_CORE_STATUS_CODE_DESCRIPTOR_CATEGORY(CategoryName, CategoryCode, CodeListMacro) \ + CodeListMacro(CPP_CORE_STATUS_CODE_DESCRIPTOR_ENTRY, CategoryName) + CPP_CORE_STATUS_CODE_CATEGORY_LIST(CPP_CORE_STATUS_CODE_DESCRIPTOR_CATEGORY) +#undef CPP_CORE_STATUS_CODE_DESCRIPTOR_CATEGORY +#undef CPP_CORE_STATUS_CODE_DESCRIPTOR_ENTRY }; inline constexpr auto kSerialConfigFieldDescriptors = []() consteval @@ -531,6 +515,6 @@ inline constexpr auto kVersionFieldDescriptors = []() consteval return nullptr; } -#undef CPP_CORE_FFI_FUNCTION_LIST +#undef CPP_CORE_ABI_FUNCTION_LIST } // namespace cpp_core diff --git a/include/cpp_core/status_code.h b/include/cpp_core/status_code.h index df07c8a..5f88e85 100644 --- a/include/cpp_core/status_code.h +++ b/include/cpp_core/status_code.h @@ -1,5 +1,7 @@ #pragma once +#include "status_code_registry.hpp" + #include #include #include @@ -67,63 +69,21 @@ struct StatusCode using ValueType = detail::ValueType; static constexpr ValueType kSuccess = 0; - struct Configuration : detail::CategoryBase - { - static constexpr ValueType kCategoryCode = 1; - static constexpr std::string_view kCategoryName{"Configuration"}; - - static constexpr Code<0> kSetBaudrateError{"SetBaudrateError"}; - static constexpr Code<1> kSetDataBitsError{"SetDataBitsError"}; - static constexpr Code<2> kSetParityError{"SetParityError"}; - static constexpr Code<3> kSetStopBitsError{"SetStopBitsError"}; - static constexpr Code<4> kSetFlowControlError{"SetFlowControlError"}; - static constexpr Code<5> kSetTimeoutError{"SetTimeoutError"}; - }; - - struct Connection : detail::CategoryBase - { - static constexpr ValueType kCategoryCode = 2; - static constexpr std::string_view kCategoryName{"Connection"}; - - static constexpr Code<0> kNotFoundError{"NotFoundError"}; - static constexpr Code<1> kInvalidHandleError{"InvalidHandleError"}; - static constexpr Code<2> kCloseHandleError{"CloseHandleError"}; - }; - - struct Io : detail::CategoryBase - { - static constexpr ValueType kCategoryCode = 3; - static constexpr std::string_view kCategoryName{"Io"}; - - static constexpr Code<0> kReadError{"ReadError"}; - static constexpr Code<1> kWriteError{"WriteError"}; - static constexpr Code<2> kAbortReadError{"AbortReadError"}; - static constexpr Code<3> kAbortWriteError{"AbortWriteError"}; - static constexpr Code<4> kBufferError{"BufferError"}; - static constexpr Code<5> kClearBufferInError{"ClearBufferInError"}; - static constexpr Code<6> kClearBufferOutError{"ClearBufferOutError"}; - }; +#define CPP_CORE_DECLARE_STATUS_CODE_MEMBER(CategoryName, LocalCode, CodeName) \ + static constexpr Code k##CodeName{#CodeName}; - struct Control : detail::CategoryBase - { - static constexpr ValueType kCategoryCode = 4; - static constexpr std::string_view kCategoryName{"Control"}; - - static constexpr Code<0> kSetDtrError{"SetDtrError"}; - static constexpr Code<1> kSetRtsError{"SetRtsError"}; - static constexpr Code<2> kGetModemStatusError{"GetModemStatusError"}; - static constexpr Code<3> kSendBreakError{"SendBreakError"}; - static constexpr Code<4> kGetStateError{"GetStateError"}; - static constexpr Code<5> kSetStateError{"SetStateError"}; +#define CPP_CORE_DECLARE_STATUS_CODE_CATEGORY(CategoryName, CategoryCode, CodeListMacro) \ + struct CategoryName : detail::CategoryBase \ + { \ + static constexpr ValueType kCategoryCode = CategoryCode; \ + static constexpr std::string_view kCategoryName{#CategoryName}; \ + CodeListMacro(CPP_CORE_DECLARE_STATUS_CODE_MEMBER, CategoryName) \ }; - struct Monitor : detail::CategoryBase - { - static constexpr ValueType kCategoryCode = 5; - static constexpr std::string_view kCategoryName{"Monitor"}; + CPP_CORE_STATUS_CODE_CATEGORY_LIST(CPP_CORE_DECLARE_STATUS_CODE_CATEGORY) - static constexpr Code<0> kMonitorError{"MonitorError"}; - }; +#undef CPP_CORE_DECLARE_STATUS_CODE_CATEGORY +#undef CPP_CORE_DECLARE_STATUS_CODE_MEMBER [[nodiscard]] static constexpr auto isError(ValueType code) noexcept -> bool { @@ -150,32 +110,15 @@ struct StatusCodes { static constexpr StatusCodeValue kSuccess = StatusCode::kSuccess; - static constexpr StatusCodeValue kSetBaudrateError = StatusCode::Configuration::kSetBaudrateError; - static constexpr StatusCodeValue kSetDataBitsError = StatusCode::Configuration::kSetDataBitsError; - static constexpr StatusCodeValue kSetParityError = StatusCode::Configuration::kSetParityError; - static constexpr StatusCodeValue kSetStopBitsError = StatusCode::Configuration::kSetStopBitsError; - static constexpr StatusCodeValue kSetFlowControlError = StatusCode::Configuration::kSetFlowControlError; - static constexpr StatusCodeValue kSetTimeoutError = StatusCode::Configuration::kSetTimeoutError; - - static constexpr StatusCodeValue kNotFoundError = StatusCode::Connection::kNotFoundError; - static constexpr StatusCodeValue kInvalidHandleError = StatusCode::Connection::kInvalidHandleError; - static constexpr StatusCodeValue kCloseHandleError = StatusCode::Connection::kCloseHandleError; - - static constexpr StatusCodeValue kReadError = StatusCode::Io::kReadError; - static constexpr StatusCodeValue kWriteError = StatusCode::Io::kWriteError; - static constexpr StatusCodeValue kAbortReadError = StatusCode::Io::kAbortReadError; - static constexpr StatusCodeValue kAbortWriteError = StatusCode::Io::kAbortWriteError; - static constexpr StatusCodeValue kBufferError = StatusCode::Io::kBufferError; - static constexpr StatusCodeValue kClearBufferInError = StatusCode::Io::kClearBufferInError; - static constexpr StatusCodeValue kClearBufferOutError = StatusCode::Io::kClearBufferOutError; - - static constexpr StatusCodeValue kSetDtrError = StatusCode::Control::kSetDtrError; - static constexpr StatusCodeValue kSetRtsError = StatusCode::Control::kSetRtsError; - static constexpr StatusCodeValue kGetModemStatusError = StatusCode::Control::kGetModemStatusError; - static constexpr StatusCodeValue kSendBreakError = StatusCode::Control::kSendBreakError; - static constexpr StatusCodeValue kGetStateError = StatusCode::Control::kGetStateError; - static constexpr StatusCodeValue kSetStateError = StatusCode::Control::kSetStateError; - - static constexpr StatusCodeValue kMonitorError = StatusCode::Monitor::kMonitorError; +#define CPP_CORE_DECLARE_STATUS_CODE_ALIAS(CategoryName, LocalCode, CodeName) \ + static constexpr StatusCodeValue k##CodeName = StatusCode::CategoryName::k##CodeName; + +#define CPP_CORE_DECLARE_STATUS_CODE_ALIAS_CATEGORY(CategoryName, CategoryCode, CodeListMacro) \ + CodeListMacro(CPP_CORE_DECLARE_STATUS_CODE_ALIAS, CategoryName) + + CPP_CORE_STATUS_CODE_CATEGORY_LIST(CPP_CORE_DECLARE_STATUS_CODE_ALIAS_CATEGORY) + +#undef CPP_CORE_DECLARE_STATUS_CODE_ALIAS_CATEGORY +#undef CPP_CORE_DECLARE_STATUS_CODE_ALIAS }; } // namespace cpp_core diff --git a/include/cpp_core/status_code_registry.hpp b/include/cpp_core/status_code_registry.hpp new file mode 100644 index 0000000..5653b6e --- /dev/null +++ b/include/cpp_core/status_code_registry.hpp @@ -0,0 +1,41 @@ +#pragma once + +#define CPP_CORE_STATUS_CODE_CONFIGURATION_LIST(X, CategoryName) \ + X(CategoryName, 0, SetBaudrateError) \ + X(CategoryName, 1, SetDataBitsError) \ + X(CategoryName, 2, SetParityError) \ + X(CategoryName, 3, SetStopBitsError) \ + X(CategoryName, 4, SetFlowControlError) \ + X(CategoryName, 5, SetTimeoutError) + +#define CPP_CORE_STATUS_CODE_CONNECTION_LIST(X, CategoryName) \ + X(CategoryName, 0, NotFoundError) \ + X(CategoryName, 1, InvalidHandleError) \ + X(CategoryName, 2, CloseHandleError) + +#define CPP_CORE_STATUS_CODE_IO_LIST(X, CategoryName) \ + X(CategoryName, 0, ReadError) \ + X(CategoryName, 1, WriteError) \ + X(CategoryName, 2, AbortReadError) \ + X(CategoryName, 3, AbortWriteError) \ + X(CategoryName, 4, BufferError) \ + X(CategoryName, 5, ClearBufferInError) \ + X(CategoryName, 6, ClearBufferOutError) + +#define CPP_CORE_STATUS_CODE_CONTROL_LIST(X, CategoryName) \ + X(CategoryName, 0, SetDtrError) \ + X(CategoryName, 1, SetRtsError) \ + X(CategoryName, 2, GetModemStatusError) \ + X(CategoryName, 3, SendBreakError) \ + X(CategoryName, 4, GetStateError) \ + X(CategoryName, 5, SetStateError) + +#define CPP_CORE_STATUS_CODE_MONITOR_LIST(X, CategoryName) \ + X(CategoryName, 0, MonitorError) + +#define CPP_CORE_STATUS_CODE_CATEGORY_LIST(X) \ + X(Configuration, 1, CPP_CORE_STATUS_CODE_CONFIGURATION_LIST) \ + X(Connection, 2, CPP_CORE_STATUS_CODE_CONNECTION_LIST) \ + X(Io, 3, CPP_CORE_STATUS_CODE_IO_LIST) \ + X(Control, 4, CPP_CORE_STATUS_CODE_CONTROL_LIST) \ + X(Monitor, 5, CPP_CORE_STATUS_CODE_MONITOR_LIST) diff --git a/tests/ffi_metadata_compile_test.cpp b/tests/ffi_metadata_compile_test.cpp index 6e9957a..ff9515a 100644 --- a/tests/ffi_metadata_compile_test.cpp +++ b/tests/ffi_metadata_compile_test.cpp @@ -2,12 +2,19 @@ static_assert(__cpp_impl_reflection >= 202603L); -static_assert(cpp_core::kFunctionDescriptors.size() == 39); -static_assert(cpp_core::kOperationDescriptors.size() == 39); -static_assert(cpp_core::kStatusCodeDescriptors.size() == 24); +static_assert(!cpp_core::kFunctionDescriptors.empty()); +static_assert(cpp_core::kFunctionDescriptors.size() == cpp_core::kOperationDescriptors.size()); +static_assert(!cpp_core::kStatusCodeDescriptors.empty()); static_assert(cpp_core::kSerialConfigFieldDescriptors.size() == 4); static_assert(cpp_core::kVersionFieldDescriptors.size() == 8); +constexpr auto kGetVersion = cpp_core::detail::kDescriptor_getVersion; +static_assert(kGetVersion.result_model == cpp_core::FunctionResultModel::kVoid); +static_assert(kGetVersion.parameters.size() == 1); +static_assert(kGetVersion.parameters[0].name == "out"); +static_assert(kGetVersion.parameters[0].abi_kind == cpp_core::AbiValueKind::kVersionStructPointer); +static_assert(kGetVersion.parameters[0].direction == cpp_core::ParameterDirection::kOut); + constexpr auto kSerialOpen = cpp_core::detail::kDescriptor_serialOpen; static_assert(kSerialOpen.result_model == cpp_core::FunctionResultModel::kHandleOrStatus); static_assert(kSerialOpen.return_abi_kind == cpp_core::AbiValueKind::kOpaqueHandle); @@ -45,6 +52,7 @@ static_assert(cpp_core::kVersionFieldDescriptors[7].name == "version_string"); static_assert(cpp_core::findFunctionDescriptor("serialOpen") != nullptr); static_assert(cpp_core::findFunctionDescriptor("serialOpen")->name == "serialOpen"); +static_assert(cpp_core::findFunctionDescriptor("doesNotExist") == nullptr); static_assert(cpp_core::findOperationDescriptor("serialOpen") != nullptr); static_assert(cpp_core::findOperationDescriptor("serialOpen")->function_name == "serialOpen"); static_assert(cpp_core::findStatusCodeDescriptor(cpp_core::StatusCodes::kMonitorError) != nullptr); From 6f357cd0b5b4f0ed354879968e104d54b97e2120 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 8 Jun 2026 21:22:50 +0200 Subject: [PATCH 04/28] feat: enhance status code management and introduce new status code descriptors --- include/cpp_core/abi_registry.hpp | 91 ++++++++------ include/cpp_core/ffi_metadata.hpp | 107 +++++++++------- .../cpp_core/interface/serial_abort_read.h | 4 +- .../cpp_core/interface/serial_abort_write.h | 4 +- include/cpp_core/status_code.h | 115 ++++++++++++++---- .../status_code_descriptor_registry.hpp | 28 +++++ include/cpp_core/status_code_registry.hpp | 40 +----- include/cpp_core/validation.hpp | 10 +- tests/ffi_metadata_compile_test.cpp | 8 +- tests/status_code_compile_test.cpp | 19 +++ 10 files changed, 269 insertions(+), 157 deletions(-) create mode 100644 include/cpp_core/status_code_descriptor_registry.hpp diff --git a/include/cpp_core/abi_registry.hpp b/include/cpp_core/abi_registry.hpp index 56b170a..c83c560 100644 --- a/include/cpp_core/abi_registry.hpp +++ b/include/cpp_core/abi_registry.hpp @@ -1,42 +1,53 @@ #pragma once -#define CPP_CORE_ABI_FUNCTION_LIST(X) \ - X(getVersion) \ - X(serialAbortRead) \ - X(serialAbortWrite) \ - X(serialClearBufferIn) \ - X(serialClearBufferOut) \ - X(serialClose) \ - X(serialDrain) \ - X(serialGetBaudrate) \ - X(serialGetCts) \ - X(serialGetDataBits) \ - X(serialGetDcd) \ - X(serialGetDsr) \ - X(serialGetFlowControl) \ - X(serialGetParity) \ - X(serialGetRi) \ - X(serialGetStopBits) \ - X(serialInBytesTotal) \ - X(serialInBytesWaiting) \ - X(serialListPorts) \ - X(serialMonitorPorts) \ - X(serialOpen) \ - X(serialOutBytesTotal) \ - X(serialOutBytesWaiting) \ - X(serialRead) \ - X(serialReadLine) \ - X(serialReadUntil) \ - X(serialReadUntilSequence) \ - X(serialSendBreak) \ - X(serialSetBaudrate) \ - X(serialSetDataBits) \ - X(serialSetDtr) \ - X(serialSetErrorCallback) \ - X(serialSetFlowControl) \ - X(serialSetParity) \ - X(serialSetReadCallback) \ - X(serialSetRts) \ - X(serialSetStopBits) \ - X(serialSetWriteCallback) \ - X(serialWrite) +#include "serial.h" + +#include + +namespace cpp_core::detail +{ + +struct AbiFunctionRegistry +{ + std::meta::info getVersion = ^^::getVersion; + std::meta::info serialAbortRead = ^^::serialAbortRead; + std::meta::info serialAbortWrite = ^^::serialAbortWrite; + std::meta::info serialClearBufferIn = ^^::serialClearBufferIn; + std::meta::info serialClearBufferOut = ^^::serialClearBufferOut; + std::meta::info serialClose = ^^::serialClose; + std::meta::info serialDrain = ^^::serialDrain; + std::meta::info serialGetBaudrate = ^^::serialGetBaudrate; + std::meta::info serialGetCts = ^^::serialGetCts; + std::meta::info serialGetDataBits = ^^::serialGetDataBits; + std::meta::info serialGetDcd = ^^::serialGetDcd; + std::meta::info serialGetDsr = ^^::serialGetDsr; + std::meta::info serialGetFlowControl = ^^::serialGetFlowControl; + std::meta::info serialGetParity = ^^::serialGetParity; + std::meta::info serialGetRi = ^^::serialGetRi; + std::meta::info serialGetStopBits = ^^::serialGetStopBits; + std::meta::info serialInBytesTotal = ^^::serialInBytesTotal; + std::meta::info serialInBytesWaiting = ^^::serialInBytesWaiting; + std::meta::info serialListPorts = ^^::serialListPorts; + std::meta::info serialMonitorPorts = ^^::serialMonitorPorts; + std::meta::info serialOpen = ^^::serialOpen; + std::meta::info serialOutBytesTotal = ^^::serialOutBytesTotal; + std::meta::info serialOutBytesWaiting = ^^::serialOutBytesWaiting; + std::meta::info serialRead = ^^::serialRead; + std::meta::info serialReadLine = ^^::serialReadLine; + std::meta::info serialReadUntil = ^^::serialReadUntil; + std::meta::info serialReadUntilSequence = ^^::serialReadUntilSequence; + std::meta::info serialSendBreak = ^^::serialSendBreak; + std::meta::info serialSetBaudrate = ^^::serialSetBaudrate; + std::meta::info serialSetDataBits = ^^::serialSetDataBits; + std::meta::info serialSetDtr = ^^::serialSetDtr; + std::meta::info serialSetErrorCallback = ^^::serialSetErrorCallback; + std::meta::info serialSetFlowControl = ^^::serialSetFlowControl; + std::meta::info serialSetParity = ^^::serialSetParity; + std::meta::info serialSetReadCallback = ^^::serialSetReadCallback; + std::meta::info serialSetRts = ^^::serialSetRts; + std::meta::info serialSetStopBits = ^^::serialSetStopBits; + std::meta::info serialSetWriteCallback = ^^::serialSetWriteCallback; + std::meta::info serialWrite = ^^::serialWrite; +}; + +} // namespace cpp_core::detail diff --git a/include/cpp_core/ffi_metadata.hpp b/include/cpp_core/ffi_metadata.hpp index eeeccc9..1571a71 100644 --- a/include/cpp_core/ffi_metadata.hpp +++ b/include/cpp_core/ffi_metadata.hpp @@ -4,7 +4,6 @@ #include "serial.h" #include "serial_config.hpp" #include "status_code.h" -#include "status_code_registry.hpp" #include #include @@ -338,6 +337,20 @@ template return std::array{makeParameterDescriptor(function_name, params[Index])...}; } +template +[[nodiscard]] consteval auto makeParameterDescriptorsForFunctionImpl(std::index_sequence) +{ + constexpr auto params = std::define_static_array(std::meta::parameters_of(FunctionInfo)); + return std::array{ + makeParameterDescriptor(std::meta::identifier_of(FunctionInfo), *(params.data() + Index))..., + }; +} + +template +inline constexpr auto kParameterDescriptorsForFunction = + makeParameterDescriptorsForFunctionImpl( + std::make_index_sequence{}); + template [[nodiscard]] consteval auto makeFunctionDescriptor(std::meta::info function_info, const std::array ¶ms) @@ -356,6 +369,11 @@ template }; } +template [[nodiscard]] consteval auto makeFunctionDescriptorFromInfo() -> FunctionDescriptor +{ + return makeFunctionDescriptor(FunctionInfo, kParameterDescriptorsForFunction); +} + template [[nodiscard]] consteval auto makeFieldDescriptorsImpl(const std::vector &fields, std::index_sequence) @@ -380,58 +398,65 @@ template }; } -template -[[nodiscard]] consteval auto makeStatusCodeDescriptor(const Code &code_value, std::string_view fallback_category = {}) - -> StatusCodeDescriptor +template [[nodiscard]] consteval auto makeOperationDescriptorFromInfo() -> OperationDescriptor +{ + return makeOperationDescriptor(FunctionInfo); +} + +template [[nodiscard]] consteval auto makeStatusCodeDescriptor(const Code &code_value) -> StatusCodeDescriptor { - const auto category = fallback_category.empty() ? code_value.category() : fallback_category; return StatusCodeDescriptor{ - .category = category, + .category = code_value.category(), .name = code_value.name(), .value = static_cast(code_value), }; } -} // namespace detail +template +[[nodiscard]] consteval auto makeAbiFunctionDescriptorsImpl(std::index_sequence) + -> std::array +{ + constexpr AbiFunctionRegistry registry{}; + constexpr auto fields = std::define_static_array( + std::meta::nonstatic_data_members_of(^^AbiFunctionRegistry, std::meta::access_context::unchecked())); -#define CPP_CORE_DECLARE_FFI_METADATA(FunctionName) \ - namespace detail \ - { \ - inline constexpr auto kParameters_##FunctionName = []() consteval \ - { \ - auto params = std::meta::parameters_of(^^FunctionName); \ - return makeParameterDescriptorsImpl(std::meta::identifier_of(^^FunctionName), params, \ - std::make_index_sequence{}); \ - }(); \ - inline constexpr auto kDescriptor_##FunctionName = makeFunctionDescriptor(^^FunctionName, kParameters_##FunctionName); \ - } + return std::array{ + makeFunctionDescriptorFromInfo()..., + }; +} -CPP_CORE_ABI_FUNCTION_LIST(CPP_CORE_DECLARE_FFI_METADATA) +template +[[nodiscard]] consteval auto makeAbiOperationDescriptorsImpl(std::index_sequence) + -> std::array +{ + constexpr AbiFunctionRegistry registry{}; + constexpr auto fields = std::define_static_array( + std::meta::nonstatic_data_members_of(^^AbiFunctionRegistry, std::meta::access_context::unchecked())); -#undef CPP_CORE_DECLARE_FFI_METADATA + return std::array{ + makeOperationDescriptorFromInfo()..., + }; +} -inline constexpr auto kFunctionDescriptors = std::array{ -#define CPP_CORE_FFI_DESCRIPTOR_ENTRY(FunctionName) detail::kDescriptor_##FunctionName, - CPP_CORE_ABI_FUNCTION_LIST(CPP_CORE_FFI_DESCRIPTOR_ENTRY) -#undef CPP_CORE_FFI_DESCRIPTOR_ENTRY -}; +} // namespace detail -inline constexpr auto kOperationDescriptors = std::array{ -#define CPP_CORE_FFI_OPERATION_ENTRY(FunctionName) detail::makeOperationDescriptor(^^FunctionName), - CPP_CORE_ABI_FUNCTION_LIST(CPP_CORE_FFI_OPERATION_ENTRY) -#undef CPP_CORE_FFI_OPERATION_ENTRY -}; +inline constexpr auto kFunctionDescriptors = []() consteval +{ + return detail::makeAbiFunctionDescriptorsImpl( + std::make_index_sequence{}); +}(); -inline constexpr auto kStatusCodeDescriptors = std::array{ - StatusCodeDescriptor{"General", "Success", StatusCode::kSuccess}, -#define CPP_CORE_STATUS_CODE_DESCRIPTOR_ENTRY(CategoryName, LocalCode, CodeName) \ - detail::makeStatusCodeDescriptor(StatusCode::CategoryName::k##CodeName), -#define CPP_CORE_STATUS_CODE_DESCRIPTOR_CATEGORY(CategoryName, CategoryCode, CodeListMacro) \ - CodeListMacro(CPP_CORE_STATUS_CODE_DESCRIPTOR_ENTRY, CategoryName) - CPP_CORE_STATUS_CODE_CATEGORY_LIST(CPP_CORE_STATUS_CODE_DESCRIPTOR_CATEGORY) -#undef CPP_CORE_STATUS_CODE_DESCRIPTOR_CATEGORY -#undef CPP_CORE_STATUS_CODE_DESCRIPTOR_ENTRY -}; +inline constexpr auto kOperationDescriptors = []() consteval +{ + return detail::makeAbiOperationDescriptorsImpl( + std::make_index_sequence{}); +}(); + +#include "status_code_descriptor_registry.hpp" inline constexpr auto kSerialConfigFieldDescriptors = []() consteval { @@ -515,6 +540,4 @@ inline constexpr auto kVersionFieldDescriptors = []() consteval return nullptr; } -#undef CPP_CORE_ABI_FUNCTION_LIST - } // namespace cpp_core diff --git a/include/cpp_core/interface/serial_abort_read.h b/include/cpp_core/interface/serial_abort_read.h index 6ac8008..8db85bb 100644 --- a/include/cpp_core/interface/serial_abort_read.h +++ b/include/cpp_core/interface/serial_abort_read.h @@ -12,11 +12,11 @@ extern "C" * @brief Abort a blocking read operation running in a different thread. * * The target read function returns immediately with - * ::cpp_core::StatusCode::Io::kAbortReadError. + * ::cpp_core::StatusCodes::kAbortReadError. * * @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::StatusCode on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. */ MODULE_API auto serialAbortRead(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_abort_write.h b/include/cpp_core/interface/serial_abort_write.h index d579d94..46fd50f 100644 --- a/include/cpp_core/interface/serial_abort_write.h +++ b/include/cpp_core/interface/serial_abort_write.h @@ -12,11 +12,11 @@ extern "C" * @brief Abort a blocking write operation running in a different thread. * * The target write function returns immediately with - * ::cpp_core::StatusCode::Io::kAbortWriteError. + * ::cpp_core::StatusCodes::kAbortWriteError. * * @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::StatusCode on error. + * @return 0 on success or a negative error code from ::cpp_core::StatusCodes on error. */ MODULE_API auto serialAbortWrite(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/status_code.h b/include/cpp_core/status_code.h index 5f88e85..e1028bd 100644 --- a/include/cpp_core/status_code.h +++ b/include/cpp_core/status_code.h @@ -1,7 +1,5 @@ #pragma once -#include "status_code_registry.hpp" - #include #include #include @@ -26,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 @@ -69,30 +75,74 @@ struct StatusCode using ValueType = detail::ValueType; static constexpr ValueType kSuccess = 0; -#define CPP_CORE_DECLARE_STATUS_CODE_MEMBER(CategoryName, LocalCode, CodeName) \ - static constexpr Code k##CodeName{#CodeName}; + struct Configuration : detail::CategoryBase + { + static constexpr ValueType kCategoryCode = 1; + static constexpr std::string_view kCategoryName{"Configuration"}; + + static constexpr Code<0> kSetBaudrateError{"SetBaudrateError"}; + static constexpr Code<1> kSetDataBitsError{"SetDataBitsError"}; + static constexpr Code<2> kSetParityError{"SetParityError"}; + static constexpr Code<3> kSetStopBitsError{"SetStopBitsError"}; + static constexpr Code<4> kSetFlowControlError{"SetFlowControlError"}; + static constexpr Code<5> kSetTimeoutError{"SetTimeoutError"}; + }; + + struct Connection : detail::CategoryBase + { + static constexpr ValueType kCategoryCode = 2; + static constexpr std::string_view kCategoryName{"Connection"}; + + static constexpr Code<0> kNotFoundError{"NotFoundError"}; + static constexpr Code<1> kInvalidHandleError{"InvalidHandleError"}; + static constexpr Code<2> kCloseHandleError{"CloseHandleError"}; + }; + + struct Io : detail::CategoryBase + { + static constexpr ValueType kCategoryCode = 3; + static constexpr std::string_view kCategoryName{"Io"}; + + static constexpr Code<0> kReadError{"ReadError"}; + static constexpr Code<1> kWriteError{"WriteError"}; + static constexpr Code<2> kAbortReadError{"AbortReadError"}; + static constexpr Code<3> kAbortWriteError{"AbortWriteError"}; + static constexpr Code<4> kBufferError{"BufferError"}; + static constexpr Code<5> kClearBufferInError{"ClearBufferInError"}; + static constexpr Code<6> kClearBufferOutError{"ClearBufferOutError"}; + }; -#define CPP_CORE_DECLARE_STATUS_CODE_CATEGORY(CategoryName, CategoryCode, CodeListMacro) \ - struct CategoryName : detail::CategoryBase \ - { \ - static constexpr ValueType kCategoryCode = CategoryCode; \ - static constexpr std::string_view kCategoryName{#CategoryName}; \ - CodeListMacro(CPP_CORE_DECLARE_STATUS_CODE_MEMBER, CategoryName) \ + struct Control : detail::CategoryBase + { + static constexpr ValueType kCategoryCode = 4; + static constexpr std::string_view kCategoryName{"Control"}; + + static constexpr Code<0> kSetDtrError{"SetDtrError"}; + static constexpr Code<1> kSetRtsError{"SetRtsError"}; + static constexpr Code<2> kGetModemStatusError{"GetModemStatusError"}; + static constexpr Code<3> kSendBreakError{"SendBreakError"}; + static constexpr Code<4> kGetStateError{"GetStateError"}; + static constexpr Code<5> kSetStateError{"SetStateError"}; }; - CPP_CORE_STATUS_CODE_CATEGORY_LIST(CPP_CORE_DECLARE_STATUS_CODE_CATEGORY) + struct Monitor : detail::CategoryBase + { + static constexpr ValueType kCategoryCode = 5; + static constexpr std::string_view kCategoryName{"Monitor"}; -#undef CPP_CORE_DECLARE_STATUS_CODE_CATEGORY -#undef CPP_CORE_DECLARE_STATUS_CODE_MEMBER + 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; @@ -103,6 +153,7 @@ struct StatusCode namespace cpp_core { + using StatusCodeValue = ::cpp_core::status_codes::detail::ValueType; using ::cpp_core::status_codes::StatusCode; @@ -110,15 +161,33 @@ struct StatusCodes { static constexpr StatusCodeValue kSuccess = StatusCode::kSuccess; -#define CPP_CORE_DECLARE_STATUS_CODE_ALIAS(CategoryName, LocalCode, CodeName) \ - static constexpr StatusCodeValue k##CodeName = StatusCode::CategoryName::k##CodeName; - -#define CPP_CORE_DECLARE_STATUS_CODE_ALIAS_CATEGORY(CategoryName, CategoryCode, CodeListMacro) \ - CodeListMacro(CPP_CORE_DECLARE_STATUS_CODE_ALIAS, CategoryName) - - CPP_CORE_STATUS_CODE_CATEGORY_LIST(CPP_CORE_DECLARE_STATUS_CODE_ALIAS_CATEGORY) - -#undef CPP_CORE_DECLARE_STATUS_CODE_ALIAS_CATEGORY -#undef CPP_CORE_DECLARE_STATUS_CODE_ALIAS + static constexpr StatusCodeValue kSetBaudrateError = StatusCode::Configuration::kSetBaudrateError; + static constexpr StatusCodeValue kSetDataBitsError = StatusCode::Configuration::kSetDataBitsError; + static constexpr StatusCodeValue kSetParityError = StatusCode::Configuration::kSetParityError; + static constexpr StatusCodeValue kSetStopBitsError = StatusCode::Configuration::kSetStopBitsError; + static constexpr StatusCodeValue kSetFlowControlError = StatusCode::Configuration::kSetFlowControlError; + static constexpr StatusCodeValue kSetTimeoutError = StatusCode::Configuration::kSetTimeoutError; + + static constexpr StatusCodeValue kNotFoundError = StatusCode::Connection::kNotFoundError; + static constexpr StatusCodeValue kInvalidHandleError = StatusCode::Connection::kInvalidHandleError; + static constexpr StatusCodeValue kCloseHandleError = StatusCode::Connection::kCloseHandleError; + + static constexpr StatusCodeValue kReadError = StatusCode::Io::kReadError; + static constexpr StatusCodeValue kWriteError = StatusCode::Io::kWriteError; + static constexpr StatusCodeValue kAbortReadError = StatusCode::Io::kAbortReadError; + static constexpr StatusCodeValue kAbortWriteError = StatusCode::Io::kAbortWriteError; + static constexpr StatusCodeValue kBufferError = StatusCode::Io::kBufferError; + static constexpr StatusCodeValue kClearBufferInError = StatusCode::Io::kClearBufferInError; + static constexpr StatusCodeValue kClearBufferOutError = StatusCode::Io::kClearBufferOutError; + + static constexpr StatusCodeValue kSetDtrError = StatusCode::Control::kSetDtrError; + static constexpr StatusCodeValue kSetRtsError = StatusCode::Control::kSetRtsError; + static constexpr StatusCodeValue kGetModemStatusError = StatusCode::Control::kGetModemStatusError; + static constexpr StatusCodeValue kSendBreakError = StatusCode::Control::kSendBreakError; + static constexpr StatusCodeValue kGetStateError = StatusCode::Control::kGetStateError; + static constexpr StatusCodeValue kSetStateError = StatusCode::Control::kSetStateError; + + static constexpr StatusCodeValue kMonitorError = StatusCode::Monitor::kMonitorError; }; + } // namespace cpp_core diff --git a/include/cpp_core/status_code_descriptor_registry.hpp b/include/cpp_core/status_code_descriptor_registry.hpp new file mode 100644 index 0000000..8389488 --- /dev/null +++ b/include/cpp_core/status_code_descriptor_registry.hpp @@ -0,0 +1,28 @@ +#pragma once + +inline constexpr auto kStatusCodeDescriptors = std::array{ + StatusCodeDescriptor{"General", "Success", StatusCode::kSuccess}, + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetBaudrateError), + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetDataBitsError), + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetParityError), + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetStopBitsError), + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetFlowControlError), + detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetTimeoutError), + detail::makeStatusCodeDescriptor(StatusCode::Connection::kNotFoundError), + detail::makeStatusCodeDescriptor(StatusCode::Connection::kInvalidHandleError), + detail::makeStatusCodeDescriptor(StatusCode::Connection::kCloseHandleError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kReadError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kWriteError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kAbortReadError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kAbortWriteError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kBufferError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kClearBufferInError), + detail::makeStatusCodeDescriptor(StatusCode::Io::kClearBufferOutError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kSetDtrError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kSetRtsError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kGetModemStatusError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kSendBreakError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kGetStateError), + detail::makeStatusCodeDescriptor(StatusCode::Control::kSetStateError), + detail::makeStatusCodeDescriptor(StatusCode::Monitor::kMonitorError), +}; diff --git a/include/cpp_core/status_code_registry.hpp b/include/cpp_core/status_code_registry.hpp index 5653b6e..62fc8f3 100644 --- a/include/cpp_core/status_code_registry.hpp +++ b/include/cpp_core/status_code_registry.hpp @@ -1,41 +1,3 @@ #pragma once -#define CPP_CORE_STATUS_CODE_CONFIGURATION_LIST(X, CategoryName) \ - X(CategoryName, 0, SetBaudrateError) \ - X(CategoryName, 1, SetDataBitsError) \ - X(CategoryName, 2, SetParityError) \ - X(CategoryName, 3, SetStopBitsError) \ - X(CategoryName, 4, SetFlowControlError) \ - X(CategoryName, 5, SetTimeoutError) - -#define CPP_CORE_STATUS_CODE_CONNECTION_LIST(X, CategoryName) \ - X(CategoryName, 0, NotFoundError) \ - X(CategoryName, 1, InvalidHandleError) \ - X(CategoryName, 2, CloseHandleError) - -#define CPP_CORE_STATUS_CODE_IO_LIST(X, CategoryName) \ - X(CategoryName, 0, ReadError) \ - X(CategoryName, 1, WriteError) \ - X(CategoryName, 2, AbortReadError) \ - X(CategoryName, 3, AbortWriteError) \ - X(CategoryName, 4, BufferError) \ - X(CategoryName, 5, ClearBufferInError) \ - X(CategoryName, 6, ClearBufferOutError) - -#define CPP_CORE_STATUS_CODE_CONTROL_LIST(X, CategoryName) \ - X(CategoryName, 0, SetDtrError) \ - X(CategoryName, 1, SetRtsError) \ - X(CategoryName, 2, GetModemStatusError) \ - X(CategoryName, 3, SendBreakError) \ - X(CategoryName, 4, GetStateError) \ - X(CategoryName, 5, SetStateError) - -#define CPP_CORE_STATUS_CODE_MONITOR_LIST(X, CategoryName) \ - X(CategoryName, 0, MonitorError) - -#define CPP_CORE_STATUS_CODE_CATEGORY_LIST(X) \ - X(Configuration, 1, CPP_CORE_STATUS_CODE_CONFIGURATION_LIST) \ - X(Connection, 2, CPP_CORE_STATUS_CODE_CONNECTION_LIST) \ - X(Io, 3, CPP_CORE_STATUS_CODE_IO_LIST) \ - X(Control, 4, CPP_CORE_STATUS_CODE_CONTROL_LIST) \ - X(Monitor, 5, CPP_CORE_STATUS_CODE_MONITOR_LIST) +#include "status_code.h" diff --git a/include/cpp_core/validation.hpp b/include/cpp_core/validation.hpp index 2ff5c80..b9cdbb8 100644 --- a/include/cpp_core/validation.hpp +++ b/include/cpp_core/validation.hpp @@ -20,7 +20,7 @@ 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), + static_cast(StatusCodes::kInvalidHandleError), "Invalid handle"); } return static_cast(StatusCode::kSuccess); @@ -36,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(StatusCodes::kNotFoundError), "Port parameter is nullptr"); } if (baudrate < 300) { return failMsg(std::forward(error_callback), - static_cast(StatusCode::Control::kSetStateError), + static_cast(StatusCodes::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(StatusCodes::kSetStateError), "Invalid data bits: must be 5-8"); } return static_cast(StatusCode::kSuccess); @@ -61,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(StatusCodes::kBufferError), "Invalid buffer or buffer_size"); } return static_cast(StatusCode::kSuccess); diff --git a/tests/ffi_metadata_compile_test.cpp b/tests/ffi_metadata_compile_test.cpp index ff9515a..9927953 100644 --- a/tests/ffi_metadata_compile_test.cpp +++ b/tests/ffi_metadata_compile_test.cpp @@ -8,14 +8,14 @@ static_assert(!cpp_core::kStatusCodeDescriptors.empty()); static_assert(cpp_core::kSerialConfigFieldDescriptors.size() == 4); static_assert(cpp_core::kVersionFieldDescriptors.size() == 8); -constexpr auto kGetVersion = cpp_core::detail::kDescriptor_getVersion; +constexpr auto &kGetVersion = *cpp_core::findFunctionDescriptor("getVersion"); static_assert(kGetVersion.result_model == cpp_core::FunctionResultModel::kVoid); static_assert(kGetVersion.parameters.size() == 1); static_assert(kGetVersion.parameters[0].name == "out"); static_assert(kGetVersion.parameters[0].abi_kind == cpp_core::AbiValueKind::kVersionStructPointer); static_assert(kGetVersion.parameters[0].direction == cpp_core::ParameterDirection::kOut); -constexpr auto kSerialOpen = cpp_core::detail::kDescriptor_serialOpen; +constexpr auto &kSerialOpen = *cpp_core::findFunctionDescriptor("serialOpen"); static_assert(kSerialOpen.result_model == cpp_core::FunctionResultModel::kHandleOrStatus); static_assert(kSerialOpen.return_abi_kind == cpp_core::AbiValueKind::kOpaqueHandle); static_assert(kSerialOpen.parameters.size() == 6); @@ -25,13 +25,13 @@ static_assert(kSerialOpen.parameters[4].optional); static_assert(kSerialOpen.parameters[5].name == "error_callback"); static_assert(kSerialOpen.parameters[5].abi_kind == cpp_core::AbiValueKind::kErrorCallback); -constexpr auto kSerialRead = cpp_core::detail::kDescriptor_serialRead; +constexpr auto &kSerialRead = *cpp_core::findFunctionDescriptor("serialRead"); static_assert(kSerialRead.result_model == cpp_core::FunctionResultModel::kValueOrStatus); static_assert(kSerialRead.parameters[1].name == "buffer"); static_assert(kSerialRead.parameters[1].abi_kind == cpp_core::AbiValueKind::kMutableBuffer); static_assert(kSerialRead.parameters[1].direction == cpp_core::ParameterDirection::kOut); -constexpr auto kSetErrorCallback = cpp_core::detail::kDescriptor_serialSetErrorCallback; +constexpr auto &kSetErrorCallback = *cpp_core::findFunctionDescriptor("serialSetErrorCallback"); static_assert(kSetErrorCallback.result_model == cpp_core::FunctionResultModel::kVoid); static_assert(kSetErrorCallback.parameters.size() == 1); static_assert(kSetErrorCallback.parameters[0].abi_kind == cpp_core::AbiValueKind::kErrorCallback); diff --git a/tests/status_code_compile_test.cpp b/tests/status_code_compile_test.cpp index d19906a..6421b64 100644 --- a/tests/status_code_compile_test.cpp +++ b/tests/status_code_compile_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,6 +51,21 @@ static_assert(cpp_core::StatusCode::Control::kSendBreakError == -403); static_assert(cpp_core::StatusCode::Control::kGetStateError == -404); static_assert(cpp_core::StatusCode::Control::kSetStateError == -405); +static_assert(cpp_core::StatusCode::Monitor::kMonitorError.category() == "Monitor"); +static_assert(cpp_core::StatusCode::Monitor::kMonitorError == -500); + +static_assert(cpp_core::StatusCodes::kSuccess == cpp_core::StatusCode::kSuccess); +static_assert(cpp_core::StatusCodes::kSetBaudrateError == cpp_core::StatusCode::Configuration::kSetBaudrateError); +static_assert(cpp_core::StatusCodes::kInvalidHandleError == cpp_core::StatusCode::Connection::kInvalidHandleError); +static_assert(cpp_core::StatusCodes::kAbortReadError == cpp_core::StatusCode::Io::kAbortReadError); +static_assert(cpp_core::StatusCodes::kSetStateError == cpp_core::StatusCode::Control::kSetStateError); +static_assert(cpp_core::StatusCodes::kMonitorError == cpp_core::StatusCode::Monitor::kMonitorError); + +static_assert(cpp_core::StatusCode::belongsTo( + cpp_core::StatusCode::Configuration::kSetBaudrateError)); +static_assert(!cpp_core::StatusCode::belongsTo( + cpp_core::StatusCode::Configuration::kSetBaudrateError)); + // Formula: result == -(kCategoryCode * 100 + LocalCode) static_assert(FakeCategory<1>::call<0>() == -100); static_assert(FakeCategory<1>::call<42>() == -142); From 22ead2533ac825f138eeec9b4b254837491f8bab Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 8 Jun 2026 21:35:40 +0200 Subject: [PATCH 05/28] feat: add strict warning options for GCC and enhance FFI metadata with new ABI function registry and status code descriptors --- CMakeLists.txt | 19 +++++++++++++++++++ include/cpp_core.h | 19 +++++++------------ include/cpp_core/error_handling.hpp | 1 + .../abi_function_registry.hpp} | 2 +- .../status_code_descriptors.hpp} | 0 include/cpp_core/ffi_metadata.hpp | 12 ++---------- include/cpp_core/result.hpp | 1 - include/cpp_core/serial.h | 3 --- include/cpp_core/serial_config.hpp | 1 + include/cpp_core/status_code_registry.hpp | 3 --- include/cpp_core/status_codes.h | 3 --- include/cpp_core/unique_resource.hpp | 1 + tools/generate_deno_symbols.cpp | 1 - 13 files changed, 32 insertions(+), 34 deletions(-) rename include/cpp_core/{abi_registry.hpp => ffi/abi_function_registry.hpp} (99%) rename include/cpp_core/{status_code_descriptor_registry.hpp => ffi/status_code_descriptors.hpp} (100%) delete mode 100644 include/cpp_core/status_code_registry.hpp delete mode 100644 include/cpp_core/status_codes.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7126a50..5a80584 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,23 @@ set(PROJECT_DESCRIPTION "Shared serial API and FFI metadata for GCC 16 / C++26 r add_library(cpp_core INTERFACE) add_library(cpp_core::cpp_core ALIAS cpp_core) +add_library(cpp_core_strict_warnings INTERFACE) + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options( + cpp_core_strict_warnings + INTERFACE + -Wall + -Wextra + -Wpedantic + -Wconversion + -Wsign-conversion + -Wshadow + -Wundef + -Werror + ) +endif() + # Public include directories target_include_directories( cpp_core @@ -83,6 +100,7 @@ target_compile_options(cpp_core INTERFACE -freflection) add_executable(cpp_core_bindgen EXCLUDE_FROM_ALL tools/generate_deno_symbols.cpp) target_link_libraries(cpp_core_bindgen PRIVATE cpp_core::cpp_core) +target_link_libraries(cpp_core_bindgen PRIVATE cpp_core_strict_warnings) include(CTest) @@ -90,6 +108,7 @@ if(BUILD_TESTING) file(GLOB CPP_CORE_COMPILE_TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/*_compile_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) set(CPP_CORE_DENO_BINDGEN_SMOKE_EXE "${CMAKE_CURRENT_BINARY_DIR}/cpp_core_bindgen_smoke_bin") add_custom_target( diff --git a/include/cpp_core.h b/include/cpp_core.h index 8d6f64a..fe1fe27 100644 --- a/include/cpp_core.h +++ b/include/cpp_core.h @@ -1,19 +1,14 @@ #pragma once -/* - * cpp_core.h - Umbrella header for the cpp_core library - * - * 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. - */ - #include "cpp_core/error_callback.h" +#include "cpp_core/error_handling.hpp" #include "cpp_core/ffi_metadata.hpp" +#include "cpp_core/result.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 c9fa654..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 { diff --git a/include/cpp_core/abi_registry.hpp b/include/cpp_core/ffi/abi_function_registry.hpp similarity index 99% rename from include/cpp_core/abi_registry.hpp rename to include/cpp_core/ffi/abi_function_registry.hpp index c83c560..f300e52 100644 --- a/include/cpp_core/abi_registry.hpp +++ b/include/cpp_core/ffi/abi_function_registry.hpp @@ -1,6 +1,6 @@ #pragma once -#include "serial.h" +#include "../serial.h" #include diff --git a/include/cpp_core/status_code_descriptor_registry.hpp b/include/cpp_core/ffi/status_code_descriptors.hpp similarity index 100% rename from include/cpp_core/status_code_descriptor_registry.hpp rename to include/cpp_core/ffi/status_code_descriptors.hpp diff --git a/include/cpp_core/ffi_metadata.hpp b/include/cpp_core/ffi_metadata.hpp index 1571a71..9450a98 100644 --- a/include/cpp_core/ffi_metadata.hpp +++ b/include/cpp_core/ffi_metadata.hpp @@ -1,6 +1,6 @@ #pragma once -#include "abi_registry.hpp" +#include "ffi/abi_function_registry.hpp" #include "serial.h" #include "serial_config.hpp" #include "status_code.h" @@ -329,14 +329,6 @@ namespace detail }; } -template -[[nodiscard]] consteval auto makeParameterDescriptorsImpl(std::string_view function_name, - const std::vector ¶ms, - std::index_sequence) -{ - return std::array{makeParameterDescriptor(function_name, params[Index])...}; -} - template [[nodiscard]] consteval auto makeParameterDescriptorsForFunctionImpl(std::index_sequence) { @@ -456,7 +448,7 @@ inline constexpr auto kOperationDescriptors = []() consteval .size()>{}); }(); -#include "status_code_descriptor_registry.hpp" +#include "ffi/status_code_descriptors.hpp" inline constexpr auto kSerialConfigFieldDescriptors = []() consteval { diff --git a/include/cpp_core/result.hpp b/include/cpp_core/result.hpp index bd28683..0d0cf59 100644 --- a/include/cpp_core/result.hpp +++ b/include/cpp_core/result.hpp @@ -4,7 +4,6 @@ #include #include -#include #include #include #include diff --git a/include/cpp_core/serial.h b/include/cpp_core/serial.h index e92c6bf..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" diff --git a/include/cpp_core/serial_config.hpp b/include/cpp_core/serial_config.hpp index 4ebe0b6..3c8b4cd 100644 --- a/include/cpp_core/serial_config.hpp +++ b/include/cpp_core/serial_config.hpp @@ -2,6 +2,7 @@ #include "strong_types.hpp" +#include #include #include diff --git a/include/cpp_core/status_code_registry.hpp b/include/cpp_core/status_code_registry.hpp deleted file mode 100644 index 62fc8f3..0000000 --- a/include/cpp_core/status_code_registry.hpp +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#include "status_code.h" diff --git a/include/cpp_core/status_codes.h b/include/cpp_core/status_codes.h deleted file mode 100644 index 62fc8f3..0000000 --- a/include/cpp_core/status_codes.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#include "status_code.h" 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/tools/generate_deno_symbols.cpp b/tools/generate_deno_symbols.cpp index 9813456..8017b0f 100644 --- a/tools/generate_deno_symbols.cpp +++ b/tools/generate_deno_symbols.cpp @@ -7,7 +7,6 @@ #include #include #include -#include namespace { From 7abdbe4ac3ac88a9d2590694cdf8faa8a7f6fa18 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 8 Jun 2026 21:44:06 +0200 Subject: [PATCH 06/28] refactor(cpp-core): remove StatusCodes aliases and tighten header layout --- .../cpp_core/interface/serial_abort_read.h | 4 +-- .../cpp_core/interface/serial_abort_write.h | 4 +-- .../interface/serial_clear_buffer_in.h | 2 +- .../interface/serial_clear_buffer_out.h | 2 +- include/cpp_core/interface/serial_close.h | 2 +- include/cpp_core/interface/serial_drain.h | 2 +- .../cpp_core/interface/serial_get_baudrate.h | 2 +- include/cpp_core/interface/serial_get_cts.h | 2 +- .../cpp_core/interface/serial_get_data_bits.h | 2 +- include/cpp_core/interface/serial_get_dcd.h | 2 +- include/cpp_core/interface/serial_get_dsr.h | 2 +- .../interface/serial_get_flow_control.h | 2 +- .../cpp_core/interface/serial_get_parity.h | 2 +- include/cpp_core/interface/serial_get_ri.h | 2 +- .../cpp_core/interface/serial_get_stop_bits.h | 2 +- .../interface/serial_in_bytes_total.h | 2 +- .../interface/serial_in_bytes_waiting.h | 2 +- .../cpp_core/interface/serial_list_ports.h | 2 +- .../cpp_core/interface/serial_monitor_ports.h | 2 +- include/cpp_core/interface/serial_open.h | 2 +- .../interface/serial_out_bytes_total.h | 2 +- .../interface/serial_out_bytes_waiting.h | 2 +- include/cpp_core/interface/serial_read.h | 2 +- include/cpp_core/interface/serial_read_line.h | 2 +- .../cpp_core/interface/serial_read_until.h | 2 +- .../interface/serial_read_until_sequence.h | 2 +- .../cpp_core/interface/serial_send_break.h | 2 +- .../cpp_core/interface/serial_set_baudrate.h | 2 +- .../cpp_core/interface/serial_set_data_bits.h | 2 +- include/cpp_core/interface/serial_set_dtr.h | 2 +- .../interface/serial_set_flow_control.h | 2 +- .../cpp_core/interface/serial_set_parity.h | 2 +- include/cpp_core/interface/serial_set_rts.h | 2 +- .../cpp_core/interface/serial_set_stop_bits.h | 2 +- include/cpp_core/interface/serial_write.h | 2 +- include/cpp_core/status_code.h | 33 ------------------- include/cpp_core/validation.hpp | 10 +++--- tests/ffi_metadata_compile_test.cpp | 2 +- tests/status_code_compile_test.cpp | 7 ---- 39 files changed, 43 insertions(+), 83 deletions(-) diff --git a/include/cpp_core/interface/serial_abort_read.h b/include/cpp_core/interface/serial_abort_read.h index 8db85bb..6ac8008 100644 --- a/include/cpp_core/interface/serial_abort_read.h +++ b/include/cpp_core/interface/serial_abort_read.h @@ -12,11 +12,11 @@ extern "C" * @brief Abort a blocking read operation running in a different thread. * * The target read function returns immediately with - * ::cpp_core::StatusCodes::kAbortReadError. + * ::cpp_core::StatusCode::Io::kAbortReadError. * * @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 serialAbortRead(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; diff --git a/include/cpp_core/interface/serial_abort_write.h b/include/cpp_core/interface/serial_abort_write.h index 46fd50f..d579d94 100644 --- a/include/cpp_core/interface/serial_abort_write.h +++ b/include/cpp_core/interface/serial_abort_write.h @@ -12,11 +12,11 @@ extern "C" * @brief Abort a blocking write operation running in a different thread. * * The target write function returns immediately with - * ::cpp_core::StatusCodes::kAbortWriteError. + * ::cpp_core::StatusCode::Io::kAbortWriteError. * * @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 serialAbortWrite(int64_t handle, ErrorCallbackT error_callback = nullptr) -> int; 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 index 54214dd..01e3b8c 100644 --- a/include/cpp_core/interface/serial_monitor_ports.h +++ b/include/cpp_core/interface/serial_monitor_ports.h @@ -16,7 +16,7 @@ extern "C" * * @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::StatusCodes on error. + * @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; 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/status_code.h b/include/cpp_core/status_code.h index e1028bd..9b447bb 100644 --- a/include/cpp_core/status_code.h +++ b/include/cpp_core/status_code.h @@ -157,37 +157,4 @@ namespace cpp_core using StatusCodeValue = ::cpp_core::status_codes::detail::ValueType; using ::cpp_core::status_codes::StatusCode; -struct StatusCodes -{ - static constexpr StatusCodeValue kSuccess = StatusCode::kSuccess; - - static constexpr StatusCodeValue kSetBaudrateError = StatusCode::Configuration::kSetBaudrateError; - static constexpr StatusCodeValue kSetDataBitsError = StatusCode::Configuration::kSetDataBitsError; - static constexpr StatusCodeValue kSetParityError = StatusCode::Configuration::kSetParityError; - static constexpr StatusCodeValue kSetStopBitsError = StatusCode::Configuration::kSetStopBitsError; - static constexpr StatusCodeValue kSetFlowControlError = StatusCode::Configuration::kSetFlowControlError; - static constexpr StatusCodeValue kSetTimeoutError = StatusCode::Configuration::kSetTimeoutError; - - static constexpr StatusCodeValue kNotFoundError = StatusCode::Connection::kNotFoundError; - static constexpr StatusCodeValue kInvalidHandleError = StatusCode::Connection::kInvalidHandleError; - static constexpr StatusCodeValue kCloseHandleError = StatusCode::Connection::kCloseHandleError; - - static constexpr StatusCodeValue kReadError = StatusCode::Io::kReadError; - static constexpr StatusCodeValue kWriteError = StatusCode::Io::kWriteError; - static constexpr StatusCodeValue kAbortReadError = StatusCode::Io::kAbortReadError; - static constexpr StatusCodeValue kAbortWriteError = StatusCode::Io::kAbortWriteError; - static constexpr StatusCodeValue kBufferError = StatusCode::Io::kBufferError; - static constexpr StatusCodeValue kClearBufferInError = StatusCode::Io::kClearBufferInError; - static constexpr StatusCodeValue kClearBufferOutError = StatusCode::Io::kClearBufferOutError; - - static constexpr StatusCodeValue kSetDtrError = StatusCode::Control::kSetDtrError; - static constexpr StatusCodeValue kSetRtsError = StatusCode::Control::kSetRtsError; - static constexpr StatusCodeValue kGetModemStatusError = StatusCode::Control::kGetModemStatusError; - static constexpr StatusCodeValue kSendBreakError = StatusCode::Control::kSendBreakError; - static constexpr StatusCodeValue kGetStateError = StatusCode::Control::kGetStateError; - static constexpr StatusCodeValue kSetStateError = StatusCode::Control::kSetStateError; - - static constexpr StatusCodeValue kMonitorError = StatusCode::Monitor::kMonitorError; -}; - } // namespace cpp_core diff --git a/include/cpp_core/validation.hpp b/include/cpp_core/validation.hpp index b9cdbb8..2ff5c80 100644 --- a/include/cpp_core/validation.hpp +++ b/include/cpp_core/validation.hpp @@ -20,7 +20,7 @@ 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(StatusCodes::kInvalidHandleError), + static_cast(StatusCode::Connection::kInvalidHandleError), "Invalid handle"); } return static_cast(StatusCode::kSuccess); @@ -36,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(StatusCodes::kNotFoundError), + static_cast(StatusCode::Connection::kNotFoundError), "Port parameter is nullptr"); } if (baudrate < 300) { return failMsg(std::forward(error_callback), - static_cast(StatusCodes::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(StatusCodes::kSetStateError), + static_cast(StatusCode::Control::kSetStateError), "Invalid data bits: must be 5-8"); } return static_cast(StatusCode::kSuccess); @@ -61,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(StatusCodes::kBufferError), + static_cast(StatusCode::Io::kBufferError), "Invalid buffer or buffer_size"); } return static_cast(StatusCode::kSuccess); diff --git a/tests/ffi_metadata_compile_test.cpp b/tests/ffi_metadata_compile_test.cpp index 9927953..8de38cc 100644 --- a/tests/ffi_metadata_compile_test.cpp +++ b/tests/ffi_metadata_compile_test.cpp @@ -55,4 +55,4 @@ static_assert(cpp_core::findFunctionDescriptor("serialOpen")->name == "serialOpe static_assert(cpp_core::findFunctionDescriptor("doesNotExist") == nullptr); static_assert(cpp_core::findOperationDescriptor("serialOpen") != nullptr); static_assert(cpp_core::findOperationDescriptor("serialOpen")->function_name == "serialOpen"); -static_assert(cpp_core::findStatusCodeDescriptor(cpp_core::StatusCodes::kMonitorError) != nullptr); +static_assert(cpp_core::findStatusCodeDescriptor(cpp_core::StatusCode::Monitor::kMonitorError) != nullptr); diff --git a/tests/status_code_compile_test.cpp b/tests/status_code_compile_test.cpp index 6421b64..a1cc45c 100644 --- a/tests/status_code_compile_test.cpp +++ b/tests/status_code_compile_test.cpp @@ -54,13 +54,6 @@ static_assert(cpp_core::StatusCode::Control::kSetStateError == -405); static_assert(cpp_core::StatusCode::Monitor::kMonitorError.category() == "Monitor"); static_assert(cpp_core::StatusCode::Monitor::kMonitorError == -500); -static_assert(cpp_core::StatusCodes::kSuccess == cpp_core::StatusCode::kSuccess); -static_assert(cpp_core::StatusCodes::kSetBaudrateError == cpp_core::StatusCode::Configuration::kSetBaudrateError); -static_assert(cpp_core::StatusCodes::kInvalidHandleError == cpp_core::StatusCode::Connection::kInvalidHandleError); -static_assert(cpp_core::StatusCodes::kAbortReadError == cpp_core::StatusCode::Io::kAbortReadError); -static_assert(cpp_core::StatusCodes::kSetStateError == cpp_core::StatusCode::Control::kSetStateError); -static_assert(cpp_core::StatusCodes::kMonitorError == cpp_core::StatusCode::Monitor::kMonitorError); - static_assert(cpp_core::StatusCode::belongsTo( cpp_core::StatusCode::Configuration::kSetBaudrateError)); static_assert(!cpp_core::StatusCode::belongsTo( From 5e1876a7b7658a1d0cc04d4d8ee604196ed61eae Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 8 Jun 2026 21:59:36 +0200 Subject: [PATCH 07/28] refactor: simplify serial function parameters and improve type definitions --- README.md | 32 +------ docs/ffi-metadata.md | 11 ++- tools/check_generated_deno_symbols.cmake | 2 +- tools/generate_deno_symbols.cpp | 112 +++++++++-------------- 4 files changed, 58 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index d35dd98..f5ced12 100644 --- a/README.md +++ b/README.md @@ -86,32 +86,14 @@ function describeStatus(code: number) { try { console.log("opening via symbol", operations.serialOpen.symbol); - handle = serial.serialOpen({ - port: "/dev/ttyUSB0", - baudrate: 115200, - data_bits: 8, - parity: 0, - stop_bits: 0, - }); + handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8, 0, 0); const writeBuffer = new Uint8Array([0x41, 0x54, 0x49, 0x0d, 0x0a]); - const bytesWritten = serial.serialWrite({ - handle, - buffer: writeBuffer, - buffer_size: writeBuffer.byteLength, - timeout_ms: 500, - multiplier: 0, - }); + const bytesWritten = serial.serialWrite(handle, writeBuffer, writeBuffer.byteLength, 500, 0); - const queued = serial.serialInBytesWaiting({ handle }); + const queued = serial.serialInBytesWaiting(handle); const readBuffer = new Uint8Array(256); - const bytesRead = serial.serialRead({ - handle, - buffer: readBuffer, - buffer_size: readBuffer.byteLength, - timeout_ms: 500, - multiplier: 0, - }); + const bytesRead = serial.serialRead(handle, readBuffer, readBuffer.byteLength, 500, 0); const payload = decoder.decode(readBuffer.subarray(0, bytesRead)); @@ -250,11 +232,7 @@ With the generated wrapper layer on top, the same call can be used more directly const dylib = Deno.dlopen(path, symbols); const serial = createBindings(dylib); -const handle = serial.serialOpen({ - port: "/dev/ttyUSB0", - baudrate: 115200, - data_bits: 8, -}); +const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8); ``` ## Using The Metadata diff --git a/docs/ffi-metadata.md b/docs/ffi-metadata.md index e043f1f..0dce79c 100644 --- a/docs/ffi-metadata.md +++ b/docs/ffi-metadata.md @@ -266,7 +266,14 @@ export const operations = { export function createBindings(dylib: Deno.DynamicLibrary) { return { - serialOpen(args: SerialOpenParams): bigint { + serialOpen( + port: string | Deno.PointerValue, + baudrate: number, + data_bits: number, + parity: number = 0, + stop_bits: number = 0, + error_callback: Deno.PointerValue | null = null, + ): bigint { // marshals strings // throws StatusCodeError on failure }, @@ -304,7 +311,7 @@ The metadata layer is useful for: * inspect exported functions from tooling or tests * generate a Deno metadata module with `cpp_core_bindgen` * generate or validate Deno FFI symbol tables -* consume `createBindings(dylib)` as a higher-level TS wrapper layer with named params and automatic error conversion +* consume `createBindings(dylib)` as a higher-level TS wrapper layer with positional arguments and automatic error conversion * generate status-code lookup tables * let Linux/Windows builds consume the same `cpp-core` revision for both headers and bindgen output * keep ABI comments and generated docs consistent with the actual declarations diff --git a/tools/check_generated_deno_symbols.cmake b/tools/check_generated_deno_symbols.cmake index f6f8085..e22a357 100644 --- a/tools/check_generated_deno_symbols.cmake +++ b/tools/check_generated_deno_symbols.cmake @@ -24,7 +24,7 @@ set(required_snippets "export const operations = {" "export class StatusCodeError extends Error" "export function createBindings(dylib: BindgenLibrary)" - "serialOpen(args: SerialOpenParams): bigint" + "serialOpen(port: string | Deno.PointerValue, baudrate: number, data_bits: number, parity: number = 0, stop_bits: number = 0, error_callback: Deno.PointerValue | null = null): bigint" ) foreach(snippet IN LISTS required_snippets) diff --git a/tools/generate_deno_symbols.cpp b/tools/generate_deno_symbols.cpp index 8017b0f..10b9f02 100644 --- a/tools/generate_deno_symbols.cpp +++ b/tools/generate_deno_symbols.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include #include @@ -66,33 +65,6 @@ struct CliOptions return "void"; } -[[nodiscard]] auto toPascalCase(std::string_view text) -> std::string -{ - std::string out; - out.reserve(text.size()); - - bool upper_next = true; - for (const char ch : text) - { - if (ch == '_' || ch == '-' || ch == ' ') - { - upper_next = true; - continue; - } - - if (upper_next) - { - out.push_back(static_cast(std::toupper(static_cast(ch)))); - upper_next = false; - continue; - } - - out.push_back(ch); - } - - return out; -} - [[nodiscard]] auto toTsParameterType(const cpp_core::ParameterDescriptor ¶meter) -> std::string_view { switch (parameter.abi_kind) @@ -148,14 +120,29 @@ struct CliOptions }); } -[[nodiscard]] auto sourceExpr(const cpp_core::ParameterDescriptor ¶meter) -> std::string +[[nodiscard]] auto optionalParameterDefault(const cpp_core::ParameterDescriptor ¶meter) + -> std::optional { - if (parameter.optional) + if (!parameter.optional) { - return std::string(parameter.name); + return std::nullopt; } - return "args." + std::string(parameter.name); + switch (parameter.abi_kind) + { + case cpp_core::AbiValueKind::kInt32: + return "0"; + case cpp_core::AbiValueKind::kErrorCallback: + case cpp_core::AbiValueKind::kNotificationCallback: + return "null"; + default: + return std::nullopt; + } +} + +[[nodiscard]] auto sourceExpr(const cpp_core::ParameterDescriptor ¶meter) -> std::string +{ + return std::string(parameter.name); } auto printUsage(std::ostream &stream) -> void @@ -255,20 +242,6 @@ auto writeStatusCodes(std::ostream &stream) -> void stream << "} as const;\n\n"; } -auto writeParamInterfaces(std::ostream &stream) -> void -{ - for (const auto &function : cpp_core::functionDescriptors()) - { - stream << "export interface " << toPascalCase(function.name) << "Params {\n"; - for (const auto ¶meter : function.parameters) - { - stream << " " << parameter.name << (parameter.optional ? "?: " : ": ") << toTsParameterType(parameter) - << ";\n"; - } - stream << "}\n\n"; - } -} - auto writeHelpers(std::ostream &stream) -> void { stream << "const textEncoder = new TextEncoder();\n\n"; @@ -360,26 +333,6 @@ auto writeWrapperBody(std::ostream &stream, const cpp_core::FunctionDescriptor & stream << " const keepAlive: KeepAlive = [];\n"; } - for (const auto ¶meter : function.parameters) - { - if (!parameter.optional) - { - continue; - } - - stream << " const " << parameter.name << " = args." << parameter.name; - if (parameter.abi_kind == cpp_core::AbiValueKind::kInt32) - { - stream << " ?? 0"; - } - else if (parameter.abi_kind == cpp_core::AbiValueKind::kErrorCallback || - parameter.abi_kind == cpp_core::AbiValueKind::kNotificationCallback) - { - stream << " ?? null"; - } - stream << ";\n"; - } - for (const auto ¶meter : function.parameters) { const std::string source = sourceExpr(parameter); @@ -462,6 +415,28 @@ auto writeWrapperBody(std::ostream &stream, const cpp_core::FunctionDescriptor & } } +auto writeWrapperSignature(std::ostream &stream, const cpp_core::FunctionDescriptor &function) -> void +{ + stream << " " << function.name << "("; + + for (std::size_t index = 0; index < function.parameters.size(); ++index) + { + if (index != 0) + { + stream << ", "; + } + + const auto ¶meter = function.parameters[index]; + stream << parameter.name << ": " << toTsParameterType(parameter); + if (const auto default_value = optionalParameterDefault(parameter)) + { + stream << " = " << *default_value; + } + } + + stream << "): " << toWrapperReturnType(function); +} + auto writeWrapperFunctions(std::ostream &stream) -> void { stream << "export type BindgenLibrary = Deno.DynamicLibrary;\n"; @@ -471,8 +446,8 @@ auto writeWrapperFunctions(std::ostream &stream) -> void for (const auto &function : cpp_core::functionDescriptors()) { - stream << " " << function.name << "(args: " << toPascalCase(function.name) - << "Params): " << toWrapperReturnType(function) << " {\n"; + writeWrapperSignature(stream, function); + stream << " {\n"; writeWrapperBody(stream, function); stream << " },\n"; } @@ -488,7 +463,6 @@ auto writeModule(std::ostream &stream) -> void writeSymbols(stream); writeOperations(stream); writeStatusCodes(stream); - writeParamInterfaces(stream); writeHelpers(stream); writeWrapperFunctions(stream); } From 9336e26a6fd8c57d88f95daaef6ea107a4557f24 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 8 Jun 2026 22:15:56 +0200 Subject: [PATCH 08/28] feat: add error callback support and enhance TypeScript wrapper generation --- CMakeLists.txt | 3 +- README.md | 87 ++++++- docs/ffi-metadata.md | 317 ----------------------- tools/check_generated_deno_symbols.cmake | 3 + tools/generate_deno_symbols.cpp | 59 ++++- 5 files changed, 142 insertions(+), 327 deletions(-) delete mode 100644 docs/ffi-metadata.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a80584..520e418 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,6 @@ if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16) message(FATAL_ERROR "cpp-core requires GCC 16 or newer.") endif() -# The project currently targets a single toolchain with C++26 reflection support. set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -63,7 +62,7 @@ get_git_version_info() generate_git_version(OUTPUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include/cpp_core) set(PROJECT_VERSION "${GIT_VERSION_MAJOR}.${GIT_VERSION_MINOR}.${GIT_VERSION_PATCH}") -set(PROJECT_DESCRIPTION "Shared serial API and FFI metadata for GCC 16 / C++26 reflection builds") +set(PROJECT_DESCRIPTION "Cross-platform helper library shared by cpp-linux-bindings and cpp-windows-bindings") # Header-only library -------------------------------------------------------- add_library(cpp_core INTERFACE) diff --git a/README.md b/README.md index f5ced12..8fac626 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,6 @@ constexpr auto statuses = cpp_core::statusCodeDescriptors(); constexpr auto config_fields = cpp_core::serialConfigFieldDescriptors(); ``` -More practical usage examples are documented in [docs/ffi-metadata.md](docs/ffi-metadata.md). - Bindgen target: ```sh @@ -58,7 +56,7 @@ The generated module contains: * `symbols` for `Deno.dlopen` * `operations` metadata * `statusCodes` plus `StatusCodeError` -* `createBindings(dylib)` which returns named TypeScript wrapper functions +* `createBindings(dylib)` which returns positional TypeScript wrapper functions Example: @@ -119,7 +117,7 @@ try { } finally { if (handle !== undefined) { try { - serial.serialClose({ handle }); + serial.serialClose(handle); } catch (error) { if (error instanceof StatusCodeError) { console.error("close failed:", describeStatus(error.code)); @@ -138,7 +136,7 @@ The metadata layer provides: * **One source of truth** for exported function names, parameter names, return semantics, and status categories * **No manual re-parsing of headers** when you want to derive Deno FFI symbol definitions or internal binding manifests -* **Generated wrapper functions** with named parameters, result decoding, and status-to-exception conversion +* **Generated wrapper functions** with positional parameters, result decoding, and status-to-exception conversion * **Safer refactors** because compile-time metadata checks fail when the exposed signatures drift * **Shared understanding of the ABI** because buffers, callbacks, UTF-8 strings, opaque handles, and status-returning functions are classified centrally * **Less version drift in consumers** because the same `cpp-core` checkout provides both the headers and the generator used by Linux/Windows builds @@ -235,9 +233,86 @@ const serial = createBindings(dylib); const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8); ``` +## TypeScript Wrapper Examples + +The generated wrapper layer is intended to feel like ordinary TypeScript, not a metadata-driven object API. + +Simple open / write / close flow: + +```ts +import { createBindings, symbols } from "./deno_bindings.ts"; + +const dylib = Deno.dlopen("./libcpp_bindings_linux.so", symbols); +const serial = createBindings(dylib); + +const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8); +serial.serialWrite(handle, new Uint8Array([0x41, 0x54, 0x0d]), 3, 500, 0); +serial.serialClose(handle); + +dylib.close(); +``` + +Using optional ABI parameters through normal TypeScript defaults: + +```ts +const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8); +const available = serial.serialInBytesWaiting(handle); +const line = new Uint8Array(256); +const bytesRead = serial.serialReadLine(handle, line, line.byteLength, 250, 2); +``` + +Handling wrapper-thrown `StatusCodeError`: + +```ts +import { + createBindings, + StatusCodeError, + statusCodes, + symbols, +} from "./deno_bindings.ts"; + +const dylib = Deno.dlopen("./libcpp_bindings_linux.so", symbols); +const serial = createBindings(dylib); + +try { + serial.serialOpen("/dev/does-not-exist", 115200, 8); +} catch (error) { + if (error instanceof StatusCodeError) { + if (error.code === statusCodes.NotFoundError) { + console.error("port not found"); + } + console.error(error.category, error.statusName, error.code); + } +} +``` + +Passing an explicit error callback pointer to the generated wrapper: + +```ts +import { + createBindings, + createErrorCallback, + symbols, +} from "./deno_bindings.ts"; + +const dylib = Deno.dlopen("./libcpp_bindings_linux.so", symbols); +const serial = createBindings(dylib); + +const errorCallback = createErrorCallback((error_code, message) => { + console.error("ffi error callback:", error_code, message ?? ""); +}); + +try { + serial.serialOpen("/dev/does-not-exist", 115200, 8, 0, 0, errorCallback.pointer); +} finally { + errorCallback.close(); + dylib.close(); +} +``` + ## Using The Metadata -For concrete usage examples, see [docs/ffi-metadata.md](docs/ffi-metadata.md). The short version is: +The short version is: * use `cpp_core::functionDescriptors()` when you want to inspect the exported C ABI * use `cpp_core::operationDescriptors()` when you want a stable per-operation view next to the raw function list diff --git a/docs/ffi-metadata.md b/docs/ffi-metadata.md deleted file mode 100644 index 0dce79c..0000000 --- a/docs/ffi-metadata.md +++ /dev/null @@ -1,317 +0,0 @@ -# FFI Metadata Guide - -`cpp_core` provides a reflection-backed metadata layer for the public C ABI. - -The important distinction is: - -* the exported runtime interface is still plain C ABI -* the description of that interface is available as typed compile-time metadata in C++ - -That gives you a place to inspect the ABI and build tooling around it without maintaining a second hand-written schema. - -## Available Metadata - -Include: - -```cpp -#include -``` - -Main entrypoints: - -* `cpp_core::functionDescriptors()` -* `cpp_core::operationDescriptors()` -* `cpp_core::statusCodeDescriptors()` -* `cpp_core::serialConfigFieldDescriptors()` -* `cpp_core::versionFieldDescriptors()` -* `cpp_core::findFunctionDescriptor(name)` -* `cpp_core::findOperationDescriptor(name)` -* `cpp_core::findStatusCodeDescriptor(code)` - -Generator tooling: - -* `cpp_core_bindgen` generates a Deno-oriented TypeScript module from the same metadata used by the headers -* the generated module includes `symbols`, metadata exports, `StatusCodeError`, and `createBindings(dylib)` - -Main descriptor types: - -* `cpp_core::FunctionDescriptor` -* `cpp_core::OperationDescriptor` -* `cpp_core::ParameterDescriptor` -* `cpp_core::StatusCodeDescriptor` -* `cpp_core::StructFieldDescriptor` -* `cpp_core::AbiValueKind` -* `cpp_core::ParameterDirection` -* `cpp_core::FunctionResultModel` - -## What Problem This Solves - -Before this layer, consumers had to learn ABI details from handwritten headers only. - -That caused three problems: - -* tools had to re-parse or duplicate knowledge about functions and parameters -* buffer/callback/string/handle semantics were implicit and spread across comments -* Deno binding generation had no canonical machine-readable source inside `cpp-core` - -Now the ABI description is available in a typed form inside the library itself. - -## Benefits - -### 1. You can inspect the exported ABI programmatically - -Example: - -```cpp -#include -#include - -auto main() -> int -{ - for (const auto &fn : cpp_core::functionDescriptors()) - { - std::cout << fn.name << " -> " << fn.return_cpp_type << '\n'; - - for (const auto ¶m : fn.parameters) - { - std::cout << " - " << param.name - << " : " << param.cpp_type - << " optional=" << param.optional - << '\n'; - } - } -} -``` - -This is useful for debugging the ABI contract, generating reports, or building glue code. - -### 2. You get a normalized ABI classification - -Each parameter is mapped into a semantic bucket via `AbiValueKind`. - -Examples: - -* `kUtf8CString` -* `kMutableBuffer` -* `kConstBuffer` -* `kOpaqueHandle` -* `kErrorCallback` -* `kNotificationCallback` - -This matters because foreign runtimes care about ABI semantics, not just raw C++ spellings. - -For example, `"void*"` by itself is not enough to know whether something is: - -* a writable byte buffer -* a UTF-8 C string -* an opaque pointer -* a callback-associated payload - -The metadata layer makes that distinction explicit. - -### 3. You can reason about result behavior - -Each function is classified with `FunctionResultModel`. - -Result models: - -* `kVoid` -* `kStatusCode` -* `kValueOrStatus` -* `kHandleOrStatus` - -That lets tooling distinguish: - -* functions that are pure commands -* functions that return status only -* functions that return a useful numeric value or a negative status -* functions like `serialOpen` that return a handle or a negative status - -This is especially useful when generating Deno wrappers that should not treat every `int` the same way. - -### 4. Status codes become machine-readable - -You can inspect names, categories, and numeric values: - -```cpp -#include -#include - -auto main() -> int -{ - for (const auto &status : cpp_core::statusCodeDescriptors()) - { - std::cout << status.category - << "::" << status.name - << " = " << static_cast(status.value) - << '\n'; - } -} -``` - -This is useful for: - -* generating TS enums or lookup tables -* mapping numeric failures back to symbolic names -* producing consistent docs for consumers - -### 5. Shared structs can be described centrally - -`SerialConfig` and `Version` expose field descriptors. - -This is useful for: - -* generated docs for shared data -* generated FFI struct definitions and byte-layout helpers -* compatibility checks between metadata and runtime-facing declarations - -## Example: Look Up One Function - -```cpp -#include -#include - -auto main() -> int -{ - const auto *fn = cpp_core::findFunctionDescriptor("serialRead"); - if (fn == nullptr) - { - return 1; - } - - std::cout << fn->name << '\n'; - std::cout << fn->signature << '\n'; - - for (const auto ¶m : fn->parameters) - { - std::cout << param.name << " -> " << param.cpp_type << '\n'; - } -} -``` - -Typical use case: - -* lookup by exported symbol name -* build FFI binding metadata for exactly that symbol -* emit wrapper code based on parameter and result classifications - -## Example: Derive Deno-Oriented Symbol Metadata - -The metadata is sufficient to drive a generator directly. - -Bindgen CLI: - -```sh -cmake --build build/gcc --target cpp_core_bindgen -./build/gcc/cpp_core_bindgen -``` - -To capture the output into a TypeScript file: - -```sh -./build/gcc/cpp_core_bindgen --output deno_bindings.ts -``` - -Example sketch: - -```cpp -#include - -constexpr auto toDenoType(cpp_core::AbiValueKind kind) -> const char * -{ - switch (kind) - { - case cpp_core::AbiValueKind::kInt32: - return "i32"; - case cpp_core::AbiValueKind::kInt64: - case cpp_core::AbiValueKind::kOpaqueHandle: - return "i64"; - case cpp_core::AbiValueKind::kUtf8CString: - case cpp_core::AbiValueKind::kMutableBuffer: - case cpp_core::AbiValueKind::kConstBuffer: - case cpp_core::AbiValueKind::kOpaquePointer: - case cpp_core::AbiValueKind::kVersionStructPointer: - case cpp_core::AbiValueKind::kErrorCallback: - case cpp_core::AbiValueKind::kNotificationCallback: - return "pointer"; - case cpp_core::AbiValueKind::kVoid: - return "void"; - } - - return "pointer"; -} -``` - -The generator emits an importable module and wraps the legacy ABI: - -```ts -// Generated from cpp-core reflection metadata. -// Do not edit manually. -// Intended for use with Deno.dlopen. -// ABI mode: legacy. - -export const symbols = { - serialOpen: { - parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"], - result: "isize", - }, - // ... -} as const; - -export const operations = { - serialOpen: { - symbol: "serialOpen", - }, -} as const; - -export function createBindings(dylib: Deno.DynamicLibrary) { - return { - serialOpen( - port: string | Deno.PointerValue, - baudrate: number, - data_bits: number, - parity: number = 0, - stop_bits: number = 0, - error_callback: Deno.PointerValue | null = null, - ): bigint { - // marshals strings - // throws StatusCodeError on failure - }, - }; -} -``` - -## Scope - -Limitations: - -* it does not encode every possible semantic rule for every parameter -* it does not expose `std::expected` itself across the external ABI - -That is deliberate. The ABI description is explicit and compile-checked. - -## Relation To `std::expected` - -The metadata layer does not expose `std::expected` across the ABI boundary. - -That would be the wrong abstraction for Deno FFI and plain C consumers. - -Instead, the model is: - -* internally: implementations can use richer C++ result handling -* externally: the ABI stays plain C -* metadata: describes exported functions, parameters, shared structs, and error semantics - -That gives you most of the practical benefits of structured error handling without making the FFI surface harder to consume. - -## Usage - -The metadata layer is useful for: - -* inspect exported functions from tooling or tests -* generate a Deno metadata module with `cpp_core_bindgen` -* generate or validate Deno FFI symbol tables -* consume `createBindings(dylib)` as a higher-level TS wrapper layer with positional arguments and automatic error conversion -* generate status-code lookup tables -* let Linux/Windows builds consume the same `cpp-core` revision for both headers and bindgen output -* keep ABI comments and generated docs consistent with the actual declarations diff --git a/tools/check_generated_deno_symbols.cmake b/tools/check_generated_deno_symbols.cmake index e22a357..a61b30f 100644 --- a/tools/check_generated_deno_symbols.cmake +++ b/tools/check_generated_deno_symbols.cmake @@ -23,6 +23,9 @@ set(required_snippets "result: \"i64\"" "export const operations = {" "export class StatusCodeError extends Error" + "export const errorCallbackDefinition = {" + "export type ErrorCallback = (error_code: number, message: string | null) => void;" + "export function createErrorCallback(callback: ErrorCallback): Deno.UnsafeCallback" "export function createBindings(dylib: BindgenLibrary)" "serialOpen(port: string | Deno.PointerValue, baudrate: number, data_bits: number, parity: number = 0, stop_bits: number = 0, error_callback: Deno.PointerValue | null = null): bigint" ) diff --git a/tools/generate_deno_symbols.cpp b/tools/generate_deno_symbols.cpp index 10b9f02..76cc460 100644 --- a/tools/generate_deno_symbols.cpp +++ b/tools/generate_deno_symbols.cpp @@ -6,10 +6,30 @@ #include #include #include +#include +#include namespace { +template struct FunctionPointerTraits; + +template struct FunctionPointerTraits +{ + using ReturnType = Return; + using ArgumentTypes = std::tuple; + static constexpr std::size_t kArity = sizeof...(Args); + + template using Argument = std::tuple_element_t; +}; + +using ErrorCallbackTraits = FunctionPointerTraits; + +static_assert(ErrorCallbackTraits::kArity == 2); +static_assert(std::is_same_v); +static_assert(std::is_same_v, int>); +static_assert(std::is_same_v, const char *>); + struct CliOptions { std::optional output_path; @@ -40,9 +60,9 @@ struct CliOptions return "pointer"; } -[[nodiscard]] constexpr auto toDenoResultType(const cpp_core::FunctionDescriptor &function) -> std::string_view +[[nodiscard]] constexpr auto toDenoResultType(cpp_core::AbiValueKind kind) -> std::string_view { - switch (function.return_abi_kind) + switch (kind) { case cpp_core::AbiValueKind::kVoid: return "void"; @@ -65,6 +85,11 @@ struct CliOptions return "void"; } +[[nodiscard]] constexpr auto toDenoResultType(const cpp_core::FunctionDescriptor &function) -> std::string_view +{ + return toDenoResultType(function.return_abi_kind); +} + [[nodiscard]] auto toTsParameterType(const cpp_core::ParameterDescriptor ¶meter) -> std::string_view { switch (parameter.abi_kind) @@ -270,6 +295,12 @@ auto writeHelpers(std::ostream &stream) -> void stream << " }\n"; stream << " return pointer;\n"; stream << "}\n\n"; + stream << "function decodeCStringPointer(value: Deno.PointerValue): string | null {\n"; + stream << " if (value === null) {\n"; + stream << " return null;\n"; + stream << " }\n"; + stream << " return new Deno.UnsafePointerView(value).getCString();\n"; + stream << "}\n\n"; stream << "function marshalCString(value: string | Deno.PointerValue, name: string, keepAlive: KeepAlive): " "Deno.PointerValue {\n"; stream << " if (typeof value === \"string\") {\n"; @@ -325,6 +356,29 @@ auto writeHelpers(std::ostream &stream) -> void stream << "}\n\n"; } +auto writeErrorCallbackSupport(std::ostream &stream) -> void +{ + stream << "export const errorCallbackDefinition = {\n"; + stream << " parameters: [\"i32\", \"pointer\"],\n"; + stream << " result: \"void\",\n"; + stream << "} as const;\n\n"; + + stream << "export type RawErrorCallback = (error_code: number, message: Deno.PointerValue) => void;\n"; + stream << "export type ErrorCallback = (error_code: number, message: string | null) => void;\n\n"; + + stream << "export function createRawErrorCallback(callback: RawErrorCallback): Deno.UnsafeCallback {\n"; + stream << " return new Deno.UnsafeCallback(errorCallbackDefinition, callback);\n"; + stream << "}\n\n"; + + stream << "export function createErrorCallback(callback: ErrorCallback): Deno.UnsafeCallback {\n"; + stream << " return new Deno.UnsafeCallback(errorCallbackDefinition, (error_code, message) => {\n"; + stream << " callback(error_code, decodeCStringPointer(message));\n"; + stream << " });\n"; + stream << "}\n\n"; +} + auto writeWrapperBody(std::ostream &stream, const cpp_core::FunctionDescriptor &function) -> void { const bool use_keep_alive = usesBufferMarshalling(function); @@ -464,6 +518,7 @@ auto writeModule(std::ostream &stream) -> void writeOperations(stream); writeStatusCodes(stream); writeHelpers(stream); + writeErrorCallbackSupport(stream); writeWrapperFunctions(stream); } From ebe892f2a58b57bedff2bca89ff588242b3f3e01 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 10 Jun 2026 21:57:19 +0200 Subject: [PATCH 09/28] refactor: update README for clarity and structure, enhancing API contract description --- README.md | 382 ++++++++++++++++++++---------------------------------- 1 file changed, 138 insertions(+), 244 deletions(-) diff --git a/README.md b/README.md index 8fac626..bd56a05 100644 --- a/README.md +++ b/README.md @@ -1,243 +1,180 @@ # 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, build-time version information, and the reflection-backed metadata used by bindgen tooling. -> [!IMPORTANT] -> -> This version of `cpp-core` requires **GCC 16**, **C++26**, and **reflection support**. -> -> **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++26 + reflection** header-only API definitions -* **Cross-platform serial I/O API** (POSIX/Windows compatible) -* **Centralized version information** from Git tags (`cpp_core::kVersion`) -* **Reflection-backed FFI metadata** for functions, status codes, and shared structs -* **C-compatible ABI** for FFI bindings (Rust, Python, Deno, etc.) +## Status -## Requirements +`cpp-core` is the canonical definition of the current reflection-based API line. -* CMake >= 3.30 -* GCC 16 or newer -* `-std=c++26` and `-freflection` -* Git (for automatic version detection) +- Supported compiler family: GNU C++ +- Supported Linux toolchain: GCC 16+ +- Supported Windows toolchain: MinGW-w64 GCC 16+ +- macOS support: not ready yet +- Required language mode: C++26 with `-freflection` +- Required build system: CMake 3.30+ -Clang and MSVC are not supported. +Clang and MSVC are not supported at this time. -## C Interface +## What It Provides -`cpp-core` defines a C-compatible interface for the platform-specific binding libraries and exposes reflection-backed -metadata for that same interface. +- Header-only C-compatible serial API definitions under `include/cpp_core` +- Reflection-backed ABI metadata for functions, operations, status codes, and shared structs +- A generated version surface used consistently across all platform bindings +- A `cpp_core_bindgen` executable that emits Deno-oriented FFI metadata and wrappers +- An installable CMake package target: `cpp_core::cpp_core` -Metadata entrypoints: +## Repository Layout -```cpp -#include +- `include/cpp_core/serial.h`: aggregated C ABI for serial operations +- `include/cpp_core/ffi_metadata.hpp`: compile-time metadata over the exported ABI +- `include/cpp_core/interface/get_version.h`: version struct and `getVersion` +- `tools/generate_deno_symbols.cpp`: bindgen entrypoint for Deno symbols and wrappers -constexpr auto functions = cpp_core::functionDescriptors(); -constexpr auto operations = cpp_core::operationDescriptors(); -constexpr auto statuses = cpp_core::statusCodeDescriptors(); -constexpr auto config_fields = cpp_core::serialConfigFieldDescriptors(); -``` +`cpp-core` hard-requires the GNU compiler family with working C++26 reflection support. During configure, CMake rejects unsupported compilers and rejects GNU builds where `-freflection` is unavailable or incomplete. -Bindgen target: +## Quick Start -```sh -cmake --build build/gcc --target cpp_core_bindgen -./build/gcc/cpp_core_bindgen --output deno_bindings.ts -``` +Consume `cpp-core` as a CMake dependency from another project: -The generated module contains: +```cmake +CPMAddPackage( + NAME cpp_core + GITHUB_REPOSITORY Serial-IO/cpp-core + GIT_TAG v1.1.0 +) -* `symbols` for `Deno.dlopen` -* `operations` metadata -* `statusCodes` plus `StatusCodeError` -* `createBindings(dylib)` which returns positional TypeScript wrapper functions +target_link_libraries(my_binding PRIVATE cpp_core::cpp_core) +``` -Example: +Use the exported headers in your implementation: -```ts -import { - createBindings, - operations, - StatusCodeError, - statusCodeInfo, - statusCodes, - symbols, -} from "./deno_bindings.ts"; +```cpp +#include +#include -const dylib = Deno.dlopen("./libcpp_bindings_linux.so", symbols); -const serial = createBindings(dylib); -const decoder = new TextDecoder(); - -let handle: bigint | undefined; - -function describeStatus(code: number) { - const info = statusCodeInfo[String(code) as keyof typeof statusCodeInfo]; - return info ? `${info.category}::${info.name}` : `UnknownStatus (${code})`; -} - -try { - console.log("opening via symbol", operations.serialOpen.symbol); - - handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8, 0, 0); - - const writeBuffer = new Uint8Array([0x41, 0x54, 0x49, 0x0d, 0x0a]); - const bytesWritten = serial.serialWrite(handle, writeBuffer, writeBuffer.byteLength, 500, 0); - - const queued = serial.serialInBytesWaiting(handle); - const readBuffer = new Uint8Array(256); - const bytesRead = serial.serialRead(handle, readBuffer, readBuffer.byteLength, 500, 0); - - const payload = decoder.decode(readBuffer.subarray(0, bytesRead)); - - console.log({ - handle, - bytesWritten, - queued, - bytesRead, - payload, - }); -} catch (error) { - if (error instanceof StatusCodeError) { - if (error.code === statusCodes.NotFoundError) { - console.error("port not found"); - } else if (error.code === statusCodes.ReadError) { - console.error("read failed"); - } else { - console.error(describeStatus(error.code)); - } - throw error; - } - throw error; -} finally { - if (handle !== undefined) { - try { - serial.serialClose(handle); - } catch (error) { - if (error instanceof StatusCodeError) { - console.error("close failed:", describeStatus(error.code)); - } else { - console.error("close failed:", error); - } - } - } - dylib.close(); -} +auto serialOpen( + void *port, + int baudrate, + int data_bits, + int parity, + int stop_bits, + ErrorCallbackT error_callback +) -> intptr_t; ``` -## Benefits +Read the version data baked into the checkout: -The metadata layer provides: - -* **One source of truth** for exported function names, parameter names, return semantics, and status categories -* **No manual re-parsing of headers** when you want to derive Deno FFI symbol definitions or internal binding manifests -* **Generated wrapper functions** with positional parameters, result decoding, and status-to-exception conversion -* **Safer refactors** because compile-time metadata checks fail when the exposed signatures drift -* **Shared understanding of the ABI** because buffers, callbacks, UTF-8 strings, opaque handles, and status-returning functions are classified centrally -* **Less version drift in consumers** because the same `cpp-core` checkout provides both the headers and the generator used by Linux/Windows builds +```cpp +#include -## Version Information +cpp_core::Version version{}; +getVersion(&version); +``` -The version is **automatically extracted from Git tags** during CMake configuration and cannot be overridden: +## Building This Repository -* **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) +Native GCC build: -All binding repositories that include this repository will use the same version information, ensuring consistency across platforms. +```sh +cmake -S . -B build/gcc -G Ninja -DCMAKE_CXX_COMPILER=g++-16 +cmake --build build/gcc +ctest --test-dir build/gcc +``` -```cpp -#include +MinGW-w64 GCC build: -constexpr auto v = cpp_core::kVersion; // v.major, v.minor, v.patch +```sh +cmake -S . -B build/mingw -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-toolchain.cmake +cmake --build build/mingw ``` -## Usage in Binding Repositories +The CMake project exports the package target and also builds these relevant targets: -Binding repositories typically include this repository as a dependency: +- `cpp_core::cpp_core`: header-only interface target +- `cpp_core_bindgen`: Deno symbol and wrapper generator +- `cpp_core_compile_tests`: compile-time validation target when testing is enabled +- `cpp_core_bindgen_smoke`: generated-output smoke check when testing is enabled -```cmake -CPMAddPackage( - NAME cpp_core - GITHUB_REPOSITORY Serial-IO/cpp-core - GIT_TAG v1.2.3 # Version tag determines the API version -) - -target_link_libraries(my_binding_library PRIVATE cpp_core::cpp_core) -``` +## ABI Surface -The binding implementation then uses the API definitions: +The main aggregated interface lives in: ```cpp #include -#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. -// Implement platform-specific functions matching the API -intptr_t serialOpen( +Example: + +```cpp +MODULE_API auto serialOpen( void *port, int baudrate, int data_bits, - int parity, - int stop_bits, - ErrorCallbackT error_callback -) { - // Platform-specific implementation (Windows/Linux/macOS) -} + int parity = 0, + int stop_bits = 0, + ErrorCallbackT error_callback = nullptr +) -> intptr_t; ``` -## Error Handling +This model keeps the ABI easy to consume from Deno, Rust, Python, or other FFI hosts without requiring C++ runtime coupling. -The API uses `status + out-params` or `value-or-negative-status`, because that maps cleanly to plain C and Deno FFI. +## Reflection Metadata -```c -intptr_t serialOpen( - const char *port, - int baudrate, - int data_bits, - int parity, - int stop_bits, - ErrorCallbackT error_callback -); +`cpp-core` exposes compile-time metadata for the ABI and the shared structs: + +```cpp +#include + +constexpr auto functions = cpp_core::functionDescriptors(); +constexpr auto operations = cpp_core::operationDescriptors(); +constexpr auto statuses = cpp_core::statusCodeDescriptors(); +constexpr auto serial_config_fields = cpp_core::serialConfigFieldDescriptors(); +constexpr auto version_fields = cpp_core::versionFieldDescriptors(); ``` -The corresponding Deno FFI shape is straightforward: +Targeted lookup helpers are also available: -```ts -const symbols = { - serialOpen: { - parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"], - result: "isize", - }, -} as const; - -const handle = dylib.symbols.serialOpen( - cstr("/dev/ttyUSB0"), - 115200, - 8, - 0, - 0, - null, -); +```cpp +constexpr auto *open_fn = cpp_core::findFunctionDescriptor("serialOpen"); +constexpr auto *open_op = cpp_core::findOperationDescriptor("serialOpen"); +constexpr auto *read_error = + cpp_core::findStatusCodeDescriptor(cpp_core::StatusCode::Io::kReadError); ``` -With the generated wrapper layer on top, the same call can be used more directly: +The metadata is the source of truth for: -```ts -const dylib = Deno.dlopen(path, symbols); -const serial = createBindings(dylib); +- exported function names +- parameter names and optionality +- ABI kind classification for strings, buffers, handles, and callbacks +- result model classification +- shared struct field layout metadata -const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8); +## Deno Bindgen + +Build and run the bindgen tool: + +```sh +cmake --build build/gcc --target cpp_core_bindgen +./build/gcc/cpp_core_bindgen --output deno_bindings.ts ``` -## TypeScript Wrapper Examples +The generated module includes: -The generated wrapper layer is intended to feel like ordinary TypeScript, not a metadata-driven object API. +- `symbols` for `Deno.dlopen` +- `operations` metadata +- `statusCodes` and status lookup data +- `StatusCodeError` +- `createBindings(dylib)` wrappers +- callback helpers such as `createErrorCallback(...)` -Simple open / write / close flow: +Minimal Deno usage: ```ts import { createBindings, symbols } from "./deno_bindings.ts"; @@ -252,74 +189,31 @@ serial.serialClose(handle); dylib.close(); ``` -Using optional ABI parameters through normal TypeScript defaults: - -```ts -const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8); -const available = serial.serialInBytesWaiting(handle); -const line = new Uint8Array(256); -const bytesRead = serial.serialReadLine(handle, line, line.byteLength, 250, 2); -``` - -Handling wrapper-thrown `StatusCodeError`: +## Versioning -```ts -import { - createBindings, - StatusCodeError, - statusCodes, - symbols, -} from "./deno_bindings.ts"; - -const dylib = Deno.dlopen("./libcpp_bindings_linux.so", symbols); -const serial = createBindings(dylib); +Version information is generated from Git during CMake configure and written into `include/cpp_core/version.hpp`. -try { - serial.serialOpen("/dev/does-not-exist", 115200, 8); -} catch (error) { - if (error instanceof StatusCodeError) { - if (error.code === statusCodes.NotFoundError) { - console.error("port not found"); - } - console.error(error.category, error.statusName, error.code); - } -} -``` +- 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` -Passing an explicit error callback pointer to the generated wrapper: +The version data is exposed through: -```ts -import { - createBindings, - createErrorCallback, - symbols, -} from "./deno_bindings.ts"; +- the `version` namespace in `include/cpp_core/version.hpp` +- the `cpp_core::Version` struct +- the `getVersion(cpp_core::Version *out)` ABI function -const dylib = Deno.dlopen("./libcpp_bindings_linux.so", symbols); -const serial = createBindings(dylib); - -const errorCallback = createErrorCallback((error_code, message) => { - console.error("ffi error callback:", error_code, message ?? ""); -}); - -try { - serial.serialOpen("/dev/does-not-exist", 115200, 8, 0, 0, errorCallback.pointer); -} finally { - errorCallback.close(); - dylib.close(); -} -``` +## Relationship to Platform Repositories -## Using The Metadata +`cpp-core` defines the contract. The platform repositories implement it. -The short version is: +- `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 -* use `cpp_core::functionDescriptors()` when you want to inspect the exported C ABI -* use `cpp_core::operationDescriptors()` when you want a stable per-operation view next to the raw function list -* use `cpp_core::statusCodeDescriptors()` when you want symbolic error information -* use `cpp_core::serialConfigFieldDescriptors()` and `cpp_core::versionFieldDescriptors()` when you want shared struct metadata -* use `cpp_core::findFunctionDescriptor()`, `cpp_core::findOperationDescriptor()`, and `cpp_core::findStatusCodeDescriptor()` for targeted lookups +Keeping the contract, metadata, and version surface here avoids ABI drift between platforms and keeps generated binding layers aligned with the actual exported API. ## License -`Apache-2.0` - Check [LICENSE](LICENSE) for more details. +Licensed under `Apache-2.0`. See [LICENSE](LICENSE). From 2ec39aee9f8c80db34cf039b2eb7130c9b542d85 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 10 Jun 2026 22:07:05 +0200 Subject: [PATCH 10/28] feat: add CI workflow for build and testing, and update Doxygen configuration --- .github/workflows/ci.yml | 48 +++++++++++++++++++++++++++++++++++ .github/workflows/doxygen.yml | 15 +++++++---- Doxyfile | 4 +-- 3 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3bd4832 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: 'CI' + +on: + push: + branches: + - '*' + pull_request: + workflow_dispatch: + +jobs: + build-and-test: + name: 'Build compile tests and bindgen smoke' + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install GCC 16 + Ninja + run: | + sudo apt-get update -qq + sudo apt-get install -y gcc-16 g++-16 ninja-build + + - name: Show tool versions + run: | + cmake --version + ninja --version + gcc-16 --version + g++-16 --version + + - name: Configure + 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 tests + run: | + cmake --build build/ci --target cpp_core_compile_tests + + - name: Run bindgen smoke test + run: | + cmake --build build/ci --target cpp_core_bindgen_smoke + + - 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..40c565f 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -6,14 +6,16 @@ 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: @@ -21,7 +23,7 @@ jobs: name: 'Build Doxygen HTML' steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Doxygen + Graphviz run: | @@ -42,7 +44,10 @@ jobs: needs: build-docs runs-on: ubuntu-latest name: 'Deploy to GitHub Pages' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v4 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 From 5360e8705fcfb74aea4899dfa582d5821e55c524 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 10 Jun 2026 22:12:42 +0200 Subject: [PATCH 11/28] fix: update CI workflow to use Ubuntu 24.04 and ensure GCC 16 installation --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bd4832..d735fe2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: jobs: build-and-test: name: 'Build compile tests and bindgen smoke' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout @@ -18,6 +18,9 @@ jobs: - name: Install GCC 16 + Ninja 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 From aeb05f65e367c50649be7b3dc288961d2071dfdf Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 10 Jun 2026 22:15:33 +0200 Subject: [PATCH 12/28] fix: update static assert for reflection implementation version --- tests/ffi_metadata_compile_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ffi_metadata_compile_test.cpp b/tests/ffi_metadata_compile_test.cpp index 8de38cc..ea9bb4f 100644 --- a/tests/ffi_metadata_compile_test.cpp +++ b/tests/ffi_metadata_compile_test.cpp @@ -1,6 +1,6 @@ #include "cpp_core/ffi_metadata.hpp" -static_assert(__cpp_impl_reflection >= 202603L); +static_assert(__cpp_impl_reflection >= 202506L); static_assert(!cpp_core::kFunctionDescriptors.empty()); static_assert(cpp_core::kFunctionDescriptors.size() == cpp_core::kOperationDescriptors.size()); From 7351f930aefaee2f8715358078bab9785d3dfe44 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Thu, 11 Jun 2026 23:37:17 +0200 Subject: [PATCH 13/28] feat: make glue code more genereic --- CMakeLists.txt | 12 +- FFI.md | 213 +++++ README.md | 65 +- cmake/mingw-toolchain.cmake | 2 +- tools/check_generated_deno_symbols.cmake | 38 - .../check_generated_typescript_bindings.cmake | 103 ++ tools/generate_deno_symbols.cpp | 557 ----------- tools/generate_typescript_bindings.cpp | 881 ++++++++++++++++++ 8 files changed, 1256 insertions(+), 615 deletions(-) create mode 100644 FFI.md delete mode 100644 tools/check_generated_deno_symbols.cmake create mode 100644 tools/check_generated_typescript_bindings.cmake delete mode 100644 tools/generate_deno_symbols.cpp create mode 100644 tools/generate_typescript_bindings.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 520e418..53bd4fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,7 +97,7 @@ target_include_directories( target_compile_features(cpp_core INTERFACE cxx_std_26) target_compile_options(cpp_core INTERFACE -freflection) -add_executable(cpp_core_bindgen EXCLUDE_FROM_ALL tools/generate_deno_symbols.cpp) +add_executable(cpp_core_bindgen EXCLUDE_FROM_ALL tools/generate_typescript_bindings.cpp) target_link_libraries(cpp_core_bindgen PRIVATE cpp_core::cpp_core) target_link_libraries(cpp_core_bindgen PRIVATE cpp_core_strict_warnings) @@ -109,7 +109,7 @@ if(BUILD_TESTING) target_link_libraries(cpp_core_compile_tests PRIVATE cpp_core::cpp_core) target_link_libraries(cpp_core_compile_tests PRIVATE cpp_core_strict_warnings) - set(CPP_CORE_DENO_BINDGEN_SMOKE_EXE "${CMAKE_CURRENT_BINARY_DIR}/cpp_core_bindgen_smoke_bin") + set(CPP_CORE_TYPESCRIPT_BINDGEN_SMOKE_EXE "${CMAKE_CURRENT_BINARY_DIR}/cpp_core_bindgen_smoke_bin") add_custom_target( cpp_core_bindgen_smoke COMMAND @@ -117,14 +117,14 @@ if(BUILD_TESTING) -E copy $ - ${CPP_CORE_DENO_BINDGEN_SMOKE_EXE} + ${CPP_CORE_TYPESCRIPT_BINDGEN_SMOKE_EXE} COMMAND ${CMAKE_COMMAND} - -DGENERATOR=${CPP_CORE_DENO_BINDGEN_SMOKE_EXE} + -DGENERATOR=${CPP_CORE_TYPESCRIPT_BINDGEN_SMOKE_EXE} -P - ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_generated_deno_symbols.cmake + ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_generated_typescript_bindings.cmake DEPENDS cpp_core_bindgen - COMMENT "Running smoke check for generated Deno bindgen output" + COMMENT "Running smoke check for generated TypeScript bindgen output" VERBATIM ) endif() diff --git a/FFI.md b/FFI.md new file mode 100644 index 0000000..06fdeb1 --- /dev/null +++ b/FFI.md @@ -0,0 +1,213 @@ +# FFI Runtime Notes + +`cpp_core_bindgen` generates a runtime-neutral TypeScript layer: + +- `symbols` ABI metadata +- `createBindings(host, dylib)` wrappers +- `createErrorCallback(host, ...)` +- built-in Deno helpers +- built-in Bun helpers +- custom adapter helpers +- `BindgenHost` / `BindgenLibrary` / `BindgenRuntimeAdapter` interfaces + +That means the generated file is not tied to one runtime anymore. Some adapters are already generated for you, and you can still plug in your own. + +## Runtime Profiles + +By default, the generator emits the generic adapter path only: + +```sh +./build/gcc/cpp_core_bindgen --output cpp_core_bindings.ts +``` + +You can opt into a runtime-specific profile: + +```sh +./build/gcc/cpp_core_bindgen --runtime deno --output cpp_core_bindings.ts +./build/gcc/cpp_core_bindgen --runtime bun --output cpp_core_bindings.ts +``` + +Supported values: + +- `generic` +- `node` +- `deno` +- `bun` + +## Quick Status + +| Runtime | Status | Notes | +| --- | --- | --- | +| Generic / Node.js | Default | Use `createHostFromRuntimeAdapter()` and `loadLibraryFromRuntimeAdapter()`. | +| Deno | Opt-in built in | Generate with `--runtime deno`, then use `createDenoHost()` and `loadDenoLibrary()`. | +| Bun | Opt-in built in | Generate with `--runtime bun`, then use `await createBunHost()` and `await loadBunLibrary()`. | + +## Common Shape + +All runtime examples follow the same pattern: + +```ts +import { + createBindings, + createErrorCallback, + createHostFromRuntimeAdapter, + loadLibraryFromRuntimeAdapter, +} from "./cpp_core_bindings.ts"; + +const host = createHostFromRuntimeAdapter(myRuntimeAdapter); +const dylib = loadLibraryFromRuntimeAdapter( + myRuntimeAdapter, + "./libcpp_bindings_linux.so", +); + +const serial = createBindings(host, dylib); +const errorCallback = createErrorCallback(host, (code, message) => { + console.error(code, message); +}); + +const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8, 0, 0, errorCallback.pointer); +serial.serialWrite(handle, new Uint8Array([0x41, 0x54, 0x0d]), 3, 500, 0); +serial.serialClose(handle); +errorCallback.close(); +``` + +The only runtime-specific parts are: + +- the runtime adapter itself +- or the built-in Deno/Bun helpers + +## Deno + +Deno helpers are generated when you build with `--runtime deno`. + +```ts +import { + createBindings, + createErrorCallback, + createDenoHost, + loadDenoLibrary, + symbols, +} from "./cpp_core_bindings.ts"; + +const host = createDenoHost(); +const dylib = loadDenoLibrary("./libcpp_bindings_linux.so", symbols); +const serial = createBindings(host, dylib); +const errorCallback = createErrorCallback(host, (code, message) => { + console.error(code, message); +}); + +const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8, 0, 0, errorCallback.pointer); +serial.serialClose(handle); +errorCallback.close(); +dylib.close(); +``` + +What the generated file already gives you: + +```ts +createDenoHost() +loadDenoLibrary(path, symbols?) +``` + +## Bun + +Bun helpers are generated when you build with `--runtime bun`. + +```ts +import { + createBindings, + createBunHost, + loadBunLibrary, + symbols, +} from "./cpp_core_bindings.ts"; + +const host = await createBunHost(); +const dylib = await loadBunLibrary("./libcpp_bindings_linux.so", symbols); +const serial = createBindings(host, dylib); + +const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8); +serial.serialClose(handle); +dylib.close(); +``` + +What the generated file already gives you: + +```ts +await createBunHost() +await loadBunLibrary(path, symbols?) +``` + +## Node.js + +Node uses the default generic output. There is no built-in Node loader implementation, because there is no single official `dlopen`-style JS API that matches Deno/Bun. The intended path is: provide a Node-side bridge and plug it into the generated custom-adapter helpers. + +```ts +import native from "./build/Release/cpp_core.node"; + +import { + createBindings, + createHostFromRuntimeAdapter, + loadLibraryFromRuntimeAdapter, + symbols, + type BindgenRuntimeAdapter, + BindgenLibrary, + NativeFunctionDefinition, + PointerValue, +} from "./cpp_core_bindings.ts"; + +type NativeAddon = { + loadLibrary(path: string, symbols: Record): BindgenLibrary; + pointerOf(value: ArrayBufferView): PointerValue | null; + readCString(pointer: PointerValue): string | null; + createCallback( + definition: NativeFunctionDefinition, + callback: (...args: unknown[]) => unknown, + ): { pointer: PointerValue; close(): void }; +}; + +const addon = native as NativeAddon; + +const adapter: BindgenRuntimeAdapter = { + loadLibrary(path, definitions) { + return addon.loadLibrary(String(path), definitions); + }, + pointerOf(value) { + return addon.pointerOf(value); + }, + readCString(pointer) { + return addon.readCString(pointer); + }, + createCallback(definition, callback) { + return addon.createCallback(definition, callback); + }, +}; + +const host = createHostFromRuntimeAdapter(adapter); +const dylib = loadLibraryFromRuntimeAdapter( + adapter, + "./libcpp_bindings_linux.so", + symbols, +); +const serial = createBindings(host, dylib); +``` + +For Node, the missing piece is still the addon implementation behind `cpp_core.node`. The generated TypeScript layer already provides the integration points: + +- `BindgenRuntimeAdapter` +- `createHostFromRuntimeAdapter(...)` +- `loadLibraryFromRuntimeAdapter(...)` + +## Practical Takeaway + +- Generic / Node: built in as a custom-adapter path +- Deno: built in when generated with `--runtime deno` +- Bun: built in when generated with `--runtime bun` +- Node loader implementation itself is still yours + +## Official References + +- Deno FFI: https://docs.deno.com/runtime/fundamentals/ffi/ +- Deno `Deno.dlopen`: https://docs.deno.com/api/deno/~/Deno.dlopen +- Bun FFI: https://bun.sh/docs/runtime/ffi +- Node addons: https://nodejs.org/api/addons.html +- Node-API: https://nodejs.org/api/n-api.html diff --git a/README.md b/README.md index bd56a05..8feb147 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Clang and MSVC are not supported at this time. - Header-only C-compatible serial API definitions under `include/cpp_core` - Reflection-backed ABI metadata for functions, operations, status codes, and shared structs - A generated version surface used consistently across all platform bindings -- A `cpp_core_bindgen` executable that emits Deno-oriented FFI metadata and wrappers +- A `cpp_core_bindgen` executable that emits TypeScript FFI metadata and wrappers - An installable CMake package target: `cpp_core::cpp_core` ## Repository Layout @@ -33,7 +33,7 @@ Clang and MSVC are not supported at this time. - `include/cpp_core/serial.h`: aggregated C ABI for serial operations - `include/cpp_core/ffi_metadata.hpp`: compile-time metadata over the exported ABI - `include/cpp_core/interface/get_version.h`: version struct and `getVersion` -- `tools/generate_deno_symbols.cpp`: bindgen entrypoint for Deno symbols and wrappers +- `tools/generate_typescript_bindings.cpp`: bindgen entrypoint for TypeScript symbols and wrappers `cpp-core` hard-requires the GNU compiler family with working C++26 reflection support. During configure, CMake rejects unsupported compilers and rejects GNU builds where `-freflection` is unavailable or incomplete. @@ -96,7 +96,7 @@ cmake --build build/mingw The CMake project exports the package target and also builds these relevant targets: - `cpp_core::cpp_core`: header-only interface target -- `cpp_core_bindgen`: Deno symbol and wrapper generator +- `cpp_core_bindgen`: TypeScript symbol and wrapper generator - `cpp_core_compile_tests`: compile-time validation target when testing is enabled - `cpp_core_bindgen_smoke`: generated-output smoke check when testing is enabled @@ -123,7 +123,7 @@ MODULE_API auto serialOpen( ) -> intptr_t; ``` -This model keeps the ABI easy to consume from Deno, Rust, Python, or other FFI hosts without requiring C++ runtime coupling. +This model keeps the ABI easy to consume from TypeScript hosts, Rust, Python, or other FFI hosts without requiring C++ runtime coupling. ## Reflection Metadata @@ -156,39 +156,78 @@ The metadata is the source of truth for: - result model classification - shared struct field layout metadata -## Deno Bindgen +## TypeScript Bindgen Build and run the bindgen tool: ```sh cmake --build build/gcc --target cpp_core_bindgen -./build/gcc/cpp_core_bindgen --output deno_bindings.ts +./build/gcc/cpp_core_bindgen --output cpp_core_bindings.ts +``` + +Optional runtime profile: + +```sh +./build/gcc/cpp_core_bindgen --runtime deno --output cpp_core_bindings.ts +./build/gcc/cpp_core_bindgen --runtime bun --output cpp_core_bindings.ts ``` The generated module includes: -- `symbols` for `Deno.dlopen` +- `symbols` ABI metadata - `operations` metadata - `statusCodes` and status lookup data - `StatusCodeError` -- `createBindings(dylib)` wrappers +- `createBindings(host, dylib)` wrappers +- custom runtime adapter helpers for Node or other hosts +- optional built-in runtime helpers when `--runtime deno` or `--runtime bun` is used - callback helpers such as `createErrorCallback(...)` -Minimal Deno usage: +Default generic usage: ```ts -import { createBindings, symbols } from "./deno_bindings.ts"; +import { + createBindings, + createHostFromRuntimeAdapter, + loadLibraryFromRuntimeAdapter, + symbols, + type BindgenRuntimeAdapter, +} from "./cpp_core_bindings.ts"; + +declare const adapter: BindgenRuntimeAdapter; + +const host = createHostFromRuntimeAdapter(adapter); +const dylib = loadLibraryFromRuntimeAdapter(adapter, "./libcpp_bindings_linux.so", symbols); +const serial = createBindings(host, dylib); +``` -const dylib = Deno.dlopen("./libcpp_bindings_linux.so", symbols); -const serial = createBindings(dylib); +Deno-specific usage with `--runtime deno`: + +```ts +import { + createBindings, + createErrorCallback, + createDenoHost, + loadDenoLibrary, + symbols, +} from "./cpp_core_bindings.ts"; + +const host = createDenoHost(); +const dylib = loadDenoLibrary("./libcpp_bindings_linux.so", symbols); +const serial = createBindings(host, dylib); +const errorCallback = createErrorCallback(host, (errorCode, message) => { + console.error(errorCode, message); +}); const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8); serial.serialWrite(handle, new Uint8Array([0x41, 0x54, 0x0d]), 3, 500, 0); serial.serialClose(handle); - +errorCallback.close(); dylib.close(); ``` +Without `--runtime`, the output stays generic. If you target Node.js or another host, use the generated custom-adapter helpers and implement the runtime bridge yourself. + ## Versioning Version information is generated from Git during CMake configure and written into `include/cpp_core/version.hpp`. diff --git a/cmake/mingw-toolchain.cmake b/cmake/mingw-toolchain.cmake index 0acb898..3582dc0 100644 --- a/cmake/mingw-toolchain.cmake +++ b/cmake/mingw-toolchain.cmake @@ -32,7 +32,7 @@ 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 Deno-loaded DLLs: +# 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/tools/check_generated_deno_symbols.cmake b/tools/check_generated_deno_symbols.cmake deleted file mode 100644 index a61b30f..0000000 --- a/tools/check_generated_deno_symbols.cmake +++ /dev/null @@ -1,38 +0,0 @@ -if(NOT DEFINED GENERATOR) - message(FATAL_ERROR "GENERATOR must point to the cpp_core_bindgen executable.") -endif() - -execute_process( - COMMAND "${GENERATOR}" - RESULT_VARIABLE generator_result - OUTPUT_VARIABLE generator_output - ERROR_VARIABLE generator_error -) - -if(NOT generator_result EQUAL 0) - message(FATAL_ERROR "Generator failed with code ${generator_result}: ${generator_error}") -endif() - -set(required_snippets - "export const symbols = {" - "serialOpen: {" - "result: \"isize\"" - "serialClose: {" - "result: \"i32\"" - "serialInBytesTotal: {" - "result: \"i64\"" - "export const operations = {" - "export class StatusCodeError extends Error" - "export const errorCallbackDefinition = {" - "export type ErrorCallback = (error_code: number, message: string | null) => void;" - "export function createErrorCallback(callback: ErrorCallback): Deno.UnsafeCallback" - "export function createBindings(dylib: BindgenLibrary)" - "serialOpen(port: string | Deno.PointerValue, baudrate: number, data_bits: number, parity: number = 0, stop_bits: number = 0, error_callback: Deno.PointerValue | null = null): bigint" -) - -foreach(snippet IN LISTS required_snippets) - string(FIND "${generator_output}" "${snippet}" snippet_index) - if(snippet_index EQUAL -1) - message(FATAL_ERROR "Missing expected snippet in generated output: ${snippet}") - endif() -endforeach() diff --git a/tools/check_generated_typescript_bindings.cmake b/tools/check_generated_typescript_bindings.cmake new file mode 100644 index 0000000..28798e5 --- /dev/null +++ b/tools/check_generated_typescript_bindings.cmake @@ -0,0 +1,103 @@ +if(NOT DEFINED GENERATOR) + message(FATAL_ERROR "GENERATOR must point to the cpp_core_bindgen executable.") +endif() + +function(run_generator output_var) + execute_process( + COMMAND "${GENERATOR}" ${ARGN} + RESULT_VARIABLE generator_result + OUTPUT_VARIABLE generator_output + ERROR_VARIABLE generator_error + ) + + if(NOT generator_result EQUAL 0) + message(FATAL_ERROR "Generator failed with code ${generator_result}: ${generator_error}") + endif() + + set(${output_var} "${generator_output}" PARENT_SCOPE) +endfunction() + +function(require_snippets content label) + foreach(snippet IN LISTS ARGN) + string(FIND "${content}" "${snippet}" snippet_index) + if(snippet_index EQUAL -1) + message(FATAL_ERROR "Missing expected snippet in ${label}: ${snippet}") + endif() + endforeach() +endfunction() + +function(forbid_snippets content label) + foreach(snippet IN LISTS ARGN) + string(FIND "${content}" "${snippet}" snippet_index) + if(NOT snippet_index EQUAL -1) + message(FATAL_ERROR "Unexpected snippet in ${label}: ${snippet}") + endif() + endforeach() +endfunction() + +set(common_required + "// Runtime profile: generic." + "export type OpaquePointer = object;" + "export type PointerValue = number | bigint | OpaquePointer;" + "export interface BindgenRuntimeAdapter {" + "export interface BindgenHost {" + "pointerOf(value: ArrayBuffer | ArrayBufferView): PointerValue | null;" + "export function createHostFromRuntimeAdapter(adapter: BindgenRuntimeAdapter): BindgenHost" + "export function loadLibraryFromRuntimeAdapter(" + "createCallback(" + "export const symbols = {" + "serialOpen: {" + "result: \"isize\"" + "serialClose: {" + "result: \"i32\"" + "serialInBytesTotal: {" + "result: \"i64\"" + "export const operations = {" + "export class StatusCodeError extends Error" + "export const errorCallbackDefinition = {" + "export type ErrorCallback = (error_code: number, message: string | null) => void;" + "export function createErrorCallback(host: BindgenHost, callback: ErrorCallback): CallbackHandle" + "export function createBindings(host: BindgenHost, dylib: BindgenLibrary)" + "serialOpen(port: string | PointerValue, baudrate: number, data_bits: number, parity: number = 0, stop_bits: number = 0, error_callback: PointerValue | null = null): bigint" +) + +set(generic_forbidden + "export function createDenoHost(): BindgenHost" + "export function loadDenoLibrary(" + "export async function createBunHost(): Promise" + "export async function loadBunLibrary(" +) + +run_generator(generic_output) +require_snippets("${generic_output}" "generic output" ${common_required}) +forbid_snippets("${generic_output}" "generic output" ${generic_forbidden}) + +set(deno_required + "// Runtime profile: deno." + "export function createDenoHost(): BindgenHost" + "export function loadDenoLibrary(" +) + +set(deno_forbidden + "export async function createBunHost(): Promise" + "export async function loadBunLibrary(" +) + +run_generator(deno_output --runtime deno) +require_snippets("${deno_output}" "deno output" ${deno_required}) +forbid_snippets("${deno_output}" "deno output" ${deno_forbidden}) + +set(bun_required + "// Runtime profile: bun." + "export async function createBunHost(): Promise" + "export async function loadBunLibrary(" +) + +set(bun_forbidden + "export function createDenoHost(): BindgenHost" + "export function loadDenoLibrary(" +) + +run_generator(bun_output --runtime bun) +require_snippets("${bun_output}" "bun output" ${bun_required}) +forbid_snippets("${bun_output}" "bun output" ${bun_forbidden}) diff --git a/tools/generate_deno_symbols.cpp b/tools/generate_deno_symbols.cpp deleted file mode 100644 index 76cc460..0000000 --- a/tools/generate_deno_symbols.cpp +++ /dev/null @@ -1,557 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -template struct FunctionPointerTraits; - -template struct FunctionPointerTraits -{ - using ReturnType = Return; - using ArgumentTypes = std::tuple; - static constexpr std::size_t kArity = sizeof...(Args); - - template using Argument = std::tuple_element_t; -}; - -using ErrorCallbackTraits = FunctionPointerTraits; - -static_assert(ErrorCallbackTraits::kArity == 2); -static_assert(std::is_same_v); -static_assert(std::is_same_v, int>); -static_assert(std::is_same_v, const char *>); - -struct CliOptions -{ - std::optional output_path; - bool show_help = false; -}; - -[[nodiscard]] constexpr auto toDenoParameterType(cpp_core::AbiValueKind kind) -> std::string_view -{ - switch (kind) - { - case cpp_core::AbiValueKind::kVoid: - return "void"; - case cpp_core::AbiValueKind::kInt32: - return "i32"; - case cpp_core::AbiValueKind::kInt64: - case cpp_core::AbiValueKind::kOpaqueHandle: - return "i64"; - case cpp_core::AbiValueKind::kUtf8CString: - case cpp_core::AbiValueKind::kMutableBuffer: - case cpp_core::AbiValueKind::kConstBuffer: - case cpp_core::AbiValueKind::kOpaquePointer: - case cpp_core::AbiValueKind::kVersionStructPointer: - case cpp_core::AbiValueKind::kErrorCallback: - case cpp_core::AbiValueKind::kNotificationCallback: - return "pointer"; - } - - return "pointer"; -} - -[[nodiscard]] constexpr auto toDenoResultType(cpp_core::AbiValueKind kind) -> std::string_view -{ - switch (kind) - { - case cpp_core::AbiValueKind::kVoid: - return "void"; - case cpp_core::AbiValueKind::kInt32: - return "i32"; - case cpp_core::AbiValueKind::kInt64: - return "i64"; - case cpp_core::AbiValueKind::kOpaqueHandle: - return "isize"; - case cpp_core::AbiValueKind::kUtf8CString: - case cpp_core::AbiValueKind::kMutableBuffer: - case cpp_core::AbiValueKind::kConstBuffer: - case cpp_core::AbiValueKind::kOpaquePointer: - case cpp_core::AbiValueKind::kVersionStructPointer: - case cpp_core::AbiValueKind::kErrorCallback: - case cpp_core::AbiValueKind::kNotificationCallback: - return "pointer"; - } - - return "void"; -} - -[[nodiscard]] constexpr auto toDenoResultType(const cpp_core::FunctionDescriptor &function) -> std::string_view -{ - return toDenoResultType(function.return_abi_kind); -} - -[[nodiscard]] auto toTsParameterType(const cpp_core::ParameterDescriptor ¶meter) -> std::string_view -{ - switch (parameter.abi_kind) - { - case cpp_core::AbiValueKind::kInt32: - return "number"; - case cpp_core::AbiValueKind::kInt64: - case cpp_core::AbiValueKind::kOpaqueHandle: - return "number | bigint"; - case cpp_core::AbiValueKind::kUtf8CString: - return "string | Deno.PointerValue"; - case cpp_core::AbiValueKind::kMutableBuffer: - return "ArrayBufferView | Deno.PointerValue"; - case cpp_core::AbiValueKind::kConstBuffer: - return "string | ArrayBufferView | Deno.PointerValue"; - case cpp_core::AbiValueKind::kOpaquePointer: - case cpp_core::AbiValueKind::kVersionStructPointer: - return "ArrayBufferView | Deno.PointerValue"; - case cpp_core::AbiValueKind::kErrorCallback: - case cpp_core::AbiValueKind::kNotificationCallback: - return "Deno.PointerValue | null"; - case cpp_core::AbiValueKind::kVoid: - return "void"; - } - - return "unknown"; -} - -[[nodiscard]] auto toWrapperReturnType(const cpp_core::FunctionDescriptor &function) -> std::string_view -{ - switch (function.result_model) - { - case cpp_core::FunctionResultModel::kVoid: - case cpp_core::FunctionResultModel::kStatusCode: - return "void"; - case cpp_core::FunctionResultModel::kValueOrStatus: - return function.return_abi_kind == cpp_core::AbiValueKind::kInt64 ? "bigint" : "number"; - case cpp_core::FunctionResultModel::kHandleOrStatus: - return "bigint"; - } - - return "unknown"; -} - -[[nodiscard]] auto usesBufferMarshalling(const cpp_core::FunctionDescriptor &function) -> bool -{ - return std::ranges::any_of(function.parameters, [](const auto ¶meter) { - return parameter.abi_kind == cpp_core::AbiValueKind::kUtf8CString || - parameter.abi_kind == cpp_core::AbiValueKind::kConstBuffer || - parameter.abi_kind == cpp_core::AbiValueKind::kMutableBuffer || - parameter.abi_kind == cpp_core::AbiValueKind::kOpaquePointer || - parameter.abi_kind == cpp_core::AbiValueKind::kVersionStructPointer; - }); -} - -[[nodiscard]] auto optionalParameterDefault(const cpp_core::ParameterDescriptor ¶meter) - -> std::optional -{ - if (!parameter.optional) - { - return std::nullopt; - } - - switch (parameter.abi_kind) - { - case cpp_core::AbiValueKind::kInt32: - return "0"; - case cpp_core::AbiValueKind::kErrorCallback: - case cpp_core::AbiValueKind::kNotificationCallback: - return "null"; - default: - return std::nullopt; - } -} - -[[nodiscard]] auto sourceExpr(const cpp_core::ParameterDescriptor ¶meter) -> std::string -{ - return std::string(parameter.name); -} - -auto printUsage(std::ostream &stream) -> void -{ - stream << "Usage: cpp_core_bindgen [--output PATH] [--help]\n"; - stream << "Generates a Deno.dlopen-compatible TypeScript module from cpp-core FFI metadata.\n"; -} - -[[nodiscard]] auto parseArgs(int argc, char **argv, CliOptions &options) -> bool -{ - for (int index = 1; index < argc; ++index) - { - const std::string_view arg = argv[index]; - if (arg == "--help" || arg == "-h") - { - options.show_help = true; - return true; - } - - if (arg == "--output") - { - if (index + 1 >= argc) - { - return false; - } - - options.output_path = std::string(argv[++index]); - continue; - } - - return false; - } - - return true; -} - -auto writeHeader(std::ostream &stream) -> void -{ - stream << "// Generated from cpp-core reflection metadata.\n"; - stream << "// Do not edit manually.\n"; - stream << "// Intended for use with Deno.dlopen.\n"; - stream << "// ABI mode: modern.\n\n"; -} - -auto writeSymbols(std::ostream &stream) -> void -{ - stream << "export const symbols = {\n"; - - for (const auto &function : cpp_core::functionDescriptors()) - { - stream << " " << function.name << ": {\n"; - stream << " parameters: ["; - - for (std::size_t index = 0; index < function.parameters.size(); ++index) - { - if (index != 0) - { - stream << ", "; - } - - stream << '"' << toDenoParameterType(function.parameters[index].abi_kind) << '"'; - } - - stream << "],\n"; - stream << " result: \"" << toDenoResultType(function) << "\",\n"; - stream << " },\n"; - } - - stream << "} as const;\n\n"; -} - -auto writeOperations(std::ostream &stream) -> void -{ - stream << "export const operations = {\n"; - for (const auto &operation : cpp_core::operationDescriptors()) - { - stream << " " << operation.name << ": { symbol: \"" << operation.function_name << "\" },\n"; - } - stream << "} as const;\n\n"; -} - -auto writeStatusCodes(std::ostream &stream) -> void -{ - stream << "export const statusCodes = {\n"; - for (const auto &descriptor : cpp_core::statusCodeDescriptors()) - { - stream << " " << descriptor.name << ": " << descriptor.value << ",\n"; - } - stream << "} as const;\n\n"; - - stream << "export const statusCodeInfo = {\n"; - for (const auto &descriptor : cpp_core::statusCodeDescriptors()) - { - stream << " \"" << descriptor.value << "\": { category: \"" << descriptor.category << "\", name: \"" - << descriptor.name << "\" },\n"; - } - stream << "} as const;\n\n"; -} - -auto writeHelpers(std::ostream &stream) -> void -{ - stream << "const textEncoder = new TextEncoder();\n\n"; - stream << "type KeepAlive = ArrayBufferView[];\n\n"; - stream << "function isArrayBufferView(value: unknown): value is ArrayBufferView {\n"; - stream << " return ArrayBuffer.isView(value);\n"; - stream << "}\n\n"; - stream << "function isPointerValue(value: unknown): value is Exclude {\n"; - stream << " return typeof value === \"number\" || typeof value === \"bigint\";\n"; - stream << "}\n\n"; - stream << "function requirePointer(\n"; - stream << " value: Deno.PointerValue | ArrayBufferView | null | undefined,\n"; - stream << " name: string,\n"; - stream << " keepAlive?: KeepAlive,\n"; - stream << "): Deno.PointerValue {\n"; - stream << " if (isPointerValue(value)) {\n"; - stream << " return value;\n"; - stream << " }\n"; - stream << " if (value == null || !isArrayBufferView(value)) {\n"; - stream << " throw new TypeError(`${name} must be a Deno.PointerValue or ArrayBufferView`);\n"; - stream << " }\n"; - stream << " keepAlive?.push(value);\n"; - stream << " const pointer = Deno.UnsafePointer.of(value as BufferSource);\n"; - stream << " if (pointer === null) {\n"; - stream << " throw new TypeError(`Failed to get pointer for ${name}`);\n"; - stream << " }\n"; - stream << " return pointer;\n"; - stream << "}\n\n"; - stream << "function decodeCStringPointer(value: Deno.PointerValue): string | null {\n"; - stream << " if (value === null) {\n"; - stream << " return null;\n"; - stream << " }\n"; - stream << " return new Deno.UnsafePointerView(value).getCString();\n"; - stream << "}\n\n"; - stream << "function marshalCString(value: string | Deno.PointerValue, name: string, keepAlive: KeepAlive): " - "Deno.PointerValue {\n"; - stream << " if (typeof value === \"string\") {\n"; - stream << " const bytes = textEncoder.encode(`${value}\\0`);\n"; - stream << " keepAlive.push(bytes);\n"; - stream << " return requirePointer(bytes, name, keepAlive);\n"; - stream << " }\n"; - stream << " return requirePointer(value, name);\n"; - stream << "}\n\n"; - stream << "function marshalConstBuffer(\n"; - stream << " value: string | ArrayBufferView | Deno.PointerValue,\n"; - stream << " name: string,\n"; - stream << " keepAlive: KeepAlive,\n"; - stream << "): Deno.PointerValue {\n"; - stream << " if (typeof value === \"string\") {\n"; - stream << " const bytes = textEncoder.encode(value);\n"; - stream << " keepAlive.push(bytes);\n"; - stream << " return requirePointer(bytes, name, keepAlive);\n"; - stream << " }\n"; - stream << " return requirePointer(value, name, keepAlive);\n"; - stream << "}\n\n"; - stream << "function marshalMutableBuffer(\n"; - stream << " value: ArrayBufferView | Deno.PointerValue,\n"; - stream << " name: string,\n"; - stream << " keepAlive: KeepAlive,\n"; - stream << "): Deno.PointerValue {\n"; - stream << " return requirePointer(value, name, keepAlive);\n"; - stream << "}\n\n"; - stream << "function toInt64(value: number | bigint): bigint {\n"; - stream << " return typeof value === \"bigint\" ? value : BigInt(value);\n"; - stream << "}\n\n"; - stream << "export class StatusCodeError extends Error {\n"; - stream << " readonly code: number;\n"; - stream << " readonly category: string;\n"; - stream << " readonly statusName: string;\n\n"; - stream << " constructor(code: number | bigint) {\n"; - stream << " const normalized = typeof code === \"bigint\" ? Number(code) : code;\n"; - stream << " const info = statusCodeInfo[String(normalized) as keyof typeof statusCodeInfo];\n"; - stream << " const category = info?.category ?? \"Unknown\";\n"; - stream << " const statusName = info?.name ?? \"UnknownStatus\";\n"; - stream << " super(`${category}::${statusName} (${normalized})`);\n"; - stream << " this.name = \"StatusCodeError\";\n"; - stream << " this.code = normalized;\n"; - stream << " this.category = category;\n"; - stream << " this.statusName = statusName;\n"; - stream << " }\n"; - stream << "}\n\n"; - stream << "function assertStatus(status: number | bigint): void {\n"; - stream << " const normalized = typeof status === \"bigint\" ? Number(status) : status;\n"; - stream << " if (normalized < 0) {\n"; - stream << " throw new StatusCodeError(normalized);\n"; - stream << " }\n"; - stream << "}\n\n"; -} - -auto writeErrorCallbackSupport(std::ostream &stream) -> void -{ - stream << "export const errorCallbackDefinition = {\n"; - stream << " parameters: [\"i32\", \"pointer\"],\n"; - stream << " result: \"void\",\n"; - stream << "} as const;\n\n"; - - stream << "export type RawErrorCallback = (error_code: number, message: Deno.PointerValue) => void;\n"; - stream << "export type ErrorCallback = (error_code: number, message: string | null) => void;\n\n"; - - stream << "export function createRawErrorCallback(callback: RawErrorCallback): Deno.UnsafeCallback {\n"; - stream << " return new Deno.UnsafeCallback(errorCallbackDefinition, callback);\n"; - stream << "}\n\n"; - - stream << "export function createErrorCallback(callback: ErrorCallback): Deno.UnsafeCallback {\n"; - stream << " return new Deno.UnsafeCallback(errorCallbackDefinition, (error_code, message) => {\n"; - stream << " callback(error_code, decodeCStringPointer(message));\n"; - stream << " });\n"; - stream << "}\n\n"; -} - -auto writeWrapperBody(std::ostream &stream, const cpp_core::FunctionDescriptor &function) -> void -{ - const bool use_keep_alive = usesBufferMarshalling(function); - if (use_keep_alive) - { - stream << " const keepAlive: KeepAlive = [];\n"; - } - - for (const auto ¶meter : function.parameters) - { - const std::string source = sourceExpr(parameter); - switch (parameter.abi_kind) - { - case cpp_core::AbiValueKind::kUtf8CString: - stream << " const " << parameter.name << "Pointer = marshalCString(" << source << ", \"" - << parameter.name << "\", keepAlive);\n"; - break; - case cpp_core::AbiValueKind::kConstBuffer: - stream << " const " << parameter.name << "Pointer = marshalConstBuffer(" << source << ", \"" - << parameter.name << "\", keepAlive);\n"; - break; - case cpp_core::AbiValueKind::kMutableBuffer: - stream << " const " << parameter.name << "Pointer = marshalMutableBuffer(" << source << ", \"" - << parameter.name << "\", keepAlive);\n"; - break; - case cpp_core::AbiValueKind::kOpaquePointer: - case cpp_core::AbiValueKind::kVersionStructPointer: - stream << " const " << parameter.name << "Pointer = requirePointer(" << source << ", \"" - << parameter.name << "\", keepAlive);\n"; - break; - case cpp_core::AbiValueKind::kInt64: - case cpp_core::AbiValueKind::kOpaqueHandle: - stream << " const " << parameter.name << "Value = toInt64(" << source << ");\n"; - break; - default: - break; - } - } - - stream << " const rawResult = dylib.symbols." << function.name << "("; - for (std::size_t index = 0; index < function.parameters.size(); ++index) - { - if (index != 0) - { - stream << ", "; - } - - const auto ¶meter = function.parameters[index]; - switch (parameter.abi_kind) - { - case cpp_core::AbiValueKind::kUtf8CString: - case cpp_core::AbiValueKind::kConstBuffer: - case cpp_core::AbiValueKind::kMutableBuffer: - case cpp_core::AbiValueKind::kOpaquePointer: - case cpp_core::AbiValueKind::kVersionStructPointer: - stream << parameter.name << "Pointer"; - break; - case cpp_core::AbiValueKind::kInt64: - case cpp_core::AbiValueKind::kOpaqueHandle: - stream << parameter.name << "Value"; - break; - default: - stream << sourceExpr(parameter); - break; - } - } - stream << ");\n"; - - if (use_keep_alive) - { - stream << " void keepAlive;\n"; - } - - switch (function.result_model) - { - case cpp_core::FunctionResultModel::kVoid: - stream << " return rawResult;\n"; - break; - case cpp_core::FunctionResultModel::kStatusCode: - stream << " assertStatus(rawResult);\n"; - stream << " return;\n"; - break; - case cpp_core::FunctionResultModel::kValueOrStatus: - case cpp_core::FunctionResultModel::kHandleOrStatus: - stream << " assertStatus(rawResult);\n"; - stream << " return rawResult;\n"; - break; - } -} - -auto writeWrapperSignature(std::ostream &stream, const cpp_core::FunctionDescriptor &function) -> void -{ - stream << " " << function.name << "("; - - for (std::size_t index = 0; index < function.parameters.size(); ++index) - { - if (index != 0) - { - stream << ", "; - } - - const auto ¶meter = function.parameters[index]; - stream << parameter.name << ": " << toTsParameterType(parameter); - if (const auto default_value = optionalParameterDefault(parameter)) - { - stream << " = " << *default_value; - } - } - - stream << "): " << toWrapperReturnType(function); -} - -auto writeWrapperFunctions(std::ostream &stream) -> void -{ - stream << "export type BindgenLibrary = Deno.DynamicLibrary;\n"; - stream << "export type BindgenSymbols = BindgenLibrary[\"symbols\"];\n\n"; - stream << "export function createBindings(dylib: BindgenLibrary) {\n"; - stream << " return {\n"; - - for (const auto &function : cpp_core::functionDescriptors()) - { - writeWrapperSignature(stream, function); - stream << " {\n"; - writeWrapperBody(stream, function); - stream << " },\n"; - } - - stream << " };\n"; - stream << "}\n\n"; - stream << "export type GeneratedBindings = ReturnType;\n"; -} - -auto writeModule(std::ostream &stream) -> void -{ - writeHeader(stream); - writeSymbols(stream); - writeOperations(stream); - writeStatusCodes(stream); - writeHelpers(stream); - writeErrorCallbackSupport(stream); - writeWrapperFunctions(stream); -} - -} // namespace - -auto main(int argc, char **argv) -> int -{ - CliOptions options; - if (!parseArgs(argc, argv, options)) - { - printUsage(std::cerr); - return 1; - } - - if (options.show_help) - { - printUsage(std::cout); - return 0; - } - - if (options.output_path.has_value()) - { - std::ofstream output(*options.output_path); - if (!output) - { - std::cerr << "Failed to open output path: " << *options.output_path << '\n'; - return 1; - } - - writeModule(output); - return 0; - } - - writeModule(std::cout); - return 0; -} diff --git a/tools/generate_typescript_bindings.cpp b/tools/generate_typescript_bindings.cpp new file mode 100644 index 0000000..1195f01 --- /dev/null +++ b/tools/generate_typescript_bindings.cpp @@ -0,0 +1,881 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +template struct FunctionPointerTraits; + +template struct FunctionPointerTraits +{ + using ReturnType = Return; + using ArgumentTypes = std::tuple; + static constexpr std::size_t kArity = sizeof...(Args); + + template using Argument = std::tuple_element_t; +}; + +using ErrorCallbackTraits = FunctionPointerTraits; + +static_assert(ErrorCallbackTraits::kArity == 2); +static_assert(std::is_same_v); +static_assert(std::is_same_v, int>); +static_assert(std::is_same_v, const char *>); + +struct CliOptions +{ + std::optional output_path; + enum class RuntimeProfile + { + kGeneric, + kNode, + kDeno, + kBun, + }; + + RuntimeProfile runtime = RuntimeProfile::kGeneric; + bool show_help = false; +}; + +[[nodiscard]] constexpr auto runtimeProfileName(CliOptions::RuntimeProfile runtime) -> std::string_view +{ + switch (runtime) + { + case CliOptions::RuntimeProfile::kGeneric: + return "generic"; + case CliOptions::RuntimeProfile::kNode: + return "node"; + case CliOptions::RuntimeProfile::kDeno: + return "deno"; + case CliOptions::RuntimeProfile::kBun: + return "bun"; + } + + return "generic"; +} + +[[nodiscard]] auto parseRuntimeProfile(std::string_view value, CliOptions::RuntimeProfile &runtime) -> bool +{ + if (value == "generic" || value == "universal") + { + runtime = CliOptions::RuntimeProfile::kGeneric; + return true; + } + + if (value == "node") + { + runtime = CliOptions::RuntimeProfile::kNode; + return true; + } + + if (value == "deno") + { + runtime = CliOptions::RuntimeProfile::kDeno; + return true; + } + + if (value == "bun") + { + runtime = CliOptions::RuntimeProfile::kBun; + return true; + } + + return false; +} + +[[nodiscard]] constexpr auto toNativeParameterType(cpp_core::AbiValueKind kind) -> std::string_view +{ + switch (kind) + { + case cpp_core::AbiValueKind::kVoid: + return "void"; + case cpp_core::AbiValueKind::kInt32: + return "i32"; + case cpp_core::AbiValueKind::kInt64: + case cpp_core::AbiValueKind::kOpaqueHandle: + return "i64"; + case cpp_core::AbiValueKind::kUtf8CString: + case cpp_core::AbiValueKind::kMutableBuffer: + case cpp_core::AbiValueKind::kConstBuffer: + case cpp_core::AbiValueKind::kOpaquePointer: + case cpp_core::AbiValueKind::kVersionStructPointer: + case cpp_core::AbiValueKind::kErrorCallback: + case cpp_core::AbiValueKind::kNotificationCallback: + return "pointer"; + } + + return "pointer"; +} + +[[nodiscard]] constexpr auto toNativeResultType(cpp_core::AbiValueKind kind) -> std::string_view +{ + switch (kind) + { + case cpp_core::AbiValueKind::kVoid: + return "void"; + case cpp_core::AbiValueKind::kInt32: + return "i32"; + case cpp_core::AbiValueKind::kInt64: + return "i64"; + case cpp_core::AbiValueKind::kOpaqueHandle: + return "isize"; + case cpp_core::AbiValueKind::kUtf8CString: + case cpp_core::AbiValueKind::kMutableBuffer: + case cpp_core::AbiValueKind::kConstBuffer: + case cpp_core::AbiValueKind::kOpaquePointer: + case cpp_core::AbiValueKind::kVersionStructPointer: + case cpp_core::AbiValueKind::kErrorCallback: + case cpp_core::AbiValueKind::kNotificationCallback: + return "pointer"; + } + + return "void"; +} + +[[nodiscard]] constexpr auto toNativeResultType(const cpp_core::FunctionDescriptor &function) -> std::string_view +{ + return toNativeResultType(function.return_abi_kind); +} + +[[nodiscard]] auto toTsParameterType(const cpp_core::ParameterDescriptor ¶meter) -> std::string_view +{ + switch (parameter.abi_kind) + { + case cpp_core::AbiValueKind::kInt32: + return "number"; + case cpp_core::AbiValueKind::kInt64: + case cpp_core::AbiValueKind::kOpaqueHandle: + return "number | bigint"; + case cpp_core::AbiValueKind::kUtf8CString: + return "string | PointerValue"; + case cpp_core::AbiValueKind::kMutableBuffer: + return "ArrayBufferView | PointerValue"; + case cpp_core::AbiValueKind::kConstBuffer: + return "string | ArrayBufferView | PointerValue"; + case cpp_core::AbiValueKind::kOpaquePointer: + case cpp_core::AbiValueKind::kVersionStructPointer: + return "ArrayBufferView | PointerValue"; + case cpp_core::AbiValueKind::kErrorCallback: + case cpp_core::AbiValueKind::kNotificationCallback: + return "PointerValue | null"; + case cpp_core::AbiValueKind::kVoid: + return "void"; + } + + return "unknown"; +} + +[[nodiscard]] auto toWrapperReturnType(const cpp_core::FunctionDescriptor &function) -> std::string_view +{ + switch (function.result_model) + { + case cpp_core::FunctionResultModel::kVoid: + case cpp_core::FunctionResultModel::kStatusCode: + return "void"; + case cpp_core::FunctionResultModel::kValueOrStatus: + return function.return_abi_kind == cpp_core::AbiValueKind::kInt64 ? "bigint" : "number"; + case cpp_core::FunctionResultModel::kHandleOrStatus: + return "bigint"; + } + + return "unknown"; +} + +[[nodiscard]] auto usesBufferMarshalling(const cpp_core::FunctionDescriptor &function) -> bool +{ + return std::ranges::any_of(function.parameters, [](const auto ¶meter) { + return parameter.abi_kind == cpp_core::AbiValueKind::kUtf8CString || + parameter.abi_kind == cpp_core::AbiValueKind::kConstBuffer || + parameter.abi_kind == cpp_core::AbiValueKind::kMutableBuffer || + parameter.abi_kind == cpp_core::AbiValueKind::kOpaquePointer || + parameter.abi_kind == cpp_core::AbiValueKind::kVersionStructPointer; + }); +} + +[[nodiscard]] auto optionalParameterDefault(const cpp_core::ParameterDescriptor ¶meter) + -> std::optional +{ + if (!parameter.optional) + { + return std::nullopt; + } + + switch (parameter.abi_kind) + { + case cpp_core::AbiValueKind::kInt32: + return "0"; + case cpp_core::AbiValueKind::kErrorCallback: + case cpp_core::AbiValueKind::kNotificationCallback: + return "null"; + default: + return std::nullopt; + } +} + +[[nodiscard]] auto sourceExpr(const cpp_core::ParameterDescriptor ¶meter) -> std::string +{ + return std::string(parameter.name); +} + +auto printUsage(std::ostream &stream) -> void +{ + stream << "Usage: cpp_core_bindgen [--output PATH] [--runtime generic|node|deno|bun] [--help]\n"; + stream << "Generates a TypeScript FFI module from cpp-core FFI metadata.\n"; +} + +[[nodiscard]] auto parseArgs(int argc, char **argv, CliOptions &options) -> bool +{ + for (int index = 1; index < argc; ++index) + { + const std::string_view arg = argv[index]; + if (arg == "--help" || arg == "-h") + { + options.show_help = true; + return true; + } + + if (arg == "--output") + { + if (index + 1 >= argc) + { + return false; + } + + options.output_path = std::string(argv[++index]); + continue; + } + + if (arg == "--runtime") + { + if (index + 1 >= argc) + { + return false; + } + + if (!parseRuntimeProfile(argv[++index], options.runtime)) + { + return false; + } + + continue; + } + + return false; + } + + return true; +} + +auto writeHeader(std::ostream &stream, CliOptions::RuntimeProfile runtime) -> void +{ + stream << "// Generated from cpp-core reflection metadata.\n"; + stream << "// Do not edit manually.\n"; + stream << "// Intended for use with a host-specific FFI adapter.\n"; + stream << "// ABI mode: modern.\n"; + stream << "// Runtime profile: " << runtimeProfileName(runtime) << ".\n\n"; +} + +auto writeHostTypes(std::ostream &stream) -> void +{ + stream << "export type OpaquePointer = object;\n"; + stream << "export type PointerValue = number | bigint | OpaquePointer;\n"; + stream << "export type NativeType = \"void\" | \"i32\" | \"i64\" | \"isize\" | \"pointer\";\n\n"; + stream << "export type NativeFunctionDefinition = {\n"; + stream << " readonly parameters: readonly NativeType[];\n"; + stream << " readonly result: NativeType;\n"; + stream << "};\n\n"; + stream << "type NativeValue = T extends \"pointer\"\n"; + stream << " ? PointerValue | null\n"; + stream << " : T extends \"i64\" | \"isize\"\n"; + stream << " ? bigint\n"; + stream << " : T extends \"i32\"\n"; + stream << " ? number\n"; + stream << " : void;\n\n"; + stream << "type NativeFunctionArguments = {\n"; + stream << " [Index in keyof Parameters]: NativeValue;\n"; + stream << "};\n\n"; + stream << "type NativeFunctionResult = NativeValue;\n\n"; + stream << "type NativeFunction = (\n"; + stream << " ...args: NativeFunctionArguments\n"; + stream << ") => NativeFunctionResult;\n\n"; + stream << "export type CallbackHandle = {\n"; + stream << " pointer: PointerValue;\n"; + stream << " close(): void;\n"; + stream << "};\n\n"; + stream << "export interface BindgenRuntimeAdapter {\n"; + stream << " loadLibrary(path: string | URL, symbols: Record): BindgenLibrary;\n"; + stream << " pointerOf(value: ArrayBuffer | ArrayBufferView): PointerValue | null;\n"; + stream << " readCString(pointer: PointerValue): string | null;\n"; + stream << " createCallback(\n"; + stream << " definition: Definition,\n"; + stream << " callback: (...args: NativeFunctionArguments) => " + "NativeFunctionResult,\n"; + stream << " ): CallbackHandle;\n"; + stream << "}\n\n"; + stream << "export interface BindgenHost {\n"; + stream << " pointerOf(value: ArrayBuffer | ArrayBufferView): PointerValue | null;\n"; + stream << " readCString(pointer: PointerValue): string | null;\n"; + stream << " createCallback(\n"; + stream << " definition: Definition,\n"; + stream << " callback: (...args: NativeFunctionArguments) => " + "NativeFunctionResult,\n"; + stream << " ): CallbackHandle;\n"; + stream << "}\n\n"; +} + +auto writeSymbols(std::ostream &stream) -> void +{ + stream << "export const symbols = {\n"; + + for (const auto &function : cpp_core::functionDescriptors()) + { + stream << " " << function.name << ": {\n"; + stream << " parameters: ["; + + for (std::size_t index = 0; index < function.parameters.size(); ++index) + { + if (index != 0) + { + stream << ", "; + } + + stream << '"' << toNativeParameterType(function.parameters[index].abi_kind) << '"'; + } + + stream << "],\n"; + stream << " result: \"" << toNativeResultType(function) << "\",\n"; + stream << " },\n"; + } + + stream << "} as const;\n\n"; + stream << "type SymbolDefinitions = typeof symbols;\n\n"; + stream << "export type BindgenSymbols = {\n"; + stream << " [Name in keyof SymbolDefinitions]: NativeFunction;\n"; + stream << "};\n\n"; + stream << "export interface BindgenLibrary {\n"; + stream << " symbols: BindgenSymbols;\n"; + stream << "}\n\n"; +} + +auto writeOperations(std::ostream &stream) -> void +{ + stream << "export const operations = {\n"; + for (const auto &operation : cpp_core::operationDescriptors()) + { + stream << " " << operation.name << ": { symbol: \"" << operation.function_name << "\" },\n"; + } + stream << "} as const;\n\n"; +} + +auto writeStatusCodes(std::ostream &stream) -> void +{ + stream << "export const statusCodes = {\n"; + for (const auto &descriptor : cpp_core::statusCodeDescriptors()) + { + stream << " " << descriptor.name << ": " << descriptor.value << ",\n"; + } + stream << "} as const;\n\n"; + + stream << "export const statusCodeInfo = {\n"; + for (const auto &descriptor : cpp_core::statusCodeDescriptors()) + { + stream << " \"" << descriptor.value << "\": { category: \"" << descriptor.category << "\", name: \"" + << descriptor.name << "\" },\n"; + } + stream << "} as const;\n\n"; +} + +auto writeHelpers(std::ostream &stream) -> void +{ + stream << "const textEncoder = new TextEncoder();\n\n"; + stream << "type KeepAlive = ArrayBufferView[];\n\n"; + stream << "function isArrayBufferView(value: unknown): value is ArrayBufferView {\n"; + stream << " return ArrayBuffer.isView(value);\n"; + stream << "}\n\n"; + stream << "function isArrayBuffer(value: unknown): value is ArrayBuffer {\n"; + stream << " return value instanceof ArrayBuffer;\n"; + stream << "}\n\n"; + stream << "function isPointerValue(value: unknown): value is PointerValue {\n"; + stream << " return value != null && !isArrayBufferView(value) && !isArrayBuffer(value) &&\n"; + stream << " (typeof value === \"number\" || typeof value === \"bigint\" || typeof value === \"object\");\n"; + stream << "}\n\n"; + stream << "function requirePointer(\n"; + stream << " host: BindgenHost,\n"; + stream << " value: PointerValue | ArrayBufferView | null | undefined,\n"; + stream << " name: string,\n"; + stream << " keepAlive?: KeepAlive,\n"; + stream << "): PointerValue {\n"; + stream << " if (isPointerValue(value)) {\n"; + stream << " return value;\n"; + stream << " }\n"; + stream << " if (value == null || !isArrayBufferView(value)) {\n"; + stream << " throw new TypeError(`${name} must be a PointerValue or ArrayBufferView`);\n"; + stream << " }\n"; + stream << " keepAlive?.push(value);\n"; + stream << " const pointer = host.pointerOf(value);\n"; + stream << " if (pointer === null) {\n"; + stream << " throw new TypeError(`Failed to get pointer for ${name}`);\n"; + stream << " }\n"; + stream << " return pointer;\n"; + stream << "}\n\n"; + stream << "function decodeCStringPointer(host: BindgenHost, value: PointerValue | null): string | null {\n"; + stream << " if (value === null) {\n"; + stream << " return null;\n"; + stream << " }\n"; + stream << " return host.readCString(value);\n"; + stream << "}\n\n"; + stream << "function marshalCString(\n"; + stream << " host: BindgenHost,\n"; + stream << " value: string | PointerValue,\n"; + stream << " name: string,\n"; + stream << " keepAlive: KeepAlive,\n"; + stream << "): PointerValue {\n"; + stream << " if (typeof value === \"string\") {\n"; + stream << " const bytes = textEncoder.encode(`${value}\\0`);\n"; + stream << " keepAlive.push(bytes);\n"; + stream << " return requirePointer(host, bytes, name, keepAlive);\n"; + stream << " }\n"; + stream << " return requirePointer(host, value, name);\n"; + stream << "}\n\n"; + stream << "function marshalConstBuffer(\n"; + stream << " host: BindgenHost,\n"; + stream << " value: string | ArrayBufferView | PointerValue,\n"; + stream << " name: string,\n"; + stream << " keepAlive: KeepAlive,\n"; + stream << "): PointerValue {\n"; + stream << " if (typeof value === \"string\") {\n"; + stream << " const bytes = textEncoder.encode(value);\n"; + stream << " keepAlive.push(bytes);\n"; + stream << " return requirePointer(host, bytes, name, keepAlive);\n"; + stream << " }\n"; + stream << " return requirePointer(host, value, name, keepAlive);\n"; + stream << "}\n\n"; + stream << "function marshalMutableBuffer(\n"; + stream << " host: BindgenHost,\n"; + stream << " value: ArrayBufferView | PointerValue,\n"; + stream << " name: string,\n"; + stream << " keepAlive: KeepAlive,\n"; + stream << "): PointerValue {\n"; + stream << " return requirePointer(host, value, name, keepAlive);\n"; + stream << "}\n\n"; + stream << "function toInt64(value: number | bigint): bigint {\n"; + stream << " return typeof value === \"bigint\" ? value : BigInt(value);\n"; + stream << "}\n\n"; + stream << "export class StatusCodeError extends Error {\n"; + stream << " readonly code: number;\n"; + stream << " readonly category: string;\n"; + stream << " readonly statusName: string;\n\n"; + stream << " constructor(code: number | bigint) {\n"; + stream << " const normalized = typeof code === \"bigint\" ? Number(code) : code;\n"; + stream << " const info = statusCodeInfo[String(normalized) as keyof typeof statusCodeInfo];\n"; + stream << " const category = info?.category ?? \"Unknown\";\n"; + stream << " const statusName = info?.name ?? \"UnknownStatus\";\n"; + stream << " super(`${category}::${statusName} (${normalized})`);\n"; + stream << " this.name = \"StatusCodeError\";\n"; + stream << " this.code = normalized;\n"; + stream << " this.category = category;\n"; + stream << " this.statusName = statusName;\n"; + stream << " }\n"; + stream << "}\n\n"; + stream << "function assertStatus(status: number | bigint): void {\n"; + stream << " const normalized = typeof status === \"bigint\" ? Number(status) : status;\n"; + stream << " if (normalized < 0) {\n"; + stream << " throw new StatusCodeError(normalized);\n"; + stream << " }\n"; + stream << "}\n\n"; +} + +auto writeCustomRuntimeAdapters(std::ostream &stream) -> void +{ + stream << "export function createHostFromRuntimeAdapter(adapter: BindgenRuntimeAdapter): BindgenHost {\n"; + stream << " return {\n"; + stream << " pointerOf(value) {\n"; + stream << " return adapter.pointerOf(value);\n"; + stream << " },\n"; + stream << " readCString(pointer) {\n"; + stream << " return adapter.readCString(pointer);\n"; + stream << " },\n"; + stream << " createCallback(definition, callback) {\n"; + stream << " return adapter.createCallback(definition, callback);\n"; + stream << " },\n"; + stream << " };\n"; + stream << "}\n\n"; + + stream << "export function loadLibraryFromRuntimeAdapter(\n"; + stream << " adapter: BindgenRuntimeAdapter,\n"; + stream << " path: string | URL,\n"; + stream << " definitions: Record = symbols,\n"; + stream << "): BindgenLibrary {\n"; + stream << " return adapter.loadLibrary(path, definitions);\n"; + stream << "}\n\n"; +} + +auto writeDenoRuntimeAdapters(std::ostream &stream) -> void +{ + stream << "function getDenoRuntime(): {\n"; + stream << " dlopen(path: string | URL, definitions: Record): BindgenLibrary & " + "{ close(): void };\n"; + stream << " UnsafePointer: { of(value: ArrayBuffer | ArrayBufferView): PointerValue | null };\n"; + stream << " UnsafePointerView: new (pointer: PointerValue) => { getCString(): string | null };\n"; + stream << " UnsafeCallback: new (\n"; + stream << " definition: NativeFunctionDefinition,\n"; + stream << " callback: (...args: unknown[]) => unknown,\n"; + stream << " ) => { pointer: PointerValue; close(): void };\n"; + stream << "} {\n"; + stream << " const runtime = (globalThis as typeof globalThis & { Deno?: unknown }).Deno;\n"; + stream << " if (!runtime) {\n"; + stream << " throw new Error(\"Deno runtime is not available.\");\n"; + stream << " }\n"; + stream << " return runtime as {\n"; + stream << " dlopen(path: string | URL, definitions: Record): BindgenLibrary & " + "{ close(): void };\n"; + stream << " UnsafePointer: { of(value: ArrayBuffer | ArrayBufferView): PointerValue | null };\n"; + stream << " UnsafePointerView: new (pointer: PointerValue) => { getCString(): string | null };\n"; + stream << " UnsafeCallback: new (\n"; + stream << " definition: NativeFunctionDefinition,\n"; + stream << " callback: (...args: unknown[]) => unknown,\n"; + stream << " ) => { pointer: PointerValue; close(): void };\n"; + stream << " };\n"; + stream << "}\n\n"; + + stream << "export function createDenoHost(): BindgenHost {\n"; + stream << " const deno = getDenoRuntime();\n"; + stream << " return {\n"; + stream << " pointerOf(value) {\n"; + stream << " return deno.UnsafePointer.of(value);\n"; + stream << " },\n"; + stream << " readCString(pointer) {\n"; + stream << " return new deno.UnsafePointerView(pointer).getCString();\n"; + stream << " },\n"; + stream << " createCallback(definition, callback) {\n"; + stream << " const unsafe = new deno.UnsafeCallback(definition, callback as (...args: unknown[]) => unknown);\n"; + stream << " return {\n"; + stream << " pointer: unsafe.pointer,\n"; + stream << " close() {\n"; + stream << " unsafe.close();\n"; + stream << " },\n"; + stream << " };\n"; + stream << " },\n"; + stream << " };\n"; + stream << "}\n\n"; + + stream << "export function loadDenoLibrary(\n"; + stream << " path: string | URL,\n"; + stream << " definitions: Record = symbols,\n"; + stream << "): BindgenLibrary & { close(): void } {\n"; + stream << " return getDenoRuntime().dlopen(path, definitions);\n"; + stream << "}\n\n"; +} + +auto writeBunRuntimeAdapters(std::ostream &stream) -> void +{ + stream << "type BunFfiModule = {\n"; + stream << " FFIType: {\n"; + stream << " void: unknown;\n"; + stream << " i32: unknown;\n"; + stream << " i64_fast: unknown;\n"; + stream << " ptr: unknown;\n"; + stream << " };\n"; + stream << " CString: new (pointer: number, byteOffset?: number, byteLength?: number) => { toString(): string };\n"; + stream << " JSCallback: new (\n"; + stream << " callback: (...args: unknown[]) => unknown,\n"; + stream << " options: { args: unknown[]; returns: unknown },\n"; + stream << " ) => { ptr: number | bigint; close(): void };\n"; + stream << " dlopen(\n"; + stream << " path: string,\n"; + stream << " symbols: Record,\n"; + stream << " ): BindgenLibrary & { close(): void };\n"; + stream << "};\n\n"; + + stream << "async function getBunFfiModule(): Promise {\n"; + stream << " const bun = (globalThis as typeof globalThis & { Bun?: unknown }).Bun;\n"; + stream << " if (!bun) {\n"; + stream << " throw new Error(\"Bun runtime is not available.\");\n"; + stream << " }\n"; + stream << " const specifier = \"bun:ffi\";\n"; + stream << " return await import(specifier) as BunFfiModule;\n"; + stream << "}\n\n"; + + stream << "function toBunFfiType(module: BunFfiModule, type: NativeType): unknown {\n"; + stream << " switch (type) {\n"; + stream << " case \"void\":\n"; + stream << " return module.FFIType.void;\n"; + stream << " case \"i32\":\n"; + stream << " return module.FFIType.i32;\n"; + stream << " case \"i64\":\n"; + stream << " case \"isize\":\n"; + stream << " return module.FFIType.i64_fast;\n"; + stream << " case \"pointer\":\n"; + stream << " return module.FFIType.ptr;\n"; + stream << " }\n"; + stream << "}\n\n"; + + stream << "export async function createBunHost(): Promise {\n"; + stream << " const ffi = await getBunFfiModule();\n"; + stream << " const bun = (globalThis as typeof globalThis & {\n"; + stream << " Bun?: { ptr(value: ArrayBuffer | ArrayBufferView): number | bigint };\n"; + stream << " }).Bun;\n"; + stream << " if (!bun) {\n"; + stream << " throw new Error(\"Bun runtime is not available.\");\n"; + stream << " }\n"; + stream << " return {\n"; + stream << " pointerOf(value) {\n"; + stream << " return bun.ptr(value);\n"; + stream << " },\n"; + stream << " readCString(pointer) {\n"; + stream << " return new ffi.CString(Number(pointer)).toString();\n"; + stream << " },\n"; + stream << " createCallback(definition, callback) {\n"; + stream << " const jsCallback = new ffi.JSCallback(callback as (...args: unknown[]) => unknown, {\n"; + stream << " args: definition.parameters.map((type) => toBunFfiType(ffi, type)),\n"; + stream << " returns: toBunFfiType(ffi, definition.result),\n"; + stream << " });\n"; + stream << " return {\n"; + stream << " pointer: jsCallback.ptr,\n"; + stream << " close() {\n"; + stream << " jsCallback.close();\n"; + stream << " },\n"; + stream << " };\n"; + stream << " },\n"; + stream << " };\n"; + stream << "}\n\n"; + + stream << "export async function loadBunLibrary(\n"; + stream << " path: string,\n"; + stream << " definitions: Record = symbols,\n"; + stream << "): Promise {\n"; + stream << " const ffi = await getBunFfiModule();\n"; + stream << " const ffiSymbols = Object.fromEntries(\n"; + stream << " Object.entries(definitions).map(([name, definition]) => [\n"; + stream << " name,\n"; + stream << " {\n"; + stream << " args: definition.parameters.map((type) => toBunFfiType(ffi, type)),\n"; + stream << " returns: toBunFfiType(ffi, definition.result),\n"; + stream << " },\n"; + stream << " ]),\n"; + stream << " );\n"; + stream << " return ffi.dlopen(path, ffiSymbols);\n"; + stream << "}\n\n"; +} + +auto writeRuntimeAdapters(std::ostream &stream, CliOptions::RuntimeProfile runtime) -> void +{ + writeCustomRuntimeAdapters(stream); + + if (runtime == CliOptions::RuntimeProfile::kDeno) + { + writeDenoRuntimeAdapters(stream); + } + else if (runtime == CliOptions::RuntimeProfile::kBun) + { + writeBunRuntimeAdapters(stream); + } +} + +auto writeErrorCallbackSupport(std::ostream &stream) -> void +{ + stream << "export const errorCallbackDefinition = {\n"; + stream << " parameters: [\"i32\", \"pointer\"],\n"; + stream << " result: \"void\",\n"; + stream << "} as const;\n\n"; + + stream << "export type RawErrorCallback = (error_code: number, message: PointerValue | null) => void;\n"; + stream << "export type ErrorCallback = (error_code: number, message: string | null) => void;\n\n"; + + stream << "export function createRawErrorCallback(host: BindgenHost, callback: RawErrorCallback): CallbackHandle " + "{\n"; + stream << " return host.createCallback(errorCallbackDefinition, callback);\n"; + stream << "}\n\n"; + + stream << "export function createErrorCallback(host: BindgenHost, callback: ErrorCallback): CallbackHandle {\n"; + stream << " return host.createCallback(errorCallbackDefinition, (error_code, message) => {\n"; + stream << " callback(error_code, decodeCStringPointer(host, message));\n"; + stream << " });\n"; + stream << "}\n\n"; +} + +auto writeWrapperBody(std::ostream &stream, const cpp_core::FunctionDescriptor &function) -> void +{ + const bool use_keep_alive = usesBufferMarshalling(function); + if (use_keep_alive) + { + stream << " const keepAlive: KeepAlive = [];\n"; + } + + for (const auto ¶meter : function.parameters) + { + const std::string source = sourceExpr(parameter); + switch (parameter.abi_kind) + { + case cpp_core::AbiValueKind::kUtf8CString: + stream << " const " << parameter.name << "Pointer = marshalCString(host, " << source << ", \"" + << parameter.name << "\", keepAlive);\n"; + break; + case cpp_core::AbiValueKind::kConstBuffer: + stream << " const " << parameter.name << "Pointer = marshalConstBuffer(host, " << source << ", \"" + << parameter.name << "\", keepAlive);\n"; + break; + case cpp_core::AbiValueKind::kMutableBuffer: + stream << " const " << parameter.name + << "Pointer = marshalMutableBuffer(host, " << source << ", \"" << parameter.name + << "\", keepAlive);\n"; + break; + case cpp_core::AbiValueKind::kOpaquePointer: + case cpp_core::AbiValueKind::kVersionStructPointer: + stream << " const " << parameter.name << "Pointer = requirePointer(host, " << source << ", \"" + << parameter.name << "\", keepAlive);\n"; + break; + case cpp_core::AbiValueKind::kInt64: + case cpp_core::AbiValueKind::kOpaqueHandle: + stream << " const " << parameter.name << "Value = toInt64(" << source << ");\n"; + break; + default: + break; + } + } + + stream << " const rawResult = dylib.symbols." << function.name << "("; + for (std::size_t index = 0; index < function.parameters.size(); ++index) + { + if (index != 0) + { + stream << ", "; + } + + const auto ¶meter = function.parameters[index]; + switch (parameter.abi_kind) + { + case cpp_core::AbiValueKind::kUtf8CString: + case cpp_core::AbiValueKind::kConstBuffer: + case cpp_core::AbiValueKind::kMutableBuffer: + case cpp_core::AbiValueKind::kOpaquePointer: + case cpp_core::AbiValueKind::kVersionStructPointer: + stream << parameter.name << "Pointer"; + break; + case cpp_core::AbiValueKind::kInt64: + case cpp_core::AbiValueKind::kOpaqueHandle: + stream << parameter.name << "Value"; + break; + default: + stream << sourceExpr(parameter); + break; + } + } + stream << ");\n"; + + if (use_keep_alive) + { + stream << " void keepAlive;\n"; + } + + switch (function.result_model) + { + case cpp_core::FunctionResultModel::kVoid: + stream << " return rawResult;\n"; + break; + case cpp_core::FunctionResultModel::kStatusCode: + stream << " assertStatus(rawResult);\n"; + stream << " return;\n"; + break; + case cpp_core::FunctionResultModel::kValueOrStatus: + case cpp_core::FunctionResultModel::kHandleOrStatus: + stream << " assertStatus(rawResult);\n"; + stream << " return rawResult;\n"; + break; + } +} + +auto writeWrapperSignature(std::ostream &stream, const cpp_core::FunctionDescriptor &function) -> void +{ + stream << " " << function.name << "("; + + for (std::size_t index = 0; index < function.parameters.size(); ++index) + { + if (index != 0) + { + stream << ", "; + } + + const auto ¶meter = function.parameters[index]; + stream << parameter.name << ": " << toTsParameterType(parameter); + if (const auto default_value = optionalParameterDefault(parameter)) + { + stream << " = " << *default_value; + } + } + + stream << "): " << toWrapperReturnType(function); +} + +auto writeWrapperFunctions(std::ostream &stream) -> void +{ + stream << "export function createBindings(host: BindgenHost, dylib: BindgenLibrary) {\n"; + stream << " return {\n"; + + for (const auto &function : cpp_core::functionDescriptors()) + { + writeWrapperSignature(stream, function); + stream << " {\n"; + writeWrapperBody(stream, function); + stream << " },\n"; + } + + stream << " };\n"; + stream << "}\n\n"; + stream << "export type GeneratedBindings = ReturnType;\n"; +} + +auto writeModule(std::ostream &stream, CliOptions::RuntimeProfile runtime) -> void +{ + writeHeader(stream, runtime); + writeHostTypes(stream); + writeSymbols(stream); + writeOperations(stream); + writeStatusCodes(stream); + writeHelpers(stream); + writeRuntimeAdapters(stream, runtime); + writeErrorCallbackSupport(stream); + writeWrapperFunctions(stream); +} + +} // namespace + +auto main(int argc, char **argv) -> int +{ + CliOptions options; + if (!parseArgs(argc, argv, options)) + { + printUsage(std::cerr); + return 1; + } + + if (options.show_help) + { + printUsage(std::cout); + return 0; + } + + if (options.output_path.has_value()) + { + std::ofstream output(*options.output_path); + if (!output) + { + std::cerr << "Failed to open output path: " << *options.output_path << '\n'; + return 1; + } + + writeModule(output, options.runtime); + return 0; + } + + writeModule(std::cout, options.runtime); + return 0; +} From a3db4cf3973e6d08f28e4e266cce5061c5b4e88e Mon Sep 17 00:00:00 2001 From: Katze719 Date: Thu, 11 Jun 2026 23:40:13 +0200 Subject: [PATCH 14/28] chore: rename workflow and compile test files --- .../{ci.yml => build_compile_tests_and_bindgen_smoke.yml} | 2 +- CMakeLists.txt | 2 +- tests/{ffi_metadata_compile_test.cpp => ffi_metadata.test.cpp} | 0 tests/{status_code_compile_test.cpp => status_code.test.cpp} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{ci.yml => build_compile_tests_and_bindgen_smoke.yml} (96%) rename tests/{ffi_metadata_compile_test.cpp => ffi_metadata.test.cpp} (100%) rename tests/{status_code_compile_test.cpp => status_code.test.cpp} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/build_compile_tests_and_bindgen_smoke.yml similarity index 96% rename from .github/workflows/ci.yml rename to .github/workflows/build_compile_tests_and_bindgen_smoke.yml index d735fe2..62fd2ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/build_compile_tests_and_bindgen_smoke.yml @@ -1,4 +1,4 @@ -name: 'CI' +name: 'Build Compile Tests And Bindgen Smoke' on: push: diff --git a/CMakeLists.txt b/CMakeLists.txt index 53bd4fb..96dbade 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,7 +104,7 @@ target_link_libraries(cpp_core_bindgen PRIVATE cpp_core_strict_warnings) include(CTest) if(BUILD_TESTING) - file(GLOB CPP_CORE_COMPILE_TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/*_compile_test.cpp") + file(GLOB CPP_CORE_COMPILE_TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.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) diff --git a/tests/ffi_metadata_compile_test.cpp b/tests/ffi_metadata.test.cpp similarity index 100% rename from tests/ffi_metadata_compile_test.cpp rename to tests/ffi_metadata.test.cpp diff --git a/tests/status_code_compile_test.cpp b/tests/status_code.test.cpp similarity index 100% rename from tests/status_code_compile_test.cpp rename to tests/status_code.test.cpp From cea69ad52afb25d8cbe87b505de2cb73539bb14f Mon Sep 17 00:00:00 2001 From: Katze719 Date: Thu, 11 Jun 2026 23:42:09 +0200 Subject: [PATCH 15/28] chore: update checkout actions to v6 --- .github/workflows/build_compile_tests_and_bindgen_smoke.yml | 2 +- .github/workflows/doxygen.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_compile_tests_and_bindgen_smoke.yml b/.github/workflows/build_compile_tests_and_bindgen_smoke.yml index 62fd2ce..ba6a1c3 100644 --- a/.github/workflows/build_compile_tests_and_bindgen_smoke.yml +++ b/.github/workflows/build_compile_tests_and_bindgen_smoke.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install GCC 16 + Ninja run: | diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index 40c565f..7d990a2 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -23,7 +23,7 @@ jobs: name: 'Build Doxygen HTML' steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Doxygen + Graphviz run: | From acefbcfa4e5883d3fd57762547def028fe2a16d5 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 15 Jun 2026 19:27:35 +0200 Subject: [PATCH 16/28] remove: codegen --- CMakeLists.txt | 66 -- FFI.md | 213 ----- README.md | 142 +-- include/cpp_core.h | 1 - .../cpp_core/ffi/abi_function_registry.hpp | 53 -- .../cpp_core/ffi/status_code_descriptors.hpp | 28 - include/cpp_core/ffi_metadata.hpp | 535 ----------- tests/ffi_metadata.test.cpp | 58 -- .../check_generated_typescript_bindings.cmake | 103 -- tools/generate_typescript_bindings.cpp | 881 ------------------ 10 files changed, 11 insertions(+), 2069 deletions(-) delete mode 100644 FFI.md delete mode 100644 include/cpp_core/ffi/abi_function_registry.hpp delete mode 100644 include/cpp_core/ffi/status_code_descriptors.hpp delete mode 100644 include/cpp_core/ffi_metadata.hpp delete mode 100644 tests/ffi_metadata.test.cpp delete mode 100644 tools/check_generated_typescript_bindings.cmake delete mode 100644 tools/generate_typescript_bindings.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 96dbade..f737df0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,47 +5,12 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) project(cpp-core LANGUAGES CXX) -if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - message(FATAL_ERROR "cpp-core requires GCC 16+ with -std=c++26 and -freflection.") -endif() - -if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16) - message(FATAL_ERROR "cpp-core requires GCC 16 or newer.") -endif() - 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) -include(CheckCXXCompilerFlag) -include(CheckCXXSourceCompiles) - -check_cxx_compiler_flag("-freflection" CPP_CORE_HAS_FREFLECTION_FLAG) -if(NOT CPP_CORE_HAS_FREFLECTION_FLAG) - message(FATAL_ERROR "GCC 16 was found, but -freflection is not supported by this compiler build.") -endif() - -set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -std=c++26 -freflection") -check_cxx_source_compiles( - " - #include - #ifndef __cpp_impl_reflection - #error Reflection feature macro missing - #endif - constexpr auto kReflectionProbe = ^^int; - static_assert(kReflectionProbe == ^^int); - int main() { return 0; } - " - CPP_CORE_HAS_WORKING_REFLECTION -) -unset(CMAKE_REQUIRED_FLAGS) - -if(NOT CPP_CORE_HAS_WORKING_REFLECTION) - message(FATAL_ERROR "Reflection support is required. Configure with GCC 16+ and a working -freflection implementation.") -endif() - include(cmake/CPM.cmake) CPMAddPackage( @@ -93,13 +58,7 @@ target_include_directories( $ ) -# Require C++26 reflection on every consumer of the interface target. target_compile_features(cpp_core INTERFACE cxx_std_26) -target_compile_options(cpp_core INTERFACE -freflection) - -add_executable(cpp_core_bindgen EXCLUDE_FROM_ALL tools/generate_typescript_bindings.cpp) -target_link_libraries(cpp_core_bindgen PRIVATE cpp_core::cpp_core) -target_link_libraries(cpp_core_bindgen PRIVATE cpp_core_strict_warnings) include(CTest) @@ -108,25 +67,6 @@ if(BUILD_TESTING) 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) - - set(CPP_CORE_TYPESCRIPT_BINDGEN_SMOKE_EXE "${CMAKE_CURRENT_BINARY_DIR}/cpp_core_bindgen_smoke_bin") - add_custom_target( - cpp_core_bindgen_smoke - COMMAND - ${CMAKE_COMMAND} - -E - copy - $ - ${CPP_CORE_TYPESCRIPT_BINDGEN_SMOKE_EXE} - COMMAND - ${CMAKE_COMMAND} - -DGENERATOR=${CPP_CORE_TYPESCRIPT_BINDGEN_SMOKE_EXE} - -P - ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_generated_typescript_bindings.cmake - DEPENDS cpp_core_bindgen - COMMENT "Running smoke check for generated TypeScript bindgen output" - VERBATIM - ) endif() # Install rules -------------------------------------------------------------- @@ -137,12 +77,6 @@ install( EXPORT cpp_coreTargets ) -install( - TARGETS cpp_core_bindgen - EXPORT cpp_coreTargets - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} -) - install( DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} diff --git a/FFI.md b/FFI.md deleted file mode 100644 index 06fdeb1..0000000 --- a/FFI.md +++ /dev/null @@ -1,213 +0,0 @@ -# FFI Runtime Notes - -`cpp_core_bindgen` generates a runtime-neutral TypeScript layer: - -- `symbols` ABI metadata -- `createBindings(host, dylib)` wrappers -- `createErrorCallback(host, ...)` -- built-in Deno helpers -- built-in Bun helpers -- custom adapter helpers -- `BindgenHost` / `BindgenLibrary` / `BindgenRuntimeAdapter` interfaces - -That means the generated file is not tied to one runtime anymore. Some adapters are already generated for you, and you can still plug in your own. - -## Runtime Profiles - -By default, the generator emits the generic adapter path only: - -```sh -./build/gcc/cpp_core_bindgen --output cpp_core_bindings.ts -``` - -You can opt into a runtime-specific profile: - -```sh -./build/gcc/cpp_core_bindgen --runtime deno --output cpp_core_bindings.ts -./build/gcc/cpp_core_bindgen --runtime bun --output cpp_core_bindings.ts -``` - -Supported values: - -- `generic` -- `node` -- `deno` -- `bun` - -## Quick Status - -| Runtime | Status | Notes | -| --- | --- | --- | -| Generic / Node.js | Default | Use `createHostFromRuntimeAdapter()` and `loadLibraryFromRuntimeAdapter()`. | -| Deno | Opt-in built in | Generate with `--runtime deno`, then use `createDenoHost()` and `loadDenoLibrary()`. | -| Bun | Opt-in built in | Generate with `--runtime bun`, then use `await createBunHost()` and `await loadBunLibrary()`. | - -## Common Shape - -All runtime examples follow the same pattern: - -```ts -import { - createBindings, - createErrorCallback, - createHostFromRuntimeAdapter, - loadLibraryFromRuntimeAdapter, -} from "./cpp_core_bindings.ts"; - -const host = createHostFromRuntimeAdapter(myRuntimeAdapter); -const dylib = loadLibraryFromRuntimeAdapter( - myRuntimeAdapter, - "./libcpp_bindings_linux.so", -); - -const serial = createBindings(host, dylib); -const errorCallback = createErrorCallback(host, (code, message) => { - console.error(code, message); -}); - -const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8, 0, 0, errorCallback.pointer); -serial.serialWrite(handle, new Uint8Array([0x41, 0x54, 0x0d]), 3, 500, 0); -serial.serialClose(handle); -errorCallback.close(); -``` - -The only runtime-specific parts are: - -- the runtime adapter itself -- or the built-in Deno/Bun helpers - -## Deno - -Deno helpers are generated when you build with `--runtime deno`. - -```ts -import { - createBindings, - createErrorCallback, - createDenoHost, - loadDenoLibrary, - symbols, -} from "./cpp_core_bindings.ts"; - -const host = createDenoHost(); -const dylib = loadDenoLibrary("./libcpp_bindings_linux.so", symbols); -const serial = createBindings(host, dylib); -const errorCallback = createErrorCallback(host, (code, message) => { - console.error(code, message); -}); - -const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8, 0, 0, errorCallback.pointer); -serial.serialClose(handle); -errorCallback.close(); -dylib.close(); -``` - -What the generated file already gives you: - -```ts -createDenoHost() -loadDenoLibrary(path, symbols?) -``` - -## Bun - -Bun helpers are generated when you build with `--runtime bun`. - -```ts -import { - createBindings, - createBunHost, - loadBunLibrary, - symbols, -} from "./cpp_core_bindings.ts"; - -const host = await createBunHost(); -const dylib = await loadBunLibrary("./libcpp_bindings_linux.so", symbols); -const serial = createBindings(host, dylib); - -const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8); -serial.serialClose(handle); -dylib.close(); -``` - -What the generated file already gives you: - -```ts -await createBunHost() -await loadBunLibrary(path, symbols?) -``` - -## Node.js - -Node uses the default generic output. There is no built-in Node loader implementation, because there is no single official `dlopen`-style JS API that matches Deno/Bun. The intended path is: provide a Node-side bridge and plug it into the generated custom-adapter helpers. - -```ts -import native from "./build/Release/cpp_core.node"; - -import { - createBindings, - createHostFromRuntimeAdapter, - loadLibraryFromRuntimeAdapter, - symbols, - type BindgenRuntimeAdapter, - BindgenLibrary, - NativeFunctionDefinition, - PointerValue, -} from "./cpp_core_bindings.ts"; - -type NativeAddon = { - loadLibrary(path: string, symbols: Record): BindgenLibrary; - pointerOf(value: ArrayBufferView): PointerValue | null; - readCString(pointer: PointerValue): string | null; - createCallback( - definition: NativeFunctionDefinition, - callback: (...args: unknown[]) => unknown, - ): { pointer: PointerValue; close(): void }; -}; - -const addon = native as NativeAddon; - -const adapter: BindgenRuntimeAdapter = { - loadLibrary(path, definitions) { - return addon.loadLibrary(String(path), definitions); - }, - pointerOf(value) { - return addon.pointerOf(value); - }, - readCString(pointer) { - return addon.readCString(pointer); - }, - createCallback(definition, callback) { - return addon.createCallback(definition, callback); - }, -}; - -const host = createHostFromRuntimeAdapter(adapter); -const dylib = loadLibraryFromRuntimeAdapter( - adapter, - "./libcpp_bindings_linux.so", - symbols, -); -const serial = createBindings(host, dylib); -``` - -For Node, the missing piece is still the addon implementation behind `cpp_core.node`. The generated TypeScript layer already provides the integration points: - -- `BindgenRuntimeAdapter` -- `createHostFromRuntimeAdapter(...)` -- `loadLibraryFromRuntimeAdapter(...)` - -## Practical Takeaway - -- Generic / Node: built in as a custom-adapter path -- Deno: built in when generated with `--runtime deno` -- Bun: built in when generated with `--runtime bun` -- Node loader implementation itself is still yours - -## Official References - -- Deno FFI: https://docs.deno.com/runtime/fundamentals/ffi/ -- Deno `Deno.dlopen`: https://docs.deno.com/api/deno/~/Deno.dlopen -- Bun FFI: https://bun.sh/docs/runtime/ffi -- Node addons: https://nodejs.org/api/addons.html -- Node-API: https://nodejs.org/api/n-api.html diff --git a/README.md b/README.md index 8feb147..a796d81 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # C++ Core -`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, build-time version information, and the reflection-backed metadata used by bindgen tooling. +`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. This repository does not provide a ready-to-load shared library by itself. It provides the contract consumed by the platform implementations: @@ -9,33 +9,25 @@ This repository does not provide a ready-to-load shared library by itself. It pr ## Status -`cpp-core` is the canonical definition of the current reflection-based API line. +`cpp-core` is the canonical definition of the current API line. -- Supported compiler family: GNU C++ -- Supported Linux toolchain: GCC 16+ -- Supported Windows toolchain: MinGW-w64 GCC 16+ +- Supported Linux toolchain: modern C++26-capable toolchain +- Supported Windows toolchain: modern C++26-capable toolchain - macOS support: not ready yet -- Required language mode: C++26 with `-freflection` +- Required language mode: C++26 - Required build system: CMake 3.30+ -Clang and MSVC are not supported at this time. - ## What It Provides - Header-only C-compatible serial API definitions under `include/cpp_core` -- Reflection-backed ABI metadata for functions, operations, status codes, and shared structs - A generated version surface used consistently across all platform bindings -- A `cpp_core_bindgen` executable that emits TypeScript FFI metadata and wrappers - An installable CMake package target: `cpp_core::cpp_core` ## Repository Layout - `include/cpp_core/serial.h`: aggregated C ABI for serial operations -- `include/cpp_core/ffi_metadata.hpp`: compile-time metadata over the exported ABI +- `include/cpp_core/status_code.h`: shared status-code model - `include/cpp_core/interface/get_version.h`: version struct and `getVersion` -- `tools/generate_typescript_bindings.cpp`: bindgen entrypoint for TypeScript symbols and wrappers - -`cpp-core` hard-requires the GNU compiler family with working C++26 reflection support. During configure, CMake rejects unsupported compilers and rejects GNU builds where `-freflection` is unavailable or incomplete. ## Quick Start @@ -78,27 +70,18 @@ getVersion(&version); ## Building This Repository -Native GCC build: - -```sh -cmake -S . -B build/gcc -G Ninja -DCMAKE_CXX_COMPILER=g++-16 -cmake --build build/gcc -ctest --test-dir build/gcc -``` - -MinGW-w64 GCC build: +Native build: ```sh -cmake -S . -B build/mingw -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-toolchain.cmake -cmake --build build/mingw +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_bindgen`: TypeScript symbol and wrapper generator - `cpp_core_compile_tests`: compile-time validation target when testing is enabled -- `cpp_core_bindgen_smoke`: generated-output smoke check when testing is enabled ## ABI Surface @@ -125,109 +108,6 @@ MODULE_API auto serialOpen( This model keeps the ABI easy to consume from TypeScript hosts, Rust, Python, or other FFI hosts without requiring C++ runtime coupling. -## Reflection Metadata - -`cpp-core` exposes compile-time metadata for the ABI and the shared structs: - -```cpp -#include - -constexpr auto functions = cpp_core::functionDescriptors(); -constexpr auto operations = cpp_core::operationDescriptors(); -constexpr auto statuses = cpp_core::statusCodeDescriptors(); -constexpr auto serial_config_fields = cpp_core::serialConfigFieldDescriptors(); -constexpr auto version_fields = cpp_core::versionFieldDescriptors(); -``` - -Targeted lookup helpers are also available: - -```cpp -constexpr auto *open_fn = cpp_core::findFunctionDescriptor("serialOpen"); -constexpr auto *open_op = cpp_core::findOperationDescriptor("serialOpen"); -constexpr auto *read_error = - cpp_core::findStatusCodeDescriptor(cpp_core::StatusCode::Io::kReadError); -``` - -The metadata is the source of truth for: - -- exported function names -- parameter names and optionality -- ABI kind classification for strings, buffers, handles, and callbacks -- result model classification -- shared struct field layout metadata - -## TypeScript Bindgen - -Build and run the bindgen tool: - -```sh -cmake --build build/gcc --target cpp_core_bindgen -./build/gcc/cpp_core_bindgen --output cpp_core_bindings.ts -``` - -Optional runtime profile: - -```sh -./build/gcc/cpp_core_bindgen --runtime deno --output cpp_core_bindings.ts -./build/gcc/cpp_core_bindgen --runtime bun --output cpp_core_bindings.ts -``` - -The generated module includes: - -- `symbols` ABI metadata -- `operations` metadata -- `statusCodes` and status lookup data -- `StatusCodeError` -- `createBindings(host, dylib)` wrappers -- custom runtime adapter helpers for Node or other hosts -- optional built-in runtime helpers when `--runtime deno` or `--runtime bun` is used -- callback helpers such as `createErrorCallback(...)` - -Default generic usage: - -```ts -import { - createBindings, - createHostFromRuntimeAdapter, - loadLibraryFromRuntimeAdapter, - symbols, - type BindgenRuntimeAdapter, -} from "./cpp_core_bindings.ts"; - -declare const adapter: BindgenRuntimeAdapter; - -const host = createHostFromRuntimeAdapter(adapter); -const dylib = loadLibraryFromRuntimeAdapter(adapter, "./libcpp_bindings_linux.so", symbols); -const serial = createBindings(host, dylib); -``` - -Deno-specific usage with `--runtime deno`: - -```ts -import { - createBindings, - createErrorCallback, - createDenoHost, - loadDenoLibrary, - symbols, -} from "./cpp_core_bindings.ts"; - -const host = createDenoHost(); -const dylib = loadDenoLibrary("./libcpp_bindings_linux.so", symbols); -const serial = createBindings(host, dylib); -const errorCallback = createErrorCallback(host, (errorCode, message) => { - console.error(errorCode, message); -}); - -const handle = serial.serialOpen("/dev/ttyUSB0", 115200, 8); -serial.serialWrite(handle, new Uint8Array([0x41, 0x54, 0x0d]), 3, 500, 0); -serial.serialClose(handle); -errorCallback.close(); -dylib.close(); -``` - -Without `--runtime`, the output stays generic. If you target Node.js or another host, use the generated custom-adapter helpers and implement the runtime bridge yourself. - ## Versioning Version information is generated from Git during CMake configure and written into `include/cpp_core/version.hpp`. @@ -251,7 +131,7 @@ The version data is exposed through: - `cpp-bindings-windows` provides the Windows DLL implementation - macOS bindings are not part of the supported line yet -Keeping the contract, metadata, and version surface here avoids ABI drift between platforms and keeps generated binding layers aligned with the actual exported API. +Keeping the contract and version surface here avoids ABI drift between platforms and keeps the shared API aligned with the actual exported implementation. ## License diff --git a/include/cpp_core.h b/include/cpp_core.h index fe1fe27..9475aa5 100644 --- a/include/cpp_core.h +++ b/include/cpp_core.h @@ -2,7 +2,6 @@ #include "cpp_core/error_callback.h" #include "cpp_core/error_handling.hpp" -#include "cpp_core/ffi_metadata.hpp" #include "cpp_core/result.hpp" #include "cpp_core/scope_guard.hpp" #include "cpp_core/serial.h" diff --git a/include/cpp_core/ffi/abi_function_registry.hpp b/include/cpp_core/ffi/abi_function_registry.hpp deleted file mode 100644 index f300e52..0000000 --- a/include/cpp_core/ffi/abi_function_registry.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include "../serial.h" - -#include - -namespace cpp_core::detail -{ - -struct AbiFunctionRegistry -{ - std::meta::info getVersion = ^^::getVersion; - std::meta::info serialAbortRead = ^^::serialAbortRead; - std::meta::info serialAbortWrite = ^^::serialAbortWrite; - std::meta::info serialClearBufferIn = ^^::serialClearBufferIn; - std::meta::info serialClearBufferOut = ^^::serialClearBufferOut; - std::meta::info serialClose = ^^::serialClose; - std::meta::info serialDrain = ^^::serialDrain; - std::meta::info serialGetBaudrate = ^^::serialGetBaudrate; - std::meta::info serialGetCts = ^^::serialGetCts; - std::meta::info serialGetDataBits = ^^::serialGetDataBits; - std::meta::info serialGetDcd = ^^::serialGetDcd; - std::meta::info serialGetDsr = ^^::serialGetDsr; - std::meta::info serialGetFlowControl = ^^::serialGetFlowControl; - std::meta::info serialGetParity = ^^::serialGetParity; - std::meta::info serialGetRi = ^^::serialGetRi; - std::meta::info serialGetStopBits = ^^::serialGetStopBits; - std::meta::info serialInBytesTotal = ^^::serialInBytesTotal; - std::meta::info serialInBytesWaiting = ^^::serialInBytesWaiting; - std::meta::info serialListPorts = ^^::serialListPorts; - std::meta::info serialMonitorPorts = ^^::serialMonitorPorts; - std::meta::info serialOpen = ^^::serialOpen; - std::meta::info serialOutBytesTotal = ^^::serialOutBytesTotal; - std::meta::info serialOutBytesWaiting = ^^::serialOutBytesWaiting; - std::meta::info serialRead = ^^::serialRead; - std::meta::info serialReadLine = ^^::serialReadLine; - std::meta::info serialReadUntil = ^^::serialReadUntil; - std::meta::info serialReadUntilSequence = ^^::serialReadUntilSequence; - std::meta::info serialSendBreak = ^^::serialSendBreak; - std::meta::info serialSetBaudrate = ^^::serialSetBaudrate; - std::meta::info serialSetDataBits = ^^::serialSetDataBits; - std::meta::info serialSetDtr = ^^::serialSetDtr; - std::meta::info serialSetErrorCallback = ^^::serialSetErrorCallback; - std::meta::info serialSetFlowControl = ^^::serialSetFlowControl; - std::meta::info serialSetParity = ^^::serialSetParity; - std::meta::info serialSetReadCallback = ^^::serialSetReadCallback; - std::meta::info serialSetRts = ^^::serialSetRts; - std::meta::info serialSetStopBits = ^^::serialSetStopBits; - std::meta::info serialSetWriteCallback = ^^::serialSetWriteCallback; - std::meta::info serialWrite = ^^::serialWrite; -}; - -} // namespace cpp_core::detail diff --git a/include/cpp_core/ffi/status_code_descriptors.hpp b/include/cpp_core/ffi/status_code_descriptors.hpp deleted file mode 100644 index 8389488..0000000 --- a/include/cpp_core/ffi/status_code_descriptors.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -inline constexpr auto kStatusCodeDescriptors = std::array{ - StatusCodeDescriptor{"General", "Success", StatusCode::kSuccess}, - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetBaudrateError), - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetDataBitsError), - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetParityError), - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetStopBitsError), - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetFlowControlError), - detail::makeStatusCodeDescriptor(StatusCode::Configuration::kSetTimeoutError), - detail::makeStatusCodeDescriptor(StatusCode::Connection::kNotFoundError), - detail::makeStatusCodeDescriptor(StatusCode::Connection::kInvalidHandleError), - detail::makeStatusCodeDescriptor(StatusCode::Connection::kCloseHandleError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kReadError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kWriteError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kAbortReadError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kAbortWriteError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kBufferError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kClearBufferInError), - detail::makeStatusCodeDescriptor(StatusCode::Io::kClearBufferOutError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kSetDtrError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kSetRtsError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kGetModemStatusError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kSendBreakError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kGetStateError), - detail::makeStatusCodeDescriptor(StatusCode::Control::kSetStateError), - detail::makeStatusCodeDescriptor(StatusCode::Monitor::kMonitorError), -}; diff --git a/include/cpp_core/ffi_metadata.hpp b/include/cpp_core/ffi_metadata.hpp deleted file mode 100644 index 9450a98..0000000 --- a/include/cpp_core/ffi_metadata.hpp +++ /dev/null @@ -1,535 +0,0 @@ -#pragma once - -#include "ffi/abi_function_registry.hpp" -#include "serial.h" -#include "serial_config.hpp" -#include "status_code.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef __cpp_impl_reflection -#error "cpp_core/ffi_metadata.hpp requires C++26 reflection support." -#endif - -namespace cpp_core -{ - -enum class AbiValueKind -{ - kVoid, - kInt32, - kInt64, - kOpaqueHandle, - kUtf8CString, - kMutableBuffer, - kConstBuffer, - kOpaquePointer, - kVersionStructPointer, - kErrorCallback, - kNotificationCallback, -}; - -enum class ParameterDirection -{ - kIn, - kOut, - kInOut, -}; - -enum class FunctionResultModel -{ - kVoid, - kStatusCode, - kValueOrStatus, - kHandleOrStatus, -}; - -struct ParameterDescriptor -{ - std::string_view name; - std::string_view cpp_type; - AbiValueKind abi_kind; - ParameterDirection direction; - bool optional; -}; - -struct FunctionDescriptor -{ - std::string_view name; - std::string_view return_cpp_type; - std::string_view signature; - AbiValueKind return_abi_kind; - FunctionResultModel result_model; - std::span parameters; -}; - -struct OperationDescriptor -{ - std::string_view name; - std::string_view function_name; -}; - -struct StatusCodeDescriptor -{ - std::string_view category; - std::string_view name; - StatusCodeValue value; -}; - -struct StructFieldDescriptor -{ - std::string_view name; - std::string_view cpp_type; - AbiValueKind abi_kind; - std::size_t offset; - std::size_t size; -}; - -namespace detail -{ - -[[nodiscard]] consteval auto overrideParameterAbiValueKind(std::string_view function_name, std::string_view parameter_name, - std::meta::info type_info) -> std::optional -{ - if (!std::meta::is_pointer_type(type_info)) - { - return std::nullopt; - } - - if (function_name == "getVersion" && parameter_name == "out") - { - return AbiValueKind::kVersionStructPointer; - } - - return std::nullopt; -} - -[[nodiscard]] consteval auto isConstPointer(std::meta::info type_info) -> bool -{ - if (!std::meta::is_pointer_type(type_info)) - { - return false; - } - - return std::meta::is_const(std::meta::remove_pointer(type_info)); -} - -[[nodiscard]] consteval auto inferAbiValueKind(std::meta::info type_info, std::string_view parameter_name) -> AbiValueKind -{ - if (std::meta::is_pointer_type(type_info)) - { - if (std::meta::display_string_of(type_info) == "cpp_core::Version*") - { - return AbiValueKind::kVersionStructPointer; - } - - if (parameter_name == "error_callback") - { - return AbiValueKind::kErrorCallback; - } - - if (parameter_name.ends_with("_callback") || parameter_name == "callback_fn") - { - return AbiValueKind::kNotificationCallback; - } - - if (parameter_name == "port") - { - return AbiValueKind::kUtf8CString; - } - - if (parameter_name == "buffer") - { - return isConstPointer(type_info) ? AbiValueKind::kConstBuffer : AbiValueKind::kMutableBuffer; - } - - if (parameter_name == "sequence" || parameter_name == "until_char") - { - return AbiValueKind::kConstBuffer; - } - - if (std::meta::display_string_of(std::meta::remove_pointer(type_info)) == "char") - { - return AbiValueKind::kUtf8CString; - } - - return AbiValueKind::kOpaquePointer; - } - - if (std::meta::is_integral_type(type_info)) - { - if (parameter_name == "handle") - { - return AbiValueKind::kOpaqueHandle; - } - - return std::meta::size_of(type_info) > sizeof(std::int32_t) ? AbiValueKind::kInt64 : AbiValueKind::kInt32; - } - - if (std::meta::is_void_type(type_info)) - { - return AbiValueKind::kVoid; - } - - return AbiValueKind::kOpaquePointer; -} - -[[nodiscard]] consteval auto inferParameterAbiValueKind(std::string_view function_name, std::string_view parameter_name, - std::meta::info type_info) -> AbiValueKind -{ - if (const auto override = overrideParameterAbiValueKind(function_name, parameter_name, type_info)) - { - return *override; - } - - return inferAbiValueKind(type_info, parameter_name); -} - -[[nodiscard]] consteval auto overrideReturnAbiValueKind(std::string_view function_name) - -> std::optional -{ - if (function_name == "serialOpen") - { - return AbiValueKind::kOpaqueHandle; - } - - return std::nullopt; -} - -[[nodiscard]] consteval auto inferReturnAbiValueKind(std::string_view function_name, std::meta::info return_type_info) - -> AbiValueKind -{ - if (std::meta::is_void_type(return_type_info)) - { - return AbiValueKind::kVoid; - } - - if (const auto override = overrideReturnAbiValueKind(function_name)) - { - return *override; - } - - if (std::meta::is_integral_type(return_type_info)) - { - return std::meta::size_of(return_type_info) > sizeof(std::int32_t) ? AbiValueKind::kInt64 : AbiValueKind::kInt32; - } - - return AbiValueKind::kOpaquePointer; -} - -[[nodiscard]] consteval auto overrideParameterDirection(std::string_view function_name, std::string_view parameter_name) - -> std::optional -{ - if (function_name == "getVersion" && parameter_name == "out") - { - return ParameterDirection::kOut; - } - - return std::nullopt; -} - -[[nodiscard]] consteval auto inferParameterDirection(std::string_view function_name, std::string_view parameter_name, - std::meta::info type_info) -> ParameterDirection -{ - if (const auto override = overrideParameterDirection(function_name, parameter_name)) - { - return *override; - } - - if (parameter_name.ends_with("_callback") || parameter_name == "callback_fn" || parameter_name == "error_callback") - { - return ParameterDirection::kIn; - } - - if (parameter_name == "out") - { - return ParameterDirection::kOut; - } - - if (parameter_name == "buffer") - { - if (function_name.starts_with("serialRead")) - { - return ParameterDirection::kOut; - } - - if (function_name.starts_with("serialWrite")) - { - return ParameterDirection::kIn; - } - } - - if (parameter_name == "sequence" || parameter_name == "until_char" || parameter_name == "port") - { - return ParameterDirection::kIn; - } - - if (std::meta::is_pointer_type(type_info)) - { - return isConstPointer(type_info) ? ParameterDirection::kIn : ParameterDirection::kInOut; - } - - return ParameterDirection::kIn; -} - -[[nodiscard]] consteval auto overrideResultModel(std::string_view function_name) - -> std::optional -{ - if (function_name == "serialOpen") - { - return FunctionResultModel::kHandleOrStatus; - } - - return std::nullopt; -} - -[[nodiscard]] consteval auto inferResultModel(std::string_view function_name, std::meta::info return_type_info) - -> FunctionResultModel -{ - if (std::meta::is_void_type(return_type_info)) - { - return FunctionResultModel::kVoid; - } - - if (const auto override = overrideResultModel(function_name)) - { - return *override; - } - - if (function_name.starts_with("serialRead") || function_name.starts_with("serialWrite") - || function_name.starts_with("serialGet") || function_name.starts_with("serialInBytes") - || function_name.starts_with("serialOutBytes") || function_name == "serialListPorts") - { - return FunctionResultModel::kValueOrStatus; - } - - return FunctionResultModel::kStatusCode; -} - -[[nodiscard]] consteval auto makeParameterDescriptor(std::string_view function_name, std::meta::info parameter_info) - -> ParameterDescriptor -{ - const auto type_info = std::meta::type_of(parameter_info); - const auto parameter_name = std::meta::identifier_of(parameter_info); - - return ParameterDescriptor{ - .name = parameter_name, - .cpp_type = std::meta::display_string_of(type_info), - .abi_kind = inferParameterAbiValueKind(function_name, parameter_name, type_info), - .direction = inferParameterDirection(function_name, parameter_name, type_info), - .optional = std::meta::has_default_argument(parameter_info), - }; -} - -template -[[nodiscard]] consteval auto makeParameterDescriptorsForFunctionImpl(std::index_sequence) -{ - constexpr auto params = std::define_static_array(std::meta::parameters_of(FunctionInfo)); - return std::array{ - makeParameterDescriptor(std::meta::identifier_of(FunctionInfo), *(params.data() + Index))..., - }; -} - -template -inline constexpr auto kParameterDescriptorsForFunction = - makeParameterDescriptorsForFunctionImpl( - std::make_index_sequence{}); - -template -[[nodiscard]] consteval auto makeFunctionDescriptor(std::meta::info function_info, - const std::array ¶ms) - -> FunctionDescriptor -{ - const auto function_name = std::meta::identifier_of(function_info); - const auto return_type = std::meta::return_type_of(function_info); - - return FunctionDescriptor{ - .name = function_name, - .return_cpp_type = std::meta::display_string_of(return_type), - .signature = std::meta::display_string_of(std::meta::type_of(function_info)), - .return_abi_kind = inferReturnAbiValueKind(function_name, return_type), - .result_model = inferResultModel(function_name, return_type), - .parameters = std::span(params), - }; -} - -template [[nodiscard]] consteval auto makeFunctionDescriptorFromInfo() -> FunctionDescriptor -{ - return makeFunctionDescriptor(FunctionInfo, kParameterDescriptorsForFunction); -} - -template -[[nodiscard]] consteval auto makeFieldDescriptorsImpl(const std::vector &fields, - std::index_sequence) -{ - return std::array{ - StructFieldDescriptor{ - .name = std::meta::identifier_of(fields[Index]), - .cpp_type = std::meta::display_string_of(std::meta::type_of(fields[Index])), - .abi_kind = inferAbiValueKind(std::meta::type_of(fields[Index]), std::meta::identifier_of(fields[Index])), - .offset = static_cast(std::meta::offset_of(fields[Index]).bytes), - .size = std::meta::size_of(std::meta::type_of(fields[Index])), - }..., - }; -} - -[[nodiscard]] consteval auto makeOperationDescriptor(std::meta::info function_info) -> OperationDescriptor -{ - const auto function_name = std::meta::identifier_of(function_info); - return OperationDescriptor{ - .name = function_name, - .function_name = function_name, - }; -} - -template [[nodiscard]] consteval auto makeOperationDescriptorFromInfo() -> OperationDescriptor -{ - return makeOperationDescriptor(FunctionInfo); -} - -template [[nodiscard]] consteval auto makeStatusCodeDescriptor(const Code &code_value) -> StatusCodeDescriptor -{ - return StatusCodeDescriptor{ - .category = code_value.category(), - .name = code_value.name(), - .value = static_cast(code_value), - }; -} - -template -[[nodiscard]] consteval auto makeAbiFunctionDescriptorsImpl(std::index_sequence) - -> std::array -{ - constexpr AbiFunctionRegistry registry{}; - constexpr auto fields = std::define_static_array( - std::meta::nonstatic_data_members_of(^^AbiFunctionRegistry, std::meta::access_context::unchecked())); - - return std::array{ - makeFunctionDescriptorFromInfo()..., - }; -} - -template -[[nodiscard]] consteval auto makeAbiOperationDescriptorsImpl(std::index_sequence) - -> std::array -{ - constexpr AbiFunctionRegistry registry{}; - constexpr auto fields = std::define_static_array( - std::meta::nonstatic_data_members_of(^^AbiFunctionRegistry, std::meta::access_context::unchecked())); - - return std::array{ - makeOperationDescriptorFromInfo()..., - }; -} - -} // namespace detail - -inline constexpr auto kFunctionDescriptors = []() consteval -{ - return detail::makeAbiFunctionDescriptorsImpl( - std::make_index_sequence{}); -}(); - -inline constexpr auto kOperationDescriptors = []() consteval -{ - return detail::makeAbiOperationDescriptorsImpl( - std::make_index_sequence{}); -}(); - -#include "ffi/status_code_descriptors.hpp" - -inline constexpr auto kSerialConfigFieldDescriptors = []() consteval -{ - auto fields = std::meta::nonstatic_data_members_of(^^SerialConfig, std::meta::access_context::unchecked()); - return detail::makeFieldDescriptorsImpl( - fields, std::make_index_sequence{}); -}(); - -inline constexpr auto kVersionFieldDescriptors = []() consteval -{ - auto fields = std::meta::nonstatic_data_members_of(^^Version, std::meta::access_context::unchecked()); - return detail::makeFieldDescriptorsImpl( - fields, - std::make_index_sequence{}); -}(); - -[[nodiscard]] constexpr auto functionDescriptors() noexcept -> std::span -{ - return kFunctionDescriptors; -} - -[[nodiscard]] constexpr auto operationDescriptors() noexcept -> std::span -{ - return kOperationDescriptors; -} - -[[nodiscard]] constexpr auto statusCodeDescriptors() noexcept -> std::span -{ - return kStatusCodeDescriptors; -} - -[[nodiscard]] constexpr auto serialConfigFieldDescriptors() noexcept -> std::span -{ - return kSerialConfigFieldDescriptors; -} - -[[nodiscard]] constexpr auto versionFieldDescriptors() noexcept -> std::span -{ - return kVersionFieldDescriptors; -} - -[[nodiscard]] constexpr auto findFunctionDescriptor(std::string_view function_name) noexcept -> const FunctionDescriptor * -{ - for (const auto &descriptor : kFunctionDescriptors) - { - if (descriptor.name == function_name) - { - return &descriptor; - } - } - - return nullptr; -} - -[[nodiscard]] constexpr auto findOperationDescriptor(std::string_view operation_name) noexcept -> const OperationDescriptor * -{ - for (const auto &descriptor : kOperationDescriptors) - { - if (descriptor.name == operation_name) - { - return &descriptor; - } - } - - return nullptr; -} - -[[nodiscard]] constexpr auto findStatusCodeDescriptor(StatusCodeValue code) noexcept -> const StatusCodeDescriptor * -{ - for (const auto &descriptor : kStatusCodeDescriptors) - { - if (descriptor.value == code) - { - return &descriptor; - } - } - - return nullptr; -} - -} // namespace cpp_core diff --git a/tests/ffi_metadata.test.cpp b/tests/ffi_metadata.test.cpp deleted file mode 100644 index ea9bb4f..0000000 --- a/tests/ffi_metadata.test.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "cpp_core/ffi_metadata.hpp" - -static_assert(__cpp_impl_reflection >= 202506L); - -static_assert(!cpp_core::kFunctionDescriptors.empty()); -static_assert(cpp_core::kFunctionDescriptors.size() == cpp_core::kOperationDescriptors.size()); -static_assert(!cpp_core::kStatusCodeDescriptors.empty()); -static_assert(cpp_core::kSerialConfigFieldDescriptors.size() == 4); -static_assert(cpp_core::kVersionFieldDescriptors.size() == 8); - -constexpr auto &kGetVersion = *cpp_core::findFunctionDescriptor("getVersion"); -static_assert(kGetVersion.result_model == cpp_core::FunctionResultModel::kVoid); -static_assert(kGetVersion.parameters.size() == 1); -static_assert(kGetVersion.parameters[0].name == "out"); -static_assert(kGetVersion.parameters[0].abi_kind == cpp_core::AbiValueKind::kVersionStructPointer); -static_assert(kGetVersion.parameters[0].direction == cpp_core::ParameterDirection::kOut); - -constexpr auto &kSerialOpen = *cpp_core::findFunctionDescriptor("serialOpen"); -static_assert(kSerialOpen.result_model == cpp_core::FunctionResultModel::kHandleOrStatus); -static_assert(kSerialOpen.return_abi_kind == cpp_core::AbiValueKind::kOpaqueHandle); -static_assert(kSerialOpen.parameters.size() == 6); -static_assert(kSerialOpen.parameters[0].name == "port"); -static_assert(kSerialOpen.parameters[0].abi_kind == cpp_core::AbiValueKind::kUtf8CString); -static_assert(kSerialOpen.parameters[4].optional); -static_assert(kSerialOpen.parameters[5].name == "error_callback"); -static_assert(kSerialOpen.parameters[5].abi_kind == cpp_core::AbiValueKind::kErrorCallback); - -constexpr auto &kSerialRead = *cpp_core::findFunctionDescriptor("serialRead"); -static_assert(kSerialRead.result_model == cpp_core::FunctionResultModel::kValueOrStatus); -static_assert(kSerialRead.parameters[1].name == "buffer"); -static_assert(kSerialRead.parameters[1].abi_kind == cpp_core::AbiValueKind::kMutableBuffer); -static_assert(kSerialRead.parameters[1].direction == cpp_core::ParameterDirection::kOut); - -constexpr auto &kSetErrorCallback = *cpp_core::findFunctionDescriptor("serialSetErrorCallback"); -static_assert(kSetErrorCallback.result_model == cpp_core::FunctionResultModel::kVoid); -static_assert(kSetErrorCallback.parameters.size() == 1); -static_assert(kSetErrorCallback.parameters[0].abi_kind == cpp_core::AbiValueKind::kErrorCallback); - -static_assert(cpp_core::kStatusCodeDescriptors[10].category == "Io"); -static_assert(cpp_core::kStatusCodeDescriptors[10].name == "ReadError"); -static_assert(cpp_core::kStatusCodeDescriptors[23].category == "Monitor"); -static_assert(cpp_core::kStatusCodeDescriptors[23].name == "MonitorError"); - -static_assert(cpp_core::kSerialConfigFieldDescriptors[0].name == "baudrate"); -static_assert(cpp_core::kSerialConfigFieldDescriptors[1].name == "data_bits"); -static_assert(cpp_core::kSerialConfigFieldDescriptors[2].name == "parity"); -static_assert(cpp_core::kSerialConfigFieldDescriptors[3].name == "stop_bits"); - -static_assert(cpp_core::kVersionFieldDescriptors[0].name == "major"); -static_assert(cpp_core::kVersionFieldDescriptors[3].name == "commit_hash_short"); -static_assert(cpp_core::kVersionFieldDescriptors[7].name == "version_string"); - -static_assert(cpp_core::findFunctionDescriptor("serialOpen") != nullptr); -static_assert(cpp_core::findFunctionDescriptor("serialOpen")->name == "serialOpen"); -static_assert(cpp_core::findFunctionDescriptor("doesNotExist") == nullptr); -static_assert(cpp_core::findOperationDescriptor("serialOpen") != nullptr); -static_assert(cpp_core::findOperationDescriptor("serialOpen")->function_name == "serialOpen"); -static_assert(cpp_core::findStatusCodeDescriptor(cpp_core::StatusCode::Monitor::kMonitorError) != nullptr); diff --git a/tools/check_generated_typescript_bindings.cmake b/tools/check_generated_typescript_bindings.cmake deleted file mode 100644 index 28798e5..0000000 --- a/tools/check_generated_typescript_bindings.cmake +++ /dev/null @@ -1,103 +0,0 @@ -if(NOT DEFINED GENERATOR) - message(FATAL_ERROR "GENERATOR must point to the cpp_core_bindgen executable.") -endif() - -function(run_generator output_var) - execute_process( - COMMAND "${GENERATOR}" ${ARGN} - RESULT_VARIABLE generator_result - OUTPUT_VARIABLE generator_output - ERROR_VARIABLE generator_error - ) - - if(NOT generator_result EQUAL 0) - message(FATAL_ERROR "Generator failed with code ${generator_result}: ${generator_error}") - endif() - - set(${output_var} "${generator_output}" PARENT_SCOPE) -endfunction() - -function(require_snippets content label) - foreach(snippet IN LISTS ARGN) - string(FIND "${content}" "${snippet}" snippet_index) - if(snippet_index EQUAL -1) - message(FATAL_ERROR "Missing expected snippet in ${label}: ${snippet}") - endif() - endforeach() -endfunction() - -function(forbid_snippets content label) - foreach(snippet IN LISTS ARGN) - string(FIND "${content}" "${snippet}" snippet_index) - if(NOT snippet_index EQUAL -1) - message(FATAL_ERROR "Unexpected snippet in ${label}: ${snippet}") - endif() - endforeach() -endfunction() - -set(common_required - "// Runtime profile: generic." - "export type OpaquePointer = object;" - "export type PointerValue = number | bigint | OpaquePointer;" - "export interface BindgenRuntimeAdapter {" - "export interface BindgenHost {" - "pointerOf(value: ArrayBuffer | ArrayBufferView): PointerValue | null;" - "export function createHostFromRuntimeAdapter(adapter: BindgenRuntimeAdapter): BindgenHost" - "export function loadLibraryFromRuntimeAdapter(" - "createCallback(" - "export const symbols = {" - "serialOpen: {" - "result: \"isize\"" - "serialClose: {" - "result: \"i32\"" - "serialInBytesTotal: {" - "result: \"i64\"" - "export const operations = {" - "export class StatusCodeError extends Error" - "export const errorCallbackDefinition = {" - "export type ErrorCallback = (error_code: number, message: string | null) => void;" - "export function createErrorCallback(host: BindgenHost, callback: ErrorCallback): CallbackHandle" - "export function createBindings(host: BindgenHost, dylib: BindgenLibrary)" - "serialOpen(port: string | PointerValue, baudrate: number, data_bits: number, parity: number = 0, stop_bits: number = 0, error_callback: PointerValue | null = null): bigint" -) - -set(generic_forbidden - "export function createDenoHost(): BindgenHost" - "export function loadDenoLibrary(" - "export async function createBunHost(): Promise" - "export async function loadBunLibrary(" -) - -run_generator(generic_output) -require_snippets("${generic_output}" "generic output" ${common_required}) -forbid_snippets("${generic_output}" "generic output" ${generic_forbidden}) - -set(deno_required - "// Runtime profile: deno." - "export function createDenoHost(): BindgenHost" - "export function loadDenoLibrary(" -) - -set(deno_forbidden - "export async function createBunHost(): Promise" - "export async function loadBunLibrary(" -) - -run_generator(deno_output --runtime deno) -require_snippets("${deno_output}" "deno output" ${deno_required}) -forbid_snippets("${deno_output}" "deno output" ${deno_forbidden}) - -set(bun_required - "// Runtime profile: bun." - "export async function createBunHost(): Promise" - "export async function loadBunLibrary(" -) - -set(bun_forbidden - "export function createDenoHost(): BindgenHost" - "export function loadDenoLibrary(" -) - -run_generator(bun_output --runtime bun) -require_snippets("${bun_output}" "bun output" ${bun_required}) -forbid_snippets("${bun_output}" "bun output" ${bun_forbidden}) diff --git a/tools/generate_typescript_bindings.cpp b/tools/generate_typescript_bindings.cpp deleted file mode 100644 index 1195f01..0000000 --- a/tools/generate_typescript_bindings.cpp +++ /dev/null @@ -1,881 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -template struct FunctionPointerTraits; - -template struct FunctionPointerTraits -{ - using ReturnType = Return; - using ArgumentTypes = std::tuple; - static constexpr std::size_t kArity = sizeof...(Args); - - template using Argument = std::tuple_element_t; -}; - -using ErrorCallbackTraits = FunctionPointerTraits; - -static_assert(ErrorCallbackTraits::kArity == 2); -static_assert(std::is_same_v); -static_assert(std::is_same_v, int>); -static_assert(std::is_same_v, const char *>); - -struct CliOptions -{ - std::optional output_path; - enum class RuntimeProfile - { - kGeneric, - kNode, - kDeno, - kBun, - }; - - RuntimeProfile runtime = RuntimeProfile::kGeneric; - bool show_help = false; -}; - -[[nodiscard]] constexpr auto runtimeProfileName(CliOptions::RuntimeProfile runtime) -> std::string_view -{ - switch (runtime) - { - case CliOptions::RuntimeProfile::kGeneric: - return "generic"; - case CliOptions::RuntimeProfile::kNode: - return "node"; - case CliOptions::RuntimeProfile::kDeno: - return "deno"; - case CliOptions::RuntimeProfile::kBun: - return "bun"; - } - - return "generic"; -} - -[[nodiscard]] auto parseRuntimeProfile(std::string_view value, CliOptions::RuntimeProfile &runtime) -> bool -{ - if (value == "generic" || value == "universal") - { - runtime = CliOptions::RuntimeProfile::kGeneric; - return true; - } - - if (value == "node") - { - runtime = CliOptions::RuntimeProfile::kNode; - return true; - } - - if (value == "deno") - { - runtime = CliOptions::RuntimeProfile::kDeno; - return true; - } - - if (value == "bun") - { - runtime = CliOptions::RuntimeProfile::kBun; - return true; - } - - return false; -} - -[[nodiscard]] constexpr auto toNativeParameterType(cpp_core::AbiValueKind kind) -> std::string_view -{ - switch (kind) - { - case cpp_core::AbiValueKind::kVoid: - return "void"; - case cpp_core::AbiValueKind::kInt32: - return "i32"; - case cpp_core::AbiValueKind::kInt64: - case cpp_core::AbiValueKind::kOpaqueHandle: - return "i64"; - case cpp_core::AbiValueKind::kUtf8CString: - case cpp_core::AbiValueKind::kMutableBuffer: - case cpp_core::AbiValueKind::kConstBuffer: - case cpp_core::AbiValueKind::kOpaquePointer: - case cpp_core::AbiValueKind::kVersionStructPointer: - case cpp_core::AbiValueKind::kErrorCallback: - case cpp_core::AbiValueKind::kNotificationCallback: - return "pointer"; - } - - return "pointer"; -} - -[[nodiscard]] constexpr auto toNativeResultType(cpp_core::AbiValueKind kind) -> std::string_view -{ - switch (kind) - { - case cpp_core::AbiValueKind::kVoid: - return "void"; - case cpp_core::AbiValueKind::kInt32: - return "i32"; - case cpp_core::AbiValueKind::kInt64: - return "i64"; - case cpp_core::AbiValueKind::kOpaqueHandle: - return "isize"; - case cpp_core::AbiValueKind::kUtf8CString: - case cpp_core::AbiValueKind::kMutableBuffer: - case cpp_core::AbiValueKind::kConstBuffer: - case cpp_core::AbiValueKind::kOpaquePointer: - case cpp_core::AbiValueKind::kVersionStructPointer: - case cpp_core::AbiValueKind::kErrorCallback: - case cpp_core::AbiValueKind::kNotificationCallback: - return "pointer"; - } - - return "void"; -} - -[[nodiscard]] constexpr auto toNativeResultType(const cpp_core::FunctionDescriptor &function) -> std::string_view -{ - return toNativeResultType(function.return_abi_kind); -} - -[[nodiscard]] auto toTsParameterType(const cpp_core::ParameterDescriptor ¶meter) -> std::string_view -{ - switch (parameter.abi_kind) - { - case cpp_core::AbiValueKind::kInt32: - return "number"; - case cpp_core::AbiValueKind::kInt64: - case cpp_core::AbiValueKind::kOpaqueHandle: - return "number | bigint"; - case cpp_core::AbiValueKind::kUtf8CString: - return "string | PointerValue"; - case cpp_core::AbiValueKind::kMutableBuffer: - return "ArrayBufferView | PointerValue"; - case cpp_core::AbiValueKind::kConstBuffer: - return "string | ArrayBufferView | PointerValue"; - case cpp_core::AbiValueKind::kOpaquePointer: - case cpp_core::AbiValueKind::kVersionStructPointer: - return "ArrayBufferView | PointerValue"; - case cpp_core::AbiValueKind::kErrorCallback: - case cpp_core::AbiValueKind::kNotificationCallback: - return "PointerValue | null"; - case cpp_core::AbiValueKind::kVoid: - return "void"; - } - - return "unknown"; -} - -[[nodiscard]] auto toWrapperReturnType(const cpp_core::FunctionDescriptor &function) -> std::string_view -{ - switch (function.result_model) - { - case cpp_core::FunctionResultModel::kVoid: - case cpp_core::FunctionResultModel::kStatusCode: - return "void"; - case cpp_core::FunctionResultModel::kValueOrStatus: - return function.return_abi_kind == cpp_core::AbiValueKind::kInt64 ? "bigint" : "number"; - case cpp_core::FunctionResultModel::kHandleOrStatus: - return "bigint"; - } - - return "unknown"; -} - -[[nodiscard]] auto usesBufferMarshalling(const cpp_core::FunctionDescriptor &function) -> bool -{ - return std::ranges::any_of(function.parameters, [](const auto ¶meter) { - return parameter.abi_kind == cpp_core::AbiValueKind::kUtf8CString || - parameter.abi_kind == cpp_core::AbiValueKind::kConstBuffer || - parameter.abi_kind == cpp_core::AbiValueKind::kMutableBuffer || - parameter.abi_kind == cpp_core::AbiValueKind::kOpaquePointer || - parameter.abi_kind == cpp_core::AbiValueKind::kVersionStructPointer; - }); -} - -[[nodiscard]] auto optionalParameterDefault(const cpp_core::ParameterDescriptor ¶meter) - -> std::optional -{ - if (!parameter.optional) - { - return std::nullopt; - } - - switch (parameter.abi_kind) - { - case cpp_core::AbiValueKind::kInt32: - return "0"; - case cpp_core::AbiValueKind::kErrorCallback: - case cpp_core::AbiValueKind::kNotificationCallback: - return "null"; - default: - return std::nullopt; - } -} - -[[nodiscard]] auto sourceExpr(const cpp_core::ParameterDescriptor ¶meter) -> std::string -{ - return std::string(parameter.name); -} - -auto printUsage(std::ostream &stream) -> void -{ - stream << "Usage: cpp_core_bindgen [--output PATH] [--runtime generic|node|deno|bun] [--help]\n"; - stream << "Generates a TypeScript FFI module from cpp-core FFI metadata.\n"; -} - -[[nodiscard]] auto parseArgs(int argc, char **argv, CliOptions &options) -> bool -{ - for (int index = 1; index < argc; ++index) - { - const std::string_view arg = argv[index]; - if (arg == "--help" || arg == "-h") - { - options.show_help = true; - return true; - } - - if (arg == "--output") - { - if (index + 1 >= argc) - { - return false; - } - - options.output_path = std::string(argv[++index]); - continue; - } - - if (arg == "--runtime") - { - if (index + 1 >= argc) - { - return false; - } - - if (!parseRuntimeProfile(argv[++index], options.runtime)) - { - return false; - } - - continue; - } - - return false; - } - - return true; -} - -auto writeHeader(std::ostream &stream, CliOptions::RuntimeProfile runtime) -> void -{ - stream << "// Generated from cpp-core reflection metadata.\n"; - stream << "// Do not edit manually.\n"; - stream << "// Intended for use with a host-specific FFI adapter.\n"; - stream << "// ABI mode: modern.\n"; - stream << "// Runtime profile: " << runtimeProfileName(runtime) << ".\n\n"; -} - -auto writeHostTypes(std::ostream &stream) -> void -{ - stream << "export type OpaquePointer = object;\n"; - stream << "export type PointerValue = number | bigint | OpaquePointer;\n"; - stream << "export type NativeType = \"void\" | \"i32\" | \"i64\" | \"isize\" | \"pointer\";\n\n"; - stream << "export type NativeFunctionDefinition = {\n"; - stream << " readonly parameters: readonly NativeType[];\n"; - stream << " readonly result: NativeType;\n"; - stream << "};\n\n"; - stream << "type NativeValue = T extends \"pointer\"\n"; - stream << " ? PointerValue | null\n"; - stream << " : T extends \"i64\" | \"isize\"\n"; - stream << " ? bigint\n"; - stream << " : T extends \"i32\"\n"; - stream << " ? number\n"; - stream << " : void;\n\n"; - stream << "type NativeFunctionArguments = {\n"; - stream << " [Index in keyof Parameters]: NativeValue;\n"; - stream << "};\n\n"; - stream << "type NativeFunctionResult = NativeValue;\n\n"; - stream << "type NativeFunction = (\n"; - stream << " ...args: NativeFunctionArguments\n"; - stream << ") => NativeFunctionResult;\n\n"; - stream << "export type CallbackHandle = {\n"; - stream << " pointer: PointerValue;\n"; - stream << " close(): void;\n"; - stream << "};\n\n"; - stream << "export interface BindgenRuntimeAdapter {\n"; - stream << " loadLibrary(path: string | URL, symbols: Record): BindgenLibrary;\n"; - stream << " pointerOf(value: ArrayBuffer | ArrayBufferView): PointerValue | null;\n"; - stream << " readCString(pointer: PointerValue): string | null;\n"; - stream << " createCallback(\n"; - stream << " definition: Definition,\n"; - stream << " callback: (...args: NativeFunctionArguments) => " - "NativeFunctionResult,\n"; - stream << " ): CallbackHandle;\n"; - stream << "}\n\n"; - stream << "export interface BindgenHost {\n"; - stream << " pointerOf(value: ArrayBuffer | ArrayBufferView): PointerValue | null;\n"; - stream << " readCString(pointer: PointerValue): string | null;\n"; - stream << " createCallback(\n"; - stream << " definition: Definition,\n"; - stream << " callback: (...args: NativeFunctionArguments) => " - "NativeFunctionResult,\n"; - stream << " ): CallbackHandle;\n"; - stream << "}\n\n"; -} - -auto writeSymbols(std::ostream &stream) -> void -{ - stream << "export const symbols = {\n"; - - for (const auto &function : cpp_core::functionDescriptors()) - { - stream << " " << function.name << ": {\n"; - stream << " parameters: ["; - - for (std::size_t index = 0; index < function.parameters.size(); ++index) - { - if (index != 0) - { - stream << ", "; - } - - stream << '"' << toNativeParameterType(function.parameters[index].abi_kind) << '"'; - } - - stream << "],\n"; - stream << " result: \"" << toNativeResultType(function) << "\",\n"; - stream << " },\n"; - } - - stream << "} as const;\n\n"; - stream << "type SymbolDefinitions = typeof symbols;\n\n"; - stream << "export type BindgenSymbols = {\n"; - stream << " [Name in keyof SymbolDefinitions]: NativeFunction;\n"; - stream << "};\n\n"; - stream << "export interface BindgenLibrary {\n"; - stream << " symbols: BindgenSymbols;\n"; - stream << "}\n\n"; -} - -auto writeOperations(std::ostream &stream) -> void -{ - stream << "export const operations = {\n"; - for (const auto &operation : cpp_core::operationDescriptors()) - { - stream << " " << operation.name << ": { symbol: \"" << operation.function_name << "\" },\n"; - } - stream << "} as const;\n\n"; -} - -auto writeStatusCodes(std::ostream &stream) -> void -{ - stream << "export const statusCodes = {\n"; - for (const auto &descriptor : cpp_core::statusCodeDescriptors()) - { - stream << " " << descriptor.name << ": " << descriptor.value << ",\n"; - } - stream << "} as const;\n\n"; - - stream << "export const statusCodeInfo = {\n"; - for (const auto &descriptor : cpp_core::statusCodeDescriptors()) - { - stream << " \"" << descriptor.value << "\": { category: \"" << descriptor.category << "\", name: \"" - << descriptor.name << "\" },\n"; - } - stream << "} as const;\n\n"; -} - -auto writeHelpers(std::ostream &stream) -> void -{ - stream << "const textEncoder = new TextEncoder();\n\n"; - stream << "type KeepAlive = ArrayBufferView[];\n\n"; - stream << "function isArrayBufferView(value: unknown): value is ArrayBufferView {\n"; - stream << " return ArrayBuffer.isView(value);\n"; - stream << "}\n\n"; - stream << "function isArrayBuffer(value: unknown): value is ArrayBuffer {\n"; - stream << " return value instanceof ArrayBuffer;\n"; - stream << "}\n\n"; - stream << "function isPointerValue(value: unknown): value is PointerValue {\n"; - stream << " return value != null && !isArrayBufferView(value) && !isArrayBuffer(value) &&\n"; - stream << " (typeof value === \"number\" || typeof value === \"bigint\" || typeof value === \"object\");\n"; - stream << "}\n\n"; - stream << "function requirePointer(\n"; - stream << " host: BindgenHost,\n"; - stream << " value: PointerValue | ArrayBufferView | null | undefined,\n"; - stream << " name: string,\n"; - stream << " keepAlive?: KeepAlive,\n"; - stream << "): PointerValue {\n"; - stream << " if (isPointerValue(value)) {\n"; - stream << " return value;\n"; - stream << " }\n"; - stream << " if (value == null || !isArrayBufferView(value)) {\n"; - stream << " throw new TypeError(`${name} must be a PointerValue or ArrayBufferView`);\n"; - stream << " }\n"; - stream << " keepAlive?.push(value);\n"; - stream << " const pointer = host.pointerOf(value);\n"; - stream << " if (pointer === null) {\n"; - stream << " throw new TypeError(`Failed to get pointer for ${name}`);\n"; - stream << " }\n"; - stream << " return pointer;\n"; - stream << "}\n\n"; - stream << "function decodeCStringPointer(host: BindgenHost, value: PointerValue | null): string | null {\n"; - stream << " if (value === null) {\n"; - stream << " return null;\n"; - stream << " }\n"; - stream << " return host.readCString(value);\n"; - stream << "}\n\n"; - stream << "function marshalCString(\n"; - stream << " host: BindgenHost,\n"; - stream << " value: string | PointerValue,\n"; - stream << " name: string,\n"; - stream << " keepAlive: KeepAlive,\n"; - stream << "): PointerValue {\n"; - stream << " if (typeof value === \"string\") {\n"; - stream << " const bytes = textEncoder.encode(`${value}\\0`);\n"; - stream << " keepAlive.push(bytes);\n"; - stream << " return requirePointer(host, bytes, name, keepAlive);\n"; - stream << " }\n"; - stream << " return requirePointer(host, value, name);\n"; - stream << "}\n\n"; - stream << "function marshalConstBuffer(\n"; - stream << " host: BindgenHost,\n"; - stream << " value: string | ArrayBufferView | PointerValue,\n"; - stream << " name: string,\n"; - stream << " keepAlive: KeepAlive,\n"; - stream << "): PointerValue {\n"; - stream << " if (typeof value === \"string\") {\n"; - stream << " const bytes = textEncoder.encode(value);\n"; - stream << " keepAlive.push(bytes);\n"; - stream << " return requirePointer(host, bytes, name, keepAlive);\n"; - stream << " }\n"; - stream << " return requirePointer(host, value, name, keepAlive);\n"; - stream << "}\n\n"; - stream << "function marshalMutableBuffer(\n"; - stream << " host: BindgenHost,\n"; - stream << " value: ArrayBufferView | PointerValue,\n"; - stream << " name: string,\n"; - stream << " keepAlive: KeepAlive,\n"; - stream << "): PointerValue {\n"; - stream << " return requirePointer(host, value, name, keepAlive);\n"; - stream << "}\n\n"; - stream << "function toInt64(value: number | bigint): bigint {\n"; - stream << " return typeof value === \"bigint\" ? value : BigInt(value);\n"; - stream << "}\n\n"; - stream << "export class StatusCodeError extends Error {\n"; - stream << " readonly code: number;\n"; - stream << " readonly category: string;\n"; - stream << " readonly statusName: string;\n\n"; - stream << " constructor(code: number | bigint) {\n"; - stream << " const normalized = typeof code === \"bigint\" ? Number(code) : code;\n"; - stream << " const info = statusCodeInfo[String(normalized) as keyof typeof statusCodeInfo];\n"; - stream << " const category = info?.category ?? \"Unknown\";\n"; - stream << " const statusName = info?.name ?? \"UnknownStatus\";\n"; - stream << " super(`${category}::${statusName} (${normalized})`);\n"; - stream << " this.name = \"StatusCodeError\";\n"; - stream << " this.code = normalized;\n"; - stream << " this.category = category;\n"; - stream << " this.statusName = statusName;\n"; - stream << " }\n"; - stream << "}\n\n"; - stream << "function assertStatus(status: number | bigint): void {\n"; - stream << " const normalized = typeof status === \"bigint\" ? Number(status) : status;\n"; - stream << " if (normalized < 0) {\n"; - stream << " throw new StatusCodeError(normalized);\n"; - stream << " }\n"; - stream << "}\n\n"; -} - -auto writeCustomRuntimeAdapters(std::ostream &stream) -> void -{ - stream << "export function createHostFromRuntimeAdapter(adapter: BindgenRuntimeAdapter): BindgenHost {\n"; - stream << " return {\n"; - stream << " pointerOf(value) {\n"; - stream << " return adapter.pointerOf(value);\n"; - stream << " },\n"; - stream << " readCString(pointer) {\n"; - stream << " return adapter.readCString(pointer);\n"; - stream << " },\n"; - stream << " createCallback(definition, callback) {\n"; - stream << " return adapter.createCallback(definition, callback);\n"; - stream << " },\n"; - stream << " };\n"; - stream << "}\n\n"; - - stream << "export function loadLibraryFromRuntimeAdapter(\n"; - stream << " adapter: BindgenRuntimeAdapter,\n"; - stream << " path: string | URL,\n"; - stream << " definitions: Record = symbols,\n"; - stream << "): BindgenLibrary {\n"; - stream << " return adapter.loadLibrary(path, definitions);\n"; - stream << "}\n\n"; -} - -auto writeDenoRuntimeAdapters(std::ostream &stream) -> void -{ - stream << "function getDenoRuntime(): {\n"; - stream << " dlopen(path: string | URL, definitions: Record): BindgenLibrary & " - "{ close(): void };\n"; - stream << " UnsafePointer: { of(value: ArrayBuffer | ArrayBufferView): PointerValue | null };\n"; - stream << " UnsafePointerView: new (pointer: PointerValue) => { getCString(): string | null };\n"; - stream << " UnsafeCallback: new (\n"; - stream << " definition: NativeFunctionDefinition,\n"; - stream << " callback: (...args: unknown[]) => unknown,\n"; - stream << " ) => { pointer: PointerValue; close(): void };\n"; - stream << "} {\n"; - stream << " const runtime = (globalThis as typeof globalThis & { Deno?: unknown }).Deno;\n"; - stream << " if (!runtime) {\n"; - stream << " throw new Error(\"Deno runtime is not available.\");\n"; - stream << " }\n"; - stream << " return runtime as {\n"; - stream << " dlopen(path: string | URL, definitions: Record): BindgenLibrary & " - "{ close(): void };\n"; - stream << " UnsafePointer: { of(value: ArrayBuffer | ArrayBufferView): PointerValue | null };\n"; - stream << " UnsafePointerView: new (pointer: PointerValue) => { getCString(): string | null };\n"; - stream << " UnsafeCallback: new (\n"; - stream << " definition: NativeFunctionDefinition,\n"; - stream << " callback: (...args: unknown[]) => unknown,\n"; - stream << " ) => { pointer: PointerValue; close(): void };\n"; - stream << " };\n"; - stream << "}\n\n"; - - stream << "export function createDenoHost(): BindgenHost {\n"; - stream << " const deno = getDenoRuntime();\n"; - stream << " return {\n"; - stream << " pointerOf(value) {\n"; - stream << " return deno.UnsafePointer.of(value);\n"; - stream << " },\n"; - stream << " readCString(pointer) {\n"; - stream << " return new deno.UnsafePointerView(pointer).getCString();\n"; - stream << " },\n"; - stream << " createCallback(definition, callback) {\n"; - stream << " const unsafe = new deno.UnsafeCallback(definition, callback as (...args: unknown[]) => unknown);\n"; - stream << " return {\n"; - stream << " pointer: unsafe.pointer,\n"; - stream << " close() {\n"; - stream << " unsafe.close();\n"; - stream << " },\n"; - stream << " };\n"; - stream << " },\n"; - stream << " };\n"; - stream << "}\n\n"; - - stream << "export function loadDenoLibrary(\n"; - stream << " path: string | URL,\n"; - stream << " definitions: Record = symbols,\n"; - stream << "): BindgenLibrary & { close(): void } {\n"; - stream << " return getDenoRuntime().dlopen(path, definitions);\n"; - stream << "}\n\n"; -} - -auto writeBunRuntimeAdapters(std::ostream &stream) -> void -{ - stream << "type BunFfiModule = {\n"; - stream << " FFIType: {\n"; - stream << " void: unknown;\n"; - stream << " i32: unknown;\n"; - stream << " i64_fast: unknown;\n"; - stream << " ptr: unknown;\n"; - stream << " };\n"; - stream << " CString: new (pointer: number, byteOffset?: number, byteLength?: number) => { toString(): string };\n"; - stream << " JSCallback: new (\n"; - stream << " callback: (...args: unknown[]) => unknown,\n"; - stream << " options: { args: unknown[]; returns: unknown },\n"; - stream << " ) => { ptr: number | bigint; close(): void };\n"; - stream << " dlopen(\n"; - stream << " path: string,\n"; - stream << " symbols: Record,\n"; - stream << " ): BindgenLibrary & { close(): void };\n"; - stream << "};\n\n"; - - stream << "async function getBunFfiModule(): Promise {\n"; - stream << " const bun = (globalThis as typeof globalThis & { Bun?: unknown }).Bun;\n"; - stream << " if (!bun) {\n"; - stream << " throw new Error(\"Bun runtime is not available.\");\n"; - stream << " }\n"; - stream << " const specifier = \"bun:ffi\";\n"; - stream << " return await import(specifier) as BunFfiModule;\n"; - stream << "}\n\n"; - - stream << "function toBunFfiType(module: BunFfiModule, type: NativeType): unknown {\n"; - stream << " switch (type) {\n"; - stream << " case \"void\":\n"; - stream << " return module.FFIType.void;\n"; - stream << " case \"i32\":\n"; - stream << " return module.FFIType.i32;\n"; - stream << " case \"i64\":\n"; - stream << " case \"isize\":\n"; - stream << " return module.FFIType.i64_fast;\n"; - stream << " case \"pointer\":\n"; - stream << " return module.FFIType.ptr;\n"; - stream << " }\n"; - stream << "}\n\n"; - - stream << "export async function createBunHost(): Promise {\n"; - stream << " const ffi = await getBunFfiModule();\n"; - stream << " const bun = (globalThis as typeof globalThis & {\n"; - stream << " Bun?: { ptr(value: ArrayBuffer | ArrayBufferView): number | bigint };\n"; - stream << " }).Bun;\n"; - stream << " if (!bun) {\n"; - stream << " throw new Error(\"Bun runtime is not available.\");\n"; - stream << " }\n"; - stream << " return {\n"; - stream << " pointerOf(value) {\n"; - stream << " return bun.ptr(value);\n"; - stream << " },\n"; - stream << " readCString(pointer) {\n"; - stream << " return new ffi.CString(Number(pointer)).toString();\n"; - stream << " },\n"; - stream << " createCallback(definition, callback) {\n"; - stream << " const jsCallback = new ffi.JSCallback(callback as (...args: unknown[]) => unknown, {\n"; - stream << " args: definition.parameters.map((type) => toBunFfiType(ffi, type)),\n"; - stream << " returns: toBunFfiType(ffi, definition.result),\n"; - stream << " });\n"; - stream << " return {\n"; - stream << " pointer: jsCallback.ptr,\n"; - stream << " close() {\n"; - stream << " jsCallback.close();\n"; - stream << " },\n"; - stream << " };\n"; - stream << " },\n"; - stream << " };\n"; - stream << "}\n\n"; - - stream << "export async function loadBunLibrary(\n"; - stream << " path: string,\n"; - stream << " definitions: Record = symbols,\n"; - stream << "): Promise {\n"; - stream << " const ffi = await getBunFfiModule();\n"; - stream << " const ffiSymbols = Object.fromEntries(\n"; - stream << " Object.entries(definitions).map(([name, definition]) => [\n"; - stream << " name,\n"; - stream << " {\n"; - stream << " args: definition.parameters.map((type) => toBunFfiType(ffi, type)),\n"; - stream << " returns: toBunFfiType(ffi, definition.result),\n"; - stream << " },\n"; - stream << " ]),\n"; - stream << " );\n"; - stream << " return ffi.dlopen(path, ffiSymbols);\n"; - stream << "}\n\n"; -} - -auto writeRuntimeAdapters(std::ostream &stream, CliOptions::RuntimeProfile runtime) -> void -{ - writeCustomRuntimeAdapters(stream); - - if (runtime == CliOptions::RuntimeProfile::kDeno) - { - writeDenoRuntimeAdapters(stream); - } - else if (runtime == CliOptions::RuntimeProfile::kBun) - { - writeBunRuntimeAdapters(stream); - } -} - -auto writeErrorCallbackSupport(std::ostream &stream) -> void -{ - stream << "export const errorCallbackDefinition = {\n"; - stream << " parameters: [\"i32\", \"pointer\"],\n"; - stream << " result: \"void\",\n"; - stream << "} as const;\n\n"; - - stream << "export type RawErrorCallback = (error_code: number, message: PointerValue | null) => void;\n"; - stream << "export type ErrorCallback = (error_code: number, message: string | null) => void;\n\n"; - - stream << "export function createRawErrorCallback(host: BindgenHost, callback: RawErrorCallback): CallbackHandle " - "{\n"; - stream << " return host.createCallback(errorCallbackDefinition, callback);\n"; - stream << "}\n\n"; - - stream << "export function createErrorCallback(host: BindgenHost, callback: ErrorCallback): CallbackHandle {\n"; - stream << " return host.createCallback(errorCallbackDefinition, (error_code, message) => {\n"; - stream << " callback(error_code, decodeCStringPointer(host, message));\n"; - stream << " });\n"; - stream << "}\n\n"; -} - -auto writeWrapperBody(std::ostream &stream, const cpp_core::FunctionDescriptor &function) -> void -{ - const bool use_keep_alive = usesBufferMarshalling(function); - if (use_keep_alive) - { - stream << " const keepAlive: KeepAlive = [];\n"; - } - - for (const auto ¶meter : function.parameters) - { - const std::string source = sourceExpr(parameter); - switch (parameter.abi_kind) - { - case cpp_core::AbiValueKind::kUtf8CString: - stream << " const " << parameter.name << "Pointer = marshalCString(host, " << source << ", \"" - << parameter.name << "\", keepAlive);\n"; - break; - case cpp_core::AbiValueKind::kConstBuffer: - stream << " const " << parameter.name << "Pointer = marshalConstBuffer(host, " << source << ", \"" - << parameter.name << "\", keepAlive);\n"; - break; - case cpp_core::AbiValueKind::kMutableBuffer: - stream << " const " << parameter.name - << "Pointer = marshalMutableBuffer(host, " << source << ", \"" << parameter.name - << "\", keepAlive);\n"; - break; - case cpp_core::AbiValueKind::kOpaquePointer: - case cpp_core::AbiValueKind::kVersionStructPointer: - stream << " const " << parameter.name << "Pointer = requirePointer(host, " << source << ", \"" - << parameter.name << "\", keepAlive);\n"; - break; - case cpp_core::AbiValueKind::kInt64: - case cpp_core::AbiValueKind::kOpaqueHandle: - stream << " const " << parameter.name << "Value = toInt64(" << source << ");\n"; - break; - default: - break; - } - } - - stream << " const rawResult = dylib.symbols." << function.name << "("; - for (std::size_t index = 0; index < function.parameters.size(); ++index) - { - if (index != 0) - { - stream << ", "; - } - - const auto ¶meter = function.parameters[index]; - switch (parameter.abi_kind) - { - case cpp_core::AbiValueKind::kUtf8CString: - case cpp_core::AbiValueKind::kConstBuffer: - case cpp_core::AbiValueKind::kMutableBuffer: - case cpp_core::AbiValueKind::kOpaquePointer: - case cpp_core::AbiValueKind::kVersionStructPointer: - stream << parameter.name << "Pointer"; - break; - case cpp_core::AbiValueKind::kInt64: - case cpp_core::AbiValueKind::kOpaqueHandle: - stream << parameter.name << "Value"; - break; - default: - stream << sourceExpr(parameter); - break; - } - } - stream << ");\n"; - - if (use_keep_alive) - { - stream << " void keepAlive;\n"; - } - - switch (function.result_model) - { - case cpp_core::FunctionResultModel::kVoid: - stream << " return rawResult;\n"; - break; - case cpp_core::FunctionResultModel::kStatusCode: - stream << " assertStatus(rawResult);\n"; - stream << " return;\n"; - break; - case cpp_core::FunctionResultModel::kValueOrStatus: - case cpp_core::FunctionResultModel::kHandleOrStatus: - stream << " assertStatus(rawResult);\n"; - stream << " return rawResult;\n"; - break; - } -} - -auto writeWrapperSignature(std::ostream &stream, const cpp_core::FunctionDescriptor &function) -> void -{ - stream << " " << function.name << "("; - - for (std::size_t index = 0; index < function.parameters.size(); ++index) - { - if (index != 0) - { - stream << ", "; - } - - const auto ¶meter = function.parameters[index]; - stream << parameter.name << ": " << toTsParameterType(parameter); - if (const auto default_value = optionalParameterDefault(parameter)) - { - stream << " = " << *default_value; - } - } - - stream << "): " << toWrapperReturnType(function); -} - -auto writeWrapperFunctions(std::ostream &stream) -> void -{ - stream << "export function createBindings(host: BindgenHost, dylib: BindgenLibrary) {\n"; - stream << " return {\n"; - - for (const auto &function : cpp_core::functionDescriptors()) - { - writeWrapperSignature(stream, function); - stream << " {\n"; - writeWrapperBody(stream, function); - stream << " },\n"; - } - - stream << " };\n"; - stream << "}\n\n"; - stream << "export type GeneratedBindings = ReturnType;\n"; -} - -auto writeModule(std::ostream &stream, CliOptions::RuntimeProfile runtime) -> void -{ - writeHeader(stream, runtime); - writeHostTypes(stream); - writeSymbols(stream); - writeOperations(stream); - writeStatusCodes(stream); - writeHelpers(stream); - writeRuntimeAdapters(stream, runtime); - writeErrorCallbackSupport(stream); - writeWrapperFunctions(stream); -} - -} // namespace - -auto main(int argc, char **argv) -> int -{ - CliOptions options; - if (!parseArgs(argc, argv, options)) - { - printUsage(std::cerr); - return 1; - } - - if (options.show_help) - { - printUsage(std::cout); - return 0; - } - - if (options.output_path.has_value()) - { - std::ofstream output(*options.output_path); - if (!output) - { - std::cerr << "Failed to open output path: " << *options.output_path << '\n'; - return 1; - } - - writeModule(output, options.runtime); - return 0; - } - - writeModule(std::cout, options.runtime); - return 0; -} From 64d3d47063e7ba00d3373deff7dd3a04e9b086b5 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 15 Jun 2026 20:04:24 +0200 Subject: [PATCH 17/28] feat: implement GCC reflection support and enhance SerialConfig validation --- CMakeLists.txt | 36 ++++++++------ README.md | 15 +++++- include/cpp_core.h | 1 + include/cpp_core/reflection.hpp | 75 ++++++++++++++++++++++++++++++ include/cpp_core/result.hpp | 49 ++++++++++--------- include/cpp_core/scope_guard.hpp | 21 ++------- include/cpp_core/serial_config.hpp | 71 ++++++++++++++++++++++++---- include/cpp_core/strong_types.hpp | 41 +++++++++++++++- tests/helper_types.test.cpp | 53 +++++++++++++++++++++ tests/reflection.test.cpp | 28 +++++++++++ tests/result.test.cpp | 54 +++++++++++++++++++++ 11 files changed, 378 insertions(+), 66 deletions(-) create mode 100644 include/cpp_core/reflection.hpp create mode 100644 tests/helper_types.test.cpp create mode 100644 tests/reflection.test.cpp create mode 100644 tests/result.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f737df0..f60a0f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,14 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_MODULE_STD 26) set(CMAKE_CXX_MODULE_EXTENSIONS OFF) +if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + message(FATAL_ERROR "cpp-core currently requires GCC 16.1+ with the GCC std::meta reflection implementation.") +endif() + +if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.1) + message(FATAL_ERROR "cpp-core currently requires GCC 16.1+.") +endif() + include(cmake/CPM.cmake) CPMAddPackage( @@ -35,20 +43,18 @@ add_library(cpp_core::cpp_core ALIAS cpp_core) add_library(cpp_core_strict_warnings INTERFACE) -if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - target_compile_options( - cpp_core_strict_warnings - INTERFACE - -Wall - -Wextra - -Wpedantic - -Wconversion - -Wsign-conversion - -Wshadow - -Wundef - -Werror - ) -endif() +target_compile_options( + cpp_core_strict_warnings + INTERFACE + -Wall + -Wextra + -Wpedantic + -Wconversion + -Wsign-conversion + -Wshadow + -Wundef + -Werror +) # Public include directories target_include_directories( @@ -58,6 +64,8 @@ target_include_directories( $ ) +target_compile_options(cpp_core INTERFACE -freflection) + target_compile_features(cpp_core INTERFACE cxx_std_26) include(CTest) diff --git a/README.md b/README.md index a796d81..565f465 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,10 @@ This repository does not provide a ready-to-load shared library by itself. It pr `cpp-core` is the canonical definition of the current API line. -- Supported Linux toolchain: modern C++26-capable toolchain -- Supported Windows toolchain: modern C++26-capable toolchain +- 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+ @@ -20,6 +22,7 @@ This repository does not provide a ready-to-load shared library by itself. It pr ## What It Provides - 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` @@ -108,6 +111,14 @@ MODULE_API auto serialOpen( 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`. diff --git a/include/cpp_core.h b/include/cpp_core.h index 9475aa5..fc7fc73 100644 --- a/include/cpp_core.h +++ b/include/cpp_core.h @@ -3,6 +3,7 @@ #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" diff --git a/include/cpp_core/reflection.hpp b/include/cpp_core/reflection.hpp new file mode 100644 index 0000000..6394c1c --- /dev/null +++ b/include/cpp_core/reflection.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace cpp_core::reflection +{ + +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/result.hpp b/include/cpp_core/result.hpp index 0d0cf59..eba0a72 100644 --- a/include/cpp_core/result.hpp +++ b/include/cpp_core/result.hpp @@ -21,7 +21,7 @@ struct Error { } - Error(StatusCodeValue 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)) { } @@ -70,7 +70,7 @@ template [[nodiscard]] constexpr auto fail(StatusCodeValue c return std::unexpected(Error{code}); } -template [[nodiscard]] auto fail(StatusCodeValue code, std::string message) -> Result +template [[nodiscard]] constexpr auto fail(StatusCodeValue code, std::string message) -> Result { return std::unexpected(Error{code, std::move(message)}); } @@ -85,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/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_config.hpp b/include/cpp_core/serial_config.hpp index 3c8b4cd..b695c9c 100644 --- a/include/cpp_core/serial_config.hpp +++ b/include/cpp_core/serial_config.hpp @@ -1,9 +1,11 @@ #pragma once +#include "result.hpp" #include "strong_types.hpp" #include #include +#include #include namespace cpp_core @@ -14,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 /** @@ -52,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/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/tests/helper_types.test.cpp b/tests/helper_types.test.cpp new file mode 100644 index 0000000..bd85662 --- /dev/null +++ b/tests/helper_types.test.cpp @@ -0,0 +1,53 @@ +#include "cpp_core/reflection.hpp" +#include "cpp_core/serial_config.hpp" +#include "cpp_core/strong_types.hpp" + +namespace cpp_core::tests::helper_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); + +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::helper_types diff --git a/tests/reflection.test.cpp b/tests/reflection.test.cpp new file mode 100644 index 0000000..1967f84 --- /dev/null +++ b/tests/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/tests/result.test.cpp b/tests/result.test.cpp new file mode 100644 index 0000000..ed17539 --- /dev/null +++ b/tests/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 From 7332e82ff27bf63535d52f2fef0845857e760150 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 15 Jun 2026 20:20:12 +0200 Subject: [PATCH 18/28] feat: add GitHub Actions workflow for compile checks --- ...nd_bindgen_smoke.yml => compile_checks.yml} | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) rename .github/workflows/{build_compile_tests_and_bindgen_smoke.yml => compile_checks.yml} (70%) diff --git a/.github/workflows/build_compile_tests_and_bindgen_smoke.yml b/.github/workflows/compile_checks.yml similarity index 70% rename from .github/workflows/build_compile_tests_and_bindgen_smoke.yml rename to .github/workflows/compile_checks.yml index ba6a1c3..ec46deb 100644 --- a/.github/workflows/build_compile_tests_and_bindgen_smoke.yml +++ b/.github/workflows/compile_checks.yml @@ -1,4 +1,4 @@ -name: 'Build Compile Tests And Bindgen Smoke' +name: 'Compile Checks' on: push: @@ -8,23 +8,23 @@ on: workflow_dispatch: jobs: - build-and-test: - name: 'Build compile tests and bindgen smoke' + compile_checks: + name: 'Compile checks' runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v6 - - name: Install GCC 16 + Ninja + - 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 + sudo apt-get install -y gcc-16 g++-16 ninja-build cmake - - name: Show tool versions + - name: Show toolchain versions run: | cmake --version ninja --version @@ -38,14 +38,10 @@ jobs: -DCMAKE_C_COMPILER=gcc-16 \ -DCMAKE_CXX_COMPILER=g++-16 - - name: Build compile tests + - name: Build compile checks run: | cmake --build build/ci --target cpp_core_compile_tests - - name: Run bindgen smoke test - run: | - cmake --build build/ci --target cpp_core_bindgen_smoke - - name: Run CTest if registered tests exist run: | ctest --test-dir build/ci --output-on-failure --no-tests=ignore From 79d96648751e8e417561e9b09d859e3d47fb6804 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Jun 2026 20:16:54 +0200 Subject: [PATCH 19/28] feat: add support for optional FFI AST export with clang++ --- CMakeLists.txt | 72 +++++++++++++++++++++++++++++++++++++ README.md | 12 +++++++ cmake/export_ast_json.cmake | 50 ++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 cmake/export_ast_json.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index f60a0f5..f5e54aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,20 @@ 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" +) + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU") message(FATAL_ERROR "cpp-core currently requires GCC 16.1+ with the GCC std::meta reflection implementation.") endif() @@ -77,6 +91,64 @@ if(BUILD_TESTING) target_link_libraries(cpp_core_compile_tests PRIVATE cpp_core_strict_warnings) endif() +if(CPP_CORE_ENABLE_AST_EXPORT) + 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) + + 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_target(cpp_core_ast_json DEPENDS "${CPP_CORE_AST_JSON_OUTPUT}") +endif() + # Install rules -------------------------------------------------------------- include(GNUInstallDirs) diff --git a/README.md b/README.md index 565f465..3b3e2d9 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,18 @@ The CMake project exports the package target and also builds these relevant targ - `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_json +``` + +- The project still configures with GCC; the AST export itself invokes `clang++` separately +- Requires `clang++` on `PATH` or `-DCPP_CORE_AST_CLANGXX=/path/to/clang++` +- Writes the combined FFI header AST JSON to `build/ast/cpp_core_ffi_ast.json` +- Exports the `#include ` surface intended for downstream FFI adapter generation + ## ABI Surface The main aggregated interface lives in: 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() From f5fdaa98128bd62f0e6473d8ad6a75fe0aee2aa7 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Jun 2026 20:44:16 +0200 Subject: [PATCH 20/28] feat: add slim JSON output for FFI API metadata and update CMake configuration --- CMakeLists.txt | 37 +++- README.md | 3 + tools/clang_ast_to_ffi_json.py | 349 +++++++++++++++++++++++++++++++++ 3 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 tools/clang_ast_to_ffi_json.py diff --git a/CMakeLists.txt b/CMakeLists.txt index f5e54aa..fbff341 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,12 @@ set( 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" +) if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU") message(FATAL_ERROR "cpp-core currently requires GCC 16.1+ with the GCC std::meta reflection implementation.") @@ -92,6 +98,8 @@ if(BUILD_TESTING) endif() if(CPP_CORE_ENABLE_AST_EXPORT) + find_package(Python3 COMPONENTS Interpreter REQUIRED) + file( GLOB _cpp_core_ast_interface_headers CONFIGURE_DEPENDS @@ -123,6 +131,7 @@ if(CPP_CORE_ENABLE_AST_EXPORT) 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 @@ -146,7 +155,25 @@ if(CPP_CORE_ENABLE_AST_EXPORT) VERBATIM ) - add_custom_target(cpp_core_ast_json DEPENDS "${CPP_CORE_AST_JSON_OUTPUT}") + 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_full_json DEPENDS "${CPP_CORE_AST_JSON_OUTPUT}") + add_custom_target(cpp_core_ast_json DEPENDS "${CPP_CORE_AST_SLIM_JSON_OUTPUT}") endif() # Install rules -------------------------------------------------------------- @@ -162,6 +189,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/README.md b/README.md index 3b3e2d9..29a0d1f 100644 --- a/README.md +++ b/README.md @@ -94,8 +94,11 @@ cmake --build build --target cpp_core_ast_json ``` - The project still configures with GCC; the AST export itself invokes `clang++` separately +- `cpp_core_ast_full_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 diff --git a/tools/clang_ast_to_ffi_json.py b/tools/clang_ast_to_ffi_json.py new file mode 100644 index 0000000..0bef2fa --- /dev/null +++ b/tools/clang_ast_to_ffi_json.py @@ -0,0 +1,349 @@ +#!/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: + match = re.match(r"^(?P.+?)\s*\(\*\)\((?P.*)\)$", type_name) + if not match: + return None + + params_text = match.group("params").strip() + params = [] if not params_text or params_text == "void" else split_top_level(params_text) + return { + "return_type": 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 + parameter["optional"] = True + else: + parameter["optional"] = False + + 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, public_header: str) -> 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"]), + "public_header": public_header, + "declared_in": normalize_header_path(declaration_file(node), source_root), + "return_type": 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, args.public_header) + 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", + "schema_version": 1, + "public_header": 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()) From b1d73624b75100c4cc207321a37351ded56e4d02 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Jun 2026 21:53:58 +0200 Subject: [PATCH 21/28] refactor: remove GCC version checks from CMakeLists.txt --- CMakeLists.txt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fbff341..813f900 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,14 +31,6 @@ set( "Output path for the reduced FFI API JSON metadata" ) -if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - message(FATAL_ERROR "cpp-core currently requires GCC 16.1+ with the GCC std::meta reflection implementation.") -endif() - -if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.1) - message(FATAL_ERROR "cpp-core currently requires GCC 16.1+.") -endif() - include(cmake/CPM.cmake) CPMAddPackage( From 51525a651f685fd113d21e6c47710ec83dea8902 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Jun 2026 22:23:44 +0200 Subject: [PATCH 22/28] refactor: standardize JSON key naming in FFI API metadata --- tools/clang_ast_to_ffi_json.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tools/clang_ast_to_ffi_json.py b/tools/clang_ast_to_ffi_json.py index 0bef2fa..e7b95fc 100644 --- a/tools/clang_ast_to_ffi_json.py +++ b/tools/clang_ast_to_ffi_json.py @@ -142,7 +142,7 @@ def parse_function_pointer(type_name: str) -> dict[str, Any] | None: params_text = match.group("params").strip() params = [] if not params_text or params_text == "void" else split_top_level(params_text) return { - "return_type": match.group("return_type").strip(), + "returnType": match.group("return_type").strip(), "parameters": params, } @@ -273,9 +273,6 @@ def build_parameter(node: dict[str, Any], doc_params: dict[str, Any]) -> dict[st default_value = extract_default_expr(default_expr) if default_value is not None: parameter["default"] = default_value - parameter["optional"] = True - else: - parameter["optional"] = False callback = parse_function_pointer(callback_source_type) if callback: @@ -288,7 +285,7 @@ def build_parameter(node: dict[str, Any], doc_params: dict[str, Any]) -> dict[st return parameter -def build_function(node: dict[str, Any], source_root: Path, public_header: str) -> dict[str, Any]: +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", {}) @@ -296,9 +293,8 @@ def build_function(node: dict[str, Any], source_root: Path, public_header: str) function: dict[str, Any] = { "name": node["name"], "symbol": node.get("mangledName", node["name"]), - "public_header": public_header, - "declared_in": normalize_header_path(declaration_file(node), source_root), - "return_type": extract_return_type(type_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", []) @@ -324,7 +320,7 @@ def main() -> int: ast = json.load(input_file) functions = [ - build_function(node, source_root, args.public_header) + build_function(node, source_root) for node in walk(ast) if is_exported_function(node, source_root) ] @@ -332,8 +328,8 @@ def main() -> int: metadata = { "schema": "cpp_core_ffi_api", - "schema_version": 1, - "public_header": args.public_header, + "schemaVersion": 1, + "publicHeader": args.public_header, "functions": functions, } From 438c055598dde512d8278f8d3c10ba4f403f8520 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Jun 2026 22:51:03 +0200 Subject: [PATCH 23/28] docs: update README to reflect current FFI generation workflow with clang++ --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 29a0d1f..98c303b 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ cmake --build build --target cpp_core_ast_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_full_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 From e1b78f2230a1b44aa231ab97a0b2f9b80688fbfb Mon Sep 17 00:00:00 2001 From: Katze719 <38188106+Katze719@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:21:14 +0200 Subject: [PATCH 24/28] Update .github/workflows/compile_checks.yml Co-authored-by: Mqx <62719703+Mqxx@users.noreply.github.com> --- .github/workflows/compile_checks.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/compile_checks.yml b/.github/workflows/compile_checks.yml index ec46deb..82ad73e 100644 --- a/.github/workflows/compile_checks.yml +++ b/.github/workflows/compile_checks.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-24.04 steps: - - name: Checkout + - name: 'Checkout repository' uses: actions/checkout@v6 - - name: Install GCC 16.1 toolchain + - name: 'Install GCC 16.1 toolchain' run: | sudo apt-get update -qq sudo apt-get install -y software-properties-common @@ -24,24 +24,24 @@ jobs: sudo apt-get update -qq sudo apt-get install -y gcc-16 g++-16 ninja-build cmake - - name: Show toolchain versions + - name: 'Show toolchain versions' run: | cmake --version ninja --version gcc-16 --version g++-16 --version - - name: Configure + - 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 + - name: 'Build compile checks' run: | cmake --build build/ci --target cpp_core_compile_tests - - name: Run CTest if registered tests exist + - name: 'Run CTest if registered tests exist' run: | ctest --test-dir build/ci --output-on-failure --no-tests=ignore From eb926641839807ba401d0c82abc9cc2d5158cde2 Mon Sep 17 00:00:00 2001 From: Katze719 <38188106+Katze719@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:21:27 +0200 Subject: [PATCH 25/28] Update .github/workflows/doxygen.yml Co-authored-by: Mqx <62719703+Mqxx@users.noreply.github.com> --- .github/workflows/doxygen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index 7d990a2..8dd35e5 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -19,7 +19,7 @@ permissions: jobs: build-docs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: 'Build Doxygen HTML' steps: - name: Checkout From 52cc43e72518d4ced17d52855a3527827958d34f Mon Sep 17 00:00:00 2001 From: Katze719 <38188106+Katze719@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:21:43 +0200 Subject: [PATCH 26/28] Update .github/workflows/doxygen.yml Co-authored-by: Mqx <62719703+Mqxx@users.noreply.github.com> --- .github/workflows/doxygen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index 8dd35e5..c36c8f7 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-24.04 name: 'Build Doxygen HTML' steps: - - name: Checkout + - name: 'Checkout repository' uses: actions/checkout@v6 - name: Install Doxygen + Graphviz From 1cf05d7f4ee209d1c1f7d332f0bb1bc9f238df0c Mon Sep 17 00:00:00 2001 From: Katze719 <38188106+Katze719@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:21:57 +0200 Subject: [PATCH 27/28] Update .github/workflows/doxygen.yml Co-authored-by: Mqx <62719703+Mqxx@users.noreply.github.com> --- .github/workflows/doxygen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index c36c8f7..52c6239 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -25,7 +25,7 @@ jobs: - 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 From 8e8bfa83cfc8fb708a1deeddee8ddc1bf8655265 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 17 Jun 2026 20:15:01 +0200 Subject: [PATCH 28/28] refactor: update CI workflows and improve test coverage for reflection and result modules --- .github/workflows/doxygen.yml | 12 ++++++------ CMakeLists.txt | 10 +++++++--- README.md | 6 +++--- include/cpp_core.h | 8 ++++++++ include/cpp_core/reflection.hpp | 1 + {tests => include/cpp_core}/reflection.test.cpp | 0 {tests => include/cpp_core}/result.test.cpp | 0 .../cpp_core/serial_config.test.cpp | 12 ++---------- {tests => include/cpp_core}/status_code.test.cpp | 10 ---------- include/cpp_core/strong_types.test.cpp | 13 +++++++++++++ tools/clang_ast_to_ffi_json.py | 8 ++++---- 11 files changed, 44 insertions(+), 36 deletions(-) rename {tests => include/cpp_core}/reflection.test.cpp (100%) rename {tests => include/cpp_core}/result.test.cpp (100%) rename tests/helper_types.test.cpp => include/cpp_core/serial_config.test.cpp (79%) rename {tests => include/cpp_core}/status_code.test.cpp (91%) create mode 100644 include/cpp_core/strong_types.test.cpp diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index 52c6239..1c2da48 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -30,24 +30,24 @@ jobs: 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 813f900..e570faf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,7 +83,11 @@ target_compile_features(cpp_core INTERFACE cxx_std_26) include(CTest) if(BUILD_TESTING) - file(GLOB CPP_CORE_COMPILE_TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.test.cpp") + 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) @@ -164,8 +168,8 @@ if(CPP_CORE_ENABLE_AST_EXPORT) VERBATIM ) - add_custom_target(cpp_core_ast_full_json DEPENDS "${CPP_CORE_AST_JSON_OUTPUT}") - add_custom_target(cpp_core_ast_json DEPENDS "${CPP_CORE_AST_SLIM_JSON_OUTPUT}") + 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 -------------------------------------------------------------- diff --git a/README.md b/README.md index 98c303b..e6415f5 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Consume `cpp-core` as a CMake dependency from another project: CPMAddPackage( NAME cpp_core GITHUB_REPOSITORY Serial-IO/cpp-core - GIT_TAG v1.1.0 + GIT_TAG vX.Y.Z ) target_link_libraries(my_binding PRIVATE cpp_core::cpp_core) @@ -90,12 +90,12 @@ 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_json +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_full_json` builds only the raw clang AST dump +- `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` diff --git a/include/cpp_core.h b/include/cpp_core.h index fc7fc73..e25e632 100644 --- a/include/cpp_core.h +++ b/include/cpp_core.h @@ -1,5 +1,13 @@ #pragma once +/* + * Umbrella header for the complete public cpp_core surface. + * + * 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" diff --git a/include/cpp_core/reflection.hpp b/include/cpp_core/reflection.hpp index 6394c1c..767746e 100644 --- a/include/cpp_core/reflection.hpp +++ b/include/cpp_core/reflection.hpp @@ -10,6 +10,7 @@ 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; diff --git a/tests/reflection.test.cpp b/include/cpp_core/reflection.test.cpp similarity index 100% rename from tests/reflection.test.cpp rename to include/cpp_core/reflection.test.cpp diff --git a/tests/result.test.cpp b/include/cpp_core/result.test.cpp similarity index 100% rename from tests/result.test.cpp rename to include/cpp_core/result.test.cpp diff --git a/tests/helper_types.test.cpp b/include/cpp_core/serial_config.test.cpp similarity index 79% rename from tests/helper_types.test.cpp rename to include/cpp_core/serial_config.test.cpp index bd85662..fe86b4d 100644 --- a/tests/helper_types.test.cpp +++ b/include/cpp_core/serial_config.test.cpp @@ -1,17 +1,9 @@ #include "cpp_core/reflection.hpp" #include "cpp_core/serial_config.hpp" -#include "cpp_core/strong_types.hpp" -namespace cpp_core::tests::helper_types +namespace cpp_core::tests::serial_config { -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); - constexpr auto kCompileTimeConfig = SerialConfig::make<115'200, 8, Parity::kEven, StopBits::kTwo>(); static_assert(kCompileTimeConfig.isValid()); static_assert(kCompileTimeConfig.baudrateValue() == Baudrate{115'200}); @@ -50,4 +42,4 @@ 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::helper_types +} // namespace cpp_core::tests::serial_config diff --git a/tests/status_code.test.cpp b/include/cpp_core/status_code.test.cpp similarity index 91% rename from tests/status_code.test.cpp rename to include/cpp_core/status_code.test.cpp index a1cc45c..5333a50 100644 --- a/tests/status_code.test.cpp +++ b/include/cpp_core/status_code.test.cpp @@ -59,25 +59,15 @@ static_assert(cpp_core::StatusCode::belongsTo( cpp_core::StatusCode::Configuration::kSetBaudrateError)); -// Formula: result == -(kCategoryCode * 100 + LocalCode) 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.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/tools/clang_ast_to_ffi_json.py b/tools/clang_ast_to_ffi_json.py index e7b95fc..699e155 100644 --- a/tools/clang_ast_to_ffi_json.py +++ b/tools/clang_ast_to_ffi_json.py @@ -135,14 +135,14 @@ def split_top_level(text: str) -> list[str]: def parse_function_pointer(type_name: str) -> dict[str, Any] | None: - match = re.match(r"^(?P.+?)\s*\(\*\)\((?P.*)\)$", type_name) - if not match: + signature_match = re.match(r"^(?P.+?)\s*\(\*\)\((?P.*)\)$", type_name) + if not signature_match: return None - params_text = match.group("params").strip() + params_text = signature_match.group("params").strip() params = [] if not params_text or params_text == "void" else split_top_level(params_text) return { - "returnType": match.group("return_type").strip(), + "returnType": signature_match.group("return_type").strip(), "parameters": params, }