diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index b6edb979..ebce45d5 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - ruby: [2.7, '3.0', 3.1, 3.2] + ruby: ['2.7', '3.0', '3.1', '3.2', '3.3'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 268a0a0f..43893db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.2 + +* Support Ruby 3.3.0. +* Split Object.call to an explicit Object.call_kw for calling methods expecting keyword arguments. + ## 4.1 Rice 4.1 builds on the 4.0 release and has a number of improvements that both polish Rice and extend its functionality. However, there are three incompatibilities to know about: diff --git a/include/rice/rice.hpp b/include/rice/rice.hpp index bac1d68d..9ac36c51 100644 --- a/include/rice/rice.hpp +++ b/include/rice/rice.hpp @@ -4672,7 +4672,8 @@ namespace Rice //! Call the Ruby method specified by 'id' on object 'obj'. /*! Pass in arguments (arg1, arg2, ...). The arguments will be converted to - * Ruby objects with to_ruby<>. + * Ruby objects with to_ruby<>. To call methods expecting keyword arguments, + * use call_kw. * * E.g.: * \code @@ -4690,6 +4691,29 @@ namespace Rice template Object call(Identifier id, Arg_Ts... args) const; + //! Call the Ruby method specified by 'id' on object 'obj'. + /*! Pass in arguments (arg1, arg2, ...). The arguments will be converted to + * Ruby objects with to_ruby<>. The final argument must be a Hash and will be treated + * as keyword arguments to the function. + * + * E.g.: + * \code + * Rice::Hash kw; + * kw[":argument"] = String("one") + * Rice::Object obj = x.call_kw("foo", kw); + * \endcode + * + * If a return type is specified, the return value will automatically be + * converted to that type as long as 'from_ruby' exists for that type. + * + * E.g.: + * \code + * float ret = x.call_kw("foo", kw); + * \endcode + */ + template + Object call_kw(Identifier id, Arg_Ts... args) const; + //! Vectorized call. /*! Calls the method identified by id with the list of arguments * identified by args. @@ -4753,6 +4777,7 @@ namespace Rice } // namespace Rice #endif // Rice__Object_defn__hpp_ + // --------- Object.ipp --------- #ifndef Rice__Object__ipp_ #define Rice__Object__ipp_ @@ -4797,7 +4822,15 @@ namespace Rice easy to duplicate by setting GC.stress to true and calling a constructor that takes multiple values like a std::pair wrapper. */ std::array values = { detail::To_Ruby>().convert(args)... }; - return detail::protect(rb_funcallv_kw, value(), id.id(), (int)values.size(), (const VALUE*)values.data(), RB_PASS_CALLED_KEYWORDS); + return detail::protect(rb_funcallv, value(), id.id(), (int)values.size(), (const VALUE*)values.data()); + } + + template + inline Object Object::call_kw(Identifier id, Arg_Ts... args) const + { + /* IMPORTANT - See call() above */ + std::array values = { detail::To_Ruby>().convert(args)... }; + return detail::protect(rb_funcallv_kw, value(), id.id(), (int)values.size(), (const VALUE*)values.data(), RB_PASS_KEYWORDS); } template @@ -4975,6 +5008,7 @@ namespace Rice::detail #endif // Rice__Object__ipp_ + // ========= Builtin_Object.hpp ========= diff --git a/rice/cpp_api/Object.ipp b/rice/cpp_api/Object.ipp index 1b7fb8d3..fbcda0e8 100644 --- a/rice/cpp_api/Object.ipp +++ b/rice/cpp_api/Object.ipp @@ -41,7 +41,15 @@ namespace Rice easy to duplicate by setting GC.stress to true and calling a constructor that takes multiple values like a std::pair wrapper. */ std::array values = { detail::To_Ruby>().convert(args)... }; - return detail::protect(rb_funcallv_kw, value(), id.id(), (int)values.size(), (const VALUE*)values.data(), RB_PASS_CALLED_KEYWORDS); + return detail::protect(rb_funcallv, value(), id.id(), (int)values.size(), (const VALUE*)values.data()); + } + + template + inline Object Object::call_kw(Identifier id, Arg_Ts... args) const + { + /* IMPORTANT - See call() above */ + std::array values = { detail::To_Ruby>().convert(args)... }; + return detail::protect(rb_funcallv_kw, value(), id.id(), (int)values.size(), (const VALUE*)values.data(), RB_PASS_KEYWORDS); } template @@ -216,4 +224,4 @@ namespace Rice::detail } }; } -#endif // Rice__Object__ipp_ \ No newline at end of file +#endif // Rice__Object__ipp_ diff --git a/rice/cpp_api/Object_defn.hpp b/rice/cpp_api/Object_defn.hpp index 913d57e5..882039af 100644 --- a/rice/cpp_api/Object_defn.hpp +++ b/rice/cpp_api/Object_defn.hpp @@ -170,7 +170,8 @@ namespace Rice //! Call the Ruby method specified by 'id' on object 'obj'. /*! Pass in arguments (arg1, arg2, ...). The arguments will be converted to - * Ruby objects with to_ruby<>. + * Ruby objects with to_ruby<>. To call methods expecting keyword arguments, + * use call_kw. * * E.g.: * \code @@ -188,6 +189,29 @@ namespace Rice template Object call(Identifier id, Arg_Ts... args) const; + //! Call the Ruby method specified by 'id' on object 'obj'. + /*! Pass in arguments (arg1, arg2, ...). The arguments will be converted to + * Ruby objects with to_ruby<>. The final argument must be a Hash and will be treated + * as keyword arguments to the function. + * + * E.g.: + * \code + * Rice::Hash kw; + * kw[":argument"] = String("one") + * Rice::Object obj = x.call_kw("foo", kw); + * \endcode + * + * If a return type is specified, the return value will automatically be + * converted to that type as long as 'from_ruby' exists for that type. + * + * E.g.: + * \code + * float ret = x.call_kw("foo", kw); + * \endcode + */ + template + Object call_kw(Identifier id, Arg_Ts... args) const; + //! Vectorized call. /*! Calls the method identified by id with the list of arguments * identified by args. @@ -250,4 +274,4 @@ namespace Rice extern Object const Undef; } // namespace Rice -#endif // Rice__Object_defn__hpp_ \ No newline at end of file +#endif // Rice__Object_defn__hpp_ diff --git a/test/embed_ruby.cpp b/test/embed_ruby.cpp index aaaa4636..c2506b0c 100644 --- a/test/embed_ruby.cpp +++ b/test/embed_ruby.cpp @@ -15,6 +15,12 @@ void embed_ruby() ruby_init(); ruby_init_loadpath(); +#if RUBY_API_VERSION_MAJOR == 3 && RUBY_API_VERSION_MINOR >= 3 + // Force the prelude / builtins + char *opts[] = { "ruby", "-e;" }; + ruby_options(2, opts); +#endif + initialized__ = true; } } diff --git a/test/test_Attribute.cpp b/test/test_Attribute.cpp index 09cbbfe8..1810500c 100644 --- a/test/test_Attribute.cpp +++ b/test/test_Attribute.cpp @@ -1,4 +1,4 @@ -#include +#include #include "unittest.hpp" #include "embed_ruby.hpp" @@ -60,9 +60,9 @@ TESTCASE(attributes) ASSERT_EXCEPTION_CHECK( Exception, o.call("read_char=", "some text"), - ASSERT_EQUAL("undefined method `read_char=' for :DataStruct", ex.what()) + ASSERT(std::string(ex.what()).find("undefined method `read_char='") == 0) ); - + // Test writeonly attribute result = o.call("write_int=", 5); ASSERT_EQUAL(5, detail::From_Ruby().convert(result.value())); @@ -70,7 +70,7 @@ TESTCASE(attributes) ASSERT_EXCEPTION_CHECK( Exception, o.call("write_int", 3), - ASSERT_EQUAL("undefined method `write_int' for :DataStruct", ex.what()) + ASSERT(std::string(ex.what()).find("undefined method `write_int'") == 0) ); // Test readwrite attribute @@ -101,7 +101,7 @@ TESTCASE(static_attributes) ASSERT_EXCEPTION_CHECK( Exception, c.call("static_string=", true), - ASSERT_EQUAL("undefined method `static_string=' for DataStruct:Class", ex.what()) + ASSERT(std::string(ex.what()).find("undefined method `static_string='") == 0) ); } @@ -127,7 +127,7 @@ TESTCASE(not_defined) { Data_Type c = define_class("DataStruct"); -#ifdef _MSC_VER +#ifdef _MSC_VER const char* message = "Type is not defined with Rice: class `anonymous namespace'::SomeClass"; #else const char* message = "Type is not defined with Rice: (anonymous namespace)::SomeClass"; diff --git a/test/test_Object.cpp b/test/test_Object.cpp index 350e02f4..74f3516e 100644 --- a/test/test_Object.cpp +++ b/test/test_Object.cpp @@ -174,20 +174,29 @@ TESTCASE(call_return_rice_object) TESTCASE(call_with_keywords) { - Module kernel = Module("Kernel"); + Module m(anonymous_module()); + + m.module_eval(R"( + def self.keywords_test(value, exception:) + if exception + raise "An exception!" + end + value + end + )"); Hash keywords; keywords[":exception"] = false; - Object result = kernel.call("Integer", "charlie", keywords); - ASSERT_EQUAL(Qnil, result.value()); + Object result = m.call_kw("keywords_test", "charlie", keywords); + ASSERT_EQUAL("charlie", detail::From_Ruby().convert(result.value())); keywords[":exception"] = true; ASSERT_EXCEPTION_CHECK( Exception, - kernel.call("Integer", "charlie", keywords), - ASSERT_EQUAL("invalid value for Integer(): \"charlie\"", ex.what()) + m.call_kw("keywords_test", "charlie", keywords), + ASSERT_EQUAL("An exception!", ex.what()) ); } @@ -240,4 +249,4 @@ TESTCASE(test_mark) Object o(INT2NUM(42)); rb_gc_start(); ASSERT_EQUAL(42, detail::From_Ruby().convert(o.value())); -} \ No newline at end of file +} diff --git a/test/test_Stl_String.cpp b/test/test_Stl_String.cpp index 0b635abf..7ac39b63 100644 --- a/test/test_Stl_String.cpp +++ b/test/test_Stl_String.cpp @@ -27,7 +27,10 @@ TESTCASE(std_string_to_ruby_encoding) Object object(value); Object encoding = object.call("encoding"); Object encodingName = encoding.call("name"); - ASSERT_EQUAL("ASCII-8BIT", detail::From_Ruby().convert(encodingName)); + std::string result = detail::From_Ruby().convert(encodingName); + if(result != "ASCII-8BIT" && result != "US-ASCII" && result != "UTF-8") { + FAIL("Encoding incorrect", "ASCII-8BIT, US-ASCII, or UTF-8 (Windows)", result); + } } TESTCASE(std_string_to_ruby_encoding_utf8) @@ -71,4 +74,4 @@ TESTCASE(std_string_from_ruby_with_binary) std::string got = detail::From_Ruby().convert(rb_str_new("\000test", 5)); ASSERT_EQUAL(5ul, got.length()); ASSERT_EQUAL(std::string("\000test", 5), got); -} \ No newline at end of file +} diff --git a/test/test_To_From_Ruby.cpp b/test/test_To_From_Ruby.cpp index 29b7990d..0f09bfbc 100644 --- a/test/test_To_From_Ruby.cpp +++ b/test/test_To_From_Ruby.cpp @@ -229,7 +229,7 @@ TESTCASE(unsigned_long_long_from_ruby) ASSERT_EXCEPTION_CHECK( Exception, detail::From_Ruby().convert(rb_str_new2("bad value")), - ASSERT_EQUAL("no implicit conversion from string", ex.what()) + ASSERT(std::string(ex.what()).find("no implicit conversion") == 0) ); } @@ -396,4 +396,4 @@ TESTCASE(char_star_from_ruby) detail::From_Ruby().convert(rb_float_new(11.11)), ASSERT_EQUAL("wrong argument type Float (expected String)", ex.what()) ); -} \ No newline at end of file +} diff --git a/test/unittest.hpp b/test/unittest.hpp index 028d5e5d..6484fd79 100644 --- a/test/unittest.hpp +++ b/test/unittest.hpp @@ -236,7 +236,7 @@ void assert_equal( if constexpr (is_streamable::value && is_streamable::value) { - strm << s_t << " != " << s_u; + strm << s_t << " != " << s_u << " (" << u << ") "; } strm << " at " << file << ":" << line; throw Assertion_Failed(strm.str()); @@ -263,6 +263,14 @@ void assert_not_equal( } } +#define FAIL(message, expect, got) \ + do \ + { \ + std::stringstream strm; \ + strm << message << " expected: " << (expect) << " got: " << (got); \ + throw Assertion_Failed(strm.str()); \ + } while(0) + #define ASSERT_EQUAL(x, y) \ do \ { \