Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### New Features

* Add opt-in Ractor-safe `InstanceRegistry` via `RICE_RACTOR_SAFE` define. When enabled, a `std::recursive_mutex` protects all mutable registry operations, allowing C extensions built with Rice to be used safely from multiple Ruby Ractors (Ruby 3.4.x). Without the define, behavior is unchanged.

## 4.11.4 (2026-03-13)

### Bug Fixes
Expand Down
19 changes: 18 additions & 1 deletion docs/why_rice.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,24 @@ Rice automatically converts common exceptions and provides a mechanism for conve

## Thread Safety

Rice provides no mechanisms for dealing with thread safety. Many common thread safety issues should be alleviated by YARV, which supports POSIX threads.
Rice provides no mechanisms for dealing with thread safety by default. Many common thread safety issues should be alleviated by YARV, which supports POSIX threads.

### Ractor Support (opt-in)

Ruby 3.4 introduced Ractors for true parallel execution. C extensions can declare themselves Ractor-safe via `rb_ext_ractor_safe(true)`, but Rice's internal `InstanceRegistry` — a `std::map` that tracks C++ object wrappers — is not thread-safe. Concurrent access from multiple Ractors corrupts the map, causing segfaults or hangs.

To enable Ractor-safe operation, define `RICE_RACTOR_SAFE` before compilation:

```ruby
# In your extconf.rb:
$CXXFLAGS << " -DRICE_RACTOR_SAFE"
```

This adds a `std::recursive_mutex` to `InstanceRegistry`, protecting all `lookup`, `add`, `remove`, and `clear` operations. Only `InstanceRegistry` requires this protection — the other Rice registries (`TypeRegistry`, `NativeRegistry`, `HandlerRegistry`, `ModuleRegistry`) are written once during initialization and are read-only at runtime.

Without the define, the generated code is identical to the current Rice — no mutex, no overhead, no behavior change.

**Compatibility:** Tested with Ruby 3.4.x Ractors. Ruby 4.x compatibility is pending validation.

## C++ Based API

Expand Down
7 changes: 7 additions & 0 deletions rice/detail/InstanceRegistry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

#include <type_traits>

#ifdef RICE_RACTOR_SAFE
#include <mutex>
#endif

namespace Rice::detail
{
class InstanceRegistry
Expand Down Expand Up @@ -30,6 +34,9 @@ namespace Rice::detail
private:
bool shouldTrack(bool isOwner) const;

#ifdef RICE_RACTOR_SAFE
std::recursive_mutex mutex_;
#endif
std::map<void*, VALUE> objectMap_;
};
} // namespace Rice::detail
Expand Down
14 changes: 14 additions & 0 deletions rice/detail/InstanceRegistry.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ namespace Rice::detail
template <typename T>
inline VALUE InstanceRegistry::lookup(T* cppInstance, bool isOwner)
{
#ifdef RICE_RACTOR_SAFE
std::lock_guard lock(mutex_);
#endif

if (!this->shouldTrack(isOwner))
{
return Qnil;
Expand All @@ -17,6 +21,10 @@ namespace Rice::detail
template <typename T>
inline void InstanceRegistry::add(T* cppInstance, VALUE rubyInstance, bool isOwner)
{
#ifdef RICE_RACTOR_SAFE
std::lock_guard lock(mutex_);
#endif

if (!this->shouldTrack(isOwner))
{
return;
Expand All @@ -27,11 +35,17 @@ namespace Rice::detail

inline void InstanceRegistry::remove(void* cppInstance)
{
#ifdef RICE_RACTOR_SAFE
std::lock_guard lock(mutex_);
#endif
this->objectMap_.erase(cppInstance);
}

inline void InstanceRegistry::clear()
{
#ifdef RICE_RACTOR_SAFE
std::lock_guard lock(mutex_);
#endif
this->objectMap_.clear();
}

Expand Down
9 changes: 9 additions & 0 deletions test/ruby/ext/extconf.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "mkmf-rice"

# Use the local Rice source (with RICE_RACTOR_SAFE support)
rice_dir = File.expand_path("../../../../rice", __dir__)
$INCFLAGS.prepend("-I#{rice_dir} ")

$CXXFLAGS << " -DRICE_RACTOR_SAFE"

create_makefile("ractor_test_ext")
54 changes: 54 additions & 0 deletions test/ruby/ext/ractor_test_ext.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Minimal Rice extension for testing Ractor safety.
// Compiled with -DRICE_RACTOR_SAFE to enable mutex-protected InstanceRegistry.

#include <rice/rice.hpp>
#include <rice/stl.hpp>

#include <vector>

namespace {
class Counter {
public:
Counter(int start) : value_(start) {}
int value() const { return value_; }
void increment() { value_++; }
private:
int value_;
};

// Object with an expensive constructor — spends significant time inside
// Rice's InstanceRegistry add/lookup cycle, widening the window for
// concurrent access from multiple Ractors.
class HeavyObject {
public:
HeavyObject(int size) {
data_.resize(size);
for (int i = 0; i < size; i++) {
data_[i] = i * i;
}
}

int sum() const {
int s = 0;
for (auto v : data_) { s += v; }
return s;
}

private:
std::vector<int> data_;
};
}

extern "C"
void Init_ractor_test_ext() {
rb_ext_ractor_safe(true);

Rice::define_class<Counter>("Counter")
.define_constructor(Rice::Constructor<Counter, int>())
.define_method("value", &Counter::value)
.define_method("increment", &Counter::increment);

Rice::define_class<HeavyObject>("HeavyObject")
.define_constructor(Rice::Constructor<HeavyObject, int>())
.define_method("sum", &HeavyObject::sum);
}
10 changes: 10 additions & 0 deletions test/ruby/ext_unsafe/extconf.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require "mkmf-rice"

# Use the local Rice source — but WITHOUT RICE_RACTOR_SAFE.
# The InstanceRegistry will have no mutex protection.
rice_dir = File.expand_path("../../../../rice", __dir__)
$INCFLAGS.prepend("-I#{rice_dir} ")

# Intentionally NO -DRICE_RACTOR_SAFE

create_makefile("ractor_test_unsafe_ext")
51 changes: 51 additions & 0 deletions test/ruby/ext_unsafe/ractor_test_unsafe_ext.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Same extension as ractor_test_ext but compiled WITHOUT -DRICE_RACTOR_SAFE.
// Used to verify that concurrent Ractor access segfaults without the mutex.

#include <rice/rice.hpp>
#include <rice/stl.hpp>

#include <vector>

namespace {
class Counter {
public:
Counter(int start) : value_(start) {}
int value() const { return value_; }
void increment() { value_++; }
private:
int value_;
};

class HeavyObject {
public:
HeavyObject(int size) {
data_.resize(size);
for (int i = 0; i < size; i++) {
data_[i] = i * i;
}
}

int sum() const {
int s = 0;
for (auto v : data_) { s += v; }
return s;
}

private:
std::vector<int> data_;
};
}

extern "C"
void Init_ractor_test_unsafe_ext() {
rb_ext_ractor_safe(true);

Rice::define_class<Counter>("Counter")
.define_constructor(Rice::Constructor<Counter, int>())
.define_method("value", &Counter::value)
.define_method("increment", &Counter::increment);

Rice::define_class<HeavyObject>("HeavyObject")
.define_constructor(Rice::Constructor<HeavyObject, int>())
.define_method("sum", &HeavyObject::sum);
}
Loading