From 173cad0b336e34419ecf73c96f88417bd5373d93 Mon Sep 17 00:00:00 2001 From: Kevin Ushey Date: Thu, 29 Oct 2026 11:04:10 -0700 Subject: [PATCH] turn exceptions during module initialization into r errors --- ChangeLog | 13 +++++++++ inst/include/Rcpp/Module.h | 39 ++++++++++++++++++++------- inst/tinytest/cpp/Module.cpp | 5 ++++ inst/tinytest/cpp/ModuleInitError.cpp | 37 +++++++++++++++++++++++++ inst/tinytest/test_module.R | 5 ++++ 5 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 inst/tinytest/cpp/ModuleInitError.cpp diff --git a/ChangeLog b/ChangeLog index 402732946..9759112b5 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,16 @@ +2026-10-29 Kevin Ushey + + * inst/include/Rcpp/Module.h (RCPP_MODULE): Wrap the module boot + function in BEGIN_RCPP / END_RCPP so that an exception thrown while the + module initializes raises an R error instead of terminating the session + (#1525) + * inst/include/Rcpp/Module.h (internal::ModuleScope): New RAII guard + clearing the current module scope however the module body exits + * inst/tinytest/cpp/ModuleInitError.cpp: New module whose initialization + throws + * inst/tinytest/cpp/Module.cpp (Module_scope_is_cleared): New + * inst/tinytest/test_module.R: Test loading the failing module + 2026-10-23 Dirk Eddelbuettel * DESCRIPTION (Version, Date): Roll micro version and date diff --git a/inst/include/Rcpp/Module.h b/inst/include/Rcpp/Module.h index 6aeca429d..7f5ff4356 100644 --- a/inst/include/Rcpp/Module.h +++ b/inst/include/Rcpp/Module.h @@ -610,18 +610,37 @@ namespace Rcpp { } +namespace Rcpp { + namespace internal { + + // makes module the target of class_<>, function(), etc. while an + // RCPP_MODULE body runs, and clears it however the body exits + class ModuleScope { + public: + explicit ModuleScope( Module* module ){ ::setCurrentScope( module ) ; } + ~ModuleScope(){ ::setCurrentScope( 0 ) ; } + + ModuleScope( const ModuleScope& ) = delete ; + ModuleScope& operator=( const ModuleScope& ) = delete ; + } ; + + } +} + #define RCPP_MODULE_BOOT(name) _rcpp_module_boot_##name -#define RCPP_MODULE(name) \ - void _rcpp_module_##name##_init() ; \ - static Rcpp::Module _rcpp_module_##name( # name ) ; \ - extern "C" SEXP _rcpp_module_boot_##name(){ \ - ::setCurrentScope( & _rcpp_module_##name ) ; \ - _rcpp_module_##name##_init( ) ; \ - Rcpp::XPtr mod_xp(& _rcpp_module_##name , false); \ - ::setCurrentScope( 0 ) ; \ - return mod_xp ; \ - } \ +// the boot function is called from R via .Call(), so exceptions thrown by +// the module body must be turned into R errors +#define RCPP_MODULE(name) \ + void _rcpp_module_##name##_init() ; \ + static Rcpp::Module _rcpp_module_##name( # name ) ; \ + extern "C" SEXP _rcpp_module_boot_##name(){ \ + BEGIN_RCPP \ + Rcpp::internal::ModuleScope _rcpp_module_scope( & _rcpp_module_##name ) ; \ + _rcpp_module_##name##_init( ) ; \ + return Rcpp::XPtr( & _rcpp_module_##name , false ) ; \ + END_RCPP \ + } \ void _rcpp_module_##name##_init() // silly little dance to suppress a 'defined but not used variable' warning diff --git a/inst/tinytest/cpp/Module.cpp b/inst/tinytest/cpp/Module.cpp index 7f3132820..02473925a 100644 --- a/inst/tinytest/cpp/Module.cpp +++ b/inst/tinytest/cpp/Module.cpp @@ -244,3 +244,8 @@ double attr_Test_get_x_pointer(ModuleTest* x) { return x->value; } +// [[Rcpp::export]] +bool Module_scope_is_cleared() { + return getCurrentScope() == NULL; +} + diff --git a/inst/tinytest/cpp/ModuleInitError.cpp b/inst/tinytest/cpp/ModuleInitError.cpp new file mode 100644 index 000000000..63831c642 --- /dev/null +++ b/inst/tinytest/cpp/ModuleInitError.cpp @@ -0,0 +1,37 @@ +// -*- mode: C++; c-indent-level: 4; c-basic-offset: 4; indent-tabs-mode: nil; -*- +// +// ModuleInitError.cpp: Rcpp R/C++ interface class library -- module whose +// initialization throws +// +// Copyright (C) 2026 Kevin Ushey +// +// This file is part of Rcpp. +// +// Rcpp is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// Rcpp is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Rcpp. If not, see . + +#include + +using namespace Rcpp; + +class ModuleInitError { +public: + ModuleInitError() {} +}; + +RCPP_MODULE(moduleInitError) { + // the parent was never exposed, so derives() throws std::range_error + class_("ModuleInitError") + .derives("NoSuchParent") + ; +} diff --git a/inst/tinytest/test_module.R b/inst/tinytest/test_module.R index db0edfc60..5b7648377 100644 --- a/inst/tinytest/test_module.R +++ b/inst/tinytest/test_module.R @@ -108,6 +108,11 @@ expect_equal(r$get(10), x10) r <- new( ModuleRandomizer ) expect_error( r$get(10L), "not initialized" ) +## an exception thrown while a module initializes raises an R error rather +## than terminating the R session, and clears the module scope (#1525) +expect_error( Rcpp::sourceCpp("cpp/ModuleInitError.cpp"), "no such class" ) +expect_true( Module_scope_is_cleared() ) + # test.Module.flexible.semantics <- function( ){ expect_equal( test_reference( seq(0,10) ), 11L ) expect_equal( test_const_reference( seq(0,10) ), 11L )