Debugging Rcpp with VSCode

a tutorial and reference

Preface

The following post details the process of connecting the VSCode debugger with Rcpp, it is a compilation from the following sources:

  1. Using VS Code to Debug R Packages with C or C++ Code
  2. vscode-rcpp-demo (GitHub repo)
  3. R packages documentation

I am running on a MacBook Pro with the M2 Pro chip, my R version is 4.3.1 and is installed via Homebrew.

Step 0: Installing necessary R packages

  1. vscDebugger : An R language connector that connects to the VSCode debugger, for R code only
  2. devtools : Necessary for building the R package
  3. usethis : For setting up the package skeleton
  4. testhat : For creating unit-tests

Step 1: Getting VSCode to work with C++

Install VSCode if you haven’t already, and add the following extensions as detailed by Davis Vaughan in the first post above

  1. C/C++ Extension Pack
  2. R extension
  3. CodeLLB

CodeLLB is the debugger that we will connect to Rcpp for debugging.

Step 2: Creating an R package

I have not been able to avoid this step. Usually for simple applications usually you would just have an .r file and a .cpp file, and in the .r file you might call

library(Rcpp)
sourceCpp('my-rcpp-implementation.cpp')

# declare variables
... 

cpp_impl(x, y, z)

But it seems that it is not possible to hook LLDB to the R session directly to debug the C++ code (perhaps there is an easy way of doing this, by linking the compiled object to LLDB, see this post ).

To debug the C++ code it is necessary to contain it in an R package, which can be created in the following ways:

  1. Clone the repo from the second link
  2. Create one using RStudio (File -> New project -> New directory -> R package with Rcpp)
  3. Using usethis::create_package (See the documentation on GitHub)

Once the R package has been created, you can move your source files to the package as follows:

  1. R files -> R/ directory
  2. C++ files -> src/ directory

Step 3: Setting up the C++ configuration in VSCode

We will now connect the VSCode debugger so that we can debug the Rcpp code. Depending on your operating system and R installation this step will not be exactly the same. So please do not blindly copy-paste what I write here.

First of all, we need to add the R/Rcpp header files to .vscode/c_cpp_properties.json so that the C/C++ extension can find them. By hitting Cmd+Shift+P in VSCode and typing ’edit configurations’ the first dropdown option should look like

dropdown

Select either (UI) or (JSON), after selecting (JSON) a file will be opened with the following contents:

{
    "configurations": [
        {
            "name": "Mac",
            "defines": [],
            "macFrameworkPath": [
                "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks"
            ],
            "compilerPath": "/usr/bin/clang",
            "cStandard": "c17",
            "cppStandard": "c++17",
            "intelliSenseMode": "macos-clang-arm64"
        }
    ],
    "version": 4
}

In the innermost dictionary we will add the include paths for the R/Rcpp header files:

{
    ...
    "includePath": [
        "${workspaceFolder}/**", 
        "/opt/homebrew/lib/R/4.3/site-library/Rcpp/include/*", 
        "/opt/homebrew/lib/R/include/*"
    ]
    ...
}

Your include paths might be different, in which case you need to determine the location of your R installation and from there the paths accordingly. Essentially, you need to find the directories containing the Rcpp.h and R.h files. You can do so by opening the R console and typing:

R.home("include") # Returns the directory that contains R.h
.libPaths() # Returns directories containing the headers for other packages, you will have to traverse this and find the Rcpp directory 

In order for devtools to compile the exported functions, it might be necessary to create a Makevars file in the src/ directory and add the following line

PKG_CPPFLAGS = -I/opt/homebrew/lib/R/4.3/site-library/Rcpp/include

Replace the path above with the location of your Rcpp header files.

Step 4: Exporting Rcpp functions (NAMESPACE)

The NAMESPACE file resides at the root of the package, and specifies the package exports. Getting the NAMESPACE right is crucial for connecting the debugger, you can either modify it manually or use roxygen2 to generate it. The devtools:: functions use roxygen2, and is also what I would recommend doing.

Manually

To do it manually you can add the following lines to the file:

useDynLib(pkgname, .registration=TRUE) # Replace with your package name
importFrom(Rcpp, evalCpp)
exportPattern("^[[:alpha:]]+")

The code above will export all functions from the package whose names start with one or more alphabetic characters.

To use roxygen2 first add the C++ files to the src/ directory. Once they have been added, annotate the functions that you want to export with these two lines:

//' @export
// [[Rcpp::export]]

The first line tells roxygen2 that the function should be included in NAMESPACE when it is generated, while the second line specifies the Rcpp export. For example, a C++ file could look like:

#include <Rcpp.h>
#include <random>
using namespace Rcpp;

//' @export
// [[Rcpp::export]]
NumericVector f_dens_cpp_1(const NumericVector& y, 
                             const NumericVector& x,
                             const NumericVector& z){
  NumericVector ret(y.length());
  for (int i = 0; i < y.length(); i++){
    double ans = 1;
    for (int j = 0; j < x.length(); j++){
      ans *= exp(y[i] * z[j] * x[j] - exp(y[i]*x[j]));
    }
    ret[i] = ans;
  }
  return ret;
}

You can also find another example from vscode-rcpp-demo in the second link above.

After you have annotated the exported the functions, run

Rcpp::compileAttributes()

in an R console opened from the package root. The command will generate an RcppExports.R file in the R/ directory.

We now create a pkgname-package.R file in the R/ directory via the following command

usethis::use_package_doc()

Which should result in a file with:

#' @keywords internal
"_PACKAGE"

## usethis namespace: start
## usethis namespace: end
NULL

You can then add the following two lines to the newly created package documentation:

#' @keywords internal
"_PACKAGE"

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

In the line starting with @useDynLib replace pkgname with the name of your package (first line in DESCRIPTION).

Generate the NAMESPACE now by executing:

devtools::document()

Step 5: Creating a unit-test to debug

Creating unit-tests is a good practice in general. Unit-tests are concise and standalone pieces of code that can be ran individually, which makes them great for debugging small individual functions – as is frequently the case programming in R.

Run the following commands in the package root:

usethis::use_testthat()
usethis::use_test('my-test')

A directory testthat/ will be created with the new test. In it you can call your Rcpp functions, for example

# Under tests/testthat/test-my-test.R

test_that("Density works", {
  dat <- read.csv('2_Poisson.csv')
  z <- dat$z
  x <- dat$x

  step_size <- 0.01
  res <- seq(0, 1, step_size)
  
  # f_dens_cpp_1 is an Rcpp function
  expect_equal(f_dens_cpp_1(res, x, z), c(0, 1, 2)) # Assert statement 
})

Note that we do not have to import the functions that we are calling, because when we later run tests by calling devtools::test(), the package devtools will import the necessary functions specified in the NAMESPACE automatically.

Step 6: Setting up the launch configuration in VSCode

The .vscode directory should hopefully be created by now in the package root. In there you will add a new file, launch.json with the following contents:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "(lldb) Launch",
            "type": "lldb",
            "request": "launch",
            "program": "/opt/homebrew/Cellar/r/4.3.1/lib/R/bin/exec/R",
            "args": [
              "--vanilla",
              "-e",
              "devtools::test()"
            ],
            "env": {
              "R_HOME" : "/opt/homebrew/Cellar/r/4.3.1/lib/R"
            },
            "terminal": "console",
            "stopOnEntry": false
        },
    ]
}

There might already exist R debugging configurations, in which case append the above configuration to the existing configurations list. There are two things that we have to look out for, which are the "program" and "R_HOME" fields.

You can find your R_HOME by executing R.home() in an R console. The R program is usually found as $R_HOME/bin/exec/R, make sure that you have the R binary and not the execution script. Doing which R will only give you the execution script and not the binary!

Step 7: Debugging the C++ code

Navigate now to the C++ code in src/, you should be able to click left to the line number to set a breakpoint breakpoint

Click on the debugger tab to the left, you should now see a green button on the top next to the launch configuration, clicking that will start the debugging session

debugview

Once the C++ function has been called, the debugger will break on the breakpoint and it is now possible to inspect variables in the runtime environment

debugsession

The debug console on the bottom panel can also be used, here you can to send commands to LLDB to inspect different variables.

Final remarks

In this tutorial I have demonstrated a working setup for debugging Rcpp using VSCode. One thing that I haven’t mentioned is how to add the include paths for all projects in VSCode globally. This dissolves the need of creating the c_cpp_properties.json file everytime in each new project. One can execute Cmd+Shift+P and then type “user settings”, open that as JSON and paste in the following:

{
    ...
    "C_Cpp.default.includePath": [
        "/opt/homebrew/lib/R/4.3/site-library/Rcpp/include/*", 
        "/opt/homebrew/lib/R/include/*"
    ],
    ...
}

This will add the R/Rcpp include paths to all future projects.

Last modified 2023.10.06