Skip to content

Commit b0bfb82

Browse files
turn exceptions during module initialization into r errors (#1526)
* turn exceptions during module initialization into r errors * restore previous module scope instead of clearing it A module loaded from within another module body (say, via a namespace load whose .onLoad calls loadModule()) would reset the outer scope to null; ModuleScope now saves and restores the previous scope. --------- Co-authored-by: Dirk Eddelbuettel <edd@debian.org>
1 parent 49d39ef commit b0bfb82

5 files changed

Lines changed: 94 additions & 11 deletions

File tree

‎ChangeLog‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
2026-10-31 Dirk Eddelbuettel <edd@debian.org>
32

43
* .github/workflows/macos.yaml (jobs): Retire macos-14 (but retain
@@ -42,6 +41,17 @@
4241
* inst/tinytest/cpp/Module.cpp: Add a class deriving from another
4342
* inst/tinytest/test_module.R: Test the new checks
4443

44+
* inst/include/Rcpp/Module.h (RCPP_MODULE): Wrap the module boot
45+
function in BEGIN_RCPP / END_RCPP so that an exception thrown while the
46+
module initializes raises an R error instead of terminating the session
47+
(#1525)
48+
* inst/include/Rcpp/Module.h (internal::ModuleScope): New RAII guard
49+
restoring the previous module scope however the module body exits
50+
* inst/tinytest/cpp/ModuleInitError.cpp: New module whose initialization
51+
throws
52+
* inst/tinytest/cpp/Module.cpp (Module_scope_is_cleared): New
53+
* inst/tinytest/test_module.R: Test loading the failing module
54+
4555
2026-10-23 Dirk Eddelbuettel <edd@debian.org>
4656

4757
* DESCRIPTION (Version, Date): Roll micro version and date

‎inst/include/Rcpp/Module.h‎

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -610,18 +610,44 @@ namespace Rcpp {
610610

611611
}
612612

613+
namespace Rcpp {
614+
namespace internal {
615+
616+
// makes module the target of class_<>, function(), etc. while an
617+
// RCPP_MODULE body runs, and restores the previous scope however the
618+
// body exits, so a module loaded from within another module body
619+
// does not clobber the outer scope
620+
class ModuleScope {
621+
public:
622+
explicit ModuleScope( Module* module ) : previous( ::getCurrentScope() ){
623+
::setCurrentScope( module ) ;
624+
}
625+
~ModuleScope(){ ::setCurrentScope( previous ) ; }
626+
627+
ModuleScope( const ModuleScope& ) = delete ;
628+
ModuleScope& operator=( const ModuleScope& ) = delete ;
629+
630+
private:
631+
Module* previous ;
632+
} ;
633+
634+
}
635+
}
636+
613637
#define RCPP_MODULE_BOOT(name) _rcpp_module_boot_##name
614638

615-
#define RCPP_MODULE(name) \
616-
void _rcpp_module_##name##_init() ; \
617-
static Rcpp::Module _rcpp_module_##name( # name ) ; \
618-
extern "C" SEXP _rcpp_module_boot_##name(){ \
619-
::setCurrentScope( & _rcpp_module_##name ) ; \
620-
_rcpp_module_##name##_init( ) ; \
621-
Rcpp::XPtr<Rcpp::Module> mod_xp(& _rcpp_module_##name , false); \
622-
::setCurrentScope( 0 ) ; \
623-
return mod_xp ; \
624-
} \
639+
// the boot function is called from R via .Call(), so exceptions thrown by
640+
// the module body must be turned into R errors
641+
#define RCPP_MODULE(name) \
642+
void _rcpp_module_##name##_init() ; \
643+
static Rcpp::Module _rcpp_module_##name( # name ) ; \
644+
extern "C" SEXP _rcpp_module_boot_##name(){ \
645+
BEGIN_RCPP \
646+
Rcpp::internal::ModuleScope _rcpp_module_scope( & _rcpp_module_##name ) ; \
647+
_rcpp_module_##name##_init( ) ; \
648+
return Rcpp::XPtr<Rcpp::Module>( & _rcpp_module_##name , false ) ; \
649+
END_RCPP \
650+
} \
625651
void _rcpp_module_##name##_init()
626652

627653
// silly little dance to suppress a 'defined but not used variable' warning

‎inst/tinytest/cpp/Module.cpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,8 @@ double attr_Test_get_x_pointer(ModuleTest* x) {
310310
return x->value;
311311
}
312312

313+
// [[Rcpp::export]]
314+
bool Module_scope_is_cleared() {
315+
return getCurrentScope() == NULL;
316+
}
317+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// -*- mode: C++; c-indent-level: 4; c-basic-offset: 4; indent-tabs-mode: nil; -*-
2+
//
3+
// ModuleInitError.cpp: Rcpp R/C++ interface class library -- module whose
4+
// initialization throws
5+
//
6+
// Copyright (C) 2026 Kevin Ushey
7+
//
8+
// This file is part of Rcpp.
9+
//
10+
// Rcpp is free software: you can redistribute it and/or modify it
11+
// under the terms of the GNU General Public License as published by
12+
// the Free Software Foundation, either version 2 of the License, or
13+
// (at your option) any later version.
14+
//
15+
// Rcpp is distributed in the hope that it will be useful, but
16+
// WITHOUT ANY WARRANTY; without even the implied warranty of
17+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
// GNU General Public License for more details.
19+
//
20+
// You should have received a copy of the GNU General Public License
21+
// along with Rcpp. If not, see <http://www.gnu.org/licenses/>.
22+
23+
#include <Rcpp.h>
24+
25+
using namespace Rcpp;
26+
27+
class ModuleInitError {
28+
public:
29+
ModuleInitError() {}
30+
};
31+
32+
RCPP_MODULE(moduleInitError) {
33+
// the parent was never exposed, so derives() throws std::range_error
34+
class_<ModuleInitError>("ModuleInitError")
35+
.derives<ModuleInitError>("NoSuchParent")
36+
;
37+
}

‎inst/tinytest/test_module.R‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ expect_equal(r$get(10), x10)
162162
r <- new( ModuleRandomizer )
163163
expect_error( r$get(10L), "not initialized" )
164164

165+
## an exception thrown while a module initializes raises an R error rather
166+
## than terminating the R session, and clears the module scope (#1525)
167+
expect_error( Rcpp::sourceCpp("cpp/ModuleInitError.cpp"), "no such class" )
168+
expect_true( Module_scope_is_cleared() )
169+
165170
# test.Module.flexible.semantics <- function( ){
166171
expect_equal( test_reference( seq(0,10) ), 11L )
167172
expect_equal( test_const_reference( seq(0,10) ), 11L )

0 commit comments

Comments
 (0)