Skip to content

Commit 73bc0ee

Browse files
authored
Merge pull request #387 from ruby-rice/dev
Dev
2 parents 7cd154d + 235b4d9 commit 73bc0ee

23 files changed

Lines changed: 783 additions & 110 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
Incompatible Changes:
6+
* `InstanceRegistry.isEnabled` (boolean) has been replaced by an `InstanceRegistry.isEnabled` which is an enum (`Off`, `Owned`, `All`).
7+
* `InstanceRegistry` now defaults to `Owned` - previously it was disabled. The goal of this change is to ensure C++ objects owned by Ruby are only wrapped once to avoid double free errors.
8+
39
## 4.10.0 (2026-02-07)
410

511
Enhancements:

docs/bindings/constructors.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ class Mat
8383
end
8484
```
8585

86+
### KeepAlive
87+
88+
When an object is cloned or duped, Rice automatically copies its [keepAlive](keep_alive.md) references to the new object. This ensures that the clone directly protects the same Ruby objects as the original. For example, if a `std::vector<MyClass*>` holds pointers to Ruby-wrapped objects, cloning the vector will ensure those objects are kept alive by both the original and the clone independently.
89+
8690
## Move Constructors
8791

8892
Rice does not support move constructors because there is no clean mapping of them to Ruby.

docs/bindings/instance_registry.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ Rice 4.1 added an instance registry which tracks which C++ objects have been wra
44

55
## Enabled
66

7-
When the instance registry is enabled, Rice will check if a C++ instance has been wrapped by a Ruby instance. If it has, then the existing Ruby instance is returned.
7+
When instance tracking is enabled (that is, mode is not `Off`), Rice will check if a C++ instance has already been wrapped by Ruby. If it has, then the existing Ruby instance is returned.
88

9-
By default, instance tracking is disabled. To turn it on:
9+
Instance tracking is configured only via mode:
1010

1111
```cpp
12-
detail::Internal::intance.instances.isEnabled = true;
12+
detail::Registries::instance.instances.mode = detail::InstanceRegistry::Mode::Owned; // default
13+
// Off -> disable tracking
14+
// Owned -> track only Ruby-owned wrapped instances
15+
// All -> track both Ruby-owned and borrowed instances
1316
```
1417

1518
## Disabled
1619

17-
When the instance registry is disabled, Rice will wrap a C++ instance in a new Ruby instance regardless of whether it is already wrapped by a Ruby instance. Therefore if you make multiple calls to a C++ method that returns the same C++ object each time via a reference or pointer, multiple wrapping Ruby objects will be created. By default having multiple Ruby objects wrap a C++ object is fine since the Ruby objects do not own the C++ object. For more information please carefully read the [C++ to Ruby](memory_management.md#c-to-ruby) topic.
20+
When mode is `Off`, Rice will wrap a C++ instance in a new Ruby instance regardless of whether it is already wrapped by a Ruby instance. Therefore if you make multiple calls to a C++ method that returns the same C++ object each time via a reference or pointer, multiple wrapping Ruby objects will be created. By default having multiple Ruby objects wrap a C++ object is fine since the Ruby objects do not own the C++ object. For more information please carefully read the [C++ to Ruby](memory_management.md#c-to-ruby) topic.
1821

1922
There is one exception to this rule, which happens when a C++ method returns back itself. Rice recognizes that the C++ object is wrapped by the Ruby object making the call, and thus it is returning self (see [return self](methods.md#return-self)).
2023

@@ -29,3 +32,11 @@ First, the implementation is not thread-safe. Due to Ruby's GIL, this is not con
2932
Second, pairs in the global map are removed when a Ruby object is freed by the garbage collector. There could be a window where a Ruby object is marked for deletion but the underlying C++ object is returned back to Ruby. Then the Ruby object would be freed resulting in a crash. It is unknown if this really happens, it has never been observed.
3033

3134
Third, a C++ instance wrapped by Ruby instance may be freed on the C++ side. As long as ownership rules have been correctly setup, this is fine. However, if a new C++ instance is created that has the same address as the deleted C++ object and then is passed to Ruby the instance tracker will return the old deleted object. This has been observed in the Rice tests. It is unknown if this is due to how the tests are written or is a more general problem.
35+
36+
## Rule for Ruby-Owned Objects
37+
38+
A useful practical rule is: if Ruby owns a C++ object, there should be only one Ruby object wrapping that C++ instance. Multiple Ruby wrappers for the same Ruby-owned C++ object can create lifetime bugs where one wrapper frees the underlying C++ object while another wrapper still exists and later dereferences a dangling pointer.
39+
40+
The instance registry addresses this by mapping C++ instance addresses to existing Ruby wrappers and reusing the same Ruby object instead of creating duplicates. In other words, when ownership is on the Ruby side, enabling instance tracking helps enforce a single-wrapper identity and avoids this class of aliasing/lifetime errors.
41+
42+
Value-return wrappers do not participate in instance tracking. When C++ returns values, Rice copies or moves them into wrapper-owned storage, so there is no stable shared native address identity to reuse. Instance tracking is therefore only meaningful for pointer/reference-style aliasing cases, not value copies.

rice/Constructor.ipp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@ namespace Rice
4545
detail::wrapConstructed<T>(self, Data_Type<T>::ruby_data_type(), data);
4646
}
4747

48-
static void initialize_copy(VALUE self, const T& other)
48+
// Takes VALUE (via Arg.setValue()) instead of const T& so we have access to
49+
// both Ruby wrappers and can copy the keepAlive list from the original to the clone.
50+
static VALUE initialize_copy(VALUE self, VALUE other)
4951
{
50-
// Call C++ copy constructor
51-
T* data = new T(other);
52-
detail::wrapConstructed<T>(self, Data_Type<T>::ruby_data_type(), data);
52+
T* otherData = detail::unwrap<T>(other, Data_Type<T>::ruby_data_type(), false);
53+
T* data = new T(*otherData);
54+
detail::wrapConstructed<T>(self, Data_Type<T>::ruby_data_type(), data, other);
55+
return self;
5356
}
5457

5558
};

rice/Data_Type.ipp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,23 @@ namespace Rice
192192

193193
if constexpr (Constructor_T::isCopyConstructor())
194194
{
195-
// Define initialize_copy that will copy the C++ object
196-
this->define_method("initialize_copy", &Constructor_T::initialize_copy, args...);
195+
// Define initialize_copy that will copy the C++ object and its keepAlive references.
196+
// We use setValue() so Rice passes the raw VALUE without conversion - this gives
197+
// initialize_copy access to both wrappers so it can copy the keepAlive list.
198+
using Rice_Arg_Tuple = std::tuple<Rice_Arg_Ts...>;
199+
constexpr std::size_t arg_index = detail::tuple_element_index_v<Rice_Arg_Tuple, Arg>;
200+
201+
if constexpr (arg_index < sizeof...(Rice_Arg_Ts))
202+
{
203+
// User provided an Arg - extract it and ensure setValue is set
204+
Arg arg = std::get<arg_index>(std::forward_as_tuple(args...));
205+
arg.setValue();
206+
this->define_method("initialize_copy", &Constructor_T::initialize_copy, arg);
207+
}
208+
else
209+
{
210+
this->define_method("initialize_copy", &Constructor_T::initialize_copy, Arg("other").setValue());
211+
}
197212
}
198213
else if constexpr (Constructor_T::isMoveConstructor())
199214
{

rice/detail/InstanceRegistry.hpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
11
#ifndef Rice__detail__InstanceRegistry__hpp_
22
#define Rice__detail__InstanceRegistry__hpp_
33

4+
#include <type_traits>
5+
46
namespace Rice::detail
57
{
68
class InstanceRegistry
79
{
810
public:
11+
enum class Mode
12+
{
13+
Off,
14+
Owned,
15+
All
16+
};
17+
918
template <typename T>
10-
VALUE lookup(T& cppInstance);
19+
VALUE lookup(T* cppInstance, bool isOwner);
1120

1221
template <typename T>
13-
VALUE lookup(T* cppInstance);
14-
VALUE lookup(void* cppInstance);
22+
void add(T* cppInstance, VALUE rubyInstance, bool isOwner);
1523

16-
void add(void* cppInstance, VALUE rubyInstance);
1724
void remove(void* cppInstance);
1825
void clear();
1926

2027
public:
21-
bool isEnabled = false;
28+
Mode mode = Mode::Owned;
2229

2330
private:
31+
bool shouldTrack(bool isOwner) const;
32+
2433
std::map<void*, VALUE> objectMap_;
2534
};
2635
} // namespace Rice::detail
2736

2837
#endif // Rice__detail__InstanceRegistry__hpp_
29-

rice/detail/InstanceRegistry.ipp

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,26 @@
33
namespace Rice::detail
44
{
55
template <typename T>
6-
inline VALUE InstanceRegistry::lookup(T& cppInstance)
6+
inline VALUE InstanceRegistry::lookup(T* cppInstance, bool isOwner)
77
{
8-
return this->lookup((void*)&cppInstance);
9-
}
10-
11-
template <typename T>
12-
inline VALUE InstanceRegistry::lookup(T* cppInstance)
13-
{
14-
return this->lookup((void*)cppInstance);
15-
}
16-
17-
inline VALUE InstanceRegistry::lookup(void* cppInstance)
18-
{
19-
if (!this->isEnabled)
20-
return Qnil;
21-
22-
auto it = this->objectMap_.find(cppInstance);
23-
if (it != this->objectMap_.end())
24-
{
25-
return it->second;
26-
}
27-
else
8+
if (!this->shouldTrack(isOwner))
289
{
2910
return Qnil;
3011
}
12+
13+
auto it = this->objectMap_.find((void*)cppInstance);
14+
return it != this->objectMap_.end() ? it->second : Qnil;
3115
}
3216

33-
inline void InstanceRegistry::add(void* cppInstance, VALUE rubyInstance)
17+
template <typename T>
18+
inline void InstanceRegistry::add(T* cppInstance, VALUE rubyInstance, bool isOwner)
3419
{
35-
if (this->isEnabled)
20+
if (!this->shouldTrack(isOwner))
3621
{
37-
this->objectMap_[cppInstance] = rubyInstance;
22+
return;
3823
}
24+
25+
this->objectMap_[(void*)cppInstance] = rubyInstance;
3926
}
4027

4128
inline void InstanceRegistry::remove(void* cppInstance)
@@ -47,4 +34,19 @@ namespace Rice::detail
4734
{
4835
this->objectMap_.clear();
4936
}
50-
} // namespace
37+
38+
inline bool InstanceRegistry::shouldTrack(bool isOwner) const
39+
{
40+
switch (this->mode)
41+
{
42+
case Mode::Off:
43+
return false;
44+
case Mode::Owned:
45+
return isOwner;
46+
case Mode::All:
47+
return true;
48+
default:
49+
return false;
50+
}
51+
}
52+
}

rice/detail/Wrapper.hpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ namespace Rice::detail
1717

1818
void ruby_mark();
1919
void addKeepAlive(VALUE value);
20+
const std::vector<VALUE>& getKeepAlive() const;
21+
void setKeepAlive(const std::vector<VALUE>& keepAlive);
2022
void setOwner(bool value);
2123

2224
protected:
@@ -37,7 +39,7 @@ namespace Rice::detail
3739
public:
3840
Wrapper(rb_data_type_t* rb_data_type, T& data);
3941
Wrapper(rb_data_type_t* rb_data_type, T&& data);
40-
~Wrapper();
42+
~Wrapper() = default;
4143
void* get(rb_data_type_t* requestedType) override;
4244

4345
private:
@@ -82,7 +84,7 @@ namespace Rice::detail
8284

8385
// ---- Helper Functions ---------
8486
template <typename T>
85-
Wrapper<T*>* wrapConstructed(VALUE value, rb_data_type_t* rb_data_type, T* data);
87+
Wrapper<T*>* wrapConstructed(VALUE value, rb_data_type_t* rb_data_type, T* data, VALUE source = Qnil);
8688

8789
template <typename T>
8890
VALUE wrap(VALUE klass, rb_data_type_t* rb_data_type, T& data, bool isOwner);
@@ -99,4 +101,4 @@ namespace Rice::detail
99101
WrapperBase* getWrapper(VALUE value);
100102
}
101103
#endif // Rice__detail__Wrapper__hpp_
102-
104+

0 commit comments

Comments
 (0)