Skip to contents

Allowing Rcpp to Interface with Custom Data Types

The RcppWrap R package provides an example of teaching Rcpp how to move a user-defined C++ type to and from R by writing custom Rcpp::as<>() and Rcpp::wrap() converters. With the converters in place, the custom type can be used directly as the argument and return type of an // [[Rcpp::export]] function.

Usage

To install the package, you must first have a compiler on your system that is compatible with R. For help on obtaining a compiler consult either macOS or Windows guides.

With a compiler in hand, one can then install the package from GitHub by:

# install.packages("remotes")
remotes::install_github("coatless-rd-rcpp/rcpp-custom-wrap-as")
library("RcppWrap")

Implementation Details

When a function takes or returns a built-in type, Rcpp already knows how to translate it between R and C++. For a user-defined type, Rcpp needs two converters: an importer, Rcpp::as<T>(), that builds the C++ value from an R object, and an exporter, Rcpp::wrap(), that turns the C++ value back into an R object. This package defines both for a small Person struct (a name and an age) that maps to and from an R list.

The converters follow the non-intrusive pattern from Rcpp’s extending vignette: the type and the converter declarations are placed in the package header so they are visible everywhere they are needed, including in the autogenerated bindings.

.
├── DESCRIPTION                         # Package metadata
├── NAMESPACE                           # Function and dependency registration
├── R                                   # R functions
   ├── RcppExports.R                   # Autogenerated R to C++ bindings by Rcpp
   └── RcppWrap-package.R              # Package documentation
├── README.md
├── RcppWrap.Rproj
├── inst
   └── include
       └── RcppWrap.h                  # Person struct + as<>/wrap<> declarations
├── man                                 # Package documentation
   ├── RcppWrap-package.Rd
   └── birthday.Rd
└── src                                 # Compiled code
    ├── Makevars                        # Adds inst/include to the include path
    ├── Makevars.win
    ├── RcppExports.cpp                 # Autogenerated R bindings (includes RcppWrap.h)
    └── custom_wrap_as.cpp              # The as<>/wrap<> definitions and exported function

The Package Header

inst/include/RcppWrap.h declares the type and the converters. The ordering matters: RcppCommon.h is included first to get Rcpp’s forward declarations, then the Person struct and the as/wrap declarations are written, and only then is the full Rcpp.h included. Declaring the converters before Rcpp.h is what lets Rcpp pick them up. Keeping them in this header, rather than in a .cpp file, is what makes them visible to the autogenerated src/RcppExports.cpp, which Rcpp::compileAttributes() arranges to include this header.

#ifndef RcppWrap_RcppWrap_H
#define RcppWrap_RcppWrap_H

// Pull in the Rcpp forward declarations, but not yet the full Rcpp.h.
#include <RcppCommon.h>

#include <string>

// The custom C++ type we want to move between R and C++.
struct Person {
  std::string name;
  int age;
};

// Declare, but do not yet define, the importer and exporter for Person.
namespace Rcpp {
  template <> Person as(SEXP obj);
  template <> SEXP wrap(const Person& p);
}

// Now bring in the full Rcpp API.
#include <Rcpp.h>

#endif

Defining the Converters

The definitions live in src/custom_wrap_as.cpp. The importer reads the name and age elements of an R list into a Person, and the exporter builds a named list from a Person. The exported birthday() function then uses both: it receives a Person through the importer, ages it by one year, and returns a Person through the exporter.

#include "RcppWrap.h"

namespace Rcpp {

// Importer: convert an R list(name = , age = ) into a Person.
template <> Person as(SEXP obj) {
  Rcpp::List lst(obj);
  Person p;
  p.name = Rcpp::as<std::string>(lst["name"]);
  p.age  = Rcpp::as<int>(lst["age"]);
  return p;
}

// Exporter: convert a Person into an R list(name = , age = ).
template <> SEXP wrap(const Person& p) {
  return Rcpp::wrap(
    Rcpp::List::create(
      Rcpp::Named("name") = p.name,
      Rcpp::Named("age")  = p.age
    )
  );
}

}

// [[Rcpp::export]]
Person birthday(Person p) {
  p.age += 1;
  return p;
}

The exported function also carries //' roxygen comments (trimmed from the excerpt here) that Rcpp::compileAttributes() copies into R/RcppExports.R to generate the help page. From R the roundtrip looks like:

birthday(list(name = "Jane", age = 30))
#> $name
#> [1] "Jane"
#>
#> $age
#> [1] 31

How the Pieces Fit Together

The declarations and the definitions are deliberately separated. The declarations sit in the package header so that every translation unit that converts a Person, including the generated src/RcppExports.cpp, can see them. The definitions sit in src/custom_wrap_as.cpp, where they are compiled once and linked into the package. So the package can find its own header, src/Makevars and src/Makevars.win add inst/include to the compiler’s include path:

PKG_CPPFLAGS = -I../inst/include

DESCRIPTION

Surfacing the C++ code with Rcpp requires Rcpp under both LinkingTo (for the headers used at compile time) and Imports (so it is available at run time).

LinkingTo:
    Rcpp
Imports:
    Rcpp (>= 1.0.12)

License

GPL (>= 2)