Skip to content

Commit b8cecc9

Browse files
authored
Merge branch 'master' into bugfix/module-boot-exceptions
2 parents ca4cc5c + 49d39ef commit b8cecc9

13 files changed

Lines changed: 330 additions & 14 deletions

File tree

‎.github/workflows/macos.yaml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ jobs:
1515
matrix:
1616
include:
1717
- {os: macos-latest}
18-
- {os: macos-14}
1918
#- {os: ubuntu-latest}
2019
#- {os: ubuntu-24.04-arm}
2120

‎ChangeLog‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,46 @@
1+
2026-10-31 Dirk Eddelbuettel <edd@debian.org>
2+
3+
* .github/workflows/macos.yaml (jobs): Retire macos-14 (but retain
4+
macos-latest)
5+
16
2026-10-29 Kevin Ushey <kevinushey@gmail.com>
27

8+
* inst/include/Rcpp/module/Module_Add_Property.h: Return *this from
9+
the property() overload taking a free-function getter and a member
10+
function setter, which fell off the end and trapped at module load
11+
(#1519)
12+
* inst/tinytest/cpp/Module.cpp: Expose one property per property()
13+
overload
14+
* inst/tinytest/test_module.R: Test them
15+
16+
* src/module.cpp (InternalFunction_invoke): Raise an error when fewer
17+
arguments are supplied than the arity stored in the function pointer's
18+
tag, instead of reading the missing ones from uninitialized memory
19+
(#1521)
20+
* inst/include/Rcpp/module/Module.h (Module::get_function): Store the
21+
arity of module functions in the pointer tag
22+
* inst/include/Rcpp/InternalFunction.h: Idem for InternalFunction
23+
* inst/tinytest/cpp/Module.cpp: Add module functions with formals
24+
* inst/tinytest/test_module.R: Test calls with too few arguments
25+
* inst/tinytest/test_internal_function.R: Idem
26+
* inst/tinytest/test_internal_function_cpp11.R: Idem
27+
28+
* R/00_classes.R (.cppfunction_formals_gets): Find the .External() call
29+
in the body of the function instead of assuming it is the second
30+
element, which it is not once coverage tools have instrumented the body
31+
* R/00_classes.R (.cppfunction_set_external_args): New helper
32+
* inst/tinytest/test_module.R: Test it with an instrumented body
33+
* inst/include/Rcpp/api/meat/as.h (as_module_object_internal): Check
34+
that the argument is a live module object, raising an error for
35+
deserialized, uninitialized and non-module objects instead of handing
36+
out a NULL or garbage address; a new overload taking the expected C++
37+
type also requires the object's class to be that type or one that
38+
derives() from it (#1523)
39+
* inst/include/Rcpp/as.h: Use the new overload when converting to
40+
module classes
41+
* inst/tinytest/cpp/Module.cpp: Add a class deriving from another
42+
* inst/tinytest/test_module.R: Test the new checks
43+
344
* inst/include/Rcpp/Module.h (RCPP_MODULE): Wrap the module boot
445
function in BEGIN_RCPP / END_RCPP so that an exception thrown while the
546
module initializes raises an R error instead of terminating the session
@@ -16,6 +57,15 @@
1657
* DESCRIPTION (Version, Date): Roll micro version and date
1758
* inst/include/Rcpp/config.h: Idem
1859

60+
2026-10-22 Kevin Ushey <kevinushey@gmail.com>
61+
62+
* src/module.cpp (CppObject__finalize): Make finalize() a no-op for a
63+
module object that was never constructed (created without arguments for a
64+
class without a default constructor); it used to fail on the dummy
65+
pointer, and the failure surfaced as an error message at garbage
66+
collection
67+
* inst/tinytest/test_module.R: Add test
68+
1969
2026-10-22 Michael Sumner <mdsumner@gmail.com>
2070

2171
* R/00_classes.R: Define C++Field, C++OverloadedMethods and

‎R/00_classes.R‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,23 @@ setClass( "C++Function",
117117
contains = "function"
118118
)
119119

120-
.cppfunction_formals_gets <- function (fun, envir = environment(fun), value) {
121-
bd <- body(fun)
122-
b2 <- bd[[2L]]
123-
for( i in seq_along(value) ){
124-
b2[[3L+i]] <- as.name( names(value)[i] )
120+
## passes the arguments named in 'names' on from the .External(entry,
121+
## pointer, ...) call in expr; the call is searched for rather than assumed
122+
## to be a top-level statement, as coverage tools wrap each statement
123+
.cppfunction_set_external_args <- function( expr, names ){
124+
if( !is.call( expr ) )
125+
return( expr )
126+
if( identical( expr[[1L]], as.name( ".External" ) ) )
127+
return( as.call( c( as.list( expr )[1:3], lapply( names, as.name ) ) ) )
128+
for( i in seq_along( expr )[-1L] ){
129+
if( is.call( expr[[i]] ) )
130+
expr[[i]] <- .cppfunction_set_external_args( expr[[i]], names )
125131
}
126-
bd[[2]] <- b2
132+
expr
133+
}
134+
135+
.cppfunction_formals_gets <- function (fun, envir = environment(fun), value) {
136+
bd <- .cppfunction_set_external_args( body(fun), names(value) )
127137
f <- fun@.Data
128138
formals(f) <- value
129139
body(f) <- bd

‎inst/include/Rcpp/InternalFunction.h‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,24 @@ namespace Rcpp{
3535

3636
RCPP_GENERATE_CTOR_ASSIGN(InternalFunction_Impl)
3737

38+
// the arity goes in the pointer tag, where InternalFunction_invoke
39+
// checks it against the number of supplied arguments
40+
3841
template <typename RESULT_TYPE, typename... Args>
3942
InternalFunction_Impl(const std::function<RESULT_TYPE(Args...)> &fun) {
43+
Shield<SEXP> arity(Rf_ScalarInteger(static_cast<int>(sizeof...(Args))));
4044
set(
4145
XPtr<Rcpp::InternalFunctionWithStdFunction::CppFunctionBaseFromStdFunction<RESULT_TYPE, Args...> >(
4246
new Rcpp::InternalFunctionWithStdFunction::CppFunctionBaseFromStdFunction<RESULT_TYPE, Args...>(fun),
43-
false
47+
false,
48+
arity
4449
)
4550
);
4651
}
4752
template <typename RESULT_TYPE, typename... T>
4853
InternalFunction_Impl(RESULT_TYPE (*fun)(T...)) {
49-
set(XPtr<CppFunctionN<RESULT_TYPE, T...> >(new CppFunctionN<RESULT_TYPE, T...>(fun), true));
54+
Shield<SEXP> arity(Rf_ScalarInteger(static_cast<int>(sizeof...(T))));
55+
set(XPtr<CppFunctionN<RESULT_TYPE, T...> >(new CppFunctionN<RESULT_TYPE, T...>(fun), true, arity));
5056
}
5157

5258
void update(SEXP){}

‎inst/include/Rcpp/api/meat/as.h‎

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,100 @@
2121
namespace Rcpp{
2222
namespace internal{
2323

24+
// the address of the C++ object held by the module object whose
25+
// environment is env (see cpp_object_initializer() in R/Module.R)
26+
inline void* module_object_address(const Environment& env){
27+
SEXP xp = env.get(".pointer") ;
28+
if( xp == R_NilValue )
29+
throw not_compatible( "Expecting a C++ module object" ) ;
30+
31+
// the dummy object of a class without a default constructor
32+
if( TYPEOF(xp) != EXTPTRSXP )
33+
throw not_initialized() ;
34+
35+
// e.g. a deserialized object
36+
void* ptr = R_ExternalPtrAddr(xp) ;
37+
if( ptr == NULL )
38+
throw ::Rcpp::exception( "external pointer is not valid" ) ;
39+
40+
return ptr ;
41+
}
42+
43+
#ifndef RCPP_NO_MODULES
44+
// whether the module class cl is the C++ class typeinfo_name, or derives
45+
// from it through class_::derives(), which records each parent by its R
46+
// class name, "Rcpp_" followed by its name in the module
47+
inline bool module_class_is( class_Base* cl, Module* module, const char* typeinfo_name ){
48+
if( cl->has_typeinfo_name(typeinfo_name) )
49+
return true ;
50+
51+
std::vector<class_Base*> todo(1, cl) ;
52+
std::set<class_Base*> seen ;
53+
while( module && !todo.empty() ){
54+
class_Base* current = todo.back() ;
55+
todo.pop_back() ;
56+
if( !seen.insert(current).second )
57+
continue ;
58+
59+
if( current->has_typeinfo_name(typeinfo_name) )
60+
return true ;
61+
62+
for( size_t i = 0; i < current->parents.size(); i++ ){
63+
const std::string& parent = current->parents[i] ;
64+
if( parent.compare(0, 5, "Rcpp_") != 0 )
65+
continue ;
66+
std::string name = parent.substr(5) ;
67+
if( module->has_class(name) )
68+
todo.push_back( module->get_class_pointer(name) ) ;
69+
}
70+
}
71+
return false ;
72+
}
73+
#endif
74+
75+
// packages call this one directly, so it stays; it cannot check the class
2476
inline void* as_module_object_internal(SEXP obj){
77+
if( !Rf_isS4(obj) )
78+
throw not_compatible( "Expecting a C++ module object" ) ;
2579
Environment env(obj) ;
26-
SEXP xp = env.get(".pointer") ;
27-
return R_ExternalPtrAddr(xp );
80+
return module_object_address(env) ;
81+
}
82+
83+
inline void* as_module_object_internal(SEXP obj, const char* typeinfo_name){
84+
if( !Rf_isS4(obj) ){
85+
throw not_compatible(
86+
"Expecting an object of C++ class '%s'",
87+
demangle(typeinfo_name)
88+
) ;
89+
}
90+
Environment env(obj) ;
91+
void* ptr = module_object_address(env) ;
92+
93+
#ifndef RCPP_NO_MODULES
94+
SEXP cppclass = env.get(".cppclass") ;
95+
if( TYPEOF(cppclass) != EXTPTRSXP || R_ExternalPtrAddr(cppclass) == NULL ){
96+
throw not_compatible(
97+
"Expecting an object of C++ class '%s'",
98+
demangle(typeinfo_name)
99+
) ;
100+
}
101+
class_Base* cl = reinterpret_cast<class_Base*>( R_ExternalPtrAddr(cppclass) ) ;
102+
103+
SEXP module_xp = env.get(".module") ;
104+
Module* module = NULL ;
105+
if( TYPEOF(module_xp) == EXTPTRSXP )
106+
module = reinterpret_cast<Module*>( R_ExternalPtrAddr(module_xp) ) ;
107+
108+
if( !module_class_is(cl, module, typeinfo_name) ){
109+
throw not_compatible(
110+
"Expecting an object of C++ class '%s', got an object of class '%s'",
111+
demangle(typeinfo_name),
112+
cl->name
113+
) ;
114+
}
115+
#endif
116+
117+
return ptr ;
28118
}
29119

30120
}

‎inst/include/Rcpp/as.h‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,17 @@ namespace Rcpp {
8989
}
9090

9191
void* as_module_object_internal(SEXP obj);
92+
void* as_module_object_internal(SEXP obj, const char* typeinfo_name);
9293

9394
template <typename T> object<T> as_module_object(SEXP x) {
94-
return (T*) as_module_object_internal(x);
95+
return (T*) as_module_object_internal(x, typeid(T).name());
9596
}
9697

9798
/** handling object<T> */
9899
template <typename T> T as(SEXP x, ::Rcpp::traits::r_type_module_object_const_pointer_tag) {
99100
typedef typename Rcpp::traits::remove_const<T>::type T_NON_CONST;
100-
return const_cast<T>((T_NON_CONST)as_module_object_internal(x));
101+
typedef typename Rcpp::traits::un_pointer<T>::type CLASS;
102+
return const_cast<T>((T_NON_CONST)as_module_object_internal(x, typeid(CLASS).name()));
101103
}
102104

103105
template <typename T> T as(SEXP x, ::Rcpp::traits::r_type_module_object_pointer_tag) {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,12 @@ namespace Rcpp {
164164
}
165165
std::string sign ;
166166
fun->signature( sign, name_.data() ) ;
167+
168+
// checked against the number of supplied arguments by InternalFunction_invoke
169+
Shield<SEXP> arity( Rf_ScalarInteger( fun->nargs() ) ) ;
170+
167171
return List::create(
168-
XPtr<CppFunction>( fun, false ),
172+
XPtr<CppFunction>( fun, false, arity ),
169173
fun->is_void(),
170174
fun->docstring,
171175
sign,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
name_,
8484
new CppProperty_GetPointer_SetMethod<Class,PROP>(GetMethod, SetMethod, docstring)
8585
) ;
86+
return *this ;
8687
}
8788

8889
template <typename PROP>

‎inst/tinytest/cpp/Module.cpp‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,24 @@ class ModuleNum{
8989
int y;
9090
};
9191

92+
class ModuleProperties {
93+
public:
94+
ModuleProperties() : value(0.0) {}
95+
96+
double get() { return value; }
97+
double get_const() const { return value; }
98+
void set(double value_) { value = value_; }
99+
100+
double value;
101+
};
102+
103+
double ModuleProperties_get(ModuleProperties* p) {
104+
return p->value;
105+
}
106+
void ModuleProperties_set(ModuleProperties* p, double value) {
107+
p->value = value;
108+
}
109+
92110
class ModuleNumber{
93111
public:
94112
ModuleNumber() : x(0.0), y(0) {};
@@ -137,6 +155,24 @@ double Test_get_x_pointer(ModuleTest* x) {
137155
return x->value;
138156
}
139157

158+
class ModuleBase {
159+
public:
160+
ModuleBase() : value(1.0) {}
161+
virtual ~ModuleBase() {}
162+
double value;
163+
};
164+
165+
class ModuleDerived : public ModuleBase {
166+
public:
167+
ModuleDerived() { value = 2.0; }
168+
};
169+
170+
RCPP_EXPOSED_CLASS_NODECL(ModuleBase)
171+
172+
double ModuleBase_value(const ModuleBase& x) {
173+
return x.value;
174+
}
175+
140176
class ModuleGadget {
141177
public:
142178
ModuleGadget() {}
@@ -166,6 +202,10 @@ RCPP_MODULE(demoModule) {
166202
function("bla1" , &bla1 );
167203
function("bla2" , &bla2 );
168204

205+
// formals with defaults, and formals shorter than the parameter list
206+
function("foo_formals", &foo, List::create(_["x"] = 2, _["y"] = 10.0));
207+
function("foo_short_formals", &foo, List::create(_["x"] = 2));
208+
169209
function("test_reference", test_reference);
170210
function("test_const_reference", test_const_reference);
171211
function("test_const", test_const);
@@ -193,6 +233,21 @@ RCPP_MODULE(demoModule) {
193233
.property("y", &ModuleNum::getY)
194234
;
195235

236+
// one property per class_::property() overload: getters are member,
237+
// const member or free functions, setters are member or free functions
238+
class_<ModuleProperties>("ModuleProperties")
239+
.constructor()
240+
.property("get", &ModuleProperties::get)
241+
.property("get_const", &ModuleProperties::get_const)
242+
.property("get_free", &ModuleProperties_get, "free getter")
243+
.property("get_set", &ModuleProperties::get, &ModuleProperties::set)
244+
.property("get_const_set", &ModuleProperties::get_const, &ModuleProperties::set)
245+
.property("get_set_free", &ModuleProperties::get, &ModuleProperties_set)
246+
.property("get_const_set_free", &ModuleProperties::get_const, &ModuleProperties_set)
247+
.property("get_free_set", &ModuleProperties_get, &ModuleProperties::set)
248+
.property("get_free_set_free", &ModuleProperties_get, &ModuleProperties_set)
249+
;
250+
196251
class_<ModuleNumber>("ModuleNumber")
197252

198253
.constructor()
@@ -217,6 +272,17 @@ RCPP_MODULE(demoModule) {
217272
.method("get" , &ModuleRandomizer::get)
218273
;
219274

275+
class_<ModuleBase>("ModuleBase")
276+
.constructor()
277+
;
278+
279+
class_<ModuleDerived>("ModuleDerived")
280+
.derives<ModuleBase>("ModuleBase")
281+
.constructor()
282+
;
283+
284+
function("ModuleBase_value", &ModuleBase_value);
285+
220286
class_<ModuleGadget>("ModuleGadget")
221287
.constructor()
222288
.method("value", static_cast<void (ModuleGadget::*)(int)>(&ModuleGadget::value))

‎inst/tinytest/test_internal_function.R‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ Rcpp::sourceCpp("cpp/InternalFunction.cpp")
2424
fun <- getAdd()
2525
expect_equal( fun(10,32), 42 )
2626

27+
## too few arguments raise an error rather than reading the missing ones
28+
## from uninitialized memory (#1521)
29+
expect_error( fun(10), "expecting 2, got 1" )
30+
2731
# test.internal_function_concatenate <- function(){
2832
fun <- getConcatenate()
2933
expect_equal( fun("Hello"," World"), "Hello World" )

0 commit comments

Comments
 (0)