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 functionThe 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>
#endifDefining 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:
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: