Skip to content

Commit 586a198

Browse files
document ownership of objects returned to r from modules (#1534)
Co-authored-by: Dirk Eddelbuettel <edd@debian.org>
1 parent 561ddc5 commit 586a198

4 files changed

Lines changed: 82 additions & 0 deletions

File tree

‎ChangeLog‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@
6666
* inst/tinytest/cpp/Module.cpp (Module_scope_is_cleared): New
6767
* inst/tinytest/test_module.R: Test loading the failing module
6868

69+
* vignettes/rmd/Rcpp-modules.Rmd: Document that objects of exposed
70+
classes returned by value or reference are copied, while a returned
71+
pointer is owned and eventually deleted by R, so that returning this or
72+
another pointer the C++ side still owns deletes the object twice (#1533)
73+
* inst/tinytest/cpp/Module.cpp: Add a class counting its destructions
74+
* inst/tinytest/test_module.R: Test that R deletes a returned pointer
75+
6976
* inst/include/Rcpp/Module.h (internal::module_class_pointer): New
7077
per-library record of the class_<Class> registered for a C++ type
7178
* inst/include/Rcpp/Module.h (internal::make_new_object): Pass it to

‎inst/tinytest/cpp/Module.cpp‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,26 @@ double ModuleBase_value(const ModuleBase& x) {
173173
return x.value;
174174
}
175175

176+
// counts its destructions, to check who owns objects returned from C++
177+
class ModuleOwned {
178+
public:
179+
ModuleOwned(int id_) : id(id_) {}
180+
~ModuleOwned() { destroyed++; }
181+
int id;
182+
static int destroyed;
183+
};
184+
int ModuleOwned::destroyed = 0;
185+
186+
RCPP_EXPOSED_CLASS_NODECL(ModuleOwned)
187+
188+
ModuleOwned* ModuleOwned_create(int id) {
189+
return new ModuleOwned(id);
190+
}
191+
192+
int ModuleOwned_destroyed() {
193+
return ModuleOwned::destroyed;
194+
}
195+
176196
class ModuleGadget {
177197
public:
178198
ModuleGadget() {}
@@ -283,6 +303,13 @@ RCPP_MODULE(demoModule) {
283303

284304
function("ModuleBase_value", &ModuleBase_value);
285305

306+
class_<ModuleOwned>("ModuleOwned")
307+
.field_readonly("id", &ModuleOwned::id)
308+
;
309+
310+
function("ModuleOwned_create", &ModuleOwned_create);
311+
function("ModuleOwned_destroyed", &ModuleOwned_destroyed);
312+
286313
class_<ModuleGadget>("ModuleGadget")
287314
.constructor()
288315
.method("value", static_cast<void (ModuleGadget::*)(int)>(&ModuleGadget::value))

‎inst/tinytest/test_module.R‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,15 @@ 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 pointer returned from C++ is owned by the R object wrapping it, and
171+
## deleted when that object is garbage collected (#1533)
172+
destroyed <- ModuleOwned_destroyed()
173+
owned <- ModuleOwned_create( 7L )
174+
expect_equal( owned$id, 7L )
175+
rm( owned )
176+
invisible( gc() )
177+
expect_equal( ModuleOwned_destroyed(), destroyed + 1L )
178+
170179
## a body that throws after registering some of its classes and functions
171180
## leaves nothing behind, so loading the module again does not end up with
172181
## two copies of what the failed run registered

‎vignettes/rmd/Rcpp-modules.Rmd‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,45 @@ bar$handleFoo(foo)
10131013
#> Got a Foo!
10141014
```
10151015

1016+
### Returning objects of exposed classes
1017+
1018+
With `RCPP_EXPOSED_WRAP` (or `RCPP_EXPOSED_CLASS`), functions and
1019+
methods can also return objects of a class exposed by a module. How
1020+
the object is passed to \proglang{R} depends on the return type:
1021+
1022+
- An object returned by value or by reference is _copied_, using the
1023+
copy constructor of the class, and the \proglang{R} object owns the
1024+
copy.
1025+
- A pointer is not copied: the \proglang{R} object takes _ownership_
1026+
of the object it points to, and deletes it when the \proglang{R}
1027+
object is garbage collected. This is what a function returning a
1028+
newly allocated object needs, like the factories shown above, but
1029+
it means that a pointer may only be returned when nothing else owns
1030+
or deletes the object it points to. Returning `this` (e.g. to chain
1031+
method calls), a pointer to a data member or to an element of a
1032+
container, or the same pointer from more than one call results in the
1033+
object being deleted more than once, which typically crashes
1034+
\proglang{R} during garbage collection.
1035+
1036+
```{Rcpp ownership, eval=FALSE}
1037+
class Counter {
1038+
public:
1039+
Counter() : n(0) {}
1040+
1041+
// fine: R owns, and eventually deletes, the new Counter
1042+
static Counter* create() { return new Counter; }
1043+
1044+
// wrong: the R object returned by inc() would delete *this
1045+
// a second time
1046+
Counter* inc() { n++; return this; }
1047+
1048+
// fine: return void, or a copy, and chain the calls in R
1049+
void increment() { n++; }
1050+
1051+
int n;
1052+
};
1053+
```
1054+
10161055
### Full example
10171056

10181057
<!-- TODO: maybe replace this by something from wls or RcppModels ? -->

0 commit comments

Comments
 (0)