Using Rcpp

notes
Author

John Benninghoff

Published

December 21, 2023

Modified

March 10, 2024

Notes on using Rcpp to implement Poker-Hand-Evaluator in the cards package.

# no libraries

Background

Rcpp provides an interface to C++, which allows rewriting R code in C++ to improve performance. The cards package implements functions that simulate dealing and evaluating poker hands in R and PH Evaluator using python via reticulate. Will the C++ version of PH Evaluator be faster?

The Thirteen Simple Steps for Creating An R Package with an External C++ Library vignette provides good high-level guidance on integrating a C++ library into an R package using base R; additional work is needed to implement Rcpp using roxygen2. The source code for the Corels package referenced in the vignette can be found on GitHub: https://github.com/corels/rcppcorels.

Test Build

To start, I followed PH Evaluator’s instructions to build and test the pheval library, which required installation of cmake using Homebrew (brew install cmake).

Example Function

As a next step, I implemented a simple example function to ensure Rcpp was working properly. After some research, I added the following code:

#include <Rcpp.h>
using namespace Rcpp;

//' Leading NA
//'
//' This function returns a logical vector identifying if
//' there are leading NA, marking the leading NAs as TRUE and
//' everything else as FALSE.
//'
//' Code from [Rcpp and Roxygen2](https://www.r-bloggers.com/2016/08/rcpp-and-roxygen2/).
//'
//' Installed with help from [usethis::use_rcpp()] and roxygen2 instructions on
//'   [Rcpp](https://roxygen2.r-lib.org/articles/roxygen2.html#rcpp).
//'
//' Steps to install:
//'
//' 1. Create `src/leading_na.cpp` (this file)
//' 1. Run [usethis::use_rcpp()], add `@importFrom Rcpp sourceCpp` and
//'    `@useDynLib cards, .registration = TRUE` to `package.R` as directed
//' 1. Run [desc::desc_normalize()] and [devtools::document()]
//'
//' @param x An integer vector
//' @export
// [[Rcpp::export]]
LogicalVector leading_na(IntegerVector x) {
  int n = x.size();
  LogicalVector leading_na(n);

  int i = 0;
  while((i < n) &&(x[i] == NA_INTEGER)) {
    leading_na[i] = TRUE;
    i++;
  }
  return leading_na;
}

After the first two steps, devtools::document() automates the addition of the leading_na() function, R documentation, and Rcpp support to the package.

Stub Function

As a next step, I implemented a stub function with help from Rcpp for everyone, specifically the chapter on Data types:

#include <Rcpp.h>
using namespace Rcpp;

//' Evaluate a poker hand using PH Evaluator
//'
//' Evaluate the rank category of a five card poker hand using
//'   [PH Evaluator](https://github.com/HenryRLee/PokerHandEvaluator).
//'
//' Currently implemented as a stub function that always returns "poker_hand".
//'
//' @return string hand rank
//' @export
// [[Rcpp::export]]
String eval_hand_phe() {
  return "poker_hand";
}

The stub works, and suggests an approach for a first implementation using Rcpp: create a function that calls phevaluator::EvaluateCards() and returns a string based on Rank.category() or Rank.describeCategory, following cpp_example.cc. A more complete implementation would use Rcpp Modules to expose the C++ classes and methods in PH Evaluator, with help from the RcppStudent R package.

Additional examples implementing C++ libraries in Rcpp I found include RcppAnnoy, written by Rcpp author and maintainer Dirk Eddelbuettel, rxylib, and RcppSundials.

Add Headers

To start the first implementation, I added the headers from cpp_example.cc:

#include <Rcpp.h>
using namespace Rcpp;

#include <phevaluator/phevaluator.h>
#include <iostream>
#include <cassert>

//' Evaluate a poker hand using PH Evaluator
//'
//' Evaluate the rank category of a five card poker hand using
//'   [PH Evaluator](https://github.com/HenryRLee/PokerHandEvaluator).
//'
//' Implemented following
//'   [`cpp_example.cc`](https://github.com/HenryRLee/PokerHandEvaluator/blob/master/cpp/examples/cpp_example.cc)
//'   and [RcppAnnoy](https://github.com/eddelbuettel/rcppannoy).
//'
//' Currently implemented as a stub function that always returns "poker_hand".
//'
//' @param hand a hand of cards (an integer vector of length 5).
//' @return string hand rank.
//' @export
// [[Rcpp::export]]
String eval_hand_phe(IntegerVector hand) {
  // phevaluator::Rank rank = phevaluator::EvaluateCards(hand[0], hand[1], hand[2], hand[3], hand[4]);
  // return rank.describeCategory();
  return "poker_hand";
}

To get the updated code to compile, I had to add src/Makevars, which I copied from RcppAnnoy, and also added the cpp/include directory from PH Evaluator to inst/.

src/Makevars:

PKG_CPPFLAGS = -I../inst/include/

While this code compiled, to get the commented code working, I’d need to add the code from cpp/src. Just dumping the files from PH Evaluator into src/ didn’t work, but the rxylib Makevars file offered a clue: adapt the Makefile from PH Evaluator to the R Makevars format to properly build the library objects. Put another way, follow how rxylib translated xlib/Makefile.am to Makevars. The Stack Overflow question referenced in rxylib provided additional details on the Makevars file format.

Full Implementation

After some experimentation, I added the following files from PH Evaluator’s cpp/src to src/:

7462.c
dptables.c
evaluator5.c
evaluator5.cc
hash.c
hash.h
hashtable.c
hashtable5.c
rank.c
tables.h
tables_bitwise.c

These were the minimal files needed to support the first implementation, which only had to support evaluation of 5 card hands.

I also added a line to src/Makevars to enable the C++ 17 standard used by PH Evaluator:

PKG_CPPFLAGS = -I../inst/include/
CXX_STD = CXX17

This nearly compiled, but failed with an error:

   duplicate symbol '_evaluate_5cards' in:
       /Users/agamemnon/GitHub/cards/src/evaluator5.o
       /Users/agamemnon/GitHub/cards/src/evaluator5.o

Ultimately this error was the result of two files with the same name but different file extensions (evaluator5.c and evaluator5.cc). Renaming evaluator5.c to evaluator_5_c.c fixed the issue, and the code below compiled and tested properly!

#include <Rcpp.h>
using namespace Rcpp;

#include <phevaluator/phevaluator.h>
#include <iostream>
#include <cassert>

//' Evaluate a poker hand using PH Evaluator
//'
//' Evaluate the rank category of a five card poker hand using
//'   [PH Evaluator](https://github.com/HenryRLee/PokerHandEvaluator).
//'
//' Implemented following
//'   [Rcpp-libraries](https://cran.r-project.org/web/packages/Rcpp/vignettes/Rcpp-libraries.pdf),
//'   PH Evaluator
//'   [`cpp_example.cc`](https://github.com/HenryRLee/PokerHandEvaluator/blob/master/cpp/examples/cpp_example.cc),
//'   and [RcppAnnoy](https://github.com/eddelbuettel/rcppannoy), with help from
//'   R-Bloggers [Rcpp and Roxygen2](https://www.r-bloggers.com/2016/08/rcpp-and-roxygen2/),
//'   [usethis::use_rcpp()], roxygen2 instructions on
//'   [Rcpp](https://roxygen2.r-lib.org/articles/roxygen2.html#rcpp),
//'   [Rcpp for everyone](https://teuder.github.io/rcpp4everyone_en/), the rxylib
//'   [`Makefile`](https://github.com/R-Lum/rxylib/blob/master/src/Makevars) and
//'   [Stack Overflow](https://stackoverflow.com/questions/43597632/understanding-the-contents-of-the-makevars-file-in-r-macros-variables-r-ma).
//'
//' `eval_hand_phe` returns one of the rank categories "Straight Flush", "Four of a Kind",
//'   "Full House", "Flush", "Straight", "Three of a Kind", "Two Pair", "One Pair", or "High Card".
//'
//' "Royal Flush" and "Jacks or Better" are not currently supported.
//'
//' @param hand a hand of cards (an integer vector of length 5).
//' @return string hand rank.
//' @examples
//' hand <- deal_hand(new_deck())
//' print_hand(hand)
//' eval_hand_phe(hand)
//' @export
// [[Rcpp::export]]
String eval_hand_phe(IntegerVector hand) {
  phevaluator::Rank rank = phevaluator::EvaluateCards(hand[0], hand[1], hand[2], hand[3], hand[4]);
  return rank.describeCategory();
}

After this, I removed the example function, added a C/C++ benchmark, and published cards 0.3.0.

Follow-up

I did some additional research, and there doesn’t seem to be a simple way to change R to compile using filename.cc.o instead of filename.o, as it goes against the intent of the R developers, as described in this Stack Overflow article:

The clear recommendation on r-devel (please check the archives) is that you should avoid Makefile logic if you can. IIRC this echoed in the Writing R Extension manual.

So, it seems the right answer is to rename the files to avoid this conflict. Additionally, after reading a tidyverse blog post on New CRAN requirements for packages with C and C++, I learned that the R C++ standard was now C++17, so I technically did not need that line in the Makevars file, but I decided to keep it as it was an upstream requirement.

I also discovered an example package that explained how to put code in a subdirectory of src/, which I plan to use for the second version using Rcpp Modules.

I later added a script to copy (or update) the PH Evaluator source and header files to the cards package, to automate future updates if needed.

After reading through the Creating R Packages chapter of Writing R Extensions and evaluating my options, I am inclined to use a simple approach to creating a “phevaluator” package, copying the necessary files into src/ (with a shell script for reproducibility), use a minimal Makevars file, and use Rcpp Modules to expose the C++ classes and methods in PH Evaluator, following the approach used by the RcppStudent R package.