Rcpp in packages

R package development workshop

Heather Turner and Ella Kaye
Department of Statistics

June 27, 2024

Overview

  • C++ and Rcpp
  • Creating an Rcpp package
  • Rcpp development workflow
  • C++ interface
  • RcppArmadillo

C++ and Rcpp

Recap

This session assumes familiarity with the C++ and Rcpp session of the R programming workshop.

Creating an Rcpp package

Create a version-controlled package

Create a new package:

usethis::create_package("~/Desktop/pkgrcpp")

Make your package a Git repo:

usethis::use_git()

Link to GitHub:

usethis::use_github()

Commit your changes to git with a message “use GitHub”.

Use Rcpp

Use Rcpp and create an initial .cpp file:

usethis::use_rcpp("add_cpp")
  • Creates a /src directory for our C++ files
  • Sets up src/.gitignore to ignore compiled files
  • Adds “Rcpp” to the “LinkingTo” and “Imports” fields in DESCRIPTION
  • Adds add_cpp.cpp ready for editing.

It also copies some code to the clipboard for us to add to R/<packagename>-package.R which we must create.

<packagename>-package.R

usethis::use_r("pkgrcpp-package.R")

Then copy the code from the clipboard into this file:

## usethis namespace: start
#' @useDynLib pkgrcpp, .registration = TRUE
#' @importFrom Rcpp sourceCpp
## usethis namespace: end
NULL

The roxygen2 comments direct to

  • import Rcpp::sourceCpp
  • add useDynLib(pkgrcpp,".registration=TRUE") to the NAMESPACE, so that the compiled code can be loaded and all C++ routines will be registered.

Add an Rcpp function

add_cpp.cpp should already have the following code:

#include <Rcpp.h>
using namespace Rcpp;

Edit add_cpp.cpp to add a C++ function and the Rcpp export comment, e.g.

// [[Rcpp::export]]
double add_cpp(double x, double y) {
  double value = x + y;
  return value;
}

Run Build > Document

Build > Document or Ctrl/Cmd + Shift + D now runs two functions:

  • Rcpp::compileAttributes() looks for // [[Rcpp::export]] and generates

    • src/RcppExports.cpp: C routines wrapping exported C++ functions
    • R/RcppExports.R: R wrappers that call the C routines.
  • devtools::document() that converts roxygen comments as usual, in particular updating the NAMESPACE.

    importFrom(Rcpp,sourceCpp)
    useDynLib(pkgrcpp, .registration = TRUE)

R wrapper

The R wrapper generated by Rcpp is as follows, in R/RcppExports.R:

# Generated by using Rcpp::compileAttributes() -> do not edit by hand
# Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393

add_cpp <- function(x, y) {
    .Call(`_pkgrcpp_add_cpp`, x, y)
}

Currently this is an internal R function – we have not taken any action to add it as an export in the NAMESPACE.

Try out the R wrapper

devtools::load_all(), Ctrl/Cmd + Shift + L, makes both internal and exported functions available for testing (without using ::: or ::).

devtools::load_all()
add_cpp(3, 4)

Your turn

  1. Commit the changes so far with a message “Use Rcpp and add add_cpp function”.
  2. Create a new .cpp file in the /src directory (using use_rcpp or File > New File). Save the file as sumC.cpp. Add the C++ function
// [[Rcpp::export]]
double sumC(NumericVector x) {
  int n = x.size();
  double total = 0;
  for(int i = 0; i < n; ++i) {
    total += x[i];
  }
  return total;
}
  1. Run Build > Document. Look at the changes to the repo by looking at the diff.
  2. Run devtools::load_all() and try out the new function.
  3. Commit your changes to git.

Rcpp development workflow

Rcpp development workflow

The development workflow is the same as for R:

  • Modify Rcpp Code -> Load All -> Explore in Console.

devtools::load_all() will detect changes in the C++ code and

  • Update src/RcppExports.cpp and R/RcppExports.R
  • Recompile the C++ code

Document the R wrapper

  • We only need to document the R wrapper if we plan to export it.

  • The R wrapper is generated by Rcpp::compileAttributes(), so we cannot add roxygen comments to export or document the function there.

  • Instead, we add comments above our C++ function, using \\'instead of #'.

C++ roxygen

In add_cpp.cpp

//' Add Two Numbers
//'
//' Return the sum of two numbers.
//' @param x
//' @param y
//' @export
// [[Rcpp::export]]
double add_cpp(double x, double y) {
  double value = x + y;
  return value;
}

In generated R/RcppExports.R

#' Add Two Numbers
#'
#' Return the sum of two numbers.
#' @param x
#' @param y
#' @export
// [[Rcpp::export]]
add_cpp <- function(x, y) {
    .Call(`_pkgrcpp_add_cpp`, x, y)
}

Your turn

  1. Copy the documentation for add_cpp and add it to add_cpp.cpp.
  2. Using the documentation for add_cpp as a guide, add documentation to sumC.cpp.
  3. Run Build > Document to update the NAMESPACE and create the documentation files for the two R wrappers.
  4. Commit your changes to git.

Aside: A note on Taskfarm

If you are running code on Taskfarm that uses a package with code compiled by Rcpp, you may see “illegal operations” errors in your jobs.

For a fix, see the Taskfarm FAQ.

Note that this can occur with any package that includes code compiled with Rcpp, not just packages that you wrote.

Exporting C++ Code

As we have seen, Rcpp::compileAttributes() creates an R interface to our exported C++ functions.

To make a C++ interface for code in a .cpp file, we should add a custom Rcpp::interfaces() attribute as follows:

#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::interfaces(r, cpp)]]

We can specify r and/or cpp to ask for an R and/or C++ interface.

C++ Interface

Rcpp::compileAttributes() generates the following header files in /inst/include:

  • <packagename>_RcppExports.h: inline definitions for all exported C++ functions.
  • <packagename>.h: includes the <packagename>_RcppExports.h. Package authors may add additional C++ code here for sharing.

All directories in inst are moved to the top level of the package directory on installation.

Your turn

  1. Add an Rcpp::interfaces() attribute to add_cpp.cpp to request both R and C++ interfaces for the add_cpp function.
  2. Run Build > Document to create the C++ interface. Take a look at the generated files.
  3. Commit your changes to git.

Importing C++ code with Rcpp C++ interface

To use C++ code from another package that has created a C++ interface for their Rcpp functions, in DESCRIPTION add

LinkingTo: otherpkg

e.g. with usethis::use_package(otherpkg, type = "LinkingTo"). Then in .cpp file, add the header file to includes

#include <Rcpp.h>
#include <otherpkg.h>

Then either using namespace otherpkg; to make the functions from otherpackage available globally, or otherpkg::fn() to access functions directly.

Example: Alternative Distribution Functions

In the R Programming workshop (final exercise of the Rcpp session), we wrote an Rcpp function to approximate \(\pi\):

double approx_pi(const int N) {
  NumericVector x = runif(N);
  NumericVector y = runif(N);
  NumericVector d = sqrt(x*x + y*y);
  return 4.0 * sum(d < 1.0) / N;
}

We can try using the dqrunif C++ functions provided by the dqrng package instead of Rcpp::runif.

See https://daqana.github.io/dqrng/index.html for more info on this package.

Your turn

  1. Install the dqrng package.
  2. Use usethis::use_package() to add dqrng to LinkingTo. It will show possible headers to include, including dqrng.h which provides the Rcpp exports.
  3. Create a new .cpp file and add the approx_pi function. Make sure to add the necessary headers and Rcpp attribute, so that you can use it after running devtools::load_all().
  4. Add a second version of the approx_pi function that uses the dqrunif C++ functions from dqrng instead.
  5. Load all and use bench::mark() to benchmark the two versions with N = 1e7, setting check = FALSE in bench::mark() as the generated random numbers will differ.
  6. Commit your changes to git

Importing other C++ code

Some packages provide a custom <packagename>.h or other C++ headers.

  • The header maybe not be called <packagename>.h!
  • The namespace may have a different name from the package or the header file and may be nested.
  • If the header include headers from other packages, you will need to add these to LinkingTo.
  • If the package has custom build settings (e.g. using C++11), you will need to use these for your package.

The documentation should clarify what is needed for a particular package.

Example: BH Package

The BH package provides header files for the Boost C++ libraries.

The header files have .hpp extension and are nested in directories. The namespace is similarly nested:

#include <Rcpp.h>
#include <boost/integer/common_factor.hpp>

// [[Rcpp::export]]
int computeGCD(int a, int b) {
    return boost::integer::gcd(a, b);
}

Include BH in LinkingTo: to add this function to a package.

RcppArmadillo

RcppArmadillo

The RcppArmadillo package provides headers for the Armadillo library for linear algebra & scientific computing.

The header to include is RcppArmadillo.h, which provides the armadillo header defining the arma namespace, e.g. 

#include <RcppArmadillo.h>
// [[Rcpp::depends(RcppArmadillo)]]

// [[Rcpp::export]]
arma::vec getEigenValues(arma::mat M) {
    return arma::eig_sym(M);
}

Using RcppArmadillo

To use RcppArmadillo in a package, we need to

  • Add RcppArmadillo to LinkingTo
  • Enable OpenMP support and link against the BLAS and LAPACK linear algebra libraries by defining compilation options in src/Makevars and src/Makevars.win.

Both can be done with usethis::use_rcpp_armadillo().

The first time you call it, you may be asked whether to install RcppArmadillo. Choose the affirmative option.

Notes: use_rcpp_armadillo()

  • As well as doing the general setup to allow a package to use RcppArmadillo, it also creates a src directory and a .cpp file to edit.

  • If you already have a .cpp file in a src directory, by default it may try to overwrite it. If it does, select one of the negative options.

    • You need to specify the name of a new .cpp as an argument instead, e.g. use_rcpp_armadillo("new_file")

    • The function adds to the top of the .cpp file

      #include <Rcpp.h>
      using namespace Rcpp;
      • This is a bug. You need instead
      #include <RcppArmadillo.h>
      // [[Rcpp::depends(RcppArmadillo)]]

Extra note for Apple Silicon Macs

If you have an Apple Silicon Mac, you will also need to do the following:

  • open your global Makevars by calling usethis::edit_r_makevars()

  • Add the following code:

    FLIBS= -L/opt/R/arm64/gfortran/lib
  • Session > Restart R (or Cmd + Shift + 0)

Your turn

  1. Run usethis::use_rcpp_armadillo("inner_prod") to set the package up to use RcppArmadillo and create a new .cpp file.

  2. In inner_prod.cpp, edit the #include statement for RcppArmadillo and add the code

// [[ Rcpp::export]]
double inner_prod (arma::vec x, arma::vec y) {
    arma::mat z = x.t() * y ;
    return(z(0)) ;
}
  1. Run Build > Document to create the R wrapper.
  2. Run devtools::load_all() to load the function and try it out.
  3. Commit your changes to git.
  4. Push all your changes to git from this session.

Credit


Congratulations! You’ve completed the course!

We now need to take a register to see who’s still here so we can award SkillsForge and Warwick Award credit.

End matter

References

License

Licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0).