diff --git a/.github/workflows/boost-asio.yml b/.github/workflows/boost-asio.yml index 973d50db7f..016f0890c8 100644 --- a/.github/workflows/boost-asio.yml +++ b/.github/workflows/boost-asio.yml @@ -26,7 +26,6 @@ jobs: - { name: clang, version: 17 } - { name: clang, version: 18 } - { name: clang, version: 20 } - - { name: gcc, version: 12 } - { name: gcc, version: 13 } - { name: gcc, version: 14 } boost_version: [1.88.0, 1.73.0] diff --git a/.github/workflows/gcc-erlang.yml b/.github/workflows/gcc-erlang.yml index d4a81d91e8..a9b701575c 100644 --- a/.github/workflows/gcc-erlang.yml +++ b/.github/workflows/gcc-erlang.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - gcc: [12, 13, 14] + gcc: [13, 14] build_type: [Debug] std: [23] diff --git a/.github/workflows/gcc-x86.yml b/.github/workflows/gcc-x86.yml index 8e0c3b1f98..2eeca7f0b2 100644 --- a/.github/workflows/gcc-x86.yml +++ b/.github/workflows/gcc-x86.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - gcc: [12, 13, 14] + gcc: [13, 14] build_type: [Debug] std: [23] diff --git a/.github/workflows/gcc.yml b/.github/workflows/gcc.yml index 92bb390d54..c90c2a9e2d 100644 --- a/.github/workflows/gcc.yml +++ b/.github/workflows/gcc.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - gcc: [12, 13, 14] + gcc: [13, 14] build_type: [Debug] std: [23] diff --git a/.github/workflows/modules.yml b/.github/workflows/modules.yml new file mode 100644 index 0000000000..bfc68cd958 --- /dev/null +++ b/.github/workflows/modules.yml @@ -0,0 +1,112 @@ +name: C++20 Modules + +on: + push: + branches: + - main + - feature/* + paths-ignore: + - '**/*.md' + - 'docs/**' + pull_request: + branches: + - main + paths-ignore: + - '**/*.md' + - 'docs/**' + workflow_dispatch: + +jobs: + clang-modules: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + clang: [20] + build_type: [Release] + std: [23] + + env: + CC: clang-${{matrix.clang}} + CXX: clang++-${{matrix.clang}} + LLVM_VERSION: ${{matrix.clang}} + + steps: + - uses: actions/checkout@v4 + + - name: Install Clang and Ninja + run: | + wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - + echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-${{matrix.clang}} main" | sudo tee /etc/apt/sources.list.d/llvm.list + sudo apt-get clean + sudo apt-get update + sudo apt-get install -y --fix-missing clang-${{matrix.clang}} libc++-${{matrix.clang}}-dev libc++abi-${{matrix.clang}}-dev ninja-build + + - name: Set path for clang + run: | + echo "PATH=/usr/lib/llvm-${{matrix.clang}}/bin:$PATH" >> $GITHUB_ENV + clang-${{matrix.clang}} --version + clang++-${{matrix.clang}} --version + + - name: Configure CMake with Modules + run: | + cmake -B ${{github.workspace}}/build -G Ninja \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DCMAKE_CXX_STANDARD=${{matrix.std}} \ + -DCMAKE_C_COMPILER=${{env.CC}} \ + -DCMAKE_CXX_COMPILER=${{env.CXX}} \ + -DCMAKE_CXX_FLAGS="-stdlib=libc++" \ + -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++ -lc++abi" \ + -Dglaze_BUILD_MODULES=ON + + - name: Build Module Tests + run: cmake --build build --target json_module_test -j $(nproc) + + - name: Run Module Tests + run: ./build/tests/module_test/json_module_test + + gcc-modules: + runs-on: ubuntu-24.04 + container: + image: gcc:15 + + strategy: + fail-fast: false + matrix: + build_type: [Release] + std: [23] + + env: + CC: gcc + CXX: g++ + + steps: + - name: Install build dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: | + apt-get update + apt-get install -y --no-install-recommends \ + ca-certificates \ + cmake \ + git \ + libssl-dev \ + ninja-build \ + python3 + update-ca-certificates + + - uses: actions/checkout@v4 + + - name: Configure CMake with Modules + run: | + cmake -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DCMAKE_CXX_STANDARD=${{matrix.std}} \ + -Dglaze_BUILD_MODULES=ON + + - name: Build Module Tests + run: cmake --build build --target json_module_test -j $(nproc) + + - name: Run Module Tests + run: ./build/tests/module_test/json_module_test diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a4ebbce2f..f2a6c9e330 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,53 @@ target_include_directories( INTERFACE "$" ) +option(glaze_BUILD_MODULES "Build C++20 module targets (requires CMake 3.28+)" OFF) + +if(glaze_BUILD_MODULES) + message(STATUS "Building with C++20 module support") + + if(CMAKE_VERSION VERSION_LESS "3.28") + message(FATAL_ERROR "glaze_BUILD_MODULES requires CMake 3.28 or later") + endif() + + # Compiler compatibility warnings + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "15") + message(WARNING "GCC 15+ recommended for C++20 modules") + endif() + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_CXX_COMPILER_ID MATCHES "AppleClang") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "20") + message(WARNING "Clang 20+ recommended for C++20 modules") + endif() + elseif(CMAKE_CXX_COMPILER_ID MATCHES "AppleClang") + message(WARNING "Apple Clang does not support CMake's module dependency scanning. Use LLVM Clang instead.") + endif() + + # JSON module target + add_library(glaze_json_module) + add_library(glaze::json_module ALIAS glaze_json_module) + + target_sources(glaze_json_module + PUBLIC + FILE_SET CXX_MODULES + BASE_DIRS + "$" + "$" + FILES "${PROJECT_SOURCE_DIR}/include/glaze/module/glaze_json.cppm" + ) + + target_compile_features(glaze_json_module PUBLIC cxx_std_23) + target_include_directories(glaze_json_module + PUBLIC "$" + ) + target_link_libraries(glaze_json_module PUBLIC glaze::glaze) + + set_target_properties(glaze_json_module PROPERTIES + EXPORT_NAME json_module + CXX_SCAN_FOR_MODULES ON + ) +endif() + if(NOT CMAKE_SKIP_INSTALL_RULES) include(cmake/install-rules.cmake) endif() diff --git a/cmake/install-rules.cmake b/cmake/install-rules.cmake index 1db5b74efa..f53afc0dd4 100644 --- a/cmake/install-rules.cmake +++ b/cmake/install-rules.cmake @@ -19,6 +19,16 @@ install( INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" ) +# Install module targets when modules are enabled +if(glaze_BUILD_MODULES) + install( + TARGETS glaze_json_module + EXPORT glazeTargets + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + FILE_SET CXX_MODULES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + ) +endif() + write_basic_package_version_file( "${package}ConfigVersion.cmake" COMPATIBILITY SameMajorVersion diff --git a/docs/installation.md b/docs/installation.md index 6606bd6082..5a3d65c65b 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -7,7 +7,7 @@ This guide covers some of the ways to install and integrate the Glaze JSON libra ### Compiler Support - **C++23** standard required - **Clang 17+** -- **GCC 12+** +- **GCC 13+** - **MSVC 2022+** - **Apple Clang (latest Xcode)** @@ -165,6 +165,117 @@ For cross-compilation to ARM or other architectures: set(glaze_ENABLE_AVX2 OFF) ``` +## C++20 Module Support + +Glaze provides opt-in C++20 module wrappers for cleaner imports and potentially faster compile times. + +### Requirements + +- **CMake 3.28+** (for `FILE_SET CXX_MODULES` support) +- **Ninja** or **Visual Studio 17.4+** generator (Make does not support modules) +- **Compiler with C++20 module support**: + - LLVM Clang 20+ with libc++ (recommended) + - GCC 15+ (Linux only) + - MSVC 19.34+ (VS 2022 17.4+) + +> **Note:** Apple Clang is not supported for modules. See [Troubleshooting](#troubleshooting) for platform-specific guidance. + +### Enabling Modules + +```cmake +cmake_minimum_required(VERSION 3.28) +project(MyProject LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) + +include(FetchContent) +FetchContent_Declare( + glaze + GIT_REPOSITORY https://github.com/stephenberry/glaze.git + GIT_TAG main + GIT_SHALLOW TRUE +) + +# Enable module support (set cache entry before the project loads) +set(glaze_BUILD_MODULES ON CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(glaze) + +add_executable(myapp main.cpp) +target_link_libraries(myapp PRIVATE glaze::json_module) + +# Enable module scanning for your target +set_target_properties(myapp PROPERTIES CXX_SCAN_FOR_MODULES ON) +``` + +### Building with Modules + +Modules require the Ninja generator: + +```bash +cmake -B build -G Ninja -Dglaze_BUILD_MODULES=ON +cmake --build build +``` + +### Usage + +When using modules, include standard library headers **before** the module import to avoid ODR conflicts: + +```cpp +// Standard headers first +#include +#include + +// Then import the module +import glaze.json; + +struct Person { + std::string name; + int age; +}; + +int main() { + Person person{"John", 30}; + + // Write to JSON + auto json = glz::write_json(person); + if (json) { + std::cout << json.value() << std::endl; + } + + // Read from JSON + auto result = glz::read_json(R"({"name":"Jane","age":25})"); + if (result) { + std::cout << result->name << std::endl; + } +} +``` + +### macOS with LLVM Clang + +On macOS, use Homebrew's LLVM Clang (not Apple Clang) and set the SDK path: + +```bash +SDKROOT=$(xcrun --show-sdk-path) +cmake -B build -G Ninja \ + -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ \ + -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang \ + -DCMAKE_OSX_SYSROOT="$SDKROOT" \ + -Dglaze_BUILD_MODULES=ON +cmake --build build +``` + +### Troubleshooting + +**"The compiler does not provide a way to discover import graph dependencies"** +- Apple Clang does not support CMake's module dependency scanning (P1689) +- Solution: Use LLVM Clang 20+ installed via Homebrew on macOS + +**Module not found / import errors** +- Ensure you're using the Ninja generator (`-G Ninja`) +- Make generators do not support C++20 modules +- Ensure `glaze_BUILD_MODULES=ON` is set before `FetchContent_MakeAvailable` + ## Example Project Setup ### Complete CMakeLists.txt Example diff --git a/include/glaze/core/reflect.hpp b/include/glaze/core/reflect.hpp index 8a66943098..5e1d09c37e 100644 --- a/include/glaze/core/reflect.hpp +++ b/include/glaze/core/reflect.hpp @@ -477,7 +477,7 @@ namespace glz template constexpr auto make_enum_to_string_map() { - constexpr auto N = reflect::size; + static constexpr auto N = reflect::size; return [&](std::index_sequence) { using key_t = std::underlying_type_t; return normal_map(std::array, N>{ diff --git a/include/glaze/module/glaze_json.cppm b/include/glaze/module/glaze_json.cppm new file mode 100644 index 0000000000..8a2b53bbd8 --- /dev/null +++ b/include/glaze/module/glaze_json.cppm @@ -0,0 +1,79 @@ +// Glaze Library +// For the license information refer to glaze.hpp + +module; + +#include + +export module glaze.json; + +export namespace glz { + + // Core Types + using glz::context; + using glz::error_code; + using glz::error_ctx; + using glz::expected; + using glz::JSON; + using glz::opts; + using glz::raw_json; + + // Reflection and Metadata + using glz::enumerate; + using glz::flags; + using glz::for_each_field; + using glz::meta; + using glz::object; + using glz::reflect; + + // JSON Reading + using glz::read_file_json; + using glz::read_json; + + // JSON Writing + using glz::write_file_json; + using glz::write_json; + + // Generic Read/Write + using glz::read; + using glz::write; + + // Error Handling + using glz::format_error; + + // JSON Utilities + using glz::minify_json; + using glz::prettify_json; + using glz::validate_json; + + // JSON Pointer + using glz::get; + using glz::set; + + // Wrappers + using glz::custom; + using glz::hide; + using glz::quoted_num; + using glz::read_constraint; + using glz::skip; + + // Container Helpers + using glz::arr; + using glz::merge; + using glz::obj; + + // NDJSON + using glz::read_ndjson; + using glz::write_ndjson; + + // JSONC + using glz::read_jsonc; + + // JSON Schema + using glz::json_schema; + using glz::write_json_schema; + + // Generic JSON + using glz::generic; + +} diff --git a/include/glaze/reflection/to_tuple.hpp b/include/glaze/reflection/to_tuple.hpp index 43767a287c..bddf5b07ca 100644 --- a/include/glaze/reflection/to_tuple.hpp +++ b/include/glaze/reflection/to_tuple.hpp @@ -54,7 +54,7 @@ namespace glz } }(); - constexpr size_t max_pure_reflection_count = 128; + inline constexpr size_t max_pure_reflection_count = 128; } template > diff --git a/include/glaze/util/fast_float.hpp b/include/glaze/util/fast_float.hpp index 49806610b4..a4f5a28d75 100644 --- a/include/glaze/util/fast_float.hpp +++ b/include/glaze/util/fast_float.hpp @@ -185,8 +185,8 @@ namespace glz::fast_float { enum class chars_format : uint64_t; namespace detail { -constexpr chars_format basic_json_fmt = chars_format(1 << 5); -constexpr chars_format basic_fortran_fmt = chars_format(1 << 6); +inline constexpr chars_format basic_json_fmt = chars_format(1 << 5); +inline constexpr chars_format basic_fortran_fmt = chars_format(1 << 6); } // namespace detail enum class chars_format : uint64_t { @@ -577,7 +577,7 @@ struct adjusted_mantissa { }; // Bias so we can get the real exponent with an invalid adjusted_mantissa. -constexpr static int32_t invalid_am_bias = -0x8000; +inline constexpr int32_t invalid_am_bias = -0x8000; // used for binary_format_lookup_tables::max_mantissa constexpr uint64_t constant_55555 = 5 * 5 * 5 * 5 * 5; @@ -1167,7 +1167,7 @@ template constexpr bool is_space(UC c) { return c < 256 && space_lut<>::value[uint8_t(c)]; } -template static constexpr uint64_t int_cmp_zeros() { +template inline constexpr uint64_t int_cmp_zeros() { static_assert((sizeof(UC) == 1) || (sizeof(UC) == 2) || (sizeof(UC) == 4), "Unsupported character size"); return (sizeof(UC) == 1) ? 0x3030303030303030 @@ -1177,7 +1177,7 @@ template static constexpr uint64_t int_cmp_zeros() { : (uint64_t(UC('0')) << 32 | UC('0')); } -template static constexpr int int_cmp_len() { +template inline constexpr int int_cmp_len() { return sizeof(uint64_t) / sizeof(UC); } @@ -2974,11 +2974,11 @@ namespace glz::fast_float { #if defined(GLZ_FASTFLOAT_64BIT) && !defined(__sparc) #define GLZ_FASTFLOAT_64BIT_LIMB 1 typedef uint64_t limb; -constexpr size_t limb_bits = 64; +inline constexpr size_t limb_bits = 64; #else #define GLZ_FASTFLOAT_32BIT_LIMB typedef uint32_t limb; -constexpr size_t limb_bits = 32; +inline constexpr size_t limb_bits = 32; #endif typedef span limb_span; @@ -3604,7 +3604,7 @@ struct bigint : pow5_tables<> { namespace glz::fast_float { // 1e0 to 1e19 -constexpr static uint64_t powers_of_ten_uint64[] = {1UL, +inline constexpr uint64_t powers_of_ten_uint64[] = {1UL, 10UL, 100UL, 1000UL, diff --git a/include/glaze/util/parse.hpp b/include/glaze/util/parse.hpp index b576154e00..1aa67f1744 100644 --- a/include/glaze/util/parse.hpp +++ b/include/glaze/util/parse.hpp @@ -211,16 +211,16 @@ namespace glz namespace unicode { - constexpr uint32_t generic_surrogate_mask = 0xF800; - constexpr uint32_t generic_surrogate_value = 0xD800; + inline constexpr uint32_t generic_surrogate_mask = 0xF800; + inline constexpr uint32_t generic_surrogate_value = 0xD800; - constexpr uint32_t surrogate_mask = 0xFC00; - constexpr uint32_t high_surrogate_value = 0xD800; - constexpr uint32_t low_surrogate_value = 0xDC00; + inline constexpr uint32_t surrogate_mask = 0xFC00; + inline constexpr uint32_t high_surrogate_value = 0xD800; + inline constexpr uint32_t low_surrogate_value = 0xDC00; - constexpr uint32_t surrogate_codepoint_offset = 0x10000; - constexpr uint32_t surrogate_codepoint_mask = 0x03FF; - constexpr uint32_t surrogate_codepoint_bits = 10; + inline constexpr uint32_t surrogate_codepoint_offset = 0x10000; + inline constexpr uint32_t surrogate_codepoint_mask = 0x03FF; + inline constexpr uint32_t surrogate_codepoint_bits = 10; } template diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ced052dd3a..df7c54f5b2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -89,6 +89,10 @@ if(glaze_EETF_FORMAT) add_subdirectory(eetf_test) endif(glaze_EETF_FORMAT) +if(glaze_BUILD_MODULES) + add_subdirectory(module_test) +endif() + # We don't run find_package_test or glaze-install_test with MSVC/Windows, because the Github action runner often chokes # Don't run find_package on Clang, because Linux runs with Clang try to use GCC standard library and have errors before Clang 18 if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") diff --git a/tests/module_test/CMakeLists.txt b/tests/module_test/CMakeLists.txt new file mode 100644 index 0000000000..6df97b0f63 --- /dev/null +++ b/tests/module_test/CMakeLists.txt @@ -0,0 +1,28 @@ +project(module_test) + +# Module tests are only built when glaze_BUILD_MODULES is enabled +if(NOT glaze_BUILD_MODULES) + return() +endif() + +# Create module test executable +add_executable(json_module_test json_module_test.cpp) + +# Enable module scanning for this target +set_target_properties(json_module_test PROPERTIES CXX_SCAN_FOR_MODULES ON) + +# Link against the JSON module target +target_link_libraries(json_module_test PRIVATE glaze::json_module ut::ut) + +target_compile_features(json_module_test PUBLIC cxx_std_23) + +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(json_module_test PRIVATE + -Wall -Wextra -pedantic + -Wno-undefined-var-template # Glaze uses extern template vars for reflection + ) +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + target_compile_options(json_module_test PRIVATE /W4) +endif() + +add_test(NAME json_module_test COMMAND json_module_test) diff --git a/tests/module_test/json_module_test.cpp b/tests/module_test/json_module_test.cpp new file mode 100644 index 0000000000..70d5ea86e1 --- /dev/null +++ b/tests/module_test/json_module_test.cpp @@ -0,0 +1,97 @@ +// Glaze Library +// For the license information refer to glaze.hpp + +// Test that the glaze.json module works correctly + +// IMPORTANT: Standard library headers and other textual headers must come +// BEFORE module imports to avoid ODR conflicts with headers included in +// the module's global module fragment. +#include + +#include "ut/ut.hpp" + +import glaze.json; + +using namespace ut; + +struct Person +{ + std::string name{}; + int age{}; +}; + +suite module_json_tests = [] { + "write_json"_test = [] { + Person person{"John", 30}; + auto result = glz::write_json(person); + expect(result.has_value()); + expect(result.value() == R"({"name":"John","age":30})") << result.value(); + }; + + "read_json"_test = [] { + std::string json = R"({"name":"Jane","age":25})"; + auto result = glz::read_json(json); + expect(result.has_value()); + expect(result->name == "Jane"); + expect(result->age == 25); + }; + + "roundtrip"_test = [] { + Person original{"Alice", 42}; + + auto json_result = glz::write_json(original); + expect(json_result.has_value()); + + auto parsed_result = glz::read_json(json_result.value()); + expect(parsed_result.has_value()); + expect(parsed_result->name == original.name); + expect(parsed_result->age == original.age); + }; + + "prettify_json"_test = [] { + std::string compact = R"({"name":"Bob","age":35})"; + auto result = glz::prettify_json(compact); + // Prettified JSON should contain newlines + expect(result.find('\n') != std::string::npos); + }; + + "minify_json"_test = [] { + std::string pretty = R"({ + "name": "Carol", + "age": 28 + })"; + auto result = glz::minify_json(pretty); + // Minified JSON should not contain newlines + expect(result.find('\n') == std::string::npos); + }; + + "error_handling"_test = [] { + std::string invalid_json = R"({"name": invalid})"; + auto result = glz::read_json(invalid_json); + expect(!result.has_value()); + }; + + "validate_json"_test = [] { + std::string valid = R"({"name":"Test","age":1})"; + std::string invalid = R"({"name": })"; + expect(!glz::validate_json(valid)); // no error = valid + expect(bool(glz::validate_json(invalid))); // error = invalid + }; + + "json_pointer_get"_test = [] { + glz::generic json{}; + json["name"] = "Dave"; + json["age"] = 40; + auto name = glz::get(json, "/name"); + expect(name.has_value()); + expect(name->get() == "Dave"); + }; + + "json_schema"_test = [] { + std::string schema = glz::write_json_schema().value_or(""); + expect(!schema.empty()); + expect(schema.find("properties") != std::string::npos); + }; +}; + +int main() { return 0; }