Skip to content
Merged

Dev #376

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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 4.9.2 (unreleased)

Enhancements:
* Add support for incomplete types (PIMPL/opaque handle patterns). Rice now uses `typeid(T*)` for forward-declared types that are never fully defined.
* Add support for `noexcept` functions, static members, and static member functions
* Add support for `Buffer<void*>` and `Pointer<void*>`

Internal:
* Refactor type handling by merging `TypeMapper` into `TypeDetail` and simplifying class hierarchy

## 4.9.1 (2026-01-04)
This release focuses on improving memory management for STL containers and attribute setters.

Expand Down
3 changes: 3 additions & 0 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ Rice is organized into several subsystems:
[Enumerators](enumerators.md)
Implementing Ruby-style iteration for C++ containers.

[Procs and Blocks](procs_and_blocks.md)
Bridging Ruby procs/blocks and C++ function pointers.

## Thread Safety

Rice itself is thread-safe for reading (method invocation). However:
Expand Down
122 changes: 122 additions & 0 deletions docs/architecture/procs_and_blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Procs and Blocks

Rice provides bidirectional support for Ruby procs/blocks and C++ function pointers. This enables two key use cases:

1. **C++ to Ruby**: Wrapping C++ function pointers as Ruby procs
2. **Ruby to C++**: Passing Ruby blocks/procs to C++ code

## C++ Function Pointers to Ruby Procs

When a C++ function returns a function pointer, Rice automatically wraps it as a Ruby `Proc`.

### Example

```cpp
int square(int i)
{
return i * i;
}

auto getSquareFunction()
{
return square; // Returns function pointer
}

// Expose to Ruby
define_module_function("square_proc", getSquareFunction);
```

Ruby code can then call:

```ruby
proc = square_proc
proc.call(9) # => 81
```

### How It Works

The `To_Ruby` specialization for function pointers uses `NativeProc` to create the wrapper:

```cpp
template<typename Return_T, typename ...Parameter_Ts>
class To_Ruby<Return_T(*)(Parameter_Ts...)>
{
VALUE convert(Proc_T proc)
{
return NativeProc<Proc_T>::createRubyProc(proc);
}
};
```

`NativeProc::createRubyProc`:

1. Creates a `NativeProc` instance storing the C++ function pointer
2. Wraps it in a Ruby proc via `rb_proc_new`
3. Attaches a finalizer to clean up the `NativeProc` when the Ruby proc is garbage collected

When Ruby invokes the proc, `NativeProc::resolve` is called which:

1. Converts Ruby arguments to C++ types
2. Calls the stored function pointer
3. Converts the return value back to Ruby

## Ruby Blocks and Procs to C++

Rice *always* converts Ruby blocks to procs. When Ruby passes a block to a method, Rice captures it via `rb_block_proc` and passes it as the last argument:

```cpp
// In Native.ipp
if (protect(rb_block_given_p))
{
std::string key = "arg_" + std::to_string(result.size());
result[key] = protect(rb_block_proc);
}
```

To receive a block in your C++ method, you must:

1. Include a `VALUE` parameter at the end of your function signature
2. Mark that parameter with `Arg("name").setValue()`

If you omit `setValue()`, Rice will attempt to convert the proc using `From_Ruby`, resulting in an error like "Proc cannot be converted to Integer".

### Invoking Procs in C++

Use Rice's `Object` class to invoke the proc. This works uniformly whether Ruby passed a block or an explicit proc:

```cpp
int squareWithProc(int i, VALUE proc)
{
Object result = Object(proc).call("call", i);
return From_Ruby<int>().convert(result);
}

define_module_function("square_with_proc", squareWithProc,
Arg("i"), Arg("proc").setValue());
```

This can be called from Ruby with a block:

```ruby
square_with_proc(7) { |i| i * i } # => 49
```

Or with an explicit proc:

```ruby
proc = Proc.new { |i| i * i }
square_with_proc(4, proc) # => 16
```

## Callbacks

For more complex callback scenarios where C++ code stores and later invokes Ruby procs (such as event handlers or async callbacks), see the [Callbacks](../bindings/callbacks.md) documentation. This covers:

- C-style callback registration
- LibFFI closures for multiple callbacks of the same type
- Passing user data through callbacks

## See Also

- [Callbacks](../bindings/callbacks.md) - C-style callback support
- [Method Binding](method_binding.md) - Native hierarchy including NativeProc and NativeCallback
2 changes: 1 addition & 1 deletion lib/rice/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Rice
VERSION = "4.9.1"
VERSION = "4.9.2"
end
7 changes: 0 additions & 7 deletions rice/Arg.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,6 @@ namespace Rice
//! Returns if the argument should be treated as a value
bool isValue() const;

//! Specifies if the argument should capture a block
virtual Arg& setBlock();

//! Returns if the argument should capture a block
bool isBlock() const;

//! Specifies if the argument is opaque and Rice should not convert it from Ruby to C++ or vice versa.
//! This is useful for callbacks and user provided data paramameters.
virtual Arg& setOpaque();
Expand All @@ -94,7 +88,6 @@ namespace Rice
//! Our saved default value
std::any defaultValue_;
bool isValue_ = false;
bool isBlock_ = false;
bool isKeepAlive_ = false;
bool isOwner_ = false;
bool isOpaque_ = false;
Expand Down
12 changes: 0 additions & 12 deletions rice/Arg.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,6 @@ namespace Rice
return isValue_;
}

inline Arg& Arg::setBlock()
{
isBlock_ = true;
isValue_ = true;
return *this;
}

inline bool Arg::isBlock() const
{
return isBlock_;
}

inline Arg& Arg::setOpaque()
{
isOpaque_ = true;
Expand Down
13 changes: 8 additions & 5 deletions rice/detail/Native.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ namespace Rice::detail
std::map<std::string, VALUE> result;

// Keyword handling
if (rb_keyword_given_p())
if (protect(rb_keyword_given_p))
{
// Keywords are stored in the last element in a hash
size_t actualArgc = argc - 1;
Expand Down Expand Up @@ -344,6 +344,13 @@ namespace Rice::detail
}
}

// If a block is given we assume it maps to the last argument
if (protect(rb_block_given_p))
{
std::string key = "arg_" + std::to_string(result.size());
result[key] = protect(rb_block_proc);
}

return result;
}

Expand Down Expand Up @@ -387,10 +394,6 @@ namespace Rice::detail
{
result[i] = parameter->defaultValueRuby();
}
else if (arg->isBlock() && rb_block_given_p())
{
result[i] = protect(rb_block_proc);
}
else if (validate)
{
std::string message = "Missing argument. Name: " + arg->name + ". Index: " + std::to_string(i) + ".";
Expand Down
13 changes: 11 additions & 2 deletions rice/detail/Parameter.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ namespace Rice::detail
}
}
}
// Incomplete types can't have default values (std::any requires complete types)
else if constexpr (!is_complete_v<intrinsic_type<T>>)
{
// No default value possible for incomplete types
}
else if constexpr (std::is_copy_constructible_v<T>)
{
if (this->arg()->hasDefaultValue())
Expand All @@ -122,7 +127,11 @@ namespace Rice::detail
template<typename T>
inline VALUE Parameter<T>::defaultValueRuby()
{
if constexpr (std::is_constructible_v<std::remove_cv_t<T>, std::remove_cv_t<std::remove_reference_t<T>>&>)
if constexpr (!is_complete_v<intrinsic_type<T>>)
{
// Incomplete types can't have default values (std::any requires complete types)
}
else if constexpr (std::is_constructible_v<std::remove_cv_t<T>, std::remove_cv_t<std::remove_reference_t<T>>&>)
{
// Remember std::is_copy_constructible_v<std::vector<std::unique_ptr<T>>>> returns true. Sigh.
// So special case vector handling
Expand All @@ -144,7 +153,7 @@ namespace Rice::detail
}
}

throw std::runtime_error("No default value set for parameter " + this->arg()->name);
throw std::runtime_error("No default value set or allowed for parameter " + this->arg()->name);
}

template<typename T>
Expand Down
2 changes: 1 addition & 1 deletion rice/detail/Proc.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ namespace Rice::detail

double is_convertible(VALUE value)
{
if (protect(rb_obj_is_proc, value) == Qtrue || protect(rb_proc_lambda_p, value) == Qtrue)
if (protect(rb_obj_is_proc, value) == Qtrue)
{
return Convertible::Exact;
}
Expand Down
6 changes: 6 additions & 0 deletions rice/detail/TypeIndexParser.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ namespace Rice::detail
{
return typeid(T);
}
else if constexpr (std::is_reference_v<T>)
{
// For incomplete reference types, strip the reference and use pointer.
// Can't use typeid(T&) because it still requires complete type on MSVC.
return typeid(std::remove_reference_t<T>*);
}
else
{
return typeid(T*);
Expand Down
7 changes: 7 additions & 0 deletions rice/detail/Wrapper.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ namespace Rice::detail
result = TypedData_Wrap_Struct(klass, rb_data_type, wrapper);
}

// Incomplete types can't be copied/moved, just wrap as reference
else if constexpr (!is_complete_v<T>)
{
wrapper = new Wrapper<T&>(rb_data_type, data);
result = TypedData_Wrap_Struct(klass, rb_data_type, wrapper);
}

// std::is_copy_constructible_v<std::vector<std::unique_ptr<T>>>> returns true. Sigh.
else if constexpr (detail::is_std_vector_v<T>)
{
Expand Down
4 changes: 2 additions & 2 deletions rice/stl/shared_ptr.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ namespace Rice
return !self;
});

if constexpr (!std::is_void_v<T>)
if constexpr (detail::is_complete_v<T> && !std::is_void_v<T>)
{
result.define_constructor(Constructor<SharedPtr_T, typename SharedPtr_T::element_type*>(), Arg("value").takeOwnership());
}

// Setup delegation to forward T's methods via get (only for non-fundamental, non-void types)
if constexpr (!std::is_void_v<T> && !std::is_fundamental_v<T>)
if constexpr (detail::is_complete_v<T> && !std::is_void_v<T> && !std::is_fundamental_v<T>)
{
detail::define_forwarding(result.klass(), Data_Type<T>::klass());
}
Expand Down
4 changes: 2 additions & 2 deletions test/test_Callback.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ TESTCASE(LambdaCallBack)
TESTCASE(BlockCallBack)
{
Module m = define_module("TestingBlock");
m.define_module_function("register_callback", registerCallback, Arg("callback").setBlock()).
m.define_module_function("register_callback", registerCallback, Arg("callback")).
define_module_function("trigger_callback", triggerCallback);

std::string code = R"(register_callback do |an_int, a_double, a_bool, a_string, an_intref|
Expand Down Expand Up @@ -127,7 +127,7 @@ namespace
TESTCASE(FunctionArg)
{
Module m = define_module("TestingFunctionArg");
m.define_module_function("function_arg", functionArg, Arg("i"), Arg("block").setBlock());
m.define_module_function("function_arg", functionArg, Arg("i"), Arg("block"));

std::string code = R"(function_arg(4) do |i|
i * i
Expand Down
Loading