From 2f03363a5cfe3374a71b6eea5515e92ace452263 Mon Sep 17 00:00:00 2001 From: Kevin Ushey Date: Thu, 22 Oct 2026 19:46:43 -0700 Subject: [PATCH 1/2] make finalize() a no-op for never-constructed module objects An object created without arguments for a class without a default constructor holds the dummy pointer, which is an environment rather than an external pointer. Its finalize() method passed it to CppObject__finalize, which failed, and the failure surfaced as "Expecting an external pointer: [type=environment]" during garbage collection. --- ChangeLog | 8 ++++++++ R/Module.R | 10 +++++++--- inst/tinytest/test_module.R | 7 +++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 364fc2244..6e29646d1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2026-10-22 Kevin Ushey + + * R/Module.R (cpp_refMethods): Make finalize() a no-op for a module + object that was never constructed (created without arguments for a class + without a default constructor); it used to fail on the dummy pointer, and + the failure surfaced as an error message at garbage collection + * inst/tinytest/test_module.R: Add test + 2026-10-22 Michael Sumner * R/00_classes.R: Define C++Field, C++OverloadedMethods and C++Constructor diff --git a/R/Module.R b/R/Module.R index a65ef2899..3af546b8a 100644 --- a/R/Module.R +++ b/R/Module.R @@ -400,14 +400,18 @@ method_wrapper <- function( METHOD, where ){ ## create a named list of the R methods to invoke C++ methods ## from the C++ class with pointer xp cpp_refMethods <- function(CLASS, where) { + # an object created without arguments for a class lacking a default + # constructor holds the dummy pointer (see cpp_object_dummy), which is + # not an external pointer: there is nothing to finalize for it finalizer <- eval( substitute( function(){ - .Call( CppObject__finalize, class_pointer , .pointer ) + if( ! identical( .pointer, dummy ) ) + .Call( CppObject__finalize, class_pointer , .pointer ) }, list( - CLASS = CLASS@pointer, CppObject__finalize = CppObject__finalize, - class_pointer = CLASS@pointer + class_pointer = CLASS@pointer, + dummy = .dummyInstancePointer ) ) ) mets <- c( diff --git a/inst/tinytest/test_module.R b/inst/tinytest/test_module.R index db0edfc60..fe8323c17 100644 --- a/inst/tinytest/test_module.R +++ b/inst/tinytest/test_module.R @@ -123,3 +123,10 @@ gctorture(TRUE) res <- gadget$value() gctorture(FALSE) expect_identical( res, c(1, 2, 3) ) + +# test.Module.finalize.dummy <- function(){ +## an object created without arguments for a class lacking a default +## constructor holds the dummy pointer; finalizing it must be a no-op +## rather than an error (which used to surface at garbage collection) +r <- new( ModuleRandomizer ) +expect_identical( r$finalize(), NULL ) From b89fcb91fc54e6939164a5f9511483debe8eeed5 Mon Sep 17 00:00:00 2001 From: Kevin Ushey Date: Thu, 22 Oct 2026 19:58:20 -0700 Subject: [PATCH 2/2] check for the dummy pointer in CppObject__finalize instead of in R --- ChangeLog | 9 +++++---- R/Module.R | 10 +++------- src/module.cpp | 7 +++++++ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6e29646d1..88f09da0f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,9 +1,10 @@ 2026-10-22 Kevin Ushey - * R/Module.R (cpp_refMethods): Make finalize() a no-op for a module - object that was never constructed (created without arguments for a class - without a default constructor); it used to fail on the dummy pointer, and - the failure surfaced as an error message at garbage collection + * src/module.cpp (CppObject__finalize): Make finalize() a no-op for a + module object that was never constructed (created without arguments for a + class without a default constructor); it used to fail on the dummy + pointer, and the failure surfaced as an error message at garbage + collection * inst/tinytest/test_module.R: Add test 2026-10-22 Michael Sumner diff --git a/R/Module.R b/R/Module.R index 3af546b8a..a65ef2899 100644 --- a/R/Module.R +++ b/R/Module.R @@ -400,18 +400,14 @@ method_wrapper <- function( METHOD, where ){ ## create a named list of the R methods to invoke C++ methods ## from the C++ class with pointer xp cpp_refMethods <- function(CLASS, where) { - # an object created without arguments for a class lacking a default - # constructor holds the dummy pointer (see cpp_object_dummy), which is - # not an external pointer: there is nothing to finalize for it finalizer <- eval( substitute( function(){ - if( ! identical( .pointer, dummy ) ) - .Call( CppObject__finalize, class_pointer , .pointer ) + .Call( CppObject__finalize, class_pointer , .pointer ) }, list( + CLASS = CLASS@pointer, CppObject__finalize = CppObject__finalize, - class_pointer = CLASS@pointer, - dummy = .dummyInstancePointer + class_pointer = CLASS@pointer ) ) ) mets <- c( diff --git a/src/module.cpp b/src/module.cpp index d90d81ffb..ca02c5504 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -108,7 +108,14 @@ RCPP_FUN_4(SEXP, CppField__set, XP_Class cl, SEXP field_xp, SEXP obj, SEXP value cl->setProperty(field_xp, obj, value); return R_NilValue; } + +extern SEXP rcpp_dummy_pointer; + RCPP_FUN_2(SEXP, CppObject__finalize, XP_Class cl, SEXP obj) { + // an object created without arguments for a class lacking a default + // constructor holds the dummy pointer: there is nothing to finalize + if (obj == rcpp_dummy_pointer) + return R_NilValue; cl->run_finalizer(obj); return R_NilValue; }