Cpp11 (R package) vendoring
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
R/Shiny Training: Should you find this blog to be of interest, kindly note that I offer personalized and group-based training sessions that may be reserved through Buy me a Coffee. Additionally, I provide training services in the Spanish language and am available to discuss means by which I may contribute to your Shiny project.
Updated 2023-05-23: I explicitly mention to add the vendoring after creating the project instead of assuming the readers know I run a commented line in the install script.
Motivation
In my previous post A step by step guide to write an R package that uses C++ code (Ubuntu) I explained the steps in my journey to use C++ function from R.
Now I will explain how I figured out how to use vendoring, which means that I copy the code for the dependencies into your project’s source tree, and in this case this means I copied the C++ headers provided by the cpp11
package into my own R package. This ensures the dependency code is fixed and stable until it is updated.
The advantage of vendoring is that changes to the cpp11
package could never break your existing code. The disadvantage is that you no longer get bugfixes and new features until you manually run cpp_vendor()
in your project.
Creating an R package with C++ code and vendoring
Similar to the previous post, I started with create_package("~/github/cpp11gaussjordan")
. I am using VSCode but all my steps also apply to RStudio.
After opening ~/github/cpp11gaussjordan
I run use_cpp11()
to have a readily availably skeleton in my project.
I run use_apache_licence()
to have a LICENSE
file and indicate in DESCRIPTION
that my package is distributed under the Apache License.
Then I run cpp_vendor()
to copy the C++ headers into inst/include
.
You can find the final result on my GitHub profile.
R side
I run use_r("cpp11gaussjordan-package")
and added the following contents.
#' @useDynLib cpp11gaussjordan, .registration = TRUE NULL #' Invert (some) square matrices #' @export #' @param A numeric matrix #' @return numeric matrix #' @examples #' A <- matrix(c(2,1,3,-1), nrow = 2, ncol = 2) #' invert_matrix(A) invert_matrix <- function(A) { invert_matrix_(A) } #' Solve (some) linear systems #' @export #' @param A numeric matrix #' @param b numeric matrix #' @return numeric matrix #' @examples #' A <- matrix(c(2,1,3,-1), nrow = 2, ncol = 2) #' b <- matrix(c(7,4), nrow = 2, ncol = 1) solve_system <- function(A,b) { solve_system_(A, b) }
Here solve_system()
is the “end-user” function, and solve_system_()
is the “development” function. The “development” functions were written in C++ and cpp11
handles adding the .Call()
when required to link the C++ code and be able to use the C++ functions from R.
C++ side
I replaced code.cpp
contents with the following lines.
#include <cpp11.hpp> #include <cpp11/doubles.hpp> using namespace cpp11; [[cpp11::register]] doubles_matrix<> invert_matrix_(doubles_matrix<> A) { // Obtain (X'X)^-1 via Gauss-Jordan // Check dimensions int N = A.nrow(); int M = A.ncol(); if (N != M) { stop("X must be a square matrix"); } // Copy the matrix writable::doubles_matrix<> Acopy(N, N); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { Acopy(i, j) = A(i, j); } } // Create the identity matrix as a starting point for Gauss-Jordan writable::doubles_matrix<> Ainv(N, N); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { if (i == j) { Ainv(i, j) = 1.0; } else { Ainv(i, j) = 0.0; } } } // Overwrite Ainv by steps with the inverse of A // in other words, find the echelon form of A for (int i = 0; i < M; i++) { double a = Acopy(i, i); for (int j = 0; j < M; j++) { Acopy(i, j) /= a; Ainv(i, j) /= a; } for (int j = 0; j < M; j++) { if (i != j) { a = Acopy(j, i); for (int k = 0; k < M; k++) { Acopy(j, k) -= Acopy(i, k) * a; Ainv(j, k) -= Ainv(i, k) * a; } } } } return Ainv; } [[cpp11::register]] doubles_matrix<> solve_system_(doubles_matrix<> A, doubles_matrix<> b) { // Use Gauss-Jordan to solve the system Ax = b // Check dimensions int N1 = A.nrow(); int M1 = A.ncol(); if (N1 != M1) { stop("A must be a square matrix"); } int N2 = b.nrow(); int M2 = b.ncol(); if (N1 != N2) { stop("b must have the same number of rows as A"); } if (M2 != 1) { stop("b must be a column vector"); } // Obtain the inverse // writable::doubles_matrix<> Acopy(N1,N1); // for (int i = 0; i < N1; i++) // { // for (int j = 0; j < N1; j++) // { // Acopy(i, j) = A(i, j); // } // } // doubles_matrix<> Ainv = invert_matrix_(Acopy); // I don´t need to create a copy in this case doubles_matrix<> Ainv = invert_matrix_(A); // Multiply Ainv by b writable::doubles_matrix<> x(N1, 1); for (int i = 0; i < N1; i++) { x(i, 0) = 0.0; for (int j = 0; j < N1; j++) { x(i, 0) += Ainv(i, j) * b(j, 0); } } return x; }
I also needed a Makevars
file to indicate the compiler (either clang
or gcc
) to use the vendored headers. The content is the following.
PKG_CPPFLAGS = -I../inst/include
Because of the vendoring option, I also had to remove the LinkingTo: cpp11
line from DESCRIPTION
.
Putting all together
I created a vscode-install.r
file in the root of the project, and I added it to .Rbuildignore
. It contains the following lines.
# cpp_vendor() # run only when updating C++ headers clean_dll() cpp_register() document() install() # load_all()
To test that the functions work, after running install()
I solved a simple system.
> library(cpp11gaussjordan) > A <- matrix(c(2,1,3,-1), nrow = 2, ncol = 2) > b <- matrix(c(7,4), nrow = 2, ncol = 1) > solve_system(A,b) [,1] [1,] 3.8 [2,] -0.2 > solve(A) %*% b [,1] [1,] 3.8 [2,] -0.2
Ask a lot
Blog posts like this are summaries of what worked for me. I asked so much on Stackoverflow, that they created the “r-lib-cpp11” for my questions. Please ask there, there is probably something I figured how to solve after many hours on Google.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.