Search code examples
rc++14rcpp

How to pass a class object from an Rcpp module back into C++?


I have a C++ codebase that I'm exposing to R using Rcpp modules. Specifically, I use an interface pattern where the class(es) I expose is actually an abstraction layer on top of the underlying object, which is the implementation.

The class(es) I'm dealing with also interact with each other, and have methods that take as arguments shared pointers to objects. I'm having trouble figuring out the right way to expose these methods to R.

Eg here is some code. The TestClass::combine method takes a pointer to another TestClass object and does stuff with it. When I try to compile this code, I get compiler errors (see below) when I add the corresponding interface method ITestClass::combine to the module.

Implementation:

class TestClass
{
public:
    TestClass(int const& n, double const& x)
        : n(n), x(x)
    {}

    const double get_x() {
        return x;
    }

    double combine(std::shared_ptr<TestClass> obj) {
        return x + obj->get_x();
    }

protected:
    int n;
    double x;
};

Interface:

//' @export ITestClass
class ITestClass
{
public:
    ITestClass(int const& in_n, double const& in_x)
        : impl(in_n, in_x)
    {}

    double get_x() {
        return impl.get_x();
    }

    double combine(ITestClass obj) {
        return impl.combine(obj.get_object_ptr());
    }

    std::shared_ptr<TestClass> get_object_ptr() {
        std::shared_ptr<TestClass> ptr(&impl);
        return ptr;
    }

private:
    TestClass impl;
};

RCPP_MODULE(RTestClassModule)
{
    class_<ITestClass>("ITestClass")
        .constructor<int, double>()
        .method("get_x", &ITestClass::get_x, "get_x")
        .method("combine", &ITestClass::combine, "combine"); // this line errors out
}

A sample of the errors I get:

    In file included from C:/Rlib/Rcpp/include/Rcpp/as.h:25,
                    from C:/Rlib/Rcpp/include/RcppCommon.h:168,
                    from C:/Rlib/Rcpp/include/Rcpp.h:27,
                    from interface1.cpp:2:
   C:/Rlib/Rcpp/include/Rcpp/internal/Exporter.h: In instantiation of 'Rcpp::traits::Exporter<T>::Exporter(SEXP) [with T = testpkg::ITestClass; SEXP = SEXPREC*]':
   C:/Rlib/Rcpp/include/Rcpp/as.h:87:41:   required from 'T Rcpp::internal::as(SEXP, Rcpp::traits::r_type_generic_tag) [with T = testpkg::ITestClass; SEXP = SEXPREC*]'
   C:/Rlib/Rcpp/include/Rcpp/as.h:152:31:   required from 'T Rcpp::as(SEXP) [with T = testpkg::ITestClass; SEXP = SEXPREC*]'
   C:/Rlib/Rcpp/include/Rcpp/InputParameter.h:34:43:   required from 'Rcpp::InputParameter<T>::operator T() [with T = testpkg::ITestClass]'
   C:/Rlib/Rcpp/include/Rcpp/module/Module_generated_CppMethod.h:111:69:   required from 'SEXPREC* Rcpp::CppMethod1<Class, RESULT_TYPE, U0>::operator()(Class*, SEXPREC**) [with Class = testpkg::ITestClass; RESULT_TYPE = double; U0 = testpkg::ITestClass; SEXP = SEXPREC*]'
   C:/Rlib/Rcpp/include/Rcpp/module/Module_generated_CppMethod.h:109:10:   required from here
   C:/Rlib/Rcpp/include/Rcpp/internal/Exporter.h:31:31: error: no matching function for 
call to 'testpkg::ITestClass::ITestClass(SEXPREC*&)'
          Exporter( SEXP x ) : t(x){}
                                  ^
   interface1.cpp:17:5: note: candidate: 'testpkg::ITestClass::ITestClass(SEXP, const int&, const double&)'
        ITestClass(SEXP in_date, int const& in_n, double const& in_x)
        ^~~~~~~~~~
   interface1.cpp:17:5: note:   candidate expects 3 arguments, 1 provided
   interface1.cpp:14:7: note: candidate: 'constexpr testpkg::ITestClass::ITestClass(const testpkg::ITestClass&)'
    class ITestClass
          ^~~~~~~~~~
   interface1.cpp:14:7: note:   no known conversion for argument 1 from 'SEXP' {aka 'SEXPREC*'} to 'const testpkg::ITestClass&'
   interface1.cpp:14:7: note: candidate: 'constexpr testpkg::ITestClass::ITestClass(testpkg::ITestClass&&)'
   interface1.cpp:14:7: note:   no known conversion for argument 1 from 'SEXP' {aka 'SEXPREC*'} to 'testpkg::ITestClass&&'

How do I define ITestClass::combine so that it can be called from R?


Solution

  • I found a better solution, one that has the preferred interface for combine and doesn't seem to run into problems with garbage collection.

    A couple of points:

    • Because the underlying API works extensively with shared pointers, rather than storing a TestClass object in impl, I store a std::shared_ptr<TestClass>. This is referenced directly, rather than creating new shared ptrs from scratch (which crash R when they get destroyed).
    • I leverage the internal structure of the returned refclass object from an Rcpp module. In particular, it has a .pointer member that is a pointer to the underlying C++ object. So I can dereference that to get the impl member.

    New interface:

    //' @export ITestClass2
    class ITestClass2
    {
    public:
        ITestClass2(int const& in_n, double const& in_x)
            : impl(in_n, in_x))
        {}
    
        double get_x()
        {
            return impl->get_x();
        }
    
        double combine(Environment obj)
        {
            SEXP objptr = obj[".pointer"];
            ITestClass2* ptr = (ITestClass2*) R_ExternalPtrAddr(objptr);
            return impl->combine(ptr->get_object_ptr());
        }
    
    // this doesn't need to be seen from R
    protected:
        std::shared_ptr<TestClass> get_object_ptr()
        {
            return impl;
        }
    
    private:
        std::shared_ptr<TestClass> impl;
    };
    
    RCPP_MODULE(RTestClassModule2)
    {
        class_<ITestClass2>("ITestClass2")
            .constructor<int, double>()
            .method("get_x", &ITestClass2::get_x, "get_x")
            .method("combine", &ITestClass2::combine, "combine")
        ;
    }
    

    Call this in R as follows:

    obj <- new(ITestClass2, 1, pi)
    obj2 <- new(ITestClass2, 2, exp(1))
    obj$combine(obj2)