Skip to content

Commit a2467cc

Browse files
run the module body only once per dll (#1528)
* turn exceptions during module initialization into r errors * run the module body only once per dll * Minor ChangeLog cleanup * clear the module when its body throws A body that registers some classes or functions and then throws left them in the static module, and the initialized flag stayed false, so a later successful load appended a second copy of each. Module::clear() drops the registrations in the catch block so the body always runs against an empty module. The C callables registered by Add() cannot be removed, but running the body again just registers them again. --------- Co-authored-by: Dirk Eddelbuettel <edd@debian.org>
1 parent b0bfb82 commit a2467cc

5 files changed

Lines changed: 125 additions & 11 deletions

File tree

‎ChangeLog‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@
4141
* inst/tinytest/cpp/Module.cpp: Add a class deriving from another
4242
* inst/tinytest/test_module.R: Test the new checks
4343

44+
* inst/include/Rcpp/Module.h (RCPP_MODULE): Run the module body only the
45+
first time the boot function is called, since it adds to the static
46+
module and every Module() call boots the module again, which duplicated
47+
constructors and methods (#1527)
48+
* inst/include/Rcpp/module/Module.h (Module::clear): New, removes the
49+
registered classes and functions
50+
* inst/include/Rcpp/Module.h (RCPP_MODULE): Clear the module when the
51+
body throws, so that a later load runs the body against an empty module
52+
instead of appending to what the failed run registered
53+
* inst/tinytest/cpp/ModuleInitRetry.cpp: New module whose first load
54+
fails after registering a class and a function
55+
* inst/tinytest/test_module.R: Test loading a module repeatedly, and
56+
loading one again after its body threw
57+
4458
* inst/include/Rcpp/Module.h (RCPP_MODULE): Wrap the module boot
4559
function in BEGIN_RCPP / END_RCPP so that an exception thrown while the
4660
module initializes raises an R error instead of terminating the session

‎inst/include/Rcpp/Module.h‎

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -637,17 +637,29 @@ namespace Rcpp {
637637
#define RCPP_MODULE_BOOT(name) _rcpp_module_boot_##name
638638

639639
// 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-
} \
640+
// the module body must be turned into R errors; it runs every time the
641+
// module is loaded, but the body adds to the static module, so it may only
642+
// run once, and a body that throws part-way leaves nothing behind so that
643+
// the next load does not append to what it registered
644+
#define RCPP_MODULE(name) \
645+
void _rcpp_module_##name##_init() ; \
646+
static Rcpp::Module _rcpp_module_##name( # name ) ; \
647+
static bool _rcpp_module_##name##_initialized = false ; \
648+
extern "C" SEXP _rcpp_module_boot_##name(){ \
649+
BEGIN_RCPP \
650+
if( ! _rcpp_module_##name##_initialized ){ \
651+
Rcpp::internal::ModuleScope _rcpp_module_scope( & _rcpp_module_##name ) ; \
652+
try { \
653+
_rcpp_module_##name##_init( ) ; \
654+
} catch( ... ) { \
655+
_rcpp_module_##name.clear() ; \
656+
throw ; \
657+
} \
658+
_rcpp_module_##name##_initialized = true ; \
659+
} \
660+
return Rcpp::XPtr<Rcpp::Module>( & _rcpp_module_##name , false ) ; \
661+
END_RCPP \
662+
} \
651663
void _rcpp_module_##name##_init()
652664

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

‎inst/include/Rcpp/module/Module.h‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,22 @@ namespace Rcpp {
203203
classes.insert( CLASS_PAIR( name_ , cptr ) ) ;
204204
}
205205

206+
// drops everything the module body registered; used when the body
207+
// throws, so that running it again starts from an empty module (the
208+
// C callables registered by Add() cannot be removed, but running the
209+
// body again simply registers them again)
210+
inline void clear(){
211+
for( MAP::iterator it = functions.begin(); it != functions.end(); ++it ){
212+
delete it->second ;
213+
}
214+
functions.clear() ;
215+
216+
for( CLASS_MAP::iterator it = classes.begin(); it != classes.end(); ++it ){
217+
delete it->second ;
218+
}
219+
classes.clear() ;
220+
}
221+
206222
inline bool has_function( const std::string& m){ // #nocov start
207223
return functions.find(m) != functions.end() ;
208224
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// -*- mode: C++; c-indent-level: 4; c-basic-offset: 4; indent-tabs-mode: nil; -*-
2+
//
3+
// ModuleInitRetry.cpp: Rcpp R/C++ interface class library -- module whose
4+
// initialization fails the first time only
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 ModuleInitRetry {
28+
public:
29+
ModuleInitRetry() {}
30+
int value() { return 42; }
31+
};
32+
33+
int retry_add_one(int x) {
34+
return x + 1;
35+
}
36+
37+
static int boot_count = 0;
38+
39+
// registers a class and a function, then throws on the first boot only, so
40+
// the second boot must not find what the first one registered
41+
RCPP_MODULE(moduleInitRetry) {
42+
class_<ModuleInitRetry>("ModuleInitRetry")
43+
.constructor()
44+
.method("value", &ModuleInitRetry::value)
45+
;
46+
function("retry_add_one", &retry_add_one);
47+
48+
if (++boot_count == 1)
49+
throw std::runtime_error("first boot fails");
50+
}

‎inst/tinytest/test_module.R‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,18 @@ expect_error( r$get(10L), "not initialized" )
167167
expect_error( Rcpp::sourceCpp("cpp/ModuleInitError.cpp"), "no such class" )
168168
expect_true( Module_scope_is_cleared() )
169169

170+
## a body that throws after registering some of its classes and functions
171+
## leaves nothing behind, so loading the module again does not end up with
172+
## two copies of what the failed run registered
173+
expect_error( Rcpp::sourceCpp("cpp/ModuleInitRetry.cpp"), "first boot fails" )
174+
is_retry_module <- function(dll) is.loaded("_rcpp_module_boot_moduleInitRetry", PACKAGE = dll[["name"]])
175+
dll <- Filter(is_retry_module, getLoadedDLLs())[[1L]]
176+
mod <- Module( "moduleInitRetry", PACKAGE = dll, mustStart = TRUE )
177+
expect_equal( length( mod$ModuleInitRetry@constructors ), 1L )
178+
expect_equal( length( mod$ModuleInitRetry@methods$value@nargs ), 1L )
179+
expect_equal( mod$retry_add_one( 41L ), 42L )
180+
expect_equal( new( mod$ModuleInitRetry )$value(), 42L )
181+
170182
# test.Module.flexible.semantics <- function( ){
171183
expect_equal( test_reference( seq(0,10) ), 11L )
172184
expect_equal( test_const_reference( seq(0,10) ), 11L )
@@ -189,3 +201,13 @@ expect_identical( res, c(1, 2, 3) )
189201
## rather than an error (which used to surface at garbage collection)
190202
r <- new( ModuleRandomizer )
191203
expect_identical( r$finalize(), NULL )
204+
205+
## loading a module again must not re-run its body, which used to add
206+
## every constructor and method to the static module once more (#1527);
207+
## this redefines the module's classes, so it comes last
208+
is_demo_module <- function(dll) is.loaded("_rcpp_module_boot_demoModule", PACKAGE = dll[["name"]])
209+
dll <- Filter(is_demo_module, getLoadedDLLs())[[1L]]
210+
mod <- Module( "demoModule", PACKAGE = dll, mustStart = TRUE )
211+
mod <- Module( "demoModule", PACKAGE = dll, mustStart = TRUE )
212+
expect_equal( length( mod$ModuleWorld@constructors ), 1L )
213+
expect_equal( length( mod$ModuleWorld@methods$greet@nargs ), 1L )

0 commit comments

Comments
 (0)