diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 42e7caa2..73499ba5 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -7,15 +7,13 @@ on: branches: [ master ] jobs: - tests: + mkmf: + name: mkmf (${{ matrix.os }}, ${{ matrix.ruby }}, C++17) strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-2025] ruby: ['3.2', '3.3', '3.4', '4.0'] - include: - - os: ubuntu-22.04 - ruby: '3.2' runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 @@ -33,4 +31,54 @@ jobs: if: always() with: name: mkmf-${{ matrix.os }}-${{ matrix.version }}-${{ matrix.ruby }} - path: test/mkmf.log \ No newline at end of file + path: test/mkmf.log + + cmake: + name: cmake (${{ matrix.os }}, C++${{ matrix.cpp_standard }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-2025] + cpp_standard: ['17', '20', '23'] + include: + - os: ubuntu-latest + preset: linux-debug + ruby: '4.0' + - os: macos-latest + preset: macos-debug + ruby: '4.0' + - os: windows-2025 + preset: msvc-debug + ruby: mswin + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + + - if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libffi-dev + + - if: runner.os == 'macOS' + run: brew install libffi + + - if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + + - if: runner.os == 'Windows' + uses: seanmiddleditch/gha-setup-ninja@v6 + + - if: runner.os == 'Windows' + shell: pwsh + run: | + git clone https://github.com/microsoft/vcpkg.git "$env:RUNNER_TEMP\vcpkg" + & "$env:RUNNER_TEMP\vcpkg\bootstrap-vcpkg.bat" + echo "VCPKG_ROOT=$env:RUNNER_TEMP\vcpkg" >> $env:GITHUB_ENV + & "$env:RUNNER_TEMP\vcpkg\vcpkg.exe" install libffi:x64-windows + + - run: cmake --preset ${{ matrix.preset }} -DCMAKE_CXX_STANDARD=${{ matrix.cpp_standard }} + - run: cmake --build --preset ${{ matrix.preset }} diff --git a/Agents.md b/AGENTS.md similarity index 74% rename from Agents.md rename to AGENTS.md index 327bd596..36440ffa 100644 --- a/Agents.md +++ b/AGENTS.md @@ -27,3 +27,7 @@ Run specific test suites by passing suite names as arguments: ```bash ./build/linux-debug/test/unittest Array Hash Iterator ``` + +## Quick Compile Testing + +To quickly test if a single test file compiles, temporarily edit `test/CMakeLists.txt` to include only that file (plus `unittest.cpp` and `embed_ruby.cpp`). Revert the change when done. diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ecf767..606d934c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,14 @@ # Changelog -## 4.11.3 (?) +## 4.11.3 (2026-02-25) ### Enhancements * Add support for `std::shared_ptr` * Add support for `std::unique_ptr` - +* Add support for non-constructible objects in std::pair, std::map, std::multimap +* Add support for wrapping pointers to abstract types that cannot be created nor destroyed +* Fix converting std::nullptr_t to Ruby ## 4.11.2 (2026-02-21) diff --git a/CMakePresets.json b/CMakePresets.json index bff8f9da..e43ae4d4 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -116,7 +116,7 @@ "toolchainFile": "$env{VCPKG_ROOT}\\scripts\\buildsystems\\vcpkg.cmake", "cacheVariables": { "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", - "CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE" + "CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /Zc:__cplusplus /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE" }, "condition": { "type": "equals", @@ -155,7 +155,7 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_CXX_COMPILER": "clang-cl.exe", - "CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE /clang:-Wno-unused-private-field", + "CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /Zc:__cplusplus /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE /clang:-Wno-unused-private-field", "CMAKE_CXX_FLAGS_DEBUG": "/Od /Zi" } }, @@ -166,7 +166,7 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_CXX_COMPILER": "clang-cl.exe", - "CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE /clang:-Wno-unused-private-field", + "CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /Zc:__cplusplus /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE /clang:-Wno-unused-private-field", "CMAKE_CXX_FLAGS_RELEASE": "/O2 /DNDEBUG", "CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON" } diff --git a/docs/packaging/build_settings.md b/docs/packaging/build_settings.md index 30933e13..b101068e 100644 --- a/docs/packaging/build_settings.md +++ b/docs/packaging/build_settings.md @@ -19,7 +19,7 @@ For MINGW: For Microsoft Visual C++ and Windows Clang: ```bash -/std:c++17 /EHs /permissive- /bigobj /utf-8 -D_ALLOW_KEYWORD_MACROS -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE +/std:c++17 /EHs /permissive- /bigobj /utf-8 /Zc:__cplusplus -D_ALLOW_KEYWORD_MACROS -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE ``` These options are described below: @@ -36,6 +36,10 @@ Second, bigobject support needs to enabled. This tells the compiler to increase Rice uses UTF-8 characters when [mapping](../stl/stl.md#automatically-generated-ruby-classes) instantiated STL templates to Ruby class names. +### __cplusplus Macro + +By default, MSVC does not update the `__cplusplus` preprocessor macro to reflect the actual C++ standard in use — it always reports `199711L`. The `/Zc:__cplusplus` flag fixes this so that Rice's `#if __cplusplus` checks work correctly. + ### Exception Handling Model For Visual C++, the default exception [model](https://learn.microsoft.com/en-us/cpp/build/reference/eh-exception-handling-model?view=msvc-170) setting of `/EHsc` crashes Ruby when calling longjmp with optimizations enabled (/O2). Therefore you must `/EHs` instead. diff --git a/include/rice/rice.hpp b/include/rice/rice.hpp index 85809b3c..b4c89380 100644 --- a/include/rice/rice.hpp +++ b/include/rice/rice.hpp @@ -8854,31 +8854,15 @@ namespace Rice::detail } } - void* convert(VALUE value) + std::nullptr_t convert(VALUE value) { if (value == Qnil) { return nullptr; } - if (this->arg_ && this->arg_->isOpaque()) - { - return (void*)value; - } - - switch (rb_type(value)) - { - case RUBY_T_NIL: - { - return nullptr; - break; - } - default: - { - throw Exception(rb_eTypeError, "wrong argument type %s (expected %s)", - detail::protect(rb_obj_classname, value), "nil"); - } - } + throw Exception(rb_eTypeError, "wrong argument type %s (expected %s)", + detail::protect(rb_obj_classname, value), "nil"); } private: Arg* arg_ = nullptr; @@ -10277,7 +10261,9 @@ namespace Rice::detail if constexpr (is_complete_v) { - if constexpr (std::is_destructible_v) + // is_abstract_v requires a complete type, so nest inside is_complete_v. + // Deleting an abstract class through a non-virtual destructor is UB. + if constexpr (std::is_destructible_v && !std::is_abstract_v) { if (this->isOwner_) { diff --git a/include/rice/stl.hpp b/include/rice/stl.hpp index 6b1fc79d..c3dca90c 100644 --- a/include/rice/stl.hpp +++ b/include/rice/stl.hpp @@ -1310,8 +1310,12 @@ namespace Rice private: void define_constructors() { - klass_.define_constructor(Constructor()) - .define_constructor(Constructor(), Arg("x").keepAlive(), Arg("y").keepAlive()); + if constexpr (std::is_default_constructible_v) + { + klass_.define_constructor(Constructor()); + } + + klass_.define_constructor(Constructor(), Arg("x").keepAlive(), Arg("y").keepAlive()); if constexpr (std::is_copy_constructible_v && std::is_copy_constructible_v) { @@ -1650,7 +1654,7 @@ namespace Rice }, Arg("key")) .define_method("[]=", [](T& map, Key_T key, Mapped_Parameter_T value) -> Mapped_T { - map[key] = value; + map.insert_or_assign(key, value); return value; }, Arg("key").keepAlive(), Arg("value").keepAlive()); @@ -1772,7 +1776,7 @@ namespace Rice // exceptions propogate back to Ruby return cpp_protect([&] { - result->operator[](From_Ruby().convert(key)) = From_Ruby().convert(value); + result->insert_or_assign(From_Ruby().convert(key), From_Ruby().convert(value)); return ST_CONTINUE; }); } @@ -3201,7 +3205,11 @@ namespace Rice if constexpr (detail::is_complete_v && !std::is_void_v) { - result.define_constructor(Constructor(), Arg("value").takeOwnership()); + // is_abstract_v requires a complete type, so it must be nested inside the is_complete_v check + if constexpr (!std::is_abstract_v) + { + result.define_constructor(Constructor(), Arg("value").takeOwnership()); + } } // Forward methods to wrapped T @@ -3256,7 +3264,7 @@ namespace Rice::detail } else if (rb_typeddata_inherited_p(this->inner_rb_data_type_, requestedType)) { - return this->data_.get(); + return (void*)this->data_.get(); } else { @@ -3931,7 +3939,7 @@ namespace Rice::detail } else if (rb_typeddata_inherited_p(this->inner_rb_data_type_, requestedType)) { - return this->data_.get(); + return (void*)this->data_.get(); } else { @@ -4171,7 +4179,7 @@ namespace Rice }, Arg("key")) .define_method("[]=", [](T& unordered_map, Key_T key, Mapped_Parameter_T value) -> Mapped_T { - unordered_map[key] = value; + unordered_map.insert_or_assign(key, value); return value; }, Arg("key").keepAlive(), Arg("value").keepAlive()); @@ -4293,7 +4301,7 @@ namespace Rice // exceptions propogate back to Ruby return cpp_protect([&] { - result->operator[](From_Ruby().convert(key)) = From_Ruby().convert(value); + result->insert_or_assign(From_Ruby().convert(key), From_Ruby().convert(value)); return ST_CONTINUE; }); } diff --git a/lib/mkmf-rice.rb b/lib/mkmf-rice.rb index a7be4a27..da4b7fcc 100644 --- a/lib/mkmf-rice.rb +++ b/lib/mkmf-rice.rb @@ -24,14 +24,16 @@ def cpp_command(outfile, opt="") # Now pull in the C++ support include MakeMakefile['C++'] -# Rice needs c++17. +# Rice needs c++17 or higher. Use --with-cxx-standard=20 to override. +std = with_config('cxx-standard', '17') + if IS_MSWIN - $CXXFLAGS += " /std:c++17 /EHs /permissive- /bigobj /utf-8" + $CXXFLAGS += " /std:c++#{std} /EHs /permissive- /bigobj /utf-8 /Zc:__cplusplus" $CPPFLAGS += " -D_ALLOW_KEYWORD_MACROS -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE" elsif IS_MINGW - $CXXFLAGS += " -std=c++17 -Wa,-mbig-obj" + $CXXFLAGS += " -std=c++#{std} -Wa,-mbig-obj" else - $CXXFLAGS += " -std=c++17" + $CXXFLAGS += " -std=c++#{std}" end # Rice needs to include its header. Let's setup the include path diff --git a/lib/rice/version.rb b/lib/rice/version.rb index a42b3e4d..864cf92f 100644 --- a/lib/rice/version.rb +++ b/lib/rice/version.rb @@ -1,3 +1,3 @@ module Rice - VERSION = "4.11.2" + VERSION = "4.11.3" end diff --git a/rice/stl/ostream.ipp b/rice/stl/ostream.ipp index d9314b2f..67f9d0ab 100644 --- a/rice/stl/ostream.ipp +++ b/rice/stl/ostream.ipp @@ -59,8 +59,12 @@ namespace Rice::stl void define_methods() { +#if __cplusplus >= 202002L + klass_.define_method("str", &std::ostringstream::str) +#else klass_.define_method("str", &std::ostringstream::str) - .define_method("str=", [](std::ostringstream& stream, const std::string& s) { stream.str(s); }, Arg("str")); +#endif + .define_method("str=", [](std::ostringstream& stream, const std::string& s) { stream.str(s); }, Arg("str")); rb_define_alias(klass_, "to_s", "str"); } diff --git a/rice/traits/function_traits.hpp b/rice/traits/function_traits.hpp index 2f6eb9ca..2bb33039 100644 --- a/rice/traits/function_traits.hpp +++ b/rice/traits/function_traits.hpp @@ -102,6 +102,27 @@ namespace Rice::detail { }; + // ref-qualified member Functions on C++ classes (C++20 uses these for std library types) + template + struct function_traits : public function_traits + { + }; + + template + struct function_traits : public function_traits + { + }; + + template + struct function_traits : public function_traits + { + }; + + template + struct function_traits : public function_traits + { + }; + /*// Functors and lambdas template struct function_traits : public function_traits diff --git a/test/test_Attribute.cpp b/test/test_Attribute.cpp index 399b84b1..5d87be0e 100644 --- a/test/test_Attribute.cpp +++ b/test/test_Attribute.cpp @@ -590,4 +590,35 @@ TESTCASE(KeepAlive) // This should work because keepAlive prevents MyClass2 from being GC'd ASSERT_NOT_EQUAL(nullptr, dataStruct->myClass2); ASSERT_EQUAL(43, dataStruct->myClass2->value); +} + +namespace +{ + struct FuncPtrStruct + { + int (*callback)(int) = nullptr; + }; +} + +TESTCASE(function_pointer_attribute) +{ + Module m = define_module("Testing"); + + Class c = define_class("FuncPtrStruct") + .define_constructor(Constructor()) + .define_attr("callback", &FuncPtrStruct::callback); + + Object o = c.call("new"); + + // Set the callback via Ruby using a lambda + std::string code = R"(struct = FuncPtrStruct.new + struct.callback = lambda { |x| x * 2 } + struct)"; + + Data_Object funcPtrStruct = m.module_eval(code); + + // Invoke the callback from C++ to verify it works + ASSERT_NOT_EQUAL(nullptr, funcPtrStruct->callback); + int result = funcPtrStruct->callback(5); + ASSERT_EQUAL(10, result); } \ No newline at end of file diff --git a/test/test_Callback.cpp b/test/test_Callback.cpp index 1a64b886..e4773c63 100644 --- a/test/test_Callback.cpp +++ b/test/test_Callback.cpp @@ -55,10 +55,8 @@ TESTCASE(LambdaCallBack) ASSERT((globalCallback != nullptr)); int ref = 4; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wwrite-strings" - char* result = triggerCallback(1, 2, true, "hello", ref); -#pragma GCC diagnostic pop + char hello[] = "hello"; + char* result = triggerCallback(1, 2, true, hello, ref); ASSERT_EQUAL("1 - 2.0 - true - hello - 4", result); }