Skip to content
Merged

Dev #377

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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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*>`
* Add support for `std::function`. Ruby procs, lambdas, and blocks can be wrapped in `std::function` objects and passed to C++ methods. C++ functions returning `std::function` are automatically wrapped.

Internal:
* Refactor type handling by merging `TypeMapper` into `TypeDetail` and simplifying class hierarchy
Expand Down
3 changes: 3 additions & 0 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ Rice is organized into several subsystems:
[Procs and Blocks](procs_and_blocks.md)
Bridging Ruby procs/blocks and C++ function pointers.

[Incomplete Types](incomplete_types.md)
Support for forward-declared types (PIMPL idiom, opaque handles).

## Thread Safety

Rice itself is thread-safe for reading (method invocation). However:
Expand Down
29 changes: 28 additions & 1 deletion docs/architecture/registries.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,28 @@ public:

## TypeRegistry

Maps C++ types to Ruby classes.
Maps C++ types to Ruby classes. The TypeRegistry serves two important purposes:

### 1. Polymorphism Support

When a C++ method returns a base class pointer that actually points to a derived class, Rice uses RTTI (`typeid`) to look up the derived type in the registry and wrap it as the correct Ruby class. For example:

```cpp
class Base { virtual ~Base() = default; };
class Derived : public Base {};

Base* create() { return new Derived(); } // Returns Derived* as Base*
```

When `create()` is called from Ruby, Rice uses `typeid(*result)` to discover the object is actually a `Derived`, looks it up in the TypeRegistry, and wraps it as `Rb::Derived` instead of `Rb::Base`.

> **Note:** This requires [RTTI](../packaging/build_settings.md#rtti) to be enabled.

### 2. Unregistered Type Detection

As Rice processes `define_class`, `define_constructor`, `define_method`, `define_attr`, etc., it tracks every C++ type it encounters. Types that are used but not yet registered are added to an "unverified" list. At the end of extension initialization, Rice can report which types were referenced but never registered with `define_class<T>()`.

This helps developers catch errors early. Without this, an unregistered type would only fail at runtime when Ruby code actually calls a method that uses that type.

```cpp
class TypeRegistry
Expand All @@ -34,19 +55,25 @@ public:

// Check if a type is registered
bool isDefined(std::type_index type);

// Verify all encountered types are registered
void verify();
};
```

**Used when:**

- `To_Ruby<T>` needs to find the Ruby class to wrap an object
- `From_Ruby<T>` validates that a Ruby object is the correct type
- Polymorphic returns need to find the derived class
- Introspection APIs list all wrapped classes
- Extension loading verifies all types are registered

**Key design:**

- Uses `std::type_index` as the key (from `typeid(T)`)
- Stores both the Ruby class VALUE and the `rb_data_type_t` pointer
- Tracks unverified types for developer feedback
- Thread-safe for reads after initialization

## NativeRegistry
Expand Down
2 changes: 2 additions & 0 deletions docs/bindings/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ void Init_test()

The second template parameter tells Rice about the inheritance relationship, enabling proper type conversions and polymorphic behavior.

> **Note:** Rice requires RTTI to be enabled for polymorphism to work correctly. When a C++ method returns a `Base*` that actually points to a `Derived` object, Rice uses RTTI to wrap it as the correct Ruby class. See [RTTI](../packaging/build_settings.md#rtti) for details.

If you want to create Ruby classes that inherit from wrapped C++ classes and override virtual methods, see the [Directors](directors.md) section.

## Method Chaining
Expand Down
50 changes: 50 additions & 0 deletions docs/bindings/iterators.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,56 @@ end

Where the result will be `[6, 4, 2]`.

## Iterator Requirements

Rice uses `std::iterator_traits` to determine the value type, reference type, and other properties of iterators. This means your iterator must either:

1. Define the standard iterator typedefs (`value_type`, `reference`, `pointer`, `difference_type`, `iterator_category`), or
2. Have a specialization of `std::iterator_traits` defined for it

Most STL iterators and well-designed C++ iterators already satisfy these requirements. However, some libraries define iterators that lack these typedefs.

### Missing Iterator Traits

If you encounter a compiler error like:

```
error C2039: 'value_type': is not a member of 'std::iterator_traits<MyIterator>'
```

You need to provide a `std::iterator_traits` specialization for that iterator. For example:

```cpp
#include <iterator>

// Specialization for an iterator that lacks proper traits
namespace std
{
template<>
struct iterator_traits<MyNamespace::MyIterator>
{
using iterator_category = forward_iterator_tag;
using value_type = MyValueType;
using difference_type = ptrdiff_t;
using pointer = MyValueType*;
using reference = MyValueType&;
};
}
```

Place this specialization before your Rice bindings code, typically right after the includes.

### Common Iterator Categories

Choose the appropriate `iterator_category` based on your iterator's capabilities:

| Category | Operations Supported |
|------------------------------|-----------------------------------------------------|
| `input_iterator_tag` | Read-only, single-pass (`++`, `*`, `==`) |
| `forward_iterator_tag` | Read/write, multi-pass (`++`, `*`, `==`) |
| `bidirectional_iterator_tag` | Forward + backward (`++`, `--`, `*`, `==`) |
| `random_access_iterator_tag` | Bidirectional + random access (`+`, `-`, `[]`, `<`) |

## Enumerator Support (External Iterators)

Ruby supports external iterators via the [Enumerator](https://ruby-doc.org/3.2.2/Enumerator.html) class. The `define_iterator` method automatically adds support for Enumerators.
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ When copying headers, use a specific release tag rather than the master branch t

## Compiler Requirements

Rice requires a C++17 compliant compiler. See [Compiler Settings](packaging/build_settings.md#compiler-settings) for the specific flags needed for each compiler.
Rice requires a C++17 compliant compiler and RTTI to be enabled. See [Compiler Settings](packaging/build_settings.md#compiler-settings) for the specific flags needed for each compiler.
9 changes: 9 additions & 0 deletions docs/packaging/build_settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ For Visual C++, the default exception [model](https://learn.microsoft.com/en-us/

For g++, you must set `-ftemplate-backtrace-limit=0` to avoid compilation errors.

### RTTI

Rice requires RTTI (Run-Time Type Information) to be enabled. RTTI is enabled by default on all major compilers, but if you have explicitly disabled it, you must re-enable it:

- **GCC/Clang**: `-frtti` (default is enabled; do not use `-fno-rtti`)
- **MSVC**: `/GR` (default is enabled; do not use `/GR-`)

RTTI is essential for Rice's polymorphism support. When a C++ method returns a base class pointer that actually points to a derived class, Rice uses `typeid` to determine the correct Ruby class to wrap it as. Without RTTI, all objects would be wrapped as their static (declared) type rather than their actual runtime type.

## Linker Settings

Ruby extensions are shared libraries that the Ruby interpreter loads at runtime using `dlopen`. These extensions reference symbols from the Ruby C API (such as `rb_define_class`, `rb_funcall`, etc.) that are provided by the Ruby interpreter itself. Since these symbols are not available at link time, the linker must be told to allow unresolved symbols.
Expand Down
122 changes: 122 additions & 0 deletions docs/stl/function.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# std::function

Rice supports `std::function`, allowing C++ code to accept and return callable objects that can interoperate with Ruby.

## Automatic Registration

Rice automatically defines Ruby wrapper classes for `std::function` types when it encounters them in a C++ API. You do not need to manually register them.

For example, if you have a C++ function:

```cpp
std::function<int(int, int)> getAdder()
{
return [](int a, int b) { return a + b; };
}
```

And bind it with Rice:

```cpp
define_module("MyModule")
.define_module_function("get_adder", &getAdder);
```

Rice will automatically create the `Std::Function≺int❨int‚ int❩≻` class and wrap the returned function.

## Using std::function in Ruby

### Calling a Function Returned from C++

When C++ returns a `std::function`, you can call it directly in Ruby:

```ruby
func = MyModule.get_adder
result = func.call(7, 6) # => 13
```

### Creating a std::function from Ruby

You can create a `std::function` wrapper from a Ruby proc, lambda, or block:

```ruby
# Using a proc
proc = Proc.new { |a, b| a * b }
func = Std::FunctionInt.new(proc)
func.call(5, 4) # => 20

# Using a lambda
lamb = ->(a, b) { a + b }
func = Std::FunctionInt.new(lamb)
func.call(3, 8) # => 11

# Using a block
func = Std::FunctionInt.new { |a, b| a - b }
func.call(10, 3) # => 7
```

### Passing std::function to C++

When a C++ function accepts a `std::function` parameter, you must first wrap your Ruby callable in an `std::function` object:

```cpp
// C++ side
int invokeFunction(std::function<int(int, int)> func, int a, int b)
{
return func(a, b);
}
```

```ruby
# Ruby side - correct approach
func = Std::FunctionInt.new { |a, b| a * b }
result = invoke_function(func, 5, 3) # => 15
```

**Important:** You cannot pass a raw Ruby proc, lambda, or block directly to a C++ function that expects a `std::function`. This design simplifies memory management by ensuring the Ruby callable's lifetime is properly tracked.

```ruby
# This will NOT work
proc = Proc.new { |a, b| a * b }
invoke_function(proc, 5, 3) # Raises an exception
```

## Manual Registration

If you prefer a custom class name, use `define_stl_function`:

```cpp
define_stl_function<int(int, int)>("FunctionInt");
```

This creates `Std::FunctionInt` instead of the auto-generated name `Std::Function≺int❨int‚ int❩≻`.

## Methods

The wrapped `std::function` class provides:

| Method | Description |
|--------|-------------|
| `new(callable)` | Creates a new std::function wrapping a Ruby proc, lambda, or block |
| `call(...)` | Invokes the function with the given arguments |
| `callable?` | Returns `true` if the function contains a valid callable target |

## Void Return Type

Functions with void return type are supported. The `call` method returns `nil` in Ruby:

```cpp
define_stl_function<void(double)>();
```

```ruby
func = Std::Function≺void❨double❩≻.new { |x| puts x }
func.call(3.14) # prints 3.14, returns nil
```

## Exception Handling

Exceptions are properly propagated in both directions:

- C++ exceptions thrown during function creation or invocation are translated to Ruby exceptions
- Ruby exceptions raised inside the callable are propagated back through the C++ call stack
17 changes: 10 additions & 7 deletions docs/stl/stl.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Wrapped classes include:

* std::exception
* std::exception_ptr
* std::function
* std::map
* std::multimap
* std::pair
Expand Down Expand Up @@ -52,13 +53,15 @@ Starting in version 4.5, Rice makes use of unicode characters to create class na

The unicode characters are:

| Character | Code Point | Name |
|:---------:|:----------:|:----------------------------|
| : | U+A789 | Modified Letter Colon |
| ≺ | U+227A | Precedes |
| ≻ | U+227B | Succeeds |
| , | U+066C | Arabic Thousands Separator |
| | U+u00A0 | Non breaking Space |
| Character | Code Point | Name |
|:---------:|:----------:|:-----------------------------------|
| ꞉ | U+A789 | Modified Letter Colon |
| ≺ | U+227A | Precedes |
| ≻ | U+227B | Succeeds |
| ‚ | U+066C | Arabic Thousands Separator |
| | U+00A0 | Non breaking Space |
| ❨ | U+2768 | Medium Left Parenthesis Ornament |
| ❩ | U+2769 | Medium Right Parenthesis Ornament |

To use this class in Ruby:

Expand Down
16 changes: 9 additions & 7 deletions docs/types/naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ Starting in version 4.5, Rice makes use of unicode characters to create class na

The unicode characters are:

| Character | Code Point | Name |
|-----------|------------|------|
| : | U+A789 | Modified Letter Colon |
| ≺ | U+227A | Precedes |
| ≻ | U+227B | Succeeds |
| , | U+066C | Arabic Thousands Separator |
| | U+u00A0 | Non breaking Space |
| Character | Code Point | Name |
|:---------:|:----------:|:-----------------------------------|
| ꞉ | U+A789 | Modified Letter Colon |
| ≺ | U+227A | Precedes |
| ≻ | U+227B | Succeeds |
| ‚ | U+066C | Arabic Thousands Separator |
| | U+00A0 | Non breaking Space |
| ❨ | U+2768 | Medium Left Parenthesis Ornament |
| ❩ | U+2769 | Medium Right Parenthesis Ornament |

To use an auto generated class in Ruby:

Expand Down
18 changes: 18 additions & 0 deletions rice/detail/TypeIndexParser.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,18 @@ namespace Rice::detail
std::regex ptrRegex = std::regex(R"(\s+\*)");
base = std::regex_replace(base, ptrRegex, "*");

// Remove spaces before left parentheses
std::regex parenRegex = std::regex(R"(\s+\()");
base = std::regex_replace(base, parenRegex, "(");

// Remove __ptr64
std::regex ptr64Regex(R"(\s*__ptr64\s*)");
base = std::regex_replace(base, ptr64Regex, "");

// Remove calling conventions (__cdecl, __stdcall, __fastcall, etc.)
std::regex callingConventionRegex(R"(\s*__cdecl|__stdcall|__fastcall)");
base = std::regex_replace(base, callingConventionRegex, "");

// Replace " >" with ">"
std::regex trailingAngleBracketSpaceRegex = std::regex(R"(\s+>)");
replaceAll(base, trailingAngleBracketSpaceRegex, ">");
Expand Down Expand Up @@ -227,6 +235,16 @@ namespace Rice::detail
//replaceAll(base, greaterThanRegex, "≻");
this->replaceAll(base, greaterThanRegex, "\u227B");

// Replace ( with Unicode Character (U+2768) - Medium Left Parenthesis Ornament
// This happens in std::function
auto leftParenRegex = std::regex(R"(\()");
this->replaceAll(base, leftParenRegex, "\u2768");

// Replace ) with Unicode Character (U+2769) - Medium Right Parenthesis Ornament
// This happens in std::function
auto rightParenRegex = std::regex(R"(\))");
this->replaceAll(base, rightParenRegex, "\u2769");

// Replace , with Unicode Character (U+066C) - Arabic Thousands Separator
auto commaRegex = std::regex(R"(,\s*)");
this->replaceAll(base, commaRegex, "\u201A");
Expand Down
6 changes: 6 additions & 0 deletions rice/detail/Wrapper.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ namespace Rice::detail
throw std::runtime_error(message);
}

if (protect(rb_obj_is_kind_of, value, rb_cProc))
{
std::string message = "The Ruby object is a proc or lambda and does not wrap a C++ object";
throw std::runtime_error(message);
}

WrapperBase* wrapper = static_cast<WrapperBase*>(RTYPEDDATA_DATA(value));

if (wrapper == nullptr)
Expand Down
1 change: 1 addition & 0 deletions rice/stl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "stl/exception.hpp"
#include "stl/exception_ptr.hpp"
#include "stl/function.hpp"
#include "stl/string.hpp"
#include "stl/string_view.hpp"
#include "stl/complex.hpp"
Expand Down
Loading