Skip to content

calling a method on an uninitialized module object aborts the R session #1495

Description

@kevinushey

For a module class without a default constructor, calling new(Class) with no arguments deliberately creates an "uninitialized" object: the generated initialize method falls back to cpp_object_dummy() (R/Module.R), which stores a dummy pointer and notes

## like initializer but a dummy for the case of no default
## constructor.  Will throw an error if the object is used.

The C++ side implements that contract in src/module.cpp:

#define CHECK_DUMMY_OBJ(p) if (p == rcpp_dummy_pointer) throw Rcpp::not_initialized()

But the three method-invocation entry points that use it -- CppMethod__invoke, CppMethod__invoke_void, and CppMethod__invoke_notvoid -- are plain .External functions with no BEGIN_RCPP/END_RCPP. The Rcpp::not_initialized exception escapes the entry point into R's C evaluator, where there is no handler, and the process calls std::terminate(): the entire R session aborts. tryCatch() on the R side never gets a chance. The intended behavior is clearly a regular R error ("C++ object not initialized. (Missing default constructor?)").

The XPtr constructor validation throws (not_compatible, null-pointer checks) in the same functions and in class__newInstance can escape the same way, though those require calling the internal entry points with malformed arguments; the dummy-object path is reachable from ordinary, documented usage.

Reproducible example

repro.cpp:

#include <Rcpp.h>

using namespace Rcpp;

class Widget {
public:
    Widget(double v_) : v(v_) {}

    double value() {
        return v;
    }

private:
    double v;
};

RCPP_MODULE(widget_module) {
    class_<Widget>("Widget")
        .constructor<double>()
        .method("value", &Widget::value);
}

repro.R:

Rcpp::sourceCpp("repro.cpp")

# Widget has no default constructor, so new(Widget) with no arguments
# takes the documented cpp_object_dummy() path
w <- methods::new(Widget)
cat("uninitialized object created\n")

# intended: an R error, "C++ object not initialized. (Missing default
# constructor?)"; actual: the C++ exception escapes .External uncaught
res <- tryCatch(w$value(), error = function(e) conditionMessage(e))
cat("caught R error:", res, "\n")

Output with R 4.6.1 (macOS, Rcpp 1.1.2 -- src/module.cpp is unchanged on current master):

uninitialized object created
libc++abi: terminating due to uncaught exception of type Rcpp::not_initialized: C++ object not initialized. (Missing default constructor?)
Abort trap: 6

Rscript exits with code 134 (SIGABRT); the final cat() is never reached.

I'll follow up with a PR that wraps these entry points in BEGIN_RCPP/END_RCPP, so the exception is converted to the intended R error. (This also lets the XPtr locals in those frames destruct properly on the error path instead of being skipped by the exception's escape.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions