diff --git a/CMakePresets.json b/CMakePresets.json index e43ae4d4..6a7e03ba 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -9,7 +9,7 @@ "installDir": "${sourceDir}/install/${presetName}", "cacheVariables": { "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", - "CMAKE_CXX_FLAGS": "-Wall -ftemplate-backtrace-limit=0 -fvisibility=hidden -fvisibility-inlines-hidden" + "CMAKE_CXX_FLAGS": "-Wall -Wno-array-bounds -ftemplate-backtrace-limit=0 -fvisibility=hidden -fvisibility-inlines-hidden" } }, { @@ -81,7 +81,7 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_CXX_COMPILER": "g++.exe", - "CMAKE_CXX_FLAGS": "-Wall -ftemplate-backtrace-limit=0 -Wa,-mbig-obj -fvisibility=hidden -fvisibility-inlines-hidden", + "CMAKE_CXX_FLAGS": "-Wall -Wno-array-bounds -ftemplate-backtrace-limit=0 -Wa,-mbig-obj -fvisibility=hidden -fvisibility-inlines-hidden", "CMAKE_CXX_FLAGS_DEBUG": "-g3 -Og -fno-omit-frame-pointer -fno-inline -gsplit-dwarf" }, "condition": { @@ -97,7 +97,7 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_CXX_COMPILER": "g++.exe", - "CMAKE_CXX_FLAGS": "-Wall -ftemplate-backtrace-limit=0 -Wa,-mbig-obj -fvisibility=hidden -fvisibility-inlines-hidden", + "CMAKE_CXX_FLAGS": "-Wall -Wno-array-bounds -ftemplate-backtrace-limit=0 -Wa,-mbig-obj -fvisibility=hidden -fvisibility-inlines-hidden", "CMAKE_CXX_FLAGS_RELEASE": "-O3 -DNDEBUG", "CMAKE_SHARED_LINKER_FLAGS_RELEASE": "-Wl,--exclude-all-symbols" }, diff --git a/bin/rice-doc.rb b/bin/rice-doc.rb old mode 100644 new mode 100755 diff --git a/bin/rice-rbs.rb b/bin/rice-rbs.rb old mode 100644 new mode 100755 diff --git a/docs/packaging/build_settings.md b/docs/packaging/build_settings.md index b101068e..baa3e4f8 100644 --- a/docs/packaging/build_settings.md +++ b/docs/packaging/build_settings.md @@ -7,13 +7,13 @@ Rice extensions requires several compiler settings to be set. These are captured For Clang and GCC: ```bash --std=c++17 -Wa,-mbig-obj -ftemplate-backtrace-limit=0 +-std=c++17 -Wno-array-bounds -Wa,-mbig-obj -ftemplate-backtrace-limit=0 ``` For MINGW: ```bash --std=c++17, -Wa,-mbig-obj +-std=c++17 -Wno-array-bounds -Wa,-mbig-obj ``` For Microsoft Visual C++ and Windows Clang: @@ -44,6 +44,14 @@ By default, MSVC does not update the `__cplusplus` preprocessor macro to reflect 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. +### Array Bounds Warning + +g++ 15 produces false positive `-Warray-bounds` warnings when inlining through Ruby's `RSTRING` macro (in `ruby/internal/core/rstring.h`). This is not a bug in Rice or Ruby. The warning is harmless but becomes a build failure if `-Werror` is enabled. Rice suppresses it with: + +```bash +-Wno-array-bounds +``` + ### Template Backtrace For g++, you must set `-ftemplate-backtrace-limit=0` to avoid compilation errors. @@ -125,6 +133,16 @@ $CXXFLAGS << " /GL" $LDFLAGS << " /LTCG" ``` +### GCC 15 LTO Assembler Bug + +g++ 15.2.1 with binutils 2.45.1 (shipped in Fedora 43) can trigger an internal assembler segfault when LTO is enabled. If you hit this, disable LTO as a workaround: + +```ruby +$CXXFLAGS += " -fno-lto" +``` + +Or with CMake, set `CMAKE_INTERPROCEDURAL_OPTIMIZATION` to `OFF`. This only affects the specific GCC/binutils version combination and should be resolved in a future binutils release. + ### Debug Symbol Splitting (GCC/Clang) For debug builds with GCC or Clang, consider using `-gsplit-dwarf` to separate debug information into `.dwo` files. This keeps the main binary smaller while preserving full debug capability: diff --git a/include/rice/rice.hpp b/include/rice/rice.hpp index 39a2f031..1729f632 100644 --- a/include/rice/rice.hpp +++ b/include/rice/rice.hpp @@ -859,6 +859,7 @@ namespace Rice::detail // ========= Anchor.hpp ========= #include +#include namespace Rice { @@ -893,7 +894,6 @@ namespace Rice VALUE get() const; private: - static void disable(VALUE); static void registerExitHandler(); inline static bool enabled_ = true; @@ -1667,23 +1667,28 @@ namespace Rice { namespace detail { - inline Anchor::Anchor(VALUE value) : value_(value) + inline Anchor::Anchor(VALUE value) { + // rb_gc_register_address() can trigger GC, so we must register the + // empty this->value_ slot before storing a heap VALUE in it. + // RB_GC_GUARD(value) keeps the ctor argument alive through the end of + // this method until the registered slot has been updated. if (!RB_SPECIAL_CONST_P(value)) { Anchor::registerExitHandler(); detail::protect(rb_gc_register_address, &this->value_); this->registered_ = true; } + this->value_ = value; + RB_GC_GUARD(value); } inline Anchor::~Anchor() { if (Anchor::enabled_ && this->registered_) { - detail::protect(rb_gc_unregister_address, &this->value_); + rb_gc_unregister_address(&this->value_); } - // Ruby auto detects VALUEs in the stack, so make sure up in case this object is on the stack this->registered_ = false; this->value_ = Qnil; } @@ -1693,17 +1698,18 @@ namespace Rice return this->value_; } - // This will be called by ruby at exit - we want to disable further unregistering - inline void Anchor::disable(VALUE) - { - Anchor::enabled_ = false; - } - inline void Anchor::registerExitHandler() { if (!Anchor::exitHandlerRegistered_) { - detail::protect(rb_set_end_proc, &Anchor::disable, Qnil); + // Use ruby_vm_at_exit which fires AFTER the VM is destroyed, + // not rb_set_end_proc which fires BEFORE. rb_set_end_proc + // runs as an end_proc in LIFO order alongside at_exit blocks, + // so its timing depends on require order — if the extension + // loads after minitest/autorun, the disable callback runs + // before tests execute, causing Anchor destructors to skip + // rb_gc_unregister_address and leave dangling root pointers. + ruby_vm_at_exit([](ruby_vm_t*) { Anchor::enabled_ = false; }); Anchor::exitHandlerRegistered_ = true; } } diff --git a/lib/mkmf-rice.rb b/lib/mkmf-rice.rb index da4b7fcc..4b74cf82 100644 --- a/lib/mkmf-rice.rb +++ b/lib/mkmf-rice.rb @@ -31,9 +31,9 @@ def cpp_command(outfile, opt="") $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++#{std} -Wa,-mbig-obj" + $CXXFLAGS += " -std=c++#{std} -Wa,-mbig-obj -Wno-array-bounds" else - $CXXFLAGS += " -std=c++#{std}" + $CXXFLAGS += " -std=c++#{std} -Wno-array-bounds" end # Rice needs to include its header. Let's setup the include path diff --git a/rice/detail/Anchor.hpp b/rice/detail/Anchor.hpp index 1b1a3de9..23333caa 100644 --- a/rice/detail/Anchor.hpp +++ b/rice/detail/Anchor.hpp @@ -2,6 +2,7 @@ #define Rice__detail__Anchor__hpp_ #include +#include namespace Rice { @@ -36,7 +37,6 @@ namespace Rice VALUE get() const; private: - static void disable(VALUE); static void registerExitHandler(); inline static bool enabled_ = true; diff --git a/rice/detail/Anchor.ipp b/rice/detail/Anchor.ipp index 05543136..7fe1dc31 100644 --- a/rice/detail/Anchor.ipp +++ b/rice/detail/Anchor.ipp @@ -2,23 +2,28 @@ namespace Rice { namespace detail { - inline Anchor::Anchor(VALUE value) : value_(value) + inline Anchor::Anchor(VALUE value) { + // rb_gc_register_address() can trigger GC, so we must register the + // empty this->value_ slot before storing a heap VALUE in it. + // RB_GC_GUARD(value) keeps the ctor argument alive through the end of + // this method until the registered slot has been updated. if (!RB_SPECIAL_CONST_P(value)) { Anchor::registerExitHandler(); detail::protect(rb_gc_register_address, &this->value_); this->registered_ = true; } + this->value_ = value; + RB_GC_GUARD(value); } inline Anchor::~Anchor() { if (Anchor::enabled_ && this->registered_) { - detail::protect(rb_gc_unregister_address, &this->value_); + rb_gc_unregister_address(&this->value_); } - // Ruby auto detects VALUEs in the stack, so make sure up in case this object is on the stack this->registered_ = false; this->value_ = Qnil; } @@ -28,17 +33,18 @@ namespace Rice return this->value_; } - // This will be called by ruby at exit - we want to disable further unregistering - inline void Anchor::disable(VALUE) - { - Anchor::enabled_ = false; - } - inline void Anchor::registerExitHandler() { if (!Anchor::exitHandlerRegistered_) { - detail::protect(rb_set_end_proc, &Anchor::disable, Qnil); + // Use ruby_vm_at_exit which fires AFTER the VM is destroyed, + // not rb_set_end_proc which fires BEFORE. rb_set_end_proc + // runs as an end_proc in LIFO order alongside at_exit blocks, + // so its timing depends on require order — if the extension + // loads after minitest/autorun, the disable callback runs + // before tests execute, causing Anchor destructors to skip + // rb_gc_unregister_address and leave dangling root pointers. + ruby_vm_at_exit([](ruby_vm_t*) { Anchor::enabled_ = false; }); Anchor::exitHandlerRegistered_ = true; } }