Skip to content
Merged

Dev #382

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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Incompatible Changes:

Think of this as similar to how you would capture a block in Ruby using the &block syntax.

* The `Data_Type<T>::define()` method has been removed. See the [Class Templates](docs/bindings/class_templates.md) documentation for the recommended approach.

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

Expand Down
14 changes: 8 additions & 6 deletions FindRuby.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ supported.

Components
^^^^^^^^^^
.. versionadded:: 4.2.2

.. versionadded:: 4.3

This module supports the following components:

Expand All @@ -32,7 +33,8 @@ are searched for.

Imported Targets
^^^^^^^^^^^^^^^^
.. versionadded:: 4.2.2

.. versionadded:: 4.3

This module defines the following :prop_tgt:`IMPORTED` targets:

Expand Down Expand Up @@ -146,7 +148,7 @@ Finding Ruby and specifying the minimum required version:
find_package(Ruby 3.2)
#]=======================================================================]

#cmake_policy(GET CMP0185 _Ruby_CMP0185)
cmake_policy(GET CMP0185 _Ruby_CMP0185)

if(NOT _Ruby_CMP0185 STREQUAL "NEW")
# Backwards compatibility
Expand Down Expand Up @@ -175,14 +177,14 @@ set(_Ruby_POSSIBLE_EXECUTABLE_NAMES ruby)
# If the user has not specified a Ruby version, create a list of Ruby versions
# to search (newest to oldest). Based on https://www.ruby-lang.org/en/downloads/releases/
if (NOT Ruby_FIND_VERSION_EXACT)
set(_Ruby_SUPPORTED_VERSIONS 40 35 34 33 32)
set(_Ruby_SUPPORTED_VERSIONS 40 34 33 32)
set(_Ruby_UNSUPPORTED_VERSIONS 31 30 27 26 25 24 23 22 21 20)
foreach (_ruby_version IN LISTS _Ruby_SUPPORTED_VERSIONS _Ruby_UNSUPPORTED_VERSIONS)
string(SUBSTRING "${_ruby_version}" 0 1 _ruby_major_version)
string(SUBSTRING "${_ruby_version}" 1 1 _ruby_minor_version)
# Append both rubyX.Y and rubyXY (eg: ruby3.4 ruby34)
list(APPEND _Ruby_POSSIBLE_EXECUTABLE_NAMES
ruby${_ruby_major_version}.${_ruby_minor_version}
list(APPEND _Ruby_POSSIBLE_EXECUTABLE_NAMES
ruby${_ruby_major_version}.${_ruby_minor_version}
ruby${_ruby_major_version}${_ruby_minor_version})
endforeach ()
endif ()
Expand Down
84 changes: 84 additions & 0 deletions docs/architecture/incomplete_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,92 @@ Since Rice only stores pointers and never copies incomplete types by value, it d
- Cannot access members of incomplete types directly
- The incomplete type must be registered before any function using it is called

## Smart Pointers to Incomplete Types

While Rice supports raw pointers and references to incomplete types, **smart pointers** (`std::shared_ptr`, `std::unique_ptr`, etc.) require special handling.

### The Problem

Smart pointers need to instantiate their deleter at compile time. When you create a `std::shared_ptr<T>` from a raw `T*`, the template must generate code to `delete` the pointer - which requires `T` to be a complete type:

```cpp
class Impl; // Forward declaration - incomplete

// This will cause a compiler warning/error:
// "deletion of pointer to incomplete type; no destructor called"
std::shared_ptr<Impl> ptr(new Impl); // Can't instantiate deleter!
```

### Rice's Solution

Rice's `define_shared_ptr<T>()` function uses `is_complete_v<T>` to detect incomplete types and skip registering constructors that would require the complete type:

```cpp
// From rice/stl/shared_ptr.ipp
if constexpr (detail::is_complete_v<T> && !std::is_void_v<T>)
{
result.define_constructor(Constructor<SharedPtr_T, T*>(),
Arg("value").takeOwnership());
}
```

This means:
- **Complete types**: Full smart pointer support including construction from raw pointers
- **Incomplete types**: Smart pointer type is registered, but constructors taking `T*` are skipped

### Passing Existing Smart Pointers

Even without the `T*` constructor, you can still pass around existing smart pointers that were created on the C++ side (where the complete type is available):

```cpp
class Widget {
public:
struct Impl;
std::shared_ptr<Impl> getImpl(); // Returns existing shared_ptr - OK!
void setImpl(std::shared_ptr<Impl> impl); // Accepts existing shared_ptr - OK!
private:
std::shared_ptr<Impl> pImpl_;
};
```

### Custom Smart Pointer Types

If you're wrapping a library with its own smart pointer type (like OpenCV's `cv::Ptr`), apply the same pattern:

```cpp
template<typename T>
Data_Type<CustomPtr<T>> define_custom_ptr()
{
// ... setup ...

// Only define T* constructor for complete types
if constexpr (detail::is_complete_v<T> && !std::is_void_v<T>)
{
result.define_constructor(Constructor<CustomPtr<T>, T*>(),
Arg("ptr").takeOwnership());
}

return result;
}
```

### Using is_complete_v

Rice provides the `detail::is_complete_v<T>` trait for detecting incomplete types:

```cpp
#include <rice/rice.hpp>

class Complete { int x; };
class Incomplete;

static_assert(Rice::detail::is_complete_v<Complete> == true);
static_assert(Rice::detail::is_complete_v<Incomplete> == false);
```

## See Also

- [Smart Pointers](smart_pointers.md) - Implementing support for custom smart pointer types
- [Pointers](../bindings/pointers.md) - General pointer handling in Rice
- [References](../bindings/references.md) - Reference handling in Rice
- [Memory Management](../bindings/memory_management.md) - Object lifetime management
4 changes: 4 additions & 0 deletions docs/architecture/smart_pointers.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,7 @@ namespace Rice
}
}
```

## See Also

- [Incomplete Types](incomplete_types.md) - Handling forward-declared types, including smart pointers to incomplete types
46 changes: 23 additions & 23 deletions docs/bindings/class_templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,47 +46,46 @@ typedef Mat_<Vec4i> Mat4i;

A naive approach to wrapping these classes is to define each one separately. Don't do that!

Instead, write a function to create wrappers. A simplified version looks like this:
Instead, write a function template that creates and returns the wrapper:

```cpp
template<typename Data_Type_T, typename _Tp>
inline void Mat__builder(Data_Type_T& klass)
template<typename _Tp>
inline Data_Type<cv::Mat_<_Tp>> Mat__instantiate(VALUE module, const char* name)
{
klass.define_constructor(Constructor<cv::Mat_::Mat_<_Tp>>()).
define_constructor(Constructor<cv::Mat_::Mat_<_Tp>, int, int>(), Arg("_rows"), Arg("_cols")).
return define_class_under<cv::Mat_<_Tp>, cv::Mat>(module, name)
.define_constructor(Constructor<cv::Mat_::Mat_<_Tp>>())
.define_constructor(Constructor<cv::Mat_::Mat_<_Tp>, int, int>(), Arg("_rows"), Arg("_cols"))

template define_iterator<cv::Mat_<_Tp>::iterator(cv::Mat_<_Tp>::*)()>(&cv::Mat_<_Tp>::begin, &cv::Mat_<_Tp>::end, "each").
template define_method<_Tp&(cv::Mat_<_Tp>::*)(int, int)>("[]", &cv::Mat_<_Tp>::operator(), Arg("row"), Arg("col")).
.template define_iterator<typename cv::Mat_<_Tp>::iterator(cv::Mat_<_Tp>::*)()>(&cv::Mat_<_Tp>::begin, &cv::Mat_<_Tp>::end, "each")
.template define_method<_Tp&(cv::Mat_<_Tp>::*)(int, int)>("[]", &cv::Mat_<_Tp>::operator(), Arg("row"), Arg("col"))

define_method("[]=", [](cv::Mat_<_Tp>& self, int row, int column, _Tp& value)
.define_method("[]=", [](cv::Mat_<_Tp>& self, int row, int column, _Tp& value)
{
self(row, column) = value;
});
};
}
```

Then call this function using the `define` method Rice provides:
Then call this function to instantiate each concrete class:

```cpp
VALUE rb_cMat1b = define_class_under<cv::Mat_<unsigned char>, cv::Mat>(rb_mCv, "Mat1b").
define(&Mat__builder<Data_Type<cv::Mat_<unsigned char>>, unsigned char>);
VALUE rb_cMat1b = Mat__instantiate<unsigned char>(rb_mCv, "Mat1b");

VALUE rb_cMat2b = define_class_under<cv::Mat_<cv::Vec<unsigned char, 2>>, cv::Mat>(rb_mCv, "Mat2b").
define(&Mat__builder<Data_Type<cv::Mat_<cv::Vec<unsigned char, 2>>>, cv::Vec<unsigned char, 2>>);
VALUE rb_cMat2b = Mat__instantiate<cv::Vec<unsigned char, 2>>(rb_mCv, "Mat2b");

...
```

There are few things to notice about the above code.

First, by convention, the method is named `"#{template_name}_builder"`. So in this case `Mat__builder`. You may of course name the method anything you want.
First, by convention, the function is named `"#{template_name}_instantiate"`. So in this case `Mat__instantiate`. You may of course name the function anything you want.

Second, the `template` keyword needs to be used in front of methods:

```cpp
template define_iterator<cv::Mat_<_Tp>::iterator(cv::Mat_<_Tp>::*)()>(&cv::Mat_<_Tp>::begin, &cv::Mat_<_Tp>::end, "each").
.template define_iterator<typename cv::Mat_<_Tp>::iterator(cv::Mat_<_Tp>::*)()>(&cv::Mat_<_Tp>::begin, &cv::Mat_<_Tp>::end, "each")

template define_method<_Tp&(cv::Mat_<_Tp>::*)(int, int)>("[]", &cv::Mat_<_Tp>::operator(), Arg("row"), Arg("col")).
.template define_method<_Tp&(cv::Mat_<_Tp>::*)(int, int)>("[]", &cv::Mat_<_Tp>::operator(), Arg("row"), Arg("col"))
```

Third, the array constructor cannot be wrapped because it uses a template parameter that is not defined:
Expand All @@ -99,11 +98,12 @@ explicit Mat_(const std::array<_Tp, _Nm>& arr, bool copyData=false);
Fourth, the `operator()` is mapped to two Ruby methods, `[]` and `[]=`.

```cpp
template define_method<_Tp&(cv::Mat_<_Tp>::*)(int, int)>("[]", &cv::Mat_<_Tp>::operator(), Arg("row"), Arg("col")).
define_method("[]=", [](cv::Mat_<_Tp>& self, int row, int column, _Tp& value)
{
self(row, column) = value;
});
.template define_method<_Tp&(cv::Mat_<_Tp>::*)(int, int)>("[]", &cv::Mat_<_Tp>::operator(), Arg("row"), Arg("col"))

.define_method("[]=", [](cv::Mat_<_Tp>& self, int row, int column, _Tp& value)
{
self(row, column) = value;
});
```

Once you have created a class builder function it is easy to create new C++ classes from class templates and wrap them in Ruby.
Once you have created an instantiation function it is easy to create new C++ classes from class templates and wrap them in Ruby.
50 changes: 42 additions & 8 deletions docs/bindings/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,40 @@ C++ and Ruby support overriding the same arithmetic operators.
| / | / |
| % | % |

## Unary Operators

C++ supports unary versions of `+`, `-`, `~`, and `!`. Ruby uses special method names for unary `+` and `-` to distinguish them from their binary counterparts.

| C++ | Ruby | Notes |
|:---:|:----:|:------|
| +a | +@ | Unary plus |
| -a | -@ | Unary minus (negation) |
| ~a | ~ | Bitwise NOT |
| !a | ! | Logical NOT |

Example:

```cpp
class Vector
{
public:
Vector operator-() const; // Unary minus
Vector operator+() const; // Unary plus
};
```

```cpp
define_method("-@", &Vector::operator-);
define_method("+@", &Vector::operator+);
```

In Ruby:

```ruby
v = Vector.new(1, 2, 3)
negated = -v # Calls -@
```

## Assignment Operators

C++ supports overriding assignment operators while Ruby does not. Thus these operators must be mapped to Ruby methods.
Expand All @@ -35,7 +69,7 @@ C++ supports overriding assignment operators while Ruby does not. Thus these ope
| -= | Not overridable | assign_minus |
| *= | Not overridable | assign_multiply |
| /= | Not overridable | assign_divide |
| %= | Not overridable | assign_plus |
| %= | Not overridable | assign_modulus |

## Bitwise Operators

Expand All @@ -57,8 +91,8 @@ C++ and Ruby support overriding the same comparison operators.
|:---:|:----:|
| == | == |
| != | != |
| > | < |
| < | > |
| > | > |
| < | < |
| >= | >= |
| <= | <= |

Expand All @@ -70,18 +104,18 @@ Ruby allows the `!` operator to be overridden but not `&&` or `||`.
|:------:|:----------------:|:------------|
| && | Not overridable | logical_and |
| \|\| | Not overridable | logical_or |
| ! | ! | decrement_pre |
| ! | ! | |

## Increment / Decrement Operators

C++ supports increment and decrement operators while Ruby does not. Thus these operators must be mapped to Ruby methods.

| C++ | Ruby | Ruby Method |
|:----:|:----------------:|:---------------|
| ++a | Not overridable | increment_pre |
| a++ | Not overridable | increment |
| --a | Not overridable | decrement_pre |
| a-- | Not overridable | decrement |
| ++a | Not overridable | increment |
| a++ | Not overridable | increment_post |
| --a | Not overridable | decrement |
| a-- | Not overridable | decrement_post |

## Other Operators

Expand Down
35 changes: 7 additions & 28 deletions rice/Data_Type.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,27 +62,6 @@ namespace Rice
template<typename Constructor_T, typename...Rice_Arg_Ts>
Data_Type<T>& define_constructor(Constructor_T constructor, Rice_Arg_Ts const& ...args);

/*! Runs a function that should define this Data_Types methods and attributes.
* This is useful when creating classes from a C++ class template.
*
* \param builder A function that addes methods/attributes to this class
*
* For example:
* \code
* void builder(Data_Type<Matrix<T, R, C>>& klass)
* {
* klass.define_method...
* return klass;
* }
*
* define_class<<Matrix<T, R, C>>>("Matrix")
* .build(&builder);
*
* \endcode
*/
template<typename Func_T>
Data_Type<T>& define(Func_T func);

//! Register a Director class for this class.
/*! For any class that uses Rice::Director to enable polymorphism
* across the languages, you need to register that director proxy
Expand Down Expand Up @@ -133,11 +112,11 @@ namespace Rice
template<typename Iterator_Func_T>
Data_Type<T>& define_iterator(Iterator_Func_T begin, Iterator_Func_T end, std::string name = "each");

template <typename Attribute_T, typename...Arg_Ts>
Data_Type<T>& define_attr(std::string name, Attribute_T attribute, AttrAccess access = AttrAccess::ReadWrite, const Arg_Ts&...args);
template <typename Attribute_T, typename...Arg_Ts>
Data_Type<T>& define_singleton_attr(std::string name, Attribute_T attribute, AttrAccess access = AttrAccess::ReadWrite, const Arg_Ts&...args);
template <typename Attribute_T, typename Access_T = AttrAccess::ReadWriteType, typename...Arg_Ts>
Data_Type<T>& define_attr(std::string name, Attribute_T attribute, Access_T access = {}, const Arg_Ts&...args);

template <typename Attribute_T, typename Access_T = AttrAccess::ReadWriteType, typename...Arg_Ts>
Data_Type<T>& define_singleton_attr(std::string name, Attribute_T attribute, Access_T access = {}, const Arg_Ts&...args);

#include "cpp_api/shared_methods.hpp"
protected:
Expand All @@ -163,8 +142,8 @@ namespace Rice
template<typename Method_T, typename...Arg_Ts>
void wrap_native_method(VALUE klass, std::string name, Method_T&& function, const Arg_Ts&...args);

template <typename Attribute_T, typename...Arg_Ts>
Data_Type<T>& define_attr_internal(VALUE klass, std::string name, Attribute_T attribute, AttrAccess access, const Arg_Ts&...args);
template <typename Attribute_T, typename Access_T, typename...Arg_Ts>
Data_Type<T>& define_attr_internal(VALUE klass, std::string name, Attribute_T attribute, Access_T access, const Arg_Ts&...args);

private:
template<typename T_>
Expand Down
Loading