From a61b76cfc9ceb6ad5a1ff92fc34fa621cc62e190 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 27 Jan 2026 15:44:32 -0800 Subject: [PATCH 01/12] Align with latest upstream. --- FindRuby.cmake | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/FindRuby.cmake b/FindRuby.cmake index 9fb06aa7..863f70a7 100644 --- a/FindRuby.cmake +++ b/FindRuby.cmake @@ -17,7 +17,8 @@ supported. Components ^^^^^^^^^^ - .. versionadded:: 4.2.2 + +.. versionadded:: 4.3 This module supports the following components: @@ -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: @@ -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 @@ -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 () From b26ce17deb484c8de0c0375196194d605a314db8 Mon Sep 17 00:00:00 2001 From: cfis Date: Sun, 25 Jan 2026 12:40:53 -0800 Subject: [PATCH 02/12] Simply define_attr to work at compile time and let the compiler deal with all permutations so we can remove runtime checks. --- rice/Data_Type.hpp | 14 ++++----- rice/Data_Type.ipp | 47 +++++++----------------------- rice/detail/NativeAttributeGet.hpp | 12 +++++--- test/test_Attribute.cpp | 33 +++++++++++---------- 4 files changed, 44 insertions(+), 62 deletions(-) diff --git a/rice/Data_Type.hpp b/rice/Data_Type.hpp index 83e36316..73ed21a7 100644 --- a/rice/Data_Type.hpp +++ b/rice/Data_Type.hpp @@ -133,11 +133,11 @@ namespace Rice template Data_Type& define_iterator(Iterator_Func_T begin, Iterator_Func_T end, std::string name = "each"); - template - Data_Type& define_attr(std::string name, Attribute_T attribute, AttrAccess access = AttrAccess::ReadWrite, const Arg_Ts&...args); - - template - Data_Type& define_singleton_attr(std::string name, Attribute_T attribute, AttrAccess access = AttrAccess::ReadWrite, const Arg_Ts&...args); + template + Data_Type& define_attr(std::string name, Attribute_T attribute, Access_T access = {}, const Arg_Ts&...args); + + template + Data_Type& define_singleton_attr(std::string name, Attribute_T attribute, Access_T access = {}, const Arg_Ts&...args); #include "cpp_api/shared_methods.hpp" protected: @@ -163,8 +163,8 @@ namespace Rice template void wrap_native_method(VALUE klass, std::string name, Method_T&& function, const Arg_Ts&...args); - template - Data_Type& define_attr_internal(VALUE klass, std::string name, Attribute_T attribute, AttrAccess access, const Arg_Ts&...args); + template + Data_Type& define_attr_internal(VALUE klass, std::string name, Attribute_T attribute, Access_T access, const Arg_Ts&...args); private: template diff --git a/rice/Data_Type.ipp b/rice/Data_Type.ipp index 8f029847..c1620802 100644 --- a/rice/Data_Type.ipp +++ b/rice/Data_Type.ipp @@ -337,61 +337,36 @@ namespace Rice } template - template - inline Data_Type& Data_Type::define_attr(std::string name, Attribute_T attribute, AttrAccess access, const Arg_Ts&...args) + template + inline Data_Type& Data_Type::define_attr(std::string name, Attribute_T attribute, Access_T access, const Arg_Ts&...args) { - return this->define_attr_internal(this->klass_, name, std::forward(attribute), access, args...); + return this->define_attr_internal(this->klass_, name, std::forward(attribute), access, args...); } template - template - inline Data_Type& Data_Type::define_singleton_attr(std::string name, Attribute_T attribute, AttrAccess access, const Arg_Ts&...args) + template + inline Data_Type& Data_Type::define_singleton_attr(std::string name, Attribute_T attribute, Access_T access, const Arg_Ts&...args) { VALUE singleton = detail::protect(rb_singleton_class, this->value()); - return this->define_attr_internal(singleton, name, std::forward(attribute), access, args...); + return this->define_attr_internal(singleton, name, std::forward(attribute), access, args...); } template - template - inline Data_Type& Data_Type::define_attr_internal(VALUE klass, std::string name, Attribute_T attribute, AttrAccess access, const Arg_Ts&...args) + template + inline Data_Type& Data_Type::define_attr_internal(VALUE klass, std::string name, Attribute_T attribute, Access_T, const Arg_Ts&...args) { using Attr_T = typename detail::attribute_traits::attr_type; // Define attribute getter - if (access == AttrAccess::ReadWrite || access == AttrAccess::Read) + if constexpr (std::is_same_v || std::is_same_v) { detail::NativeAttributeGet::define(klass, name, std::forward(attribute), args...); } // Define attribute setter - // Define attribute setter - if (access == AttrAccess::ReadWrite || access == AttrAccess::Write) + if constexpr (std::is_same_v || std::is_same_v) { - // This seems super hacky - must be a better way? - constexpr bool checkWriteAccess = !std::is_reference_v && - !std::is_pointer_v && - !std::is_fundamental_v && - !std::is_enum_v; - - if constexpr (std::is_const_v) - { - throw std::runtime_error("Cannot define attribute writer for a const attribute: " + name); - } - // Attributes are set using assignment operator like this: - // myInstance.attribute = newvalue - else if constexpr (checkWriteAccess && !std::is_assignable_v) - { - throw std::runtime_error("Cannot define attribute writer for a non assignable attribute: " + name); - } - // From_Ruby returns a copy of the value for non-reference and non-pointers, thus needs to be copy constructable - else if constexpr (checkWriteAccess && !std::is_copy_constructible_v) - { - throw std::runtime_error("Cannot define attribute writer for a non copy constructible attribute: " + name); - } - else - { - detail::NativeAttributeSet::define(klass, name, std::forward(attribute), args...); - } + detail::NativeAttributeSet::define(klass, name, std::forward(attribute), args...); } return *this; diff --git a/rice/detail/NativeAttributeGet.hpp b/rice/detail/NativeAttributeGet.hpp index cd897153..e4049c53 100644 --- a/rice/detail/NativeAttributeGet.hpp +++ b/rice/detail/NativeAttributeGet.hpp @@ -3,11 +3,15 @@ namespace Rice { - enum class AttrAccess + struct AttrAccess { - ReadWrite, - Read, - Write + struct ReadWriteType {}; + struct ReadType {}; + struct WriteType {}; + + static constexpr ReadWriteType ReadWrite{}; + static constexpr ReadType Read{}; + static constexpr WriteType Write{}; }; namespace detail diff --git a/test/test_Attribute.cpp b/test/test_Attribute.cpp index f803732f..73cb737e 100644 --- a/test/test_Attribute.cpp +++ b/test/test_Attribute.cpp @@ -242,11 +242,12 @@ TESTCASE(const_attribute) Data_Type c = define_class("DataStruct") .define_constructor(Constructor()); - ASSERT_EXCEPTION_CHECK( - std::exception, - c.define_attr("const_int", &DataStruct::constInt), - ASSERT_EQUAL(ex.what(), "Cannot define attribute writer for a const attribute: const_int") - ); + // This now fails at compile time + // ASSERT_EXCEPTION_CHECK( + // std::exception, + // c.define_attr("const_int", &DataStruct::constInt), + // ASSERT_EQUAL(ex.what(), "Cannot define attribute writer for a const attribute: const_int") + //); c.define_attr("const_int", &DataStruct::constInt, AttrAccess::Read); Data_Object o = c.call("new"); @@ -269,11 +270,12 @@ TESTCASE(not_assignable) Data_Type c = define_class("DataStruct") .define_constructor(Constructor()); - ASSERT_EXCEPTION_CHECK( - std::exception, - c.define_attr("not_assignable", &DataStruct::notAssignable), - ASSERT_EQUAL(ex.what(), "Cannot define attribute writer for a non assignable attribute: not_assignable") - ); + // Now fails at compile time + // ASSERT_EXCEPTION_CHECK( + // std::exception, + // c.define_attr("not_assignable", &DataStruct::notAssignable), + // ASSERT_EQUAL(ex.what(), "Cannot define attribute writer for a non assignable attribute: not_assignable") + //); c.define_attr("not_assignable", &DataStruct::notAssignable, AttrAccess::Read); Data_Object notAssignable = notAssignableClass.call("new"); @@ -298,11 +300,12 @@ TESTCASE(not_copyable) Data_Type c = define_class("DataStruct") .define_constructor(Constructor()); - ASSERT_EXCEPTION_CHECK( - std::exception, - c.define_attr("not_copyable", &DataStruct::notCopyable), - ASSERT_EQUAL(ex.what(), "Cannot define attribute writer for a non copy constructible attribute: not_copyable") - ); + // This is now a compile time error + //ASSERT_EXCEPTION_CHECK( + // std::exception, + // c.define_attr("not_copyable", &DataStruct::notCopyable), + // ASSERT_EQUAL(ex.what(), "Cannot define attribute writer for a non copy constructible attribute: not_copyable") + //); c.define_attr("not_copyable", &DataStruct::notCopyable, AttrAccess::Read); Data_Object notCopyable = notCopyableClass.call("new"); From 3448904a25bb80ae6eb660d26311a322d82f4ff5 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 27 Jan 2026 22:15:32 -0800 Subject: [PATCH 03/12] Fix increment/decrement operator naming in docs - Pre (++a, --a): increment, decrement (simple names for common case) - Post (a++, a--): increment_post, decrement_post - Also fix ! operator row that incorrectly said "decrement_pre" Co-Authored-By: Claude Opus 4.5 --- docs/bindings/operators.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/bindings/operators.md b/docs/bindings/operators.md index 48fdc8b5..ade0c20a 100644 --- a/docs/bindings/operators.md +++ b/docs/bindings/operators.md @@ -70,7 +70,7 @@ Ruby allows the `!` operator to be overridden but not `&&` or `||`. |:------:|:----------------:|:------------| | && | Not overridable | logical_and | | \|\| | Not overridable | logical_or | -| ! | ! | decrement_pre | +| ! | ! | | ## Increment / Decrement Operators @@ -78,10 +78,10 @@ C++ supports increment and decrement operators while Ruby does not. Thus these o | 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 From 230a17264c886199b6332fc42137c58a0a460837 Mon Sep 17 00:00:00 2001 From: cfis Date: Sat, 31 Jan 2026 15:18:35 -0800 Subject: [PATCH 04/12] Add tests for #klass and default constructor. --- test/test_Data_Type.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/test_Data_Type.cpp b/test/test_Data_Type.cpp index 59e9dbb0..2e5babe8 100644 --- a/test/test_Data_Type.cpp +++ b/test/test_Data_Type.cpp @@ -9,6 +9,11 @@ using namespace Rice; TESTSUITE(Data_Type); +namespace +{ + class KlassTestClass; +} + SETUP(Data_Type) { embed_ruby(); @@ -17,6 +22,7 @@ SETUP(Data_Type) TEARDOWN(Data_Type) { Rice::detail::Registries::instance.types.clearUnverifiedTypes(); + Data_Type::unbind(); rb_gc_start(); } @@ -970,3 +976,34 @@ TESTCASE(pointer_of_pointer_ranges) Object result = m.module_eval(code); ASSERT_EQUAL(21, detail::From_Ruby().convert(result)); } + +namespace +{ + class KlassTestClass + { + }; +} + +TESTCASE(klass) +{ + Class c = define_class("KlassTestClass") + .define_constructor(Constructor()); + + Class klass = Data_Type::klass(); + ASSERT_EQUAL(c, klass); + + String name = klass.name(); + ASSERT_EQUAL("KlassTestClass", name.str()); +} + +TESTCASE(dataType) +{ + Class c = define_class("KlassTestClass") + .define_constructor(Constructor()); + + Data_Type dataType = Data_Type(); + ASSERT_EQUAL(c, dataType); + + String name = dataType.name(); + ASSERT_EQUAL("KlassTestClass", name.str()); +} From f202349d0373a7c53c8cf4f7d5cd94c75b74d5ef Mon Sep 17 00:00:00 2001 From: cfis Date: Sun, 1 Feb 2026 18:24:26 -0800 Subject: [PATCH 05/12] Remove unneeded comments. --- rice/detail/TypeIndexParser.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/rice/detail/TypeIndexParser.hpp b/rice/detail/TypeIndexParser.hpp index c60033db..f51f5aee 100644 --- a/rice/detail/TypeIndexParser.hpp +++ b/rice/detail/TypeIndexParser.hpp @@ -33,11 +33,9 @@ namespace Rice::detail class TypeDetail { public: - // From TypeIndexParser std::string name(); std::string simplifiedName(); - // From TypeMapper VALUE rubyKlass(); std::string rubyName(); From 58044a5e957199a5fad94fd6c7fe8c0ee9d86dc3 Mon Sep 17 00:00:00 2001 From: cfis Date: Sun, 1 Feb 2026 18:55:02 -0800 Subject: [PATCH 06/12] Simplify wrapping C++ template classes. Remove the Data_Type::define method since it doesn't serve a useful purpose. Simplify creating instantiation methods (used to be builder) and calling them. --- CHANGELOG.md | 2 ++ docs/bindings/class_templates.md | 46 ++++++++++++++++---------------- rice/Data_Type.hpp | 21 --------------- rice/Data_Type.ipp | 8 ------ test/test_Template.cpp | 42 +++++++++++++---------------- 5 files changed, 44 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d81ba92..fba9929f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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::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. diff --git a/docs/bindings/class_templates.md b/docs/bindings/class_templates.md index 9abbd2d8..af09aebe 100644 --- a/docs/bindings/class_templates.md +++ b/docs/bindings/class_templates.md @@ -46,47 +46,46 @@ typedef Mat_ 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 -inline void Mat__builder(Data_Type_T& klass) +template +inline Data_Type> Mat__instantiate(VALUE module, const char* name) { - klass.define_constructor(Constructor>()). - define_constructor(Constructor, int, int>(), Arg("_rows"), Arg("_cols")). + return define_class_under, cv::Mat>(module, name) + .define_constructor(Constructor>()) + .define_constructor(Constructor, int, int>(), Arg("_rows"), Arg("_cols")) - template define_iterator::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::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>(rb_mCv, "Mat1b"). - define(&Mat__builder>, unsigned char>); +VALUE rb_cMat1b = Mat__instantiate(rb_mCv, "Mat1b"); -VALUE rb_cMat2b = define_class_under>, cv::Mat>(rb_mCv, "Mat2b"). - define(&Mat__builder>>, cv::Vec>); +VALUE rb_cMat2b = Mat__instantiate>(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::iterator(cv::Mat_<_Tp>::*)()>(&cv::Mat_<_Tp>::begin, &cv::Mat_<_Tp>::end, "each"). +.template define_iterator::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: @@ -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. diff --git a/rice/Data_Type.hpp b/rice/Data_Type.hpp index 73ed21a7..ad9b5ecb 100644 --- a/rice/Data_Type.hpp +++ b/rice/Data_Type.hpp @@ -62,27 +62,6 @@ namespace Rice template Data_Type& 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>& klass) - * { - * klass.define_method... - * return klass; - * } - * - * define_class<>>("Matrix") - * .build(&builder); - * - * \endcode - */ - template - Data_Type& 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 diff --git a/rice/Data_Type.ipp b/rice/Data_Type.ipp index c1620802..7753780a 100644 --- a/rice/Data_Type.ipp +++ b/rice/Data_Type.ipp @@ -194,14 +194,6 @@ namespace Rice return *this; } - template - template - inline Data_Type& Data_Type::define(Function_T func) - { - func(*this); - return *this; - } - template template inline Data_Type& Data_Type::define_director() diff --git a/test/test_Template.cpp b/test/test_Template.cpp index 391fc722..b74cf420 100644 --- a/test/test_Template.cpp +++ b/test/test_Template.cpp @@ -44,10 +44,11 @@ namespace }; } -template -void MyVector_builder(Data_Type_T& klass) +template +Data_Type MyVector_instantiate(const char* name) { - klass.define_constructor(Constructor>()) + return Rice::define_class>(name) + .define_constructor(Constructor>()) .define_method("add", &MyVector::add) .define_method("size", &MyVector::size) .define_attr("empty", &MyVector::empty, Rice::AttrAccess::Read); @@ -55,8 +56,7 @@ void MyVector_builder(Data_Type_T& klass) TESTCASE(my_vector) { - Class C1 = define_class>("MyVecInt"). - define(&MyVector_builder>, int>); + Class C1 = MyVector_instantiate("MyVecInt"); Object o1 = C1.create(); Object result1 = o1.instance_eval("empty"); @@ -72,8 +72,7 @@ TESTCASE(my_vector) result1 = o1.instance_eval("size"); ASSERT_EQUAL(1, detail::From_Ruby().convert(result1.value())); - Class C2 = define_class>("MyVecInt"). - define(&MyVector_builder>, std::string>); + Class C2 = MyVector_instantiate("MyVecInt"); Object o2 = C2.create(); Object result2 = o2.instance_eval("empty"); @@ -118,25 +117,26 @@ namespace }; } -template -void Matrix_builder(Data_Type_T& klass) +template +Data_Type> Matrix_instantiate(const char* name) { - klass.define_constructor(Constructor>()) + return Rice::define_class>(name) + .define_constructor(Constructor>()) .define_method("rows", &Matrix::rows) .define_method("cols", &Matrix::cols); } -template -void Scalar_builder(Data_Type_T& klass) +template +Data_Type> Scalar_instantiate(const char* name) { - klass.define_constructor(Constructor>()) + return Rice::define_class, Matrix>(name) + .define_constructor(Constructor>()) .define_method("size", &Scalar::size); } TESTCASE(matrix) { - Class C = define_class>("Matrixf54"). - define(&Matrix_builder>, float, 5, 4>); + Class C = Matrix_instantiate("Matrixf54"); Object o = C.create(); @@ -146,8 +146,7 @@ TESTCASE(matrix) TESTCASE(duplicate_template) { - Class C1 = define_class>("MatrixFirst"). - define(&Matrix_builder>, float, 6, 4>); + Class C1 = Matrix_instantiate("MatrixFirst"); String name = C1.name(); ASSERT_EQUAL("MatrixFirst", name.str()); @@ -156,8 +155,7 @@ TESTCASE(duplicate_template) bool result = aClass1.is_equal(C1); ASSERT(result); - Class C2 = define_class>("MatrixSecond"). - define(&Matrix_builder>, float, 6, 4>); + Class C2 = Matrix_instantiate("MatrixSecond"); // The first definition name is the one that wins! name = C2.name(); @@ -173,11 +171,9 @@ TESTCASE(duplicate_template) TESTCASE(template_inheritance) { - Class MatrixClass = define_class>("Matrixf51"). - define(&Matrix_builder>, float, 5, 1>); + Class MatrixClass = Matrix_instantiate("Matrixf51"); - Class ScalarClass = define_class, Matrix>("Scalarf5"). - define(&Scalar_builder>, float, 5>); + Class ScalarClass = Scalar_instantiate("Scalarf5"); Object o = ScalarClass.create(); From ee7305a96eeb4a89a6709991edca8895abfccf50 Mon Sep 17 00:00:00 2001 From: cfis Date: Sun, 1 Feb 2026 19:56:01 -0800 Subject: [PATCH 07/12] Correctly validate arrays of objects. --- rice/detail/Type.hpp | 6 ++++++ rice/detail/Type.ipp | 6 ++++++ test/test_Attribute.cpp | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/rice/detail/Type.hpp b/rice/detail/Type.hpp index 72b027b9..f94ccc95 100644 --- a/rice/detail/Type.hpp +++ b/rice/detail/Type.hpp @@ -33,6 +33,12 @@ namespace Rice::detail static bool verify(); }; + template + struct Type + { + static bool verify(); + }; + template void verifyType(); diff --git a/rice/detail/Type.ipp b/rice/detail/Type.ipp index cf338762..a36bcc47 100644 --- a/rice/detail/Type.ipp +++ b/rice/detail/Type.ipp @@ -34,6 +34,12 @@ namespace Rice::detail return Type::verify(); } + template + inline bool Type::verify() + { + return Type::verify(); + } + template void verifyType() { diff --git a/test/test_Attribute.cpp b/test/test_Attribute.cpp index 73cb737e..3ad03952 100644 --- a/test/test_Attribute.cpp +++ b/test/test_Attribute.cpp @@ -83,6 +83,7 @@ namespace NotAssignable notAssignable; NotCopyable notCopyable; char buf[2] = { '0', '1' }; + std::vector vectors[2] = { std::vector{2}, std::vector{3} }; OldEnum oldEnum = OldValue1; NewEnum newEnum = NewEnum::NewValue1; MyClass2* myClass2 = nullptr; @@ -183,6 +184,8 @@ TESTCASE(Enums) .define_attr("oldEnum", &DataStruct::oldEnum) .define_attr("newEnum", &DataStruct::newEnum); + Rice::detail::Registries::instance.types.validateTypes(); + Object o = c.call("new"); DataStruct* dataStruct = detail::From_Ruby().convert(o); ASSERT_NOT_EQUAL(nullptr, dataStruct); @@ -210,6 +213,8 @@ TESTCASE(Array) .define_constructor(Constructor()) .define_attr("buf", &DataStruct::buf, Rice::AttrAccess::Read); + Rice::detail::Registries::instance.types.validateTypes(); + Object o = c.call("new"); DataStruct* dataStruct = detail::From_Ruby().convert(o); ASSERT_NOT_EQUAL(nullptr, dataStruct); @@ -228,6 +233,8 @@ TESTCASE(vector) .define_attr("vector", &VecStruct::vector, Rice::AttrAccess::Read) .define_method("vector_size", &VecStruct::vecSize); + Rice::detail::Registries::instance.types.validateTypes(); + std::string code = R"(struct = VecStruct.new([1, 2]) # Access the attribute array = struct.vector.to_a @@ -237,6 +244,35 @@ TESTCASE(vector) ASSERT_EQUAL(2, detail::From_Ruby().convert(result)); } +TESTCASE(arrayOfvector) +{ + Module m = define_module("Testing"); + + Class c = define_class("DataStruct") + .define_constructor(Constructor()) + .define_attr("vectors", &DataStruct::vectors, Rice::AttrAccess::Read); + + Rice::detail::Registries::instance.types.validateTypes(); + + std::string code = R"(struct = DataStruct.new + struct.vectors.class.name)"; + + Object result = m.module_eval(code); + ASSERT_EQUAL("Rice::Buffer≺vector≺int≻≻", detail::From_Ruby().convert(result)); + + code = R"(struct = DataStruct.new + struct.vectors[0].class.name)"; + + result = m.module_eval(code); + ASSERT_EQUAL("Std::Vector≺int≻", detail::From_Ruby().convert(result)); + + code = R"(struct = DataStruct.new + struct.vectors[1].first)"; + + result = m.module_eval(code); + ASSERT_EQUAL(3, detail::From_Ruby().convert(result)); +} + TESTCASE(const_attribute) { Data_Type c = define_class("DataStruct") From 75f4b24324e84b98e10028ca1fcee94012e0b534 Mon Sep 17 00:00:00 2001 From: cfis Date: Mon, 2 Feb 2026 21:10:14 -0800 Subject: [PATCH 08/12] Update operator documentation. --- docs/bindings/operators.md | 40 +++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/bindings/operators.md b/docs/bindings/operators.md index ade0c20a..d79fd0c5 100644 --- a/docs/bindings/operators.md +++ b/docs/bindings/operators.md @@ -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. @@ -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 @@ -57,8 +91,8 @@ C++ and Ruby support overriding the same comparison operators. |:---:|:----:| | == | == | | != | != | -| > | < | -| < | > | +| > | > | +| < | < | | >= | >= | | <= | <= | From c16432c13e8d8cf0f6a36270b7832557440445d5 Mon Sep 17 00:00:00 2001 From: cfis Date: Mon, 2 Feb 2026 21:11:48 -0800 Subject: [PATCH 09/12] Add test for non-bound classes. --- test/test_Data_Type.cpp | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/test/test_Data_Type.cpp b/test/test_Data_Type.cpp index 2e5babe8..059863dd 100644 --- a/test/test_Data_Type.cpp +++ b/test/test_Data_Type.cpp @@ -11,6 +11,9 @@ TESTSUITE(Data_Type); namespace { + class MyClass; + class MyClass2; + class MyClass3; class KlassTestClass; } @@ -22,6 +25,9 @@ SETUP(Data_Type) TEARDOWN(Data_Type) { Rice::detail::Registries::instance.types.clearUnverifiedTypes(); + Data_Type::unbind(); + Data_Type::unbind(); + Data_Type::unbind(); Data_Type::unbind(); rb_gc_start(); } @@ -298,7 +304,31 @@ TESTCASE(static_singleton_function_lambda) ASSERT_EQUAL(42, detail::From_Ruby().convert(result)); } -namespace { +namespace +{ + class MyClass3 + { + }; +} + +TESTCASE(not_bound) +{ + Module m = define_module("Testing"); + + Data_Type dataType; + dataType. + define_method("something", [](MyClass3&) -> std::string + { + return "Should raise error"; + }); + + std::string code = R"(Object.new.something)"; + String result = m.module_eval(code); + ASSERT_EQUAL("foo", result.c_str()); +} + +namespace +{ class BaseClass { public: @@ -366,7 +396,8 @@ TESTCASE(subclass_override_initializer) ); } -namespace { +namespace +{ float with_reference_defaults_x; std::string with_reference_defaults_str; From f0410b35b40e3724bf9487c3f8c010e0f233fb15 Mon Sep 17 00:00:00 2001 From: cfis Date: Tue, 3 Feb 2026 19:55:19 -0800 Subject: [PATCH 10/12] Fix failing test. --- test/test_Data_Type.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/test_Data_Type.cpp b/test/test_Data_Type.cpp index 059863dd..77644440 100644 --- a/test/test_Data_Type.cpp +++ b/test/test_Data_Type.cpp @@ -318,14 +318,24 @@ TESTCASE(not_bound) Data_Type dataType; dataType. define_method("something", [](MyClass3&) -> std::string - { - return "Should raise error"; - }); + { + return "Should raise error"; + }); std::string code = R"(Object.new.something)"; - String result = m.module_eval(code); - ASSERT_EQUAL("foo", result.c_str()); -} + +#ifdef _MSC_VER + std::string message = "Type is not defined with Rice: class `anonymous namespace'::MyClass3"; +#else + std::string message = "Type is not defined with Rice: (anonymous namespace)::MyClass3"; +#endif + + ASSERT_EXCEPTION_CHECK( + Exception, + m.module_eval(code), + ASSERT_EQUAL(message, ex.what()) + ); +} namespace { From 2ff8a65647eb0f41fe859811a4bb800139d743e1 Mon Sep 17 00:00:00 2001 From: cfis Date: Tue, 3 Feb 2026 19:55:41 -0800 Subject: [PATCH 11/12] Update docs on smart pointers and incomplete types. --- docs/architecture/incomplete_types.md | 84 +++++++++++++++++++++++++++ docs/architecture/smart_pointers.md | 4 ++ 2 files changed, 88 insertions(+) diff --git a/docs/architecture/incomplete_types.md b/docs/architecture/incomplete_types.md index 9c0a6161..745587e7 100644 --- a/docs/architecture/incomplete_types.md +++ b/docs/architecture/incomplete_types.md @@ -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` 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 ptr(new Impl); // Can't instantiate deleter! +``` + +### Rice's Solution + +Rice's `define_shared_ptr()` function uses `is_complete_v` 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 && !std::is_void_v) +{ + result.define_constructor(Constructor(), + 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 getImpl(); // Returns existing shared_ptr - OK! + void setImpl(std::shared_ptr impl); // Accepts existing shared_ptr - OK! +private: + std::shared_ptr 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 +Data_Type> define_custom_ptr() +{ + // ... setup ... + + // Only define T* constructor for complete types + if constexpr (detail::is_complete_v && !std::is_void_v) + { + result.define_constructor(Constructor, T*>(), + Arg("ptr").takeOwnership()); + } + + return result; +} +``` + +### Using is_complete_v + +Rice provides the `detail::is_complete_v` trait for detecting incomplete types: + +```cpp +#include + +class Complete { int x; }; +class Incomplete; + +static_assert(Rice::detail::is_complete_v == true); +static_assert(Rice::detail::is_complete_v == 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 diff --git a/docs/architecture/smart_pointers.md b/docs/architecture/smart_pointers.md index 9e9c3899..acf02633 100644 --- a/docs/architecture/smart_pointers.md +++ b/docs/architecture/smart_pointers.md @@ -412,3 +412,7 @@ namespace Rice } } ``` + +## See Also + +- [Incomplete Types](incomplete_types.md) - Handling forward-declared types, including smart pointers to incomplete types From 2c92ddb4f180698704c07f7b95a4763d222314a9 Mon Sep 17 00:00:00 2001 From: cfis Date: Tue, 3 Feb 2026 19:56:04 -0800 Subject: [PATCH 12/12] Use method_missing instead of forwards for more flexibility. --- rice/detail/Forwards.hpp | 18 ------------ rice/detail/Forwards.ipp | 60 ---------------------------------------- rice/rice.hpp | 3 -- rice/stl/shared_ptr.ipp | 12 ++++++-- rice/stl/unique_ptr.ipp | 12 ++++++-- 5 files changed, 20 insertions(+), 85 deletions(-) delete mode 100644 rice/detail/Forwards.hpp delete mode 100644 rice/detail/Forwards.ipp diff --git a/rice/detail/Forwards.hpp b/rice/detail/Forwards.hpp deleted file mode 100644 index 219b3f7f..00000000 --- a/rice/detail/Forwards.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef Rice__detail__Forwards__hpp_ -#define Rice__detail__Forwards__hpp_ - -namespace Rice::detail -{ - // Setup method forwarding from a wrapper class to its wrapped type using Ruby's Forwardable. - // This allows calling methods on the wrapper that get delegated to the wrapped object via - // a "get" method that returns the wrapped object. - // - // Parameters: - // wrapper_klass - The Ruby class to add forwarding to (e.g., SharedPtr_MyClass) - // wrapped_klass - The Ruby class whose methods should be forwarded (e.g., MyClass) - void define_forwarding(VALUE wrapper_klass, VALUE wrapped_klass); -} - -#include "Forwards.ipp" - -#endif // Rice__detail__Forwards__hpp_ diff --git a/rice/detail/Forwards.ipp b/rice/detail/Forwards.ipp deleted file mode 100644 index b30216cc..00000000 --- a/rice/detail/Forwards.ipp +++ /dev/null @@ -1,60 +0,0 @@ -namespace Rice::detail -{ - inline void define_forwarding(VALUE wrapper_klass, VALUE wrapped_klass) - { - protect(rb_require, "forwardable"); - Object forwardable = Object(rb_cObject).const_get("Forwardable"); - Object(wrapper_klass).extend(forwardable.value()); - - // Get wrapper class's method names to avoid conflicts - std::set wrapperMethodSet; - for (Native* native : Registries::instance.natives.lookup(wrapper_klass, NativeKind::Method)) - { - wrapperMethodSet.insert(native->name()); - } - for (Native* native : Registries::instance.natives.lookup(wrapper_klass, NativeKind::AttributeReader)) - { - wrapperMethodSet.insert(native->name()); - } - for (Native* native : Registries::instance.natives.lookup(wrapper_klass, NativeKind::AttributeWriter)) - { - wrapperMethodSet.insert(native->name() + "="); - } - - // Get wrapped class's method names from the registry, including ancestor classes - std::set wrappedMethodSet; - Class klass(wrapped_klass); - while (klass.value() != rb_cObject && klass.value() != Qnil) - { - for (Native* native : Registries::instance.natives.lookup(klass.value(), NativeKind::Method)) - { - wrappedMethodSet.insert(native->name()); - } - for (Native* native : Registries::instance.natives.lookup(klass.value(), NativeKind::AttributeReader)) - { - wrappedMethodSet.insert(native->name()); - } - for (Native* native : Registries::instance.natives.lookup(klass.value(), NativeKind::AttributeWriter)) - { - wrappedMethodSet.insert(native->name() + "="); - } - - klass = klass.superclass(); - } - - // Build the arguments array for def_delegators: [:get, :method1, :method2, ...] - // Skip methods that are already defined on the wrapper class - Array args; - args.push(Symbol("get")); - for (const std::string& method : wrappedMethodSet) - { - if (wrapperMethodSet.find(method) == wrapperMethodSet.end()) - { - args.push(Symbol(method)); - } - } - - // Call def_delegators(*args) - Object(wrapper_klass).vcall("def_delegators", args); - } -} diff --git a/rice/rice.hpp b/rice/rice.hpp index acb92c16..4da8dfaa 100644 --- a/rice/rice.hpp +++ b/rice/rice.hpp @@ -161,9 +161,6 @@ // Dependent on Module, Class, Array and String #include "forward_declares.ipp" -// Dependent on Module, Array, Symbol - used by stl smart pointers -#include "detail/Forwards.hpp" - // For now include libc support - maybe should be separate header file someday #include "libc/file.hpp" diff --git a/rice/stl/shared_ptr.ipp b/rice/stl/shared_ptr.ipp index 9017815b..e5f1fa7e 100644 --- a/rice/stl/shared_ptr.ipp +++ b/rice/stl/shared_ptr.ipp @@ -35,10 +35,18 @@ namespace Rice result.define_constructor(Constructor(), Arg("value").takeOwnership()); } - // Setup delegation to forward T's methods via get (only for non-fundamental, non-void types) + // Forward methods to wrapped T if constexpr (detail::is_complete_v && !std::is_void_v && !std::is_fundamental_v) { - detail::define_forwarding(result.klass(), Data_Type::klass()); + result.instance_eval(R"( + define_method(:method_missing) do |method_name, *args, &block| + self.get.send(method_name, *args, &block) + end + + define_method(:respond_to_missing?) do |method_name, include_private = false| + self.get.send(method_name, *args, &block) + end + )"); } return result; diff --git a/rice/stl/unique_ptr.ipp b/rice/stl/unique_ptr.ipp index 46ed82b3..2d4c5cc6 100644 --- a/rice/stl/unique_ptr.ipp +++ b/rice/stl/unique_ptr.ipp @@ -31,10 +31,18 @@ namespace Rice return !self; }); - // Setup delegation to forward T's methods via get (only for non-fundamental, non-void types) + // Forward methods to wrapped T if constexpr (!std::is_void_v && !std::is_fundamental_v) { - detail::define_forwarding(result.klass(), Data_Type::klass()); + result.instance_eval(R"( + define_method(:method_missing) do |method_name, *args, &block| + self.get.send(method_name, *args, &block) + end + + define_method(:respond_to_missing?) do |method_name, include_private = false| + self.get.send(method_name, *args, &block) + end + )"); } return result;