diff --git a/ChangeLog b/ChangeLog index e8595ef78..8ff2d69b1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +2026-08-03 Kevin Ushey + + * inst/include/Rcpp/module/class.h (invoke): Protect freshly + computed method results while wrapping them in the result list + (#1493) + * inst/include/Rcpp/module/Module.h (invoke): Idem for module + function results + * inst/tinytest/cpp/Module.cpp: Add regression test + * inst/tinytest/test_module.R: Idem 2026-08-03 Kevin Ushey * inst/include/Rcpp/api/meat/proxy.h: Protect fresh SEXPs returned diff --git a/inst/include/Rcpp/module/Module.h b/inst/include/Rcpp/module/Module.h index 43edc20e0..160b53ae5 100644 --- a/inst/include/Rcpp/module/Module.h +++ b/inst/include/Rcpp/module/Module.h @@ -60,8 +60,9 @@ namespace Rcpp { throw std::range_error( "incorrect number of arguments" ) ; } + Shield res( fun->operator()( args ) ) ; return List::create( - _["result"] = fun->operator()( args ), + _["result"] = static_cast(res), _["void"] = fun->is_void() ) ; } diff --git a/inst/include/Rcpp/module/class.h b/inst/include/Rcpp/module/class.h index 7ada6b65e..a8a1a9563 100644 --- a/inst/include/Rcpp/module/class.h +++ b/inst/include/Rcpp/module/class.h @@ -193,7 +193,8 @@ m->operator()( XP(object), args ); return Rcpp::List::create( true ) ; } else { - return Rcpp::List::create( false, m->operator()( XP(object), args ) ) ; + Shield res( m->operator()( XP(object), args ) ) ; + return Rcpp::List::create( false, static_cast(res) ) ; } END_RCPP } diff --git a/inst/tinytest/cpp/Module.cpp b/inst/tinytest/cpp/Module.cpp index 6fa487b0d..b267c18d3 100644 --- a/inst/tinytest/cpp/Module.cpp +++ b/inst/tinytest/cpp/Module.cpp @@ -136,6 +136,27 @@ double Test_get_x_pointer(ModuleTest* x) { return x->value; } +class ModuleGadget { +public: + ModuleGadget() {} + + // void overload: forces dispatch through class_::invoke(), which + // wraps method results as list(voidness, result) + void value(int x) { + (void) x; + } + + // non-void overload: nothing protects the raw SEXP result while + // class_::invoke() allocates the result list + SEXP value() { + SEXP x = Rf_allocVector(REALSXP, 3); + REAL(x)[0] = 1; + REAL(x)[1] = 2; + REAL(x)[2] = 3; + return x; + } +}; + RCPP_MODULE(demoModule) { function("hello", &hello); function("bar" , &bar ); @@ -194,6 +215,12 @@ RCPP_MODULE(demoModule) { .method("get" , &ModuleRandomizer::get) ; + + class_("ModuleGadget") + .constructor() + .method("value", static_cast(&ModuleGadget::value)) + .method("value", static_cast(&ModuleGadget::value)) + ; } // [[Rcpp::export]] diff --git a/inst/tinytest/test_module.R b/inst/tinytest/test_module.R index 960a33e96..5d1c027e4 100644 --- a/inst/tinytest/test_module.R +++ b/inst/tinytest/test_module.R @@ -106,3 +106,14 @@ expect_equal(r$get(10), x10) expect_equal( test_reference( seq(0,10) ), 11L ) expect_equal( test_const_reference( seq(0,10) ), 11L ) expect_equal( test_const( seq(0,10) ), 11L ) + +## mixed-voidness method overloads dispatch through class_::invoke(), +## which must protect the freshly allocated method result while it +## wraps it in the result list (#1493); under gctorture every +## allocation triggers a collection, so a single call exercises the +## unprotected window deterministically +gadget <- new( ModuleGadget ) +gctorture(TRUE) +res <- gadget$value() +gctorture(FALSE) +expect_identical( res, c(1, 2, 3) )