3 min read

Include C code in R

Intro

This is a short summary of how to add C source code into an R package. I wrote this a year ago as a course material summary (STAT 580) and believed that I would remember all the basic details forever. Obviously, I was wrong. So I’m not 100 percent sure about the following contents and can only hope that this is still helpful to, I don’t know, at least myself in the future :) Enjoy!

Assume we already have an R package called tinyCRexample that is working and willing to be added some C source code. There are four things that we need to pay attention to:

  • tinyCRexample/src: we need to create this new folder. It holds all C source code, an initialization file init.c, and Makevars and Makevars.win
  • tinyCRexample/inst/include: we need to create this new folder (if you don’t have previously). It holds header files tinyCRexample.h and tinyCRexampleAPI.h
  • tinyCRexample/R we should already have this folder. We need to add some R scripts holding wrappers of our .Call() or .c() functions
  • tinyCRexample/NAMESPACE we should already have this, too. If we use roxygen2 it should be generated automatically.

src

  • Include all .c files here, and do not include any .o or .so or .dll files.

  • Create an initialization file init.c

An example of init.c

#include "tinyCRexample.h"
#include <Rconfig.h>
#include <Rinternals.h>
#include <R_ext/Rdynload.h>

static const
R_CallMethodDef callMethods[] = {
        {"add",                 (DL_FUNC) &add_,                 2},
        {"compute_cross_corr",  (DL_FUNC) &COMPUTE_CROSS_CORR_,  3}, 
        {"na_trim_cmps",        (DL_FUNC) &NA_TRIM_,             1},
        {"local_max_cmps",      (DL_FUNC) &LOCAL_MAX_,           2},
        {NULL,                   NULL,                           0}
};

void R_init_CMPS(DllInfo *info)
{
  R_registerRoutines(info,
                     NULL,
                     callMethods,
                     NULL,
                     NULL);

  R_useDynamicSymbols(info, TRUE);

  /* used by external packages linking to internal xxx code from C */
  R_RegisterCCallable("CMPS","add",                     (DL_FUNC) &add_);
  R_RegisterCCallable("CMPS","compute_cross_corr",      (DL_FUNC) &COMPUTE_CROSS_CORR_);
  R_RegisterCCallable("CMPS","na_trim_cmps",            (DL_FUNC) &NA_TRIM_);
  R_RegisterCCallable("CMPS","local_max_cmps",          (DL_FUNC) &LOCAL_MAX_);
  
}

[TODO] Some explanation

Note that #include "tinyCRexample.h" is only needed for init.c.

  • Makevars and Makevars.win are used to set compile flags. The minimum we would need is
// Makevars
PKG_CPPFLAGS = -I../inst/include
// Makevars.win
PKG_CPPFLAGS = -I../inst/include

inst/include

  • tinyCRexample.h
#include <R.h>
#include <Rinternals.h>

SEXP add_(SEXP x_, SEXP y_);
SEXP COMPUTE_CROSS_CORR_(SEXP xx_in, SEXP yy_in, SEXP minoverlap_in);
SEXP NA_TRIM_(SEXP seq_in);
SEXP LOCAL_MAX_(SEXP seq_in, SEXP MAX_MIN_in);
  • tinyCRexampleAPI.h
#include <tinyCRexample.h>

#include <R_ext/Rdynload.h>
#include <R.h>
#include <Rinternals.h>

SEXP add_(SEXP x, SEXP y) {
  static SEXP(*fun)(SEXP, SEXP) = NULL;
  if (fun == NULL)
    fun = (SEXP(*)(SEXP, SEXP)) R_GetCCallable("CMPS", "add");
  return fun(x, y);
}

SEXP COMPUTE_CROSS_CORR_(SEXP xx_in, SEXP yy_in, SEXP minoverlap_in) {
  static SEXP(*fun)(SEXP, SEXP, SEXP) = NULL;
  if (fun == NULL)
    fun = (SEXP(*)(SEXP, SEXP, SEXP)) R_GetCCallable("CMPS", "compute_cross_corr");
  return fun(xx_in, yy_in, minoverlap_in);
}

SEXP _NA_TRIM(SEXP seq_in) {
  static SEXP(*fun)(SEXP) = NULL;
  if (fun == NULL)
    fun = (SEXP(*)(SEXP)) R_GetCCallable("CMPS", "na_trim_cmps");
  return fun(seq_in);  
}

SEXP LOCAL_MAX_(SEXP seq_in, SEXP MAX_MIN_in) {
  static SEXP(*fun)(SEXP, SEXP) = NULL;
  if (fun == NULL)
    fun = (SEXP(*)(SEXP, SEXP)) R_GetCCallable("CMPS", "local_max_cmps");
  return fun(seq_in, MAX_MIN_in);  
}

[TODO] Provide package API of exportable functions (main idea: others can include this API header file and use these exported functions )

tinyCRexample/R

Create wrapper functions and include them in the R folder

Note that we used roxygen2, it will automatically edit NAMESPACE.

#' @useDynLib tinyCRexample COMPUTE_CROSS_CORR_
compute_cross_corr <- function(x, y, min.overlap) .Call(COMPUTE_CROSS_CORR_, x, y, min.overlap)

#' @useDynLib tinyCRexample NA_TRIM_
na_trim_cmps <- function(x) .Call(NA_TRIM_, x)

NAMESPACE

If we ever gave a wrong name in #' @useDynLib tinyCRexample COMPUTE_CROSS_CORR_, for example

#' @useDynLib ABC COMPUTE_CROSS_CORR_
compute_cross_corr <- function(x, y, min.overlap) .Call(COMPUTE_CROSS_CORR_, x, y, min.overlap)

and did a package check, NAMESPACE would be edited and become:

useDynLib(ABC,COMPUTE_CROSS_CORR_) # this is wrong
useDynLib(CMPS,LOCAL_MAX_)
useDynLib(CMPS,NA_TRIM_)

Then many errors would pop out since there is no shared library object called ABC

The solution is to delete NAMESPACE and regenerate it by using roxygen2::roxygenise() after we have corrected the package name in #' @useDynLib