Interfacing C, C++ and Fortran

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. This page compiles and runs code, so besides base R it used the GNU compilers gcc and g++ 13.3 and gfortran 13.3 with GNU make (all three are in the container image), and these add-on packages: Rcpp 1.1.2 for the C++ sections, bench 1.1.4 for one benchmark, pkgbuild 1.4.8 to ask whether build tools exist, and usethis 3.2.2, devtools 2.5.2 and roxygen2 8.1.1 to put compiled code into a copy of the shelf package; lintr 3.4.0 was used only to check the R scripts. cpp11 0.5.5 is installed and is named in one sentence, not run. RcppArmadillo, RcppParallel and inline are not installed, so what the page says about them is prose, and Rtools45 on Windows and Apple’s Command Line Tools on macOS were not available either: everything said about those toolchains comes from the manuals and is marked as not run.

R is an interpreter written in C, and it was designed from the start to be extended with compiled code: a great part of base R is C and Fortran called through the same entry points described here. This page is about the other direction, the code you write yourself. It explains how a C, C++ or Fortran function becomes a shared object that R loads (R CMD SHLIB and dyn.load()), the four interfaces that call it (.Call(), .External(), .C() and .Fortran()), how C code reads and builds R objects through the SEXP type and keeps them safe from the garbage collector (PROTECT and UNPROTECT), how routines are registered so that R can check them (R_registerRoutines() and useDynLib in NAMESPACE), what goes in src/Makevars, how Rcpp reduces the C++ boilerplate, which parts of R’s C API a package may use, and what each platform needs installed. Every example was compiled and run, and the error and warning messages are the ones R printed.

Every R example is a script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)), and the lines that start with #> are what it printed. The scripts also pass the default lintr linters. Every C, C++ and Fortran file shown is the exact file that was compiled; it was also compiled with -Wall -pedantic set in a personal Makevars file (shown below) and gave no warning; the transcripts of the build commands show the default build. The compiler command lines R prints are long and name the container’s directories and distribution, so the page shows them with the flags shortened to the ones that matter, and omits the line that names the compiler version and the operating system; where that happens the text says so. The compiled examples use small data typed into the scripts (a handful of loan durations in days, a few ISBN strings), not the CSV files of the running scenario described on the section index, because the compiled objects live in their own directory; the package examples use the shelf package of Creating Packages, copied and extended with a src/ directory, with paths shown as ~/shelf. The shared-object extension is .so on Linux and macOS and .dll on Windows, so the scripts write paste0("add", .Platform$dynlib.ext); where the page says add.so, read add.dll on Windows.

This page links to the references for the three languages that this one calls: the C Reference (and its Build and Tooling page), the C++ Reference (and its Program Structure and Compilation page, which explains extern "C"), and the Fortran Reference, whose pages Interoperability with C and Calling Fortran from C, C++ and Python were added by issue #277 and cover the Fortran side of what is shown here from R. Measuring whether compiled code is worth writing belongs to Performance, Profiling and Benchmarking; how R stores the objects that C code receives is the subject of Memory and Copy Semantics; calling Python instead is Python Interop with reticulate.

Why leave R, and when not to

Most of R’s speed already comes from compiled code. sum(), %*%, sort(), lm() and the arithmetic on vectors all end in C or Fortran that someone else wrote and tested, so the first answer to a slow script is usually a better use of those: vectorise, avoid growing objects, choose another algorithm, or use a package such as data.table that is compiled for that job. Performance, Profiling and Benchmarking shows how to measure, and Vectorisation and Iteration covers the byte-code compiler and preallocation that make plain R loops much faster than their reputation. The manual Writing R Extensions says the same: before using .Call() or .External() it advises to consider whether interpreted code is fast enough, and whether .C() is enough, because the first two give much more control and impose much greater responsibilities (Writing R Extensions: Handling R objects in C).

There are four good reasons to write compiled code anyway.

  • A loop that cannot be vectorised. Each iteration depends on the previous one (a simulation, a recursive filter, a state machine, a dynamic-programming table), so the work cannot be handed to a built-in that operates on whole vectors.

  • An existing library. A numerical routine in Fortran, a parser in C, a geometry engine in C++: wrapping it is far cheaper than porting it. The Fortran BLAS and LAPACK page describes the libraries R itself links.

  • Memory control. R copies on modification (see Memory and Copy Semantics); compiled code can fill a preallocated buffer in place.

  • A data structure R does not have. A heap, a hash table, a trie, a spatial index, kept alive between calls behind an external pointer.

The price is real. Compiled code is the one place where a mistake does not give an R error but a crash of the whole R process, or worse, a wrong answer with no message. It needs a compiler on every machine that installs the package from source, it has to be kept in step with changes of the R C API (this page’s last sections), it is harder to debug, and it makes a package harder to get onto CRAN. The rule of thumb in the R community is to profile first, change the algorithm second, and write compiled code last; the example near the end of this page puts numbers on why.

The interfaces at a glance

Interface R side C or Fortran side Use it when

.Call()

.Call(sym, a, b), up to 65 arguments

SEXP f(SEXP a, SEXP b)

The default. Any R object in, any R object out; arguments are not copied; the function does its own type checks.

.External()

.External(sym, a, b)

SEXP f(SEXP args), one pairlist

A variable number of arguments, or the argument names (tags) matter.

.C()

.C(sym, a, b) returns a list of the modified arguments

void f(double *a, int *n), pointers only

Old code, or a routine that only reads and writes plain numeric vectors; no R headers needed.

.Fortran()

.Fortran(sym, a, b), same as .C()

subroutine f(a, n), lower-case name

Existing Fortran 77 or Fortran 90 subroutines; for modern Fortran, bind© and .C().

Rcpp attributes

an ordinary R function generated for you

a C++ function with // [[Rcpp::export]]

C++ code that wants vectors, lists and data frames as classes, with conversion and error handling generated.

The rest of the page follows that order: the C route with .Call() first, because everything else is a variation of it, then .C() and .Fortran(), .External(), and finally C++ and Rcpp.

The toolchain: what R needs on each platform

R compiles C, C++ and Fortran with the compilers it was configured with, and it records the commands in etc/Makeconf inside the R installation. R CMD config prints any of those variables, and R CMD SHLIB and R CMD INSTALL read them, so the compiler is never named in your own build files. The script below asks whether R can find a working compiler (pkgbuild::has_build_tools() compiles a small C file with R CMD SHLIB, which is the real test), prints the shared-object extension of this platform, and prints the C, C++ and Fortran compiler commands R uses. On this machine the C compiler is selected with the C23 option (-std=gnu2x is gcc 13’s spelling of it) and the C++ compiler with the C++20 option, which are the defaults of R 4.5.0 and R 4.6.0 respectively; the version notes at the end of the page give the NEWS entries.

# Is there a compiler R can use? TRUE means make and a C compiler were found.
pkgbuild::has_build_tools(debug = FALSE)
#> [1] TRUE

# The shared-object extension of this platform.
.Platform$dynlib.ext
#> [1] ".so"

# What R was configured to compile with (the same as `R CMD config CC`).
r_bin <- file.path(R.home("bin"), "R")
for (variable in c("CC", "CXX", "FC")) {
  value <- system2(r_bin, c("CMD", "config", variable), stdout = TRUE)
  cat(sprintf("%s: %s\n", variable, value))
}
#> CC: gcc -std=gnu2x
#> CXX: g++ -std=gnu++20
#> FC: gfortran

The help page of has_build_tools() adds that debug = TRUE prints extra information, and that on Windows a failure such as R CMD config CC returning status 127 means that Rtools is not on the PATH (pkgbuild package documentation: has_build_tools). What has to be installed differs by platform.

Platform What to install Notes

Linux

A C compiler, a C++ compiler, a Fortran compiler and make; on Debian-based systems the build-essential and gfortran packages cover all of them, and r-base-dev brings the headers.

This is the setup of the container used for this page.

macOS

Apple’s Command Line Tools, installed with xcode-select --install, for the C and C++ compilers (Apple’s clang); a Fortran compiler is separate.

Xcode itself does not contain a Fortran compiler. The R-admin manual and the macOS tools page recommend the universal GNU Fortran 14.2 installer for current R, and say that to compile packages you must use the same Fortran that built your R, so check the download page of your R version.

Windows

Rtools45, the toolchain for R 4.5.0 and later (including R-devel), installed from the CRAN Rtools page.

Rtools45 provides GCC 14.2 (MinGW-w64) and MSYS2 build tools. Earlier R versions use Rtools44 (R 4.4.x), Rtools43, Rtools42 and Rtools40; each page of the Rtools portal lists the R versions it serves. R must be able to find it; usually the installer arranges that.

The R Installation and Administration manual describes the macOS requirement in its section on installing packages (R Installation and Administration: macOS packages), and the Fortran compiler for macOS in its appendix on building R (R Installation and Administration: Fortran compiler). The macOS tools page (Tools for macOS: mac.r-project.org) is the download page for the Fortran compiler and notes the compiler to use for each R build. The R for Windows FAQ says the way to build a DLL by hand is the same command as elsewhere: R CMD SHLIB foo.c bar.f, run with the R of the installed version (R for Windows FAQ 8.3: How do I include compiled C code?), and the Rtools portal at Rtools: Toolchains for building R and R packages from source on Windows lists which Rtools serves which R. None of the Windows or macOS steps was run for this page.

A source package that contains only R code needs none of this; the toolchain is needed only by packages with a src/ directory and by the code you compile yourself. Binary packages from CRAN (Windows and macOS) and from distribution or Posit Package Manager repositories on Linux are compiled for you, which is why many R users never install a compiler.

From C source to a loaded shared object

The smallest .Call function

A function called through .Call() takes R objects as arguments and returns one. In C, an R object of any type is a SEXP (a pointer to R’s internal node type, described in the next section), so the function takes SEXP arguments and returns a SEXP. The smallest useful function adds two numbers. Rf_asReal() coerces the first element of its argument to a C double, and Rf_ScalarReal() builds a length-one numeric vector from a double; the header Rinternals.h declares both, and R.h the rest of the common API.

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

SEXP add(SEXP x, SEXP y) {
  return Rf_ScalarReal(Rf_asReal(x) + Rf_asReal(y));
}

R CMD SHLIB turns the source file into a shared object. It calls make with R’s own rules, so it uses the compiler, the flags and the link line recorded when R was built. The transcript shows the two commands it runs, with the flags shortened (the real lines carry the include directory of this R installation and about a dozen hardening and diagnostic flags) and without the first line it prints, which names the compiler version and the operating system.

R CMD SHLIB add.c
#> gcc -std=gnu2x -I"<R_HOME>/include" -fPIC -O2 -c add.c -o add.o
#> gcc -std=gnu2x -shared -o add.so add.o -L"<R_HOME>/lib" -lR

The first command compiles add.c into an object file; the second links the object file into add.so against libR. The options come from R CMD SHLIB --help; the transcript is the complete help except its last line, which gives the address for bug reports (the odd "alsp" in the --use-C17 line is in R’s own text).

R CMD SHLIB --help
#> Usage: R CMD SHLIB [options] files | linker options
#>
#> Build a shared object for dynamic loading from the specified source or
#> object files (which are automagically made from their sources) or
#> linker options.  If not given via '--output', the name for the shared
#> object is determined from the first source or object file.
#>
#> Options:
#>   -h, --help		print short help message and exit
#>   -v, --version		print version info and exit
#>   -o, --output=LIB	use LIB as (full) name for the built library
#>   -c, --clean		remove files created during compilation
#>   --preclean		remove files created during a previous run
#>   -n, --dry-run		dry run, showing commands that would be used
#>   --use-LTO		use Link-Time Optimization
#>   --no-use-LTO		do not use Link-Time Optimization
#>   --use-C17	        use a C standard at most C17 (alsp C90, C99)
#>   --use-C23	        use a C standard at least C23
#>
#> Windows only:
#>   -d, --debug		build a debug DLL
#>
#> [exit 0]

R CMD SHLIB accepts C (.c), C++ (.cc, .cpp), fixed-form Fortran (.f), free-form Fortran (.f90, .f95), Objective C and Objective C++ sources, or object files, or linker options. Files for the Fortran preprocessor (.F) are not accepted. The name of the shared object is taken from the first file unless -o is given. Two details from the manual are worth keeping in mind: make does not rebuild when only the flags changed (use --preclean), and a file named test.c next to test.f makes R CMD SHLIB test.f compile the C file instead (Writing R Extensions: Creating shared objects).

dyn.load, is.loaded and the symbol table

dyn.load() loads a shared object into the running R process, and .Call() then finds the C function by its name. The manual advises against assuming a file extension, and recommends file.path(path1, path2, paste0("mylib", .Platform$dynlib.ext)). The script loads add.so, calls the function with doubles, integers and a missing value, shows that only the first element of each argument is used (because the C code reads it with Rf_asReal()), and shows the two ways to be exact about which shared object a name belongs to: the PACKAGE argument, and a native symbol object from getNativeSymbolInfo().

# Load the shared object built by `R CMD SHLIB add.c`, then call into it.
dyn.load(paste0("add", .Platform$dynlib.ext))
is.loaded("add")
#> [1] TRUE
.Call("add", 1, 2)
#> [1] 3
.Call("add", 1L, 2L)
#> [1] 3
.Call("add", NA_real_, 2)
#> [1] NA
.Call("add", c(5, 6), c(10, 20))
#> [1] 15

# Pass PACKAGE so that the search is confined to this shared object.
add <- function(x, y) .Call("add", x, y, PACKAGE = "add")
add(0.1, 0.2) == 0.3
#> [1] FALSE
add(0.1, 0.2)
#> [1] 0.3

# The loaded objects, and the symbol as an R object.
"add" %in% names(getLoadedDLLs())
#> [1] TRUE
info <- getNativeSymbolInfo("add", PACKAGE = "add")
class(info)
#> [1] "NativeSymbolInfo"
info$name
#> [1] "add"

# Unloading is rarely needed; it lets the file be rebuilt on Windows.
invisible(dyn.unload(paste0("add", .Platform$dynlib.ext)))
is.loaded("add")
#> [1] FALSE

The loaded shared object is listed by getLoadedDLLs(), whose elements are named after the file without the extension. dyn.unload() removes it; the manual calls unloading unnecessary and not safe in general, but needed on some platforms (Windows) to allow the DLL to be rebuilt. Unloading a DLL and reloading a rebuilt one with the same name may not work on every platform (Writing R Extensions: dyn.load and dyn.unload). In practice, when you edit a C file during development, restart R after rebuilding, or use pkgload::load_all() (below), which handles unloading for a package.

PACKAGE matters more than it looks. A bare .Call("add", x, y) asks R to search every loaded shared object, and R’s search order is most recently loaded first; if two packages define a C function called add, the wrong one can be called, with the wrong number of arguments, and that normally crashes R. Writing R Extensions explains that PACKAGE confines the search and speeds it up, and the help page of .Call recommends it or a native symbol object for anything called often. The manual’s timing section measured the difference as small but real for functions called millions of times (Writing R Extensions: Registering native routines).

Errors from the lookup

When the name cannot be found, R says so in a message that is the same on every platform. These are the real texts, captured by tryCatch() (the container’s directory is replaced by <dir> so that the script, not the page, hides the path). The last three lines show the same failure for .C(), .Fortran() and .External(); only .Fortran() says "Fortran symbol name".

dyn.load(paste0("add", .Platform$dynlib.ext))
message_of <- function(expr) {
  tryCatch(expr, error = \(e) {
    gsub(getwd(), "<dir>", conditionMessage(e), fixed = TRUE)
  })
}

# A symbol that is not in any loaded shared object.
message_of(.Call("nope"))
#> [1] "C symbol name \"nope\" not in load table"

# A symbol searched in the wrong shared object, or in one that is not loaded.
message_of(.Call("nope", PACKAGE = "add"))
#> [1] "\"nope\" not available for .Call() for package \"add\""
message_of(.Call("add", 1, 2, PACKAGE = "nosuchdll"))
#> [1] "\"add\" not available for .Call() for package \"nosuchdll\""

# A file that does not exist (the second line is the operating system's text).
message_of(dyn.load("missing.so"))
#> [1] "unable to load shared object '<dir>/missing.so':\n  <dir>/missing.so: cannot open shared object file: No such file or directory"

# The same symbol name on the other three interfaces.
message_of(.C("nope"))
#> [1] "C symbol name \"nope\" not in load table"
message_of(.Fortran("nope"))
#> [1] "Fortran symbol name \"nope\" not in load table"
message_of(.External("nope"))
#> [1] "C symbol name \"nope\" not in load table"

The second line of the dyn.load() message is the operating system’s own text (this one is glibc’s), so it differs on macOS and Windows. All four lookup errors have the same remedy: check the spelling, check that the shared object was built from the current source and loaded, and check that the function is not static and, in C++, is declared extern "C".

Handling R objects in C

What a SEXP is

Every R object that C code touches is a SEXP, a pointer to a structure the headers call SEXPREC. Think of it as a variant type: the same pointer type refers to a numeric vector, a list, a function, an environment, a call or NULL, and TYPEOF(x) returns a code that says which. C code must therefore check the type before it uses the data, and it gets at the data through accessor functions, never by looking inside the structure (the structure is not part of the API and its layout is allowed to change). The types that package code meets most often are these.

TYPEOF() code R object How C reads or builds it

REALSXP

numeric (double) vector

double *REAL(x) or the read-only const double *REAL_RO(x); Rf_allocVector(REALSXP, n)

INTSXP

integer vector (also the codes of a factor)

int *INTEGER(x), INTEGER_RO(x); missing value NA_INTEGER

LGLSXP

logical vector

int *LOGICAL(x), LOGICAL_RO(x); values 0, 1 or NA_LOGICAL

CPLXSXP and RAWSXP

complex and raw vectors

COMPLEX(x) (an Rcomplex has members r and i), RAW(x)

STRSXP

character vector

STRING_ELT(x, i) gives a CHARSXP; CHAR() of that gives a const char *; SET_STRING_ELT() stores one

VECSXP

list (generic vector)

VECTOR_ELT(x, i), SET_VECTOR_ELT(x, i, v)

NILSXP

NULL

the constant R_NilValue

LISTSXP, LANGSXP

pairlist, call

CAR, CDR, TAG; see .External() and the callback example below

CLOSXP, ENVSXP, SYMSXP

function, environment, name

Rf_isFunction(), Rf_eval(), Rf_install()

Two types in that table deserve a remark. A CHARSXP is not an R-level object but the immutable, shared element of a character vector: R keeps one copy of each distinct string in a global cache, so C code must never modify the bytes behind CHAR(). And LISTSXP is the old Lisp-style pairlist; an R list is a VECSXP, which is why the test for an R list is Rf_isNewList() and not Rf_isList() (Writing R Extensions: Handling lists). R Internals describes all the types, including the ones that are not for packages (R Internals: SEXPs).

The Rf_ prefix and R_NO_REMAP

The functions in the manual’s examples carry an Rf_ prefix (Rf_allocVector, Rf_error, Rf_ScalarReal), and so do all the examples on this page. R’s headers also define short names for many of them (allocVector, error, length), which are macros that expand to the prefixed names, and those short names clash easily with system headers and other libraries. Defining R_NO_REMAP before any R header removes the macros and leaves only the prefixed names. The manual states the facts that matter: the prefix is optional in C code and recommended for new C code; since R 4.5.0 R_NO_REMAP is always defined when the R headers are included from C++, so C++ code must use the Rf_ names; and a package that defines it itself should guard it with #ifndef R_NO_REMAP to avoid compiler warnings about redefinition (Writing R Extensions: The R API).

The NEWS entries behind that: R 4.4.0 added the environment variable _R_CXX_USE_NO_REMAP_, which makes R CMD INSTALL (and so R CMD check) compile C++ code with -DR_NO_REMAP, and said that this was planned to become the default; R 4.5.0 made it the default and revised the manual to describe the remapped entry points (R NEWS: Changes in R 4.4.0 and 4.5.0). usethis::use_c() writes a template that starts with #define R_NO_REMAP, as the shelf example later shows. Two related switches exist for other headers: R_NO_REMAP_RMATH does the same for Rmath.h (the distribution functions), and STRICT_R_HEADERS, which removes the old S-compatibility definitions (PI, Calloc, Realloc, Free and the DOUBLE_* constants), has been the default since R 4.5.0, with R_Calloc, R_Realloc, R_Free and M_PI as the replacements.

A vector in, a vector out

The next file has three functions that show the pattern every .Call() routine follows: check or coerce the arguments, allocate the result, fill it through a pointer, copy the attributes that should survive, and return. overdue_days() requires a double vector and returns the days beyond the loan limit element by element; is_late() coerces to integers and returns a logical vector; count_late() calls is_late() and counts.

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

/* Days beyond the loan period, element by element; NA and NaN pass through. */
SEXP overdue_days(SEXP days, SEXP limit) {
  if (!Rf_isReal(days)) {
    Rf_error("'days' must be a double vector");
  }
  R_xlen_t n = XLENGTH(days);
  double lim = Rf_asReal(limit);
  const double *d = REAL_RO(days);
  SEXP out = PROTECT(Rf_allocVector(REALSXP, n));
  double *o = REAL(out);
  for (R_xlen_t i = 0; i < n; i++) {
    if (ISNAN(d[i])) {
      o[i] = d[i];
    } else {
      o[i] = d[i] > lim ? d[i] - lim : 0.0;
    }
  }
  SEXP nms = Rf_getAttrib(days, R_NamesSymbol);
  if (nms != R_NilValue) {
    Rf_setAttrib(out, R_NamesSymbol, nms);
  }
  UNPROTECT(1);
  return out;
}

/* TRUE when a loan is late, NA when the number of days is missing. */
SEXP is_late(SEXP days, SEXP limit) {
  SEXP d = PROTECT(Rf_coerceVector(days, INTSXP));
  int lim = Rf_asInteger(limit);
  R_xlen_t n = XLENGTH(d);
  SEXP out = PROTECT(Rf_allocVector(LGLSXP, n));
  const int *pd = INTEGER_RO(d);
  int *po = LOGICAL(out);
  for (R_xlen_t i = 0; i < n; i++) {
    po[i] = pd[i] == NA_INTEGER ? NA_LOGICAL : pd[i] > lim;
  }
  UNPROTECT(2);
  return out;
}

/* How many loans are late; missing values are not counted. */
SEXP count_late(SEXP days, SEXP limit) {
  SEXP flags = PROTECT(is_late(days, limit));
  const int *p = LOGICAL_RO(flags);
  R_xlen_t n = XLENGTH(flags);
  int total = 0;
  for (R_xlen_t i = 0; i < n; i++) {
    if (p[i] != NA_LOGICAL && p[i]) {
      total++;
    }
  }
  UNPROTECT(1);
  return Rf_ScalarInteger(total);
}

Several points in the file are worth a line each.

  • XLENGTH() and R_xlen_t are for lengths. R vectors can be longer than 2^31 - 1 elements on 64-bit platforms (the length is a signed 64-bit integer, with a practical limit of 52 bits), and LENGTH() and Rf_length() return an int that cannot hold such lengths; code that loops with an int index works until the day someone passes a very long vector (R Internals: Long vectors).

  • REAL_RO() returns a const pointer. The vector accessors have read-only variants (INTEGER_RO, LOGICAL_RO, DATAPTR_RO), and for a character vector or a list only read-only access exists, because writing straight into those would break the memory manager’s assumptions. The manual adds that these pointers are valid only for vectors of positive length: a zero-length vector has no data, so memcpy() of zero elements through such a pointer is undefined behaviour unless the length is checked first (Writing R Extensions: Vector accessor functions). top_days() below guards its copy for this reason.

  • REAL(x) is a function call, not a field access. Store the pointer in a variable before the loop, as the file does, instead of calling REAL(x)[i] on every iteration.

  • The result is allocated with Rf_allocVector(REALSXP, n). Unlike numeric(n) in R, Rf_allocVector() does not initialise the elements of an atomic vector (it only does so for lists and character vectors), so every element must be written before the object is returned.

  • The names are copied explicitly, with Rf_getAttrib() and Rf_setAttrib(). Attributes are not copied automatically; a function that should keep names, dim or class has to do it.

  • PROTECT() and UNPROTECT() bracket the lifetime of the new object; the section after next explains why.

The script builds the R wrappers, calls the functions on a vector of loan durations that contains a missing value (NA) and a not-a-number (NaN), and shows what the type check does. storage.mode<- in the wrapper converts to double while keeping the names, which as.double() would drop.

# Built with `R CMD SHLIB vectors.c`; the loan durations are typed in.
dyn.load(paste0("vectors", .Platform$dynlib.ext))
overdue_days <- function(days, limit = 21) {
  storage.mode(days) <- "double"
  .Call("overdue_days", days, limit, PACKAGE = "vectors")
}
is_late <- function(days, limit = 21L) {
  .Call("is_late", days, limit, PACKAGE = "vectors")
}
count_late <- function(days, limit = 21L) {
  .Call("count_late", days, limit, PACKAGE = "vectors")
}

days <- c(
  loan1 = 14, loan2 = 35, loan3 = NA, loan4 = 21, loan5 = 28.5, loan6 = NaN
)
overdue_days(days)
#> loan1 loan2 loan3 loan4 loan5 loan6
#>   0.0  14.0    NA   0.0   7.5   NaN
overdue_days(days, limit = 14)
#> loan1 loan2 loan3 loan4 loan5 loan6
#>   0.0  21.0    NA   7.0  14.5   NaN
overdue_days(numeric())
#> numeric(0)

# Names survive; NA and NaN are told apart.
is.na(overdue_days(days))
#> loan1 loan2 loan3 loan4 loan5 loan6
#> FALSE FALSE  TRUE FALSE FALSE  TRUE
is.nan(overdue_days(days))
#> loan1 loan2 loan3 loan4 loan5 loan6
#> FALSE FALSE FALSE FALSE FALSE  TRUE

# The C code coerces, so integers and doubles both work (28.5 is cut to 28).
is_late(days)
#> [1] FALSE  TRUE    NA FALSE  TRUE    NA
count_late(days)
#> [1] 2
count_late(c(30L, 10L, NA, 22L))
#> [1] 2
typeof(count_late(days))
#> [1] "integer"

# A wrong type is an error raised from C.
tryCatch(
  .Call("overdue_days", "14", 21, PACKAGE = "vectors"),
  error = conditionMessage
)
#> [1] "'days' must be a double vector"

overdue_days() passes both NA and NaN through unchanged because it copies the element when ISNAN() is true. That is a deliberate choice: the next table shows that C distinguishes the two only with ISNA(), and it is easy to turn every NaN into NA by writing NA_REAL where the input should have been kept.

Missing values in C

R’s missing values are values of the C types, and each type has its own constant and its own test.

Type Missing value Test

double

NA_REAL (a particular NaN bit pattern)

ISNA(x) is true for NA only; ISNAN(x) is true for NA and NaN; R_FINITE(x) is false for NA, NaN, Inf and -Inf; R_IsNaN(x) is true for NaN only. Never compare with ==.

int (integer)

NA_INTEGER (the smallest int)

x == NA_INTEGER

int (logical)

NA_LOGICAL

x == NA_LOGICAL; any other non-zero value is TRUE

SEXP (character element)

NA_STRING

STRING_ELT(x, i) == NA_STRING (pointer comparison; the string is "NA" to CHAR())

The manual adds that these tests apply to doubles only, that isfinite() should not be used in place of R_FINITE(), and that Inf and -Inf are tested by comparing with R_PosInf and R_NegInf. .Call() passes missing values straight into the C code; it is .C() that refuses them unless NAOK = TRUE, as a later section shows.

Character vectors

A character vector is a STRSXP: a vector whose elements are CHARSXP nodes. Reading goes through STRING_ELT() and CHAR(); writing builds a CHARSXP with Rf_mkChar() (from a NUL-terminated C string), Rf_mkCharLen() (from part of a buffer) or Rf_mkCharLenCE() (with an encoding mark) and stores it with SET_STRING_ELT(). Rf_mkString() makes a whole length-one character vector from a C string, and Rf_ScalarString() from a CHARSXP. The function below keeps the first blank-delimited word of each string, passes NA through, and, because it copies the bytes of an element it did not interpret, copies the encoding mark of the original with it.

#include <string.h>
#include <R.h>
#include <Rinternals.h>

/* The first blank-delimited word of each string; NA stays NA. */
SEXP first_word(SEXP x) {
  if (!Rf_isString(x)) {
    Rf_error("'x' must be a character vector");
  }
  R_xlen_t n = XLENGTH(x);
  SEXP out = PROTECT(Rf_allocVector(STRSXP, n));
  for (R_xlen_t i = 0; i < n; i++) {
    SEXP el = STRING_ELT(x, i);
    if (el == NA_STRING) {
      SET_STRING_ELT(out, i, NA_STRING);
      continue;
    }
    const char *s = CHAR(el);
    int len = (int) strcspn(s, " \t");
    SET_STRING_ELT(out, i, Rf_mkCharLenCE(s, len, Rf_getCharCE(el)));
  }
  UNPROTECT(1);
  return out;
}

The encoding rules are the part of this that goes wrong in practice. A CHARSXP can be marked as UTF-8 or Latin-1, and the bytes behind CHAR() are in whatever encoding the mark says. When C code needs to interpret the characters, the manual recommends Rf_translateChar() (to the current locale’s encoding) or Rf_translateCharUTF8() (to UTF-8, which almost always represents the string faithfully), because both handle any input and allocate with R_alloc(); and when it only moves a string unchanged, as here, it should keep the mark with Rf_getCharCE() and Rf_mkCharLenCE() (Writing R Extensions: Character encoding issues). The help page of .C() adds a reason to prefer .Call() for text: a missing string reaches .C() code as the text "NA", which cannot be told from a real "NA".

Lists, classes and matrices

Lists, classes and dimensions are all attributes and vectors of vectors. loan_stats() below builds a named list with Rf_mkNamed() (which takes a C array of names ended by an empty string), fills its three elements with SET_VECTOR_ELT(), and sets the class attribute with Rf_classgets(). day_gaps() allocates a matrix with Rf_allocMatrix() (column-major, like R) and sets dimnames to a two-element list, reusing the names of the input for both dimensions. A data frame is a list of equal-length columns with names, row.names and class attributes; it is simplest to build the columns in C and call as.data.frame() or data.frame() in the R wrapper (Rcpp, later, does it in one expression).

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

/* A classed, named list: count, mean and maximum of the non-missing days. */
SEXP loan_stats(SEXP days) {
  SEXP d = PROTECT(Rf_coerceVector(days, REALSXP));
  const double *p = REAL_RO(d);
  R_xlen_t len = XLENGTH(d);
  int n = 0;
  double sum = 0.0, max = R_NegInf;
  for (R_xlen_t i = 0; i < len; i++) {
    if (ISNAN(p[i])) {
      continue;
    }
    n++;
    sum += p[i];
    if (p[i] > max) {
      max = p[i];
    }
  }
  const char *names[] = {"n", "mean", "max", ""};
  SEXP out = PROTECT(Rf_mkNamed(VECSXP, names));
  SET_VECTOR_ELT(out, 0, Rf_ScalarInteger(n));
  SET_VECTOR_ELT(out, 1, Rf_ScalarReal(n > 0 ? sum / n : NA_REAL));
  SET_VECTOR_ELT(out, 2, Rf_ScalarReal(n > 0 ? max : NA_REAL));
  SEXP cls = PROTECT(Rf_mkString("loan_stats"));
  Rf_classgets(out, cls);
  UNPROTECT(3);
  return out;
}

/* A matrix of differences x[i] - x[j], with names(x) as both dimnames. */
SEXP day_gaps(SEXP x) {
  SEXP xr = PROTECT(Rf_coerceVector(x, REALSXP));
  R_xlen_t n = XLENGTH(xr);
  const double *p = REAL_RO(xr);
  SEXP out = PROTECT(Rf_allocMatrix(REALSXP, (int) n, (int) n));
  double *o = REAL(out);
  for (R_xlen_t j = 0; j < n; j++) {
    for (R_xlen_t i = 0; i < n; i++) {
      o[i + n * j] = p[i] - p[j];
    }
  }
  SEXP nms = Rf_getAttrib(x, R_NamesSymbol);
  if (nms != R_NilValue) {
    SEXP dn = PROTECT(Rf_allocVector(VECSXP, 2));
    SET_VECTOR_ELT(dn, 0, nms);
    SET_VECTOR_ELT(dn, 1, nms);
    Rf_setAttrib(out, R_DimNamesSymbol, dn);
    UNPROTECT(1);
  }
  UNPROTECT(2);
  return out;
}

Both functions follow the protection rule: every object that is created and then kept while something else is allocated is protected, and the count of UNPROTECT() matches. In loan_stats() the three scalars are not protected separately because they are handed to SET_VECTOR_ELT() immediately, and the list that now owns them is protected. The manual explains this: protecting an object protects everything it points to, so the elements of a protected list are protected. In day_gaps() the dimnames list is protected with its own PROTECT() and released with an UNPROTECT(1) as soon as it has been attached.

One script exercises both strings.c and structures.c. It feeds first_word() an ASCII vector with an NA and an empty string, then an accented name written as a UTF-8 string literal: the result is still marked UTF-8. It then prints the list from loan_stats() and gives the class a print method in R, and finally shows the dimnames of the matrix.

# Built with `R CMD SHLIB strings.c` and `R CMD SHLIB structures.c`.
dyn.load(paste0("strings", .Platform$dynlib.ext))
dyn.load(paste0("structures", .Platform$dynlib.ext))

authors <- c("Ursula K. Le Guin", "Iris  Fenwick", NA, "Mara Quill", "")
.Call("first_word", authors, PACKAGE = "strings")
#> [1] "Ursula" "Iris"   NA       "Mara"   ""

# Encoding is kept: a UTF-8 string stays marked as UTF-8.
x <- "André Pinto"
Encoding(.Call("first_word", x, PACKAGE = "strings"))
#> [1] "UTF-8"
.Call("first_word", x, PACKAGE = "strings")
#> [1] "André"

days <- c(loan1 = 14, loan2 = 35, loan3 = NA, loan4 = 21)
stats <- .Call("loan_stats", days, PACKAGE = "structures")
class(stats)
#> [1] "loan_stats"
names(stats)
#> [1] "n"    "mean" "max"
unclass(stats)
#> $n
#> [1] 3
#>
#> $mean
#> [1] 23.33333
#>
#> $max
#> [1] 35
#>
print.loan_stats <- function(x, ...) {
  cat(sprintf("%d loans, mean %.1f days, longest %g\n", x$n, x$mean, x$max))
  invisible(x)
}
stats
#> 3 loans, mean 23.3 days, longest 35

# A matrix with dimnames set from C.
.Call("day_gaps", days, PACKAGE = "structures")
#>       loan1 loan2 loan3 loan4
#> loan1     0   -21    NA    -7
#> loan2    21     0    NA    14
#> loan3    NA    NA    NA    NA
#> loan4     7   -14    NA     0
dim(.Call("day_gaps", c(1, 2, 3), PACKAGE = "structures"))
#> [1] 3 3

Errors, warnings, output and interrupts

C code reports problems to R with Rf_error() and Rf_warning(), which take a printf-style format and have the same effect as stop() and warning() in R code. Output goes through Rprintf() (R’s standard output, which a GUI console, sink() or a parallel worker may redirect) and REprintf() (R’s error stream), never printf() to the C library’s stdout, whose text may not appear in the console at all; writing complete lines is advised. A long loop should call R_CheckUserInterrupt() now and then, because no part of R can be interrupted while it runs compiled code otherwise.

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

/* Stops on a negative value, warns about a missing one. */
SEXP checked_sqrt(SEXP x) {
  double v = Rf_asReal(x);
  if (ISNAN(v)) {
    Rf_warning("missing value, returning NA");
    return Rf_ScalarReal(NA_REAL);
  }
  if (v < 0) {
    Rf_error("'x' must be non-negative, not %g", v);
  }
  return Rf_ScalarReal(sqrt(v));
}

/* Prints a summary on standard output and a note on standard error. */
SEXP report(SEXP days) {
  SEXP d = PROTECT(Rf_coerceVector(days, REALSXP));
  R_xlen_t n = XLENGTH(d);
  Rprintf("%lld loans\n", (long long) n);
  REprintf("(note: written to the error stream)\n");
  UNPROTECT(1);
  return R_NilValue;
}

/* A long loop that can be interrupted from the console. */
SEXP busy(SEXP iterations) {
  double steps = Rf_asReal(iterations);
  double acc = 0.0;
  for (double i = 0; i < steps; i++) {
    if (((long long) i) % 1000000 == 0) {
      R_CheckUserInterrupt();
    }
    acc += 1.0;
  }
  return Rf_ScalarReal(acc);
}

There are rules to follow in these functions. Rf_error() never returns: R implements it as a non-local jump (a longjmp) to the nearest tryCatch() or the top level, skipping the C code between, so nothing after the call runs: memory from R_alloc() is reclaimed by R on an error, memory from R_Calloc() is not, and R restores the protect stack by itself. The format string is a template, so a message that comes from data must go through "%s" (Rf_error("%s", msg)), or a % in it will be interpreted. Rf_errorcall(call, ...) and Rf_warningcall(call, ...) take the call to blame, and R_NilValue omits it. The script shows what R makes of them. The error is an ordinary simpleError whose call is that of the R function that called .Call(); capture.output() sees the Rprintf() line, and its type = "message" form sees the REprintf() line.

# Built with `R CMD SHLIB conditions.c`.
dyn.load(paste0("conditions", .Platform$dynlib.ext))
checked_sqrt <- function(x) .Call("checked_sqrt", x, PACKAGE = "conditions")

checked_sqrt(2)
#> [1] 1.414214

# Rf_error() becomes an ordinary R error, blamed on the calling R function.
err <- tryCatch(checked_sqrt(-4), error = identity)
conditionMessage(err)
#> [1] "'x' must be non-negative, not -4"
class(err)
#> [1] "simpleError" "error"       "condition"
deparse(conditionCall(err))
#> [1] "checked_sqrt(-4)"

# Rf_warning() becomes an ordinary R warning.
warn <- tryCatch(checked_sqrt(NA), warning = identity)
conditionMessage(warn)
#> [1] "missing value, returning NA"
class(warn)
#> [1] "simpleWarning" "warning"       "condition"

# Rprintf() goes through R's output connection and REprintf() through its
# error connection, so each can be captured on its own.
report <- function(days) {
  invisible(.Call("report", days, PACKAGE = "conditions"))
}
msg <- capture.output(
  out <- capture.output(report(c(14, 21, 35))),
  type = "message"
)
out
#> [1] "3 loans"
msg
#> [1] "(note: written to the error stream)"

# R_CheckUserInterrupt() lets Ctrl-C stop a long loop; without one, it cannot be
# stopped. This run is short enough to finish.
busy <- function(n) .Call("busy", n, PACKAGE = "conditions")
busy(5e6)
#> [1] 5e+06

The call shown in deparse(conditionCall(err)) is checked_sqrt(-4), the wrapper, which is what a user of the function wants in the message; the unwrapped .Call("checked_sqrt", x, PACKAGE = "conditions") would not help them. The last lines run busy(), the loop with R_CheckUserInterrupt(); it finishes here because nobody presses Ctrl-C. The manual gives a version for Fortran, rchkusr(), and adds that the code behind many API entry points can itself be interrupted or raise an error, so any C code that holds a resource should be written with that in mind (Writing R Extensions: Allowing interrupts).

When the resource is a file, a lock, or a buffer from R_Calloc(), the tools for a clean exit are in the manual’s section on condition handling and cleanup code: R_UnwindProtect() runs a cleanup function on both normal return and non-local jumps, R_ExecWithCleanup() is the older and simpler form, and R_tryCatch() and R_tryEval() catch conditions (Writing R Extensions: Condition handling and cleanup code). An alternative that avoids the problem is to keep the allocation in R: allocate the buffer in R (numeric(n)) and pass it in.

Calling R from C

C code can call back into R. The routine below receives a function f, a vector and an environment, and applies the function to each element with Rf_lang2() (which builds the call f(arg)) and Rf_eval(). Each intermediate object is protected, and the check of the result’s type is made in C, since the function could return anything. The environment rho is passed from R as environment() because the API has no supported entry point for the current environment (the manual’s table of replacements tells packages to call environment() in R and pass the result).

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

/* Call the R function f on each element of x and collect the numbers. */
SEXP map_dbl(SEXP f, SEXP x, SEXP rho) {
  if (!Rf_isFunction(f)) {
    Rf_error("'f' must be a function");
  }
  SEXP xr = PROTECT(Rf_coerceVector(x, REALSXP));
  R_xlen_t n = XLENGTH(xr);
  SEXP out = PROTECT(Rf_allocVector(REALSXP, n));
  for (R_xlen_t i = 0; i < n; i++) {
    SEXP arg = PROTECT(Rf_ScalarReal(REAL(xr)[i]));
    SEXP call = PROTECT(Rf_lang2(f, arg));
    SEXP res = PROTECT(Rf_eval(call, rho));
    if (!Rf_isNumeric(res) || XLENGTH(res) != 1) {
      Rf_error("'f' must return a single number");
    }
    REAL(out)[i] = Rf_asReal(res);
    UNPROTECT(3);
  }
  UNPROTECT(2);
  return out;
}

An error inside f is an ordinary R error: it unwinds through the C function and arrives at the caller’s tryCatch(), so the message is the one that f raised. The two checks in the C code give the other two messages.

# Built with `R CMD SHLIB callback.c`.
dyn.load(paste0("callback", .Platform$dynlib.ext))
map_dbl <- function(f, x) {
  .Call("map_dbl", f, x, environment(), PACKAGE = "callback")
}

late_fee <- function(days) pmax(days - 21, 0) * 0.25
map_dbl(late_fee, c(14, 21, 35, 49))
#> [1] 0.0 0.0 3.5 7.0
map_dbl(\(d) d / 7, 1:3)
#> [1] 0.1428571 0.2857143 0.4285714

# Errors raised by the R function pass through the C code to the caller.
tryCatch(map_dbl(\(d) stop("boom at ", d), 1:3), error = conditionMessage)
#> [1] "boom at 1"
tryCatch(map_dbl(\(d) c(d, d), 1:3), error = conditionMessage)
#> [1] "'f' must return a single number"
tryCatch(map_dbl("late_fee", 1:3), error = conditionMessage)
#> [1] "'f' must be a function"

A callback costs as much as an R function call, so a loop that calls R on every element gives back most of what the C code saved. Callbacks are for the occasional call (an objective function in an optimiser, a user hook) rather than for the inner loop.

The rules that keep a .Call routine safe

The manual’s rules for .Call() and .External() code can be written as a short list.

  • Treat the arguments as read-only. .Call() and .External() do not copy their arguments, so a vector that C code modifies in place changes every R variable that shares it. R shares freely (y <- x copies nothing, as Assignment, Names and Values shows). Copy with Rf_duplicate() (or Rf_shallow_duplicate() when only the top level changes) before modifying, and see Memory and Copy Semantics for how R decides when a copy is needed. The macros NAMED and SET_NAMED that older books use were replaced by reference counting in R 4.0.0; package code should use MAYBE_SHARED(), MAYBE_REFERENCED(), NO_REFERENCES() and MARK_NOT_MUTABLE(), which are documented in the section on named objects and copying (Writing R Extensions: Named objects and copying).

  • Check the types. A wrong guess gives a segmentation fault, not an R error. Test with TYPEOF() or Rf_isReal() and friends, or coerce with Rf_coerceVector() (which allocates, so the result is protected) or the scalar readers Rf_asReal(), Rf_asInteger() and Rf_asLogical(), which ignore all but the first element, as the add() example showed. Rf_asRboolean() (R 4.5.0) is the stricter version that errors on a length other than one. Coercion in C does not dispatch on class (a factor is just an integer), so it is usually better to coerce in the R wrapper.

  • Allocate with R and protect what you allocate (next section), and never keep a SEXP in a global C variable across calls unless it is registered with R_PreserveObject().

  • Stay on the main thread. The manual is explicit that the R API may be called only from the main thread of the R process; using it from another thread can corrupt memory.

  • Prefer the documented API. The last section of the page is about which entry points are documented.

PROTECT and UNPROTECT

Why objects need protecting

R frees the memory of objects automatically, by garbage collection, and it can run the collector whenever it allocates anything. An object that C code has just created and holds only in a local variable is invisible to the collector: nothing in R’s own data structures refers to it, so the next allocation can free it, and the pointer in the C variable then points at memory that may be reused. The result is not an error but corrupt output or a crash, often much later and somewhere else. PROTECT() puts the object on a stack that the collector treats as a root, and UNPROTECT(n) pops the last n entries. Writing R Extensions states the rule precisely: it is the object that is protected, not the pointer variable, so assigning a different object to p after PROTECT(p) leaves the new object unprotected (Writing R Extensions: Handling the effects of garbage collection).

The protect stack has rules, and the manual lists four of them for functions that cooperate.

  • Balance. PROTECT and UNPROTECT calls must balance in each function, and a function may unprotect only what it protected itself. A jump out of the function (an error, an interrupt) restores the stack automatically.

  • Callers protect arguments. The caller keeps every argument protected for the whole call, which is why .Call() routines do not protect their arguments.

  • Return values are unprotected. An object returned by a function must be protected at once by the caller, even if the callee only returned something that already existed, because it may have been duplicated.

  • Everything allocates. Assume that any API function or macro may allocate and so may trigger a collection; the code that is safe today because a function happens not to allocate can fail after a change in R.

The simplest style follows from them: SEXP x = PROTECT(Rf_allocVector(...)); on creation, one UNPROTECT(n) before every return, with n the number of objects protected in that function. The stack has a fixed size (10,000 entries by default), so a loop must not protect something new on every iteration without releasing it, and the better design is to store objects in a protected container (a list) or to unprotect them immediately after use. callback.c above does exactly that: it protects three objects inside the loop and UNPROTECT(3) at the end of each iteration. Anything allocated and handed straight to SET_VECTOR_ELT() or SET_STRING_ELT() is safe from then on, as loan_stats() shows.

Changing a protected value: PROTECT_WITH_INDEX

The rule that the object is protected, and not the pointer, means that code that replaces a protected object (by coercing, duplicating or growing it) must protect the replacement. When the old object is no longer needed, PROTECT_WITH_INDEX(s, &ipx) records the stack position and REPROTECT(s, ipx) stores a new object in that position, so the stack does not grow. total_days() below coerces an integer argument to double only if it needs to, and keeps whichever object it ends up using protected under one index.

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

/* Bug: one PROTECT too many, so the protect stack is one entry deeper. */
SEXP leaky(SEXP x) {
  SEXP out = PROTECT(Rf_allocVector(REALSXP, 1));
  REAL(out)[0] = Rf_asReal(x) * 2.0;
  return out;
}

/* Bug: one UNPROTECT too many, so the caller's entry is dropped. */
SEXP too_many(SEXP x) {
  SEXP out = PROTECT(Rf_allocVector(REALSXP, 1));
  REAL(out)[0] = Rf_asReal(x) * 2.0;
  UNPROTECT(2);
  return out;
}

/* Correct: the coerced copy replaces the protected value in place. */
SEXP total_days(SEXP x) {
  PROTECT_INDEX ipx;
  SEXP v = x;
  PROTECT_WITH_INDEX(v, &ipx);
  if (TYPEOF(v) != REALSXP) {
    REPROTECT(v = Rf_coerceVector(v, REALSXP), ipx);
  }
  double total = 0.0;
  const double *p = REAL_RO(v);
  for (R_xlen_t i = 0; i < XLENGTH(v); i++) {
    total += p[i];
  }
  UNPROTECT(1);
  return Rf_ScalarReal(total);
}

The file also contains the two classic mistakes, in functions that compile without a warning and return the right number: leaky() has one PROTECT() too many (it forgot UNPROTECT(1)), and too_many() has one UNPROTECT() too many. .Call() checks the protect stack when a routine returns, and when the count has changed it prints a warning. The text of that warning is real; the two numbers are the size of the protect stack before and after the call, so they depend on how deeply R is nested at that moment, and the values below are the ones from a top-level Rscript run. A leak makes the second number larger, an excess UNPROTECT() makes it smaller. The messages are written directly to the error stream when the routine returns, before the value is printed, so they are not conditions: tryCatch(..., warning = ) and withCallingHandlers() do not see them (in this test the handler did not run). The wrong routines still return 2.

# Built with `R CMD SHLIB protect.c`.
dyn.load(paste0("protect", .Platform$dynlib.ext))

# Correct code: PROTECT_WITH_INDEX and REPROTECT keep the coerced copy alive.
total_days <- function(x) .Call("total_days", x, PACKAGE = "protect")
total_days(c(14, 21, 35))
#> [1] 70
total_days(1:4)
#> [1] 10

# Unbalanced code still returns its value; the protect stack is what is wrong.
# R notices when the .Call returns and prints a warning on the console. The two
# numbers in it are the stack sizes before and after, and they vary from run to
# run (they depend on how deeply R is nested at that moment).
.Call("leaky", 1, PACKAGE = "protect")
#> Warning: stack imbalance in '.Call', 2 then 3
#> [1] 2
.Call("too_many", 1, PACKAGE = "protect")
#> Warning: stack imbalance in '.Call', 2 then 1
#> [1] 2

The messages catch only a mismatch in the count. A missing PROTECT() is silent until the collector runs at the wrong moment, and that is much harder to find. R has two tools for it. gctorture(TRUE) makes the collector run on nearly every allocation, so an unprotected object is freed at once and the crash happens close to its cause; R CMD check --use-gct runs a package’s examples, tests and vignettes that way (R documentation: gctorture). A build of R configured with --enable-strict-barrier also marks freed nodes, so touching one raises an error that names the old type. The sections on checking memory access in Writing R Extensions describe these, Valgrind and the address sanitizer (Writing R Extensions: Checking memory access).

R_PreserveObject and the other ways to keep an object alive

PROTECT() is for the duration of one call. An object that has to survive between calls (a cache, a handle returned to R as an external pointer) is kept alive with R_PreserveObject(x) and released with R_ReleaseObject(x); the manual calls this less efficient than normal protection and to be used sparingly, because releasing it is then the author’s responsibility, usually from a finalizer. For parsers there is a multi-set interface (R_NewPreciousMSet, R_PreserveInMSet, R_ReleaseFromMSet), and the old UNPROTECT_PTR() is dangerous to mix with UNPROTECT() and is not for new code. External pointers, which wrap a C pointer in an R object with a finalizer, are the way to return a C data structure to R, and have their own section (Writing R Extensions: External pointers and weak references).

Memory allocation: R_alloc and R_Calloc

C code needs memory that is not an R object: a scratch buffer, a working copy. R provides two families, and the difference is who cleans up (Writing R Extensions: Memory allocation).

Function Lifetime Use it for

R_alloc(n, size)

Until the .C(), .Call() or .External() call returns, or vmaxset() releases it; freed on an error or interrupt.

Scratch space inside one call. Preferred, because no error path can leak it. Aligned for double only; R_allocLD() is aligned for long double.

R_Calloc(n, type), R_Realloc(p, n, type), R_Free(p)

Until R_Free(). A wrapper over malloc that signals an R error if allocation fails.

Memory that outlives one call, or too large to hold for the whole call. The caller must free it on every path, including errors and interrupts (often with on.exit() in the R wrapper).

S_alloc()

As R_alloc, zero-filled.

Avoid for large sizes: its length argument is a long, which is too small on 64-bit Windows.

memory.c shows both. median_days() copies the non-missing values into an R_alloc() buffer, sorts it with R_rsort() (R’s own sort for doubles) and reads the middle; top_days() sorts a copy held in an R_Calloc() buffer and frees it with R_Free() before returning. A check on the length precedes the Memcpy(), for the reason given above about zero-length vectors. Memory from R_Calloc() does not necessarily come from the same pool as malloc(), so it must not be passed to free() or strdup().

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

/* Median of the non-missing days: scratch space from R_alloc. */
SEXP median_days(SEXP days) {
  SEXP d = PROTECT(Rf_coerceVector(days, REALSXP));
  R_xlen_t n = XLENGTH(d);
  const double *p = REAL_RO(d);
  double *buf = (double *) R_alloc((size_t) n, sizeof(double));
  R_xlen_t m = 0;
  for (R_xlen_t i = 0; i < n; i++) {
    if (!ISNAN(p[i])) {
      buf[m++] = p[i];
    }
  }
  double med = NA_REAL;
  if (m > 0) {
    R_rsort(buf, (int) m);
    med = (m % 2 == 1) ? buf[m / 2] : 0.5 * (buf[m / 2 - 1] + buf[m / 2]);
  }
  UNPROTECT(1);
  return Rf_ScalarReal(med);
}

/* The k largest values, in decreasing order: a buffer from R_Calloc. */
SEXP top_days(SEXP days, SEXP k) {
  SEXP d = PROTECT(Rf_coerceVector(days, REALSXP));
  R_xlen_t n = XLENGTH(d);
  int kk = Rf_asInteger(k);
  if (kk == NA_INTEGER || kk < 0) {
    Rf_error("'k' must be a non-negative integer");
  }
  if (kk > n) {
    kk = (int) n;
  }
  double *buf = R_Calloc((size_t) n, double);
  if (n > 0) {
    Memcpy(buf, REAL_RO(d), (size_t) n);
  }
  R_rsort(buf, (int) n);
  SEXP out = PROTECT(Rf_allocVector(REALSXP, kk));
  for (int i = 0; i < kk; i++) {
    REAL(out)[i] = buf[n - 1 - i];
  }
  R_Free(buf);
  UNPROTECT(2);
  return out;
}
# Built with `R CMD SHLIB memory.c`.
dyn.load(paste0("memory", .Platform$dynlib.ext))
median_days <- function(days) .Call("median_days", days, PACKAGE = "memory")
top_days <- function(days, k) {
  .Call("top_days", days, as.integer(k), PACKAGE = "memory")
}

days <- c(14, 35, NA, 21, 28, 7, 49)
median_days(days)
#> [1] 24.5
median(days, na.rm = TRUE)
#> [1] 24.5
median_days(c(NA_real_, NA_real_))
#> [1] NA
median_days(1:4)
#> [1] 2.5

top_days(days, 3)
#> [1] NA 49 35
top_days(c(5, 1, 3), 10)
#> [1] 5 3 1
tryCatch(top_days(days, -1), error = conditionMessage)
#> [1] "'k' must be a non-negative integer"

The macros were named Calloc, Realloc and Free in older code. The prefixed forms were introduced in R 3.4.0 and are preferred; the unprefixed ones, with the old constant PI, were removed from the headers in R 4.5.0 when strict R headers became the default (the manual says so, and the NEWS entry names them as the legacy definitions that went). R_Calloc and its relatives expand to calls to R_chk_calloc, R_chk_realloc and R_chk_free, which the manual says not to call directly. The next file is old code of exactly that kind.

#include <R.h>
#include <Rinternals.h>
SEXP f(SEXP x) {
  double *buf = Calloc(10, double);
  Free(buf);
  return Rf_ScalarReal(PI);
}

On R 4.6.1 it does not compile. This is the compiler’s complaint (filtered to the lines that matter), and the build’s exit status, 1.

R CMD SHLIB old_macros.c 2>&1 | grep -E ": (warning|error):"
#> old_macros.c:4:17: warning: implicit declaration of function ‘Calloc’; did you mean ‘calloc’? [-Wimplicit-function-declaration]
#> old_macros.c:4:28: error: expected expression before ‘double’
#> old_macros.c:5:3: warning: implicit declaration of function ‘Free’; did you mean ‘free’? [-Wimplicit-function-declaration]
#> old_macros.c:6:24: error: ‘PI’ undeclared (first use in this function)

The fix is the prefix: R_Calloc(10, double), R_Free(buf) and M_PI.

Registering native routines

The ISBN check as a .Call routine

So far every routine was found by its name, by searching the symbols of the shared object. The rest of the page uses one routine as its running example: the ISBN-13 check of the shelf package. It takes a character vector and returns a logical vector. Its rule is the one is_valid_isbn13() implements in R: keep the digits, reject an X, require exactly 13 digits, and require that the digits multiplied alternately by 1 and 3 add up to a multiple of 10. NA and the empty string are FALSE. The file has a header with the prototype, and starts with #define R_NO_REMAP, so every R function in it carries the Rf_ prefix. The constant ISBN_DIGITS has a default and can be overridden from the compiler command line, which the src/Makevars section uses.

#define R_NO_REMAP
#include <ctype.h>
#include <R.h>
#include <Rinternals.h>
#include "isbn.h"

#ifndef ISBN_DIGITS
#define ISBN_DIGITS 13
#endif

/* One string: keep the digits, reject an X, weight 1 and 3 alternately. */
static int valid_isbn(const char *s) {
  int n = 0, sum = 0;
  for (; *s != '\0'; s++) {
    if (isdigit((unsigned char) *s)) {
      if (n == ISBN_DIGITS) {
        return 0;
      }
      sum += (*s - '0') * (n % 2 == 0 ? 1 : 3);
      n++;
    } else if (*s == 'X' || *s == 'x') {
      return 0;
    }
  }
  return n == ISBN_DIGITS && sum % 10 == 0;
}

/* Vectorised: FALSE for NA, for the empty string and for a wrong check digit. */
SEXP isbn13_valid(SEXP x) {
  if (!Rf_isString(x)) {
    Rf_error("'x' must be a character vector");
  }
  R_xlen_t n = XLENGTH(x);
  SEXP out = PROTECT(Rf_allocVector(LGLSXP, n));
  int *po = LOGICAL(out);
  for (R_xlen_t i = 0; i < n; i++) {
    SEXP el = STRING_ELT(x, i);
    po[i] = el != NA_STRING && valid_isbn(CHAR(el));
  }
  UNPROTECT(1);
  return out;
}
#ifndef SHELF_ISBN_H
#define SHELF_ISBN_H

#include <Rinternals.h>

SEXP isbn13_valid(SEXP x);

#endif

Built with R CMD SHLIB isbn.c and loaded as before, the routine agrees with the R version on a vector that contains valid ISBNs in hyphenated and bare forms, one with a wrong check digit, the empty string, NA, a ten-digit ISBN-10 and a string with an X. The comparison function is the R version from the shelf package, copied into the script because the script runs in the directory of the compiled objects.

# Built with `R CMD SHLIB isbn.c`; the ISBN strings are typed in.
dyn.load(paste0("isbn", .Platform$dynlib.ext))
isbn13_c <- function(x) .Call("isbn13_valid", as.character(x), PACKAGE = "isbn")

# The same rule in R, for comparison.
isbn13_r <- function(x) {
  digits <- gsub("[^0-9X]", "", toupper(x))
  ok <- !is.na(digits) & nchar(digits) == 13L &
    !grepl("X", digits, fixed = TRUE)
  vapply(seq_along(digits), \(i) {
    if (!ok[[i]]) {
      return(FALSE)
    }
    d <- as.integer(strsplit(digits[[i]], "")[[1L]])
    sum(d * rep(c(1L, 3L), length.out = 13L)) %% 10L == 0L
  }, logical(1L))
}

isbns <- c(
  "978-0-306-40615-7", "9780001000100", "978-0-01-100112-9",
  "", NA, "978-0-00-200029-1", "0-306-40615-2", "978-0-00-X00029-1"
)
isbn13_c(isbns)
#> [1]  TRUE  TRUE FALSE FALSE FALSE  TRUE FALSE FALSE
identical(isbn13_c(isbns), isbn13_r(isbns))
#> [1] TRUE
isbn13_c(character())
#> logical(0)
tryCatch(isbn13_c(9780306406157), error = conditionMessage)
#> [1] TRUE
tryCatch(
  .Call("isbn13_valid", 9780306406157, PACKAGE = "isbn"),
  error = conditionMessage
)
#> [1] "'x' must be a character vector"

R_registerRoutines and R_init_<name>

Registration replaces the search by a table that the shared object builds when it is loaded. When R loads foo.so, it looks for a function named R_init_foo and calls it with a DllInfo pointer; when it unloads the object, it calls R_unload_foo if there is one. The name is made from the file name without its extension, so it must be a valid part of a C identifier: the manual advises ASCII letters, digits and underscores only. The initialiser calls R_registerRoutines() with five arguments: the DllInfo, and one NULL-terminated array for each interface, in the order .C(), .Call(), .Fortran(), .External(). The array element types are R_CMethodDef, R_CallMethodDef, R_FortranMethodDef and R_ExternalMethodDef. Each entry holds the name by which R refers to the routine, a function pointer cast to DL_FUNC, and the number of arguments; .External() routines use -1 for "any number". The entries for .C() and .Fortran() may add an array of argument types (REALSXP, INTSXP, LGLSXP, STRSXP, VECSXP, SINGLESXP) that R checks on every call.

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

static const R_CallMethodDef call_methods[] = {
  {"isbn13_valid", (DL_FUNC) &isbn13_valid, 1},
  {NULL, NULL, 0}
};

void R_init_shelf(DllInfo *dll) {
  R_registerRoutines(dll, NULL, call_methods, NULL, NULL);
  R_useDynamicSymbols(dll, FALSE);
  R_forceSymbols(dll, TRUE);
}

Two more calls close the loophole of finding things by string. R_useDynamicSymbols(dll, FALSE) says that the shared object is not to be searched for symbols not in the table, so .Call("name") finds only registered names. R_forceSymbols(dll, TRUE) says that foreign function calls into this object must name the entry point with an R object (a native symbol object), and not with a character string at all. The manual’s example uses a package mypkg with a registered routine reg and an unregistered unreg: without registration both .Call("reg") and .Call("unreg") work; with R_useDynamicSymbols(dll, FALSE) only the first does; with useDynLib(mypkg, .registration = TRUE, .fixes = "C_") in NAMESPACE, .Call(C_reg) works too; and with R_forceSymbols(dll, TRUE) only .Call(C_reg) works. That last arrangement is what a package normally wants: all its own calls go directly to its own code, and nobody finds its entry points by accident. A user who needs one from outside, for debugging, can write .Call(mypkg:::C_reg).

The next script builds this object as shelf.so (R CMD SHLIB -o shelf.so isbn.c init.c; the file name makes R_init_shelf() the initialiser), loads it next to the unregistered isbn.so, and shows the registered routine as an R object, the call by object, the call by string on the unregistered object, and what the two registered-only settings do to the string calls and to a wrong number of arguments. The object returned by getDLLRegisteredRoutines() lists the table; routines$.Call$isbn13_valid is a native symbol object.

# Built with `R CMD SHLIB isbn.c` and `R CMD SHLIB -o shelf.so isbn.c init.c`.
# The file name decides which initialiser R looks for: shelf.so runs
# R_init_shelf(), while isbn.so has none and registers nothing.
dyn.load(paste0("isbn", .Platform$dynlib.ext))
dyn.load(paste0("shelf", .Platform$dynlib.ext))
isbns <- c("978-0-306-40615-7", "978-0-01-100112-9", NA)

# What the initialiser registered.
routines <- getDLLRegisteredRoutines("shelf")
class(routines)
#> [1] "DLLRegisteredRoutines"
names(routines)
#> [1] ".C"        ".Call"     ".Fortran"  ".External"
routines$.Call
#>              numParameters
#> isbn13_valid             1

# A registered routine is an R object with a name, an address and an arity.
info <- routines$.Call$isbn13_valid
class(info)
#> [1] "CallRoutine"      "NativeSymbolInfo"
names(info)
#> [1] "name"          "address"       "dll"           "numParameters"
info$name
#> [1] "isbn13_valid"
info$numParameters
#> [1] 1

# Calling through the object skips the lookup by name.
.Call(info, isbns)
#> [1]  TRUE FALSE FALSE

# Without registration a string finds any exported symbol in the shared object.
.Call("isbn13_valid", isbns, PACKAGE = "isbn")
#> [1]  TRUE FALSE FALSE

# R_useDynamicSymbols(dll, FALSE) and R_forceSymbols(dll, TRUE): only objects.
message_of <- function(expr) tryCatch(expr, error = conditionMessage)
message_of(.Call("isbn13_valid", isbns, PACKAGE = "shelf"))
#> [1] "\"isbn13_valid\" not available for .Call() for package \"shelf\""
message_of(.Call("valid_isbn", isbns, PACKAGE = "shelf"))
#> [1] "\"valid_isbn\" not available for .Call() for package \"shelf\""

# The registered arity is checked on every call.
message_of(.Call(info, isbns, isbns))
#> [1] "Incorrect number of arguments (2), expecting 1 for 'isbn13_valid'"
message_of(.Call(info))
#> [1] "Incorrect number of arguments (0), expecting 1 for 'isbn13_valid'"

Both error messages are R’s. The string call to the forced object says that the name is not available "for package shelf", although the routine exists, because the object refuses string lookups; and a wrong number of arguments is an error that names the routine and both counts, instead of undefined behaviour in C.

The skeleton generator

tools::package_native_routine_registration_skeleton() writes the init.c for you. It reads the R code of a package for .C(), .Call(), .Fortran() and .External() calls, takes the routine names and the number of arguments from the calls, and prints C source with the FIXME comment that reminds you to check the declarations against the C code. NEWS for R 3.4.0 says it automated registration for over 90% of the CRAN packages that lacked it, and that many of its failures were real bugs in the packages: dozens called entry points with varying numbers of arguments or entry points that were not there. The script builds a throwaway package whose R code still calls .Call("isbn13_valid", x, PACKAGE = "shelf") and runs the tool on it.

# A throwaway package whose R code still calls the C routine by name.
pkg <- file.path(tempdir(), "shelf")
dir.create(file.path(pkg, "R"), recursive = TRUE)
writeLines(
  c(
    "is_valid_isbn13_c <- function(x) {",
    '  .Call("isbn13_valid", x, PACKAGE = "shelf")',
    "}"
  ),
  file.path(pkg, "R", "isbn-c.R")
)
writeLines(
  c("export(is_valid_isbn13_c)", "useDynLib(shelf)"),
  file.path(pkg, "NAMESPACE")
)
writeLines(
  c(
    "Package: shelf", "Version: 0.1.0", "Title: Skeleton",
    "Description: Skeleton.", "License: MIT"
  ),
  file.path(pkg, "DESCRIPTION")
)

# The tool reads the R code and prints a registration skeleton to the console.
tools::package_native_routine_registration_skeleton(pkg)
#> #include <R.h>
#> #include <Rinternals.h>
#> #include <stdlib.h> // for NULL
#> #include <R_ext/Rdynload.h>
#>
#> /* FIXME:
#>    Check these declarations against the C/Fortran source code.
#> */
#>
#> /* .Call calls */
#> extern SEXP isbn13_valid(SEXP);
#>
#> static const R_CallMethodDef CallEntries[] = {
#>     {"isbn13_valid", (DL_FUNC) &isbn13_valid, 1},
#>     {NULL, NULL, 0}
#> };
#>
#> void R_init_shelf(DllInfo *dll)
#> {
#>     R_registerRoutines(dll, NULL, CallEntries, NULL, NULL);
#>     R_useDynamicSymbols(dll, FALSE);
#> }
unlink(pkg, recursive = TRUE)

The output is the starting point, not the end: the prototype extern SEXP isbn13_valid(SEXP); is guessed from the call and belongs in a header (as isbn.h here), the argument counts have to be checked, and R_forceSymbols() is not added. The manual’s walk-through of converting the splines package lists the remaining steps: add useDynLib(pkg, .registration = TRUE) to NAMESPACE; change .Call("name", ..., PACKAGE = "pkg") calls to .Call(name, ...) with the R object; add R_useDynamicSymbols(dll, FALSE) and then R_forceSymbols(dll, TRUE); and consider hiding the C functions from the symbol table (Writing R Extensions: Example: converting a package to use registration). Hiding is done with attribute_hidden on each function, or with PKG_CFLAGS = $(C_VISIBILITY) in src/Makevars together with attribute_visible on R_init_pkg alone (Writing R Extensions: Controlling visibility); it removes any chance of a name clash and speeds up loading slightly. Fortran entry points cannot use attribute_hidden; the variable F_VISIBILITY does the job for them.

useDynLib in NAMESPACE

The NAMESPACE file loads the shared object and, with .registration = TRUE, creates in the package’s namespace one R variable for each registered routine. The directive has more forms, all from the manual.

NAMESPACE directive Effect

useDynLib(shelf)

Loads shelf.so when the namespace is loaded. Calls still use PACKAGE = "shelf" and strings.

useDynLib(shelf, .registration = TRUE)

Loads the object and defines an R variable for every registered routine, named as registered (isbn13_valid), so code can say .Call(isbn13_valid, x).

useDynLib(shelf, .registration = TRUE, .fixes = "C_")

As above with a prefix: the variable is C_isbn13_valid. stats uses this, which is why its code reads .C(C_pansari, ...). KernSmooth uses .fixes = "F_" for Fortran.

useDynLib(shelf, isbn13_valid) or useDynLib(shelf, sym = isbn13_valid)

Looks up the listed symbols once, at load time, and defines variables for them (optionally renamed). Superseded by registration.

dll = useDynLib(shelf)

Also assigns the DLL object to the variable dll, so .Call(dll$isbn13_valid, x) looks the symbol up at each call.

The manual adds a caution: using .registration or .fixes for a package that does not register its routines only slows loading, and once routines are registered, the new R variables must not be exported by accident by an exportPattern() in NAMESPACE. With roxygen2 the directive is written in the R sources as @useDynLib shelf, .registration = TRUE (and the same for the .fixes argument), usually in the package-level help file, and devtools::document() copies it into NAMESPACE. The next section does exactly that, and usethis::use_c() and usethis::use_rcpp() print the lines to paste.

R also checks all of this. R CMD check tests every foreign function call for a PACKAGE argument, or a way to deduce the DLL, and tools::checkFF() reports the cases that need attention (R documentation: checkFF). Since R 3.4.0, R CMD check --as-cran notes a package that neither registers its routines nor declares what it does about symbol search. The environment variable _R_CHECK_NATIVE_ROUTINE_REGISTRATION_ makes R CMD check report packages whose DLL lacks the registration entry points (it needs the nm utility on the PATH, and objdump on Windows); the section on tools in R Internals documents it (R Internals: Tools). The real report is shown in the section on the C API, from a package that fails it.

Compiled code in a package: the shelf example

A package that contains compiled code has a src/ directory. R CMD INSTALL builds it with the same R CMD SHLIB machinery as above: when src/ has sources with a known extension, or a Makevars file, and no Makefile, it reads Makevars first, then R’s system makefile, then the user’s personal Makevars, compiles every source file in src/ and links them into shelf.so (or shelf.dll). The shared object is installed in the libs/ directory of the installed package, and useDynLib in NAMESPACE loads it with the namespace. A src/Makefile, if present, replaces this mechanism; the manual says a Makevars is better and a Makefile should be needed only exceptionally (Writing R Extensions: Creating shared objects). Package structure in general is the subject of Creating Packages, namespaces of Packages, Libraries and Namespaces, and documentation and roxygen2 of Documentation with Rd, roxygen2 and Vignettes.

The shelf package of those pages has an R function is_valid_isbn13(). This section adds a C version, is_valid_isbn13_c(), to a copy of the package: the files isbn.c, isbn.h and init.c shown above, a two-line src/Makevars, an R wrapper, and the useDynLib directive. The Rcpp version of the same function is a second copy, later. Paths in the transcripts are shortened to ~/shelf.

Creating src/ with usethis

usethis::use_c() creates src/, a .gitignore inside it that keeps the compiled .o, .so and .dll files out of version control, and a template source file. It prints the lines to paste into the package-level R file. Both in this transcript and below, the path is shortened.

> usethis::use_c("isbn")
✔ Setting active project to "~/shelf".
✔ Creating 'src/'.
✔ Adding "*.o", "*.so", and "*.dll" to 'src/.gitignore'.
☐ Copy and paste the following lines into 'R/shelf-package.R':
  ## usethis namespace: start
  #' @useDynLib shelf, .registration = TRUE
  ## usethis namespace: end
  NULL
✔ Writing 'src/isbn.c'.
☐ Edit 'src/isbn.c'.

The template src/isbn.c has three lines (#define R_NO_REMAP, #include <R.h> and #include <Rinternals.h>); it was replaced by the isbn.c shown earlier. The R file named in the message holds the roxygen tag, and a second file has the wrapper. The wrapper coerces with as.character() in R, where a factor is converted properly, and passes the registered symbol object to .Call(); because NAMESPACE has .registration = TRUE, the variable isbn13_valid exists in the namespace.

## usethis namespace: start
#' @useDynLib shelf, .registration = TRUE
## usethis namespace: end
NULL
#' Check ISBN-13 strings with compiled code
#'
#' `is_valid_isbn13_c()` gives the same answers as [is_valid_isbn13()], but the
#' check runs in C through the `.Call()` interface (`src/isbn.c`).
#'
#' @param x A character vector of ISBNs; `NA` and empty strings are allowed.
#' @returns A logical vector of the same length as `x`, `FALSE` for `NA` and for
#'   the empty string.
#' @examples
#' is_valid_isbn13_c(c("978-0-306-40615-7", "978-0-01-100112-9", NA))
#' @export
is_valid_isbn13_c <- function(x) {
  .Call(isbn13_valid, as.character(x))
}

devtools::document() and load_all()

devtools::document() runs roxygen2, which loads the package first, so it compiles src/. pkgload::load_all() (called by devtools::load_all()) compiles src/ with pkgbuild::compile_dll() when a source file is newer than the object, and loads the result with the R code, which is the edit-compile-test loop of package development. It builds with debugging flags (-UNDEBUG -Wall -pedantic -g -O0), so compiler warnings appear while you work, and the transcript below shows them quiet because the code compiles cleanly. Two details are visible in the output: the -DISBN_DIGITS=13 that comes from src/Makevars, and that Writing 'NAMESPACE' appears twice, once before the package is compiled and loaded and once after. The compiler lines are shortened, the first line that names the compiler is omitted, and so is the line that prints the temporary library the package is installed into.

> devtools::document()
ℹ Updating shelf documentation
Writing 'NAMESPACE'
ℹ Loading shelf
ℹ Re-compiling shelf (debug build)
── R CMD INSTALL ───────────────────────────────────────────────────────────────
* installing *source* package ‘shelf’ ...
** this is package ‘shelf’ version ‘0.1.0’
** using staged installation
** libs
gcc -std=gnu2x -I"<R_HOME>/include" -DISBN_DIGITS=13 -fPIC -UNDEBUG -Wall -pedantic -g -O0 -c init.c -o init.o
gcc -std=gnu2x -I"<R_HOME>/include" -DISBN_DIGITS=13 -fPIC -UNDEBUG -Wall -pedantic -g -O0 -c isbn.c -o isbn.o
gcc -std=gnu2x -shared -o shelf.so init.o isbn.o -L"<R_HOME>/lib" -lR
** checking absolute paths in shared objects and dynamic libraries
* DONE (shelf)
shelf 0.1.0: loan period is 21 days
Writing 'NAMESPACE'
Writing 'is_valid_isbn13_c.Rd'

The generated NAMESPACE gains two lines, the export of the new function and the useDynLib() directive; the rest is the file of the earlier pages.

# Generated by roxygen2: do not edit by hand

S3method(format,book)
S3method(print,book)
S3method(print,summary.book)
S3method(summary,book)
export(book)
export(clean_books)
export(clean_loans)
export(is_valid_isbn13)
export(is_valid_isbn13_c)
export(loan_summary)
export(loans_per_month)
export(new_book)
export(normalise_isbn)
export(overdue_loans)
export(validate_book)
useDynLib(shelf, .registration = TRUE)

One practical trap is worth knowing. make knows which .c file produced which .o file, but not which headers a file includes. If only isbn.h changes, nothing is rebuilt, and the second R CMD INSTALL of an unchanged tree says so, with this real line in place of the compiler lines:

** libs
make: Nothing to be done for 'all'.

Delete the .o and .so files in src/ (or use R CMD INSTALL --preclean, or pkgbuild::clean_dll()) after changing a header, a Makevars file or the compiler flags. R CMD build cleans src/ itself when it makes the tarball (the build prints * cleaning src), which is why compiled objects never belong in a source package.

R CMD INSTALL

The command a user types is R CMD INSTALL shelf in the directory that contains the package. It compiles src/ with R’s own flags (not the debug flags of load_all()), installs, and tests that the installed package loads. The transcript keeps the lines that concern the compiled code, shortens the compiler lines as before, and marks the omitted ones with [...]; the container’s temporary paths in the other lines are dropped.

R CMD INSTALL shelf
#> * installing *source* package ‘shelf’ ...
#> ** this is package ‘shelf’ version ‘0.1.0’
#> ** using staged installation
#> ** libs
#> gcc -std=gnu2x -I"<R_HOME>/include" -DISBN_DIGITS=13 -fPIC -O2 -c init.c -o init.o
#> gcc -std=gnu2x -I"<R_HOME>/include" -DISBN_DIGITS=13 -fPIC -O2 -c isbn.c -o isbn.o
#> gcc -std=gnu2x -shared -o shelf.so init.o isbn.o -L"<R_HOME>/lib" -lR
#> ** R
#> ** data
#> [...]
#> ** testing if installed package can be loaded from temporary location
#> ** checking absolute paths in shared objects and dynamic libraries
#> ** testing if installed package can be loaded from final location
#> ** testing if installed package keeps a record of temporary installation path
#> * DONE (shelf)

With the package installed, the script loads it and checks the compiled function against the R one on the books data set of the package, and looks at what registration created: the routine in the registration table with its arity, the R variable isbn13_valid in the namespace, and the loaded DLL.

# The shelf copy with src/isbn.c installed by `R CMD INSTALL shelf`.
library(shelf)
#> shelf 0.1.0: loan period is 21 days

is_valid_isbn13_c(c("978-0-306-40615-7", "978-0-01-100112-9", NA, ""))
#> [1]  TRUE FALSE FALSE FALSE
identical(is_valid_isbn13_c(books$isbn), is_valid_isbn13(books$isbn))
#> [1] TRUE
sum(is_valid_isbn13_c(books$isbn))
#> [1] 14

# The registration table is part of the namespace.
getDLLRegisteredRoutines("shelf")$.Call$isbn13_valid$numParameters
#> [1] 1
ns <- asNamespace("shelf")
class(get("isbn13_valid", envir = ns))
#> [1] "CallRoutine"      "NativeSymbolInfo"
dll <- getLoadedDLLs()[["shelf"]]
class(dll)
#> [1] "DLLInfo"
basename(dll[["path"]])
#> [1] "shelf.so"

# The R function calls the symbol object, never a string.
body(is_valid_isbn13_c)
#> {
#>     .Call(isbn13_valid, as.character(x))
#> }

# tools::checkFF() inspects foreign function calls; no output means no findings.
tools::checkFF(package = "shelf")

The compiled function and the R function return the same logical vector on the sixteen ISBNs of the data set, of which fourteen are valid. tools::checkFF() found nothing to report.

R CMD check

R CMD check has several checks for compiled code. The transcript is the relevant part of R CMD check --as-cran --no-manual --no-tests --ignore-vignettes on the built tarball, with the remote CRAN-incoming checks off and the header that names the platform removed. Every line shown is OK. The checks that did not appear are those about the quick options used here (vignettes and tests were skipped, so this is not the full check of the earlier pages).

* checking whether package ‘shelf’ can be installed ... OK
* checking foreign function calls ... OK
* checking line endings in C/C++/Fortran sources/headers ... OK
* checking line endings in Makefiles ... OK
* checking compilation flags in Makevars ... OK
* checking for GNU extensions in Makefiles ... OK
* checking for portable use of $(BLAS_LIBS) and $(LAPACK_LIBS) ... OK
* checking use of PKG_*FLAGS in Makefiles ... OK
* checking use of SHLIB_OPENMP_*FLAGS in Makefiles ... OK
* checking pragmas in C/C++ headers and code ... OK
* checking compiled code ... OK

Two of those lines concern files you write for compiled code: "checking compilation flags in Makevars" (the check that rejects -Wall and friends in PKG_CFLAGS, shown to fail in the section on src/Makevars) and "checking compiled code" (the check of the API usage, shown to fail in the section on the C API). The container’s R was built with distribution flags, so the full check also reported a NOTE that the compilation used non-portable flags such as -Wformat; it comes from how this R was built, not from the package, and is not shown.

src/Makevars and the compiler flags

src/Makevars is a makefile fragment that R CMD SHLIB includes before R’s own Makeconf. It is the place for settings that the package needs, and it is not the place for settings that belong to the person or the machine that builds it. The manual lists the variables.

Variable Use

PKG_CPPFLAGS

Preprocessor options for C and C++ files: -I include paths, -D and -U definitions, -include, -pthread. Include paths belong here and not in PKG_CFLAGS or PKG_CXXFLAGS: there a platform include path can take precedence, which has caused hard-to-debug failures.

PKG_CFLAGS, PKG_CXXFLAGS, PKG_FFLAGS, PKG_FCFLAGS

Extra compiler flags for C, C++, fixed-form Fortran and free-form Fortran. PKG_FCFLAGS exists for the rare package that needs different flags for the two Fortran forms.

PKG_LIBS

Linker options: -L and -l for libraries the code uses. Macros from R’s Makeconf are available for numerical code: LAPACK_LIBS, BLAS_LIBS and FLIBS, which must appear in that order; FLIBS is added automatically when the package has Fortran sources.

CXX_STD

The C++ standard: CXX17, CXX20, CXX23 or CXX26 (not a make directive: no quotes, no trailing comment). For C there is no variable; the standard is set in SystemRequirements as USE_C17 or USE_C23 for a package that depends on R (>= 4.3.0).

OBJECTS

The list of object files to build, when the package does not want every source in src/ (for example to include files from subdirectories). If Makevars sets it, R CMD SHLIB does not.

C_VISIBILITY, CXX_VISIBILITY, F_VISIBILITY

Macros from R’s Makeconf that hold the compiler’s flag for hiding symbols; used as PKG_CFLAGS = $(C_VISIBILITY).

SHLIB_OPENMP_CFLAGS, SHLIB_OPENMP_CXXFLAGS, SHLIB_OPENMP_FFLAGS

The OpenMP flags for the compiler R was built with; see below.

src/Makevars is read on Linux and macOS; on Windows Makevars.win is read instead, and since R 4.2.0 Makevars.ucrt is preferred to Makevars.win when both exist (NEWS for R 4.2.0, which also added Makefile.ucrt, configure.ucrt and cleanup.ucrt). A configure script that writes Makevars from Makevars.in is the next step when the package has to adapt to the system. The Makevars of the shelf package is two lines, a comment and a definition. PKG_CPPFLAGS = -DISBN_DIGITS=13 is a preprocessor option and so belongs in that variable; it passes the digit count to isbn.c, which has #ifndef ISBN_DIGITS to supply a default when the file is compiled alone.

# Preprocessor definitions belong in PKG_CPPFLAGS, not in the compiler flags.
PKG_CPPFLAGS = -DISBN_DIGITS=13

Why a package must not set -Wall

The manual says in so many words not to put compiler-specific diagnostic flags in a distributed Makevars: -O2, -Wall, -pedantic and every other -W flag are specific to GCC and compilers that copy its options (for Oracle’s compilers, -W flags passed arguments to compiler phases). Variables such as CPPFLAGS and CFLAGS themselves must not be set at all, because users and sites set them. R CMD check enforces this. This is the real output of R CMD check --as-cran on a deliberately wrong tiny package (probe, shown in the section on the C API) whose src/Makevars is the single line PKG_CFLAGS = -Wall -O3:

* checking compilation flags in Makevars ... WARNING
Non-portable flags in variable 'PKG_CFLAGS':
  -Wall -O3

The right place for warnings is the developer’s own Makevars file. The manual encourages package developers to enable a reasonable amount of compiler diagnostics this way, for example -Wall -pedantic for GCC and LLVM. The personal file is ~/.R/Makevars (or ~/.R/Makevars-<platform>, or Makevars.win on Windows), or any file named by the environment variable R_MAKEVARS_USER; there is also a site-wide Makevars.site and R_MAKEVARS_SITE; the R FAQ answers the question of changing flags in one short entry that points to the same manual section (R FAQ 8.4: How can I change compilation flags?). The file used for every example on this page (its path given with R_MAKEVARS_USER for the test run) is:

CFLAGS = -O0 -Wall -pedantic
CXXFLAGS = -O0 -Wall -pedantic
FFLAGS = -O0 -Wall -pedantic
FCFLAGS = -O0 -Wall -pedantic

It sets the flags for all four compilers (the variables are CFLAGS, CXXFLAGS, FFLAGS and FCFLAGS) and switches off optimisation so that the compiler’s analysis is simple. When the C standard or C++ standard is non-default the matching variable (C17FLAGS, CXX20FLAGS, and so on) must be set instead. The effect is easy to see on a file that is sloppy in the way -Wall catches:

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

SEXP double_it(SEXP x) {
  int unused = 0;
  return Rf_ScalarReal(2.0 * Rf_asReal(x));
}
R_MAKEVARS_USER=user-makevars R CMD SHLIB --preclean sloppy.c
#> gcc -std=gnu2x -I"<R_HOME>/include" -fPIC -O0 -Wall -pedantic -c sloppy.c -o sloppy.o
#> sloppy.c: In function ‘double_it’:
#> sloppy.c:5:7: warning: unused variable ‘unused’ [-Wunused-variable]
#>     5 |   int unused = 0;
#>       |       ^~~~~~
#> gcc -std=gnu2x -shared -o sloppy.so sloppy.o -L"<R_HOME>/lib" -lR

The -Wextra flag is stricter than -Wall and is worth trying, with one known exception: the idiom (DL_FUNC) &name in a registration table converts a function pointer to another function pointer type, and -Wextra (through -Wcast-function-type) reports it on each entry. That is why the file above uses -Wall -pedantic. All the C, C++ and Fortran files in this page were built with these flags and produced no warning.

The C++ standard: CXX_STD

R compiles package C++ code with a standard that depends on the R version, and the manual’s advice is to say which standard the code needs. The NEWS entries are: R 4.3.0 changed the default to C++17 where available (C++14 or C++11 otherwise); the manual says that from R 4.4.0 only a C++17 compiler is selected as the default; R 4.6.0 changed the default to C++20 where available, and otherwise C++17, and C++11 and C++14 are no longer supported; and packages can request C++17 if they need it. Newer standards (C++23, C++26) can be requested as the compiler allows. The request is made in two places that must agree: CXX_STD = CXX17 in src/Makevars (and Makevars.win, Makevars.ucrt), and SystemRequirements: C++17 in DESCRIPTION; if they differ, SystemRequirements wins, and R CMD check checks that they match. Code that wants a feature of a newer standard should test for it, with the standard’s feature-test macros, rather than assume it.

The next pair of runs makes this visible. std.cpp returns the value of __cplusplus that its compiler saw, and a constant that comes from the command line.

#define R_NO_REMAP
#include <R.h>
#include <Rinternals.h>

#ifndef LOAN_DAYS
#define LOAN_DAYS 21
#endif

/* The language standard the compiler was asked for, as 201703 or 202002. */
extern "C" SEXP cpp_standard() {
  return Rf_ScalarInteger((int) __cplusplus);
}

/* A value injected from the command line by -DLOAN_DAYS=... */
extern "C" SEXP loan_days_default() {
  return Rf_ScalarInteger(LOAN_DAYS);
}
CXX_STD = CXX17
PKG_CPPFLAGS = -DLOAN_DAYS=28

Built in a directory with no Makevars, the compiler uses R’s default; built in a directory with the file above, R CMD SHLIB honours CXX_STD and PKG_CPPFLAGS. The compile line of the second build is

R CMD SHLIB std.cpp
#> specified C++17
#> using C++17
#> g++ -std=gnu++17 -I"<R_HOME>/include" -DLOAN_DAYS=28 -fPIC -O2 -c std.cpp -o std.o
#> g++ -std=gnu++17 -shared -o std.so std.o -L"<R_HOME>/lib" -lR

(without the line that names the compiler and the system, and with the flags shortened as before; specified C++17 and using C++17 are R’s own lines, and the -DLOAN_DAYS=28 comes from PKG_CPPFLAGS). The script loads whichever std.so is in the current directory and asks it. Run in the directory that was built without a Makevars, it prints the lines shown after the code.

# std.cpp is built twice: by `R CMD SHLIB std.cpp` in one directory, and again
# in a directory with a Makevars file that sets CXX_STD and PKG_CPPFLAGS. Run
# this script from either directory, after building there.
dyn.load(paste0("std", .Platform$dynlib.ext))
c(
  standard = .Call("cpp_standard", PACKAGE = "std"),
  loan_days = .Call("loan_days_default", PACKAGE = "std")
)
#>  standard loan_days
#>    202002        21

Run in the directory that has the Makevars (the previous script, saved as check-standard.R one directory up), it reports the standard and the constant that the file asked for:

cd with-makevars
Rscript ../check-standard.R
#>  standard loan_days
#>    201703        28

The numbers are the values of __cplusplus: 202002 is C++20 (the R 4.6 default) and 201703 is C++17. For a package that has src/Makefile instead of Makevars, the manual shows how to take the compiler from R CMD config CXX20 and its relatives; a configure script must do the same, and must not hard-code g++.

C standards, and the Fortran and OpenMP variables

For C, R 4.5.0 made the configure script choose a compiler option that selects C23 when one is available (gcc 15 defaults to C23 on its own). The effect on package code is stricter C: bool, true and false are keywords, K&R-style declarations are gone, and an empty parameter list now means no parameters, so code that relied on an unprototyped declaration can stop compiling. A package that cannot move can ask for an older standard with USE_C17 (or USE_C99, USE_C90) in SystemRequirements, and a user can force one when installing with R CMD INSTALL --use-C17; the same option exists for R CMD SHLIB, as its help shows. The __STDC_VERSION__ macro tells which standard the compiler claims (202311L for C23; some compilers used 202000L for a draft), but the manual advises testing for the feature the code needs and not the version (Writing R Extensions: C standards). The Fortran side has its own page in this section, and the Fortran Reference covers the language.

OpenMP is the usual way to use several threads inside compiled code. The compiler flag differs between compilers, so a package takes it from R: it puts $(SHLIB_OPENMP_CFLAGS) in both PKG_CFLAGS and PKG_LIBS for C code (and SHLIB_OPENMP_CXXFLAGS and SHLIB_OPENMP_FFLAGS for the other two languages, with a different recipe for Fortran), guards OpenMP-specific code with #ifdef _OPENMP, and includes the macros only when the code really uses OpenMP. Some toolchains, Apple’s clang among them, have no OpenMP support at all, and a note in the manual says that the C++ macro applies to the default C++ compiler and not necessarily to the one selected for C++17 or C++20, for which a configure check is advised. Writing -fopenmp directly into PKG_CXXFLAGS works with gcc and fails elsewhere, which is why R CMD check has a check named "checking use of SHLIB_OPENMP_*FLAGS in Makefiles" (it appears in the transcript of the earlier section). Calling the R API from threaded code is, in the manual’s words, for experts only and strongly discouraged: the R API changes internal data structures, and errors must be signalled only from the main thread. The threading model of R’s parallel packages is the subject of Parallel and Asynchronous Computing; the manual’s section is OpenMP support.

.C: plain pointers

How .C passes arguments

.C() is the traditional interface and the simplest to write for: the C function takes pointers to plain C data and returns void. R copies each argument, passes a pointer to the copy, and returns a list of the (possibly modified) copies, with the names that were given to the arguments. No R header is needed, and the C function does not have to know it is called from R. The mapping from R storage modes to C types is fixed.

R storage mode C type Fortran type

integer, logical

int *

integer

double (numeric)

double * (or float * with the Csingle attribute, set by as.single())

double precision (or real)

complex

Rcomplex *, a structure with members r and i

double complex

character

char **

one string only, as character*255; deprecated

raw

unsigned char *

not allowed

list, other

SEXP * or SEXP (read-only; use .Call() instead)

not allowed

A logical is sent as 0, 1 or INT_MIN (the last only when NAOK = TRUE), and the code should return one of the three values. The C type for an R integer is int, which is 32 bits on every platform R runs on, and not long: code ported from S-PLUS that used long * is wrong on most 64-bit platforms (Writing R Extensions: Interface functions .C and .Fortran). Strings need most care. On entry the contents of a character vector are copied to a char ** array; the C code can shorten a string by writing a '\0', but it cannot lengthen one, and a missing string arrives as the text "NA". For anything that is not plain numbers, use .Call().

A .C routine

The convolution of two sequences, the manual’s own example, makes the routine. convolve() takes the two sequences and their lengths as pointers and fills a result array that the caller allocated, since a C function cannot return a vector through .C(); the R wrapper allocates it with double(length(a) + length(b) - 1) and names it so that $ab extracts it from the returned list. The file also registers a second name, convolve_checked, for the same function with its argument types, which is how the type check below is made.

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

/* .C passes pointers to copies of the arguments and returns them as a list. */
void convolve(double *a, int *na, double *b, int *nb, double *ab) {
  int nab = *na + *nb - 1;
  for (int i = 0; i < nab; i++) {
    ab[i] = 0.0;
  }
  for (int i = 0; i < *na; i++) {
    for (int j = 0; j < *nb; j++) {
      ab[i + j] += a[i] * b[j];
    }
  }
}

/* The same routine under a second name, registered with argument types. */
static R_NativePrimitiveArgType convolve_types[] = {
  REALSXP, INTSXP, REALSXP, INTSXP, REALSXP
};

static const R_CMethodDef c_methods[] = {
  {"convolve_checked", (DL_FUNC) &convolve, 5, convolve_types},
  {NULL, NULL, 0, NULL}
};

void R_init_convolve(DllInfo *dll) {
  R_registerRoutines(dll, c_methods, NULL, NULL, NULL);
}

The script shows the list that .C() returns, the refusal of missing values, the effect of a type mistake on an unregistered routine, the registered version of the check, and the old DUP argument. A few results need explaining.

  • DUP and ENCODING are accepted for old code and ignored. Before R 3.2.0 DUP = FALSE avoided the copy; since that version the NEWS says it is ignored and that the arguments are duplicated as DUP = TRUE would do, and the help page says .Call() is much preferred. The script shows that adding DUP = FALSE changes nothing.

  • A NA, NaN or Inf in an argument is an error: NA/NaN/Inf in foreign function call (arg 1). NAOK = TRUE turns the check off, and the values then reach the C code as they are; the result 1 NA shows that NA * 1 is NA.

  • A mistake in the type is not an error at all. Passing the double 3 where the C code reads an int * makes the C code see the bit pattern of a double, whose low four bytes are zero on a little-endian machine, so na is 0 and the result is 0 0 0 0 instead of 0 1 2 3, with no message. The same mistake with a character string for a double * would read pointer values as numbers. The manual says as much: coerce every argument to the right mode before calling (as.double(), as.integer()).

  • Registering the argument types gives the check: wrong type for argument 2 in call to convolve_checked for the double, and Incorrect number of arguments (4), expecting 5 for 'convolve_checked' for the missing one, naming the routine and both counts. A mismatch between the number of types in the array and the number of arguments cannot be checked by the compiler, so count them.

# Built with `R CMD SHLIB convolve.c`.
dyn.load(paste0("convolve", .Platform$dynlib.ext))

# .C copies every argument, passes pointers, and returns them as a list.
convolve <- function(a, b) {
  .C("convolve",
    as.double(a), as.integer(length(a)),
    as.double(b), as.integer(length(b)),
    ab = double(length(a) + length(b) - 1),
    PACKAGE = "convolve"
  )$ab
}
convolve(c(1, 2, 3), c(0, 1, 0.5))
#> [1] 0.0 1.0 2.5 4.0 1.5
res <- .C("convolve", c(1, 2), 2L, c(3, 4), 2L, ab = double(3),
  PACKAGE = "convolve"
)
str(res)
#> List of 5
#>  $   : num [1:2] 1 2
#>  $   : int 2
#>  $   : num [1:2] 3 4
#>  $   : int 2
#>  $ ab: num [1:3] 3 10 8

# DUP and ENCODING are accepted for old code and ignored: arguments are copied.
args <- list("convolve", c(1, 2), 2L, c(3, 4), 2L, ab = double(3))
identical(
  do.call(.C, c(args, DUP = FALSE, PACKAGE = "convolve")),
  do.call(.C, c(args, PACKAGE = "convolve"))
)
#> [1] TRUE

# Missing and non-finite values are refused unless NAOK = TRUE.
message_of <- function(expr) tryCatch(expr, error = conditionMessage)
message_of(convolve(c(1, NA, 3), 1))
#> [1] "NA/NaN/Inf in foreign function call (arg 1)"
.C("convolve", c(1, NA), 2L, 1, 1L, ab = double(2),
  NAOK = TRUE, PACKAGE = "convolve"
)$ab
#> [1]  1 NA

# Nothing checks the types of an unregistered routine. A double where C reads
# an int (here na = 3) is not an error; on a little-endian machine the low
# four bytes of 3.0 are zero, so C sees na = 0 and the answer is wrong.
.C("convolve", c(1, 2, 3), 3, c(0, 1), 2L, ab = double(4),
  PACKAGE = "convolve"
)$ab
#> [1] 0 0 0 0

# The registered copy of the routine knows its types and its arity.
message_of(.C("convolve_checked", c(1, 2, 3), 3, c(0, 1), 2L, ab = double(4)))
#> [1] "wrong type for argument 2 in call to convolve_checked"
message_of(.C("convolve_checked", c(1, 2, 3), 3L, c(0, 1), 2L))
#> [1] "Incorrect number of arguments (4), expecting 5 for 'convolve_checked'"
.C("convolve_checked", c(1, 2, 3), 3L, c(0, 1), 2L, ab = double(4))$ab
#> [1] 0 1 2 3

Detecting writes outside an argument

A fairly common cause of crashes in .C() and .Fortran() code is writing a little beyond the end of an argument, which corrupts something else and crashes R later. options(CBoundsCheck = TRUE) (or the environment variable R_C_BOUNDS_CHECK=yes) changes how R copies the arguments: it adds 64 guard bytes at both ends of each copy and checks them on return. overrun() below has the classic off-by-one, a loop that runs to i <= *n.

/* Bug: the loop runs to i == *n, one element past the end of x. */
void overrun(double *x, int *n) {
  for (int i = 0; i <= *n; i++) {
    x[i] = 0.0;
  }
}
# Built with `R CMD SHLIB overrun.c`: the C code writes one element too many.
dyn.load(paste0("overrun", .Platform$dynlib.ext))

# options(CBoundsCheck = TRUE) makes .C and .Fortran pad each argument with 64
# guard bytes at both ends and check them when the routine returns.
getOption("CBoundsCheck")
#> [1] FALSE
options(CBoundsCheck = TRUE)
tryCatch(
  .C("overrun", c(1, 2, 3), 3L, PACKAGE = "overrun"),
  error = conditionMessage
)
#> [1] "array over-run in .C(\"overrun\") in double argument 1\n"

The message names the call, the type and the argument number. It is a debugging aid: it makes every call slower, it is off by default, and it detects only writes in the guard zones.

.Fortran: subroutines

.Fortran() calls a Fortran subroutine with the same copy-and-return protocol as .C(), and the same argument table. Three rules are specific to Fortran. All the Fortran compilers R supports map symbol names to lower case, so .Fortran("loan_totals") looks for the lower-case name, and so does registration. Arguments are passed by reference, which is what Fortran expects. And the compiler decorates the symbol (often a trailing underscore); R detects the convention when it is built, which is why the R code never writes the underscore, and why C code that calls Fortran must use the macros F77_NAME, F77_CALL and F77_SUB from R_ext/RS.h, never the underscore. The help page of .Fortran() adds that a Fortran name with an underscore in it is not valid Fortran 77, and its translation can differ between compilers, so the portable advice is to use plain lower-case names (R documentation: .Fortran).

R CMD SHLIB takes .f as fixed-form Fortran (which includes Fortran 77), passing it to the compiler that R was configured with, and .f90 or .f95 as free-form. Both go to the same compiler. The manual’s advice for modern code is to use .f90, to put a requirement in SystemRequirements (for example Fortran 2018) since the standard the compiler accepts is a property of the compiler, and, when the package has several source files that use modules, to tell make the compilation order in src/Makevars, because module files are produced by compiling and make cannot see the dependency. For a module iface used by cmi.f90 and dmi.f90 the line is cmi.o dmi.o: iface.o (Writing R Extensions: Using modern Fortran code). The compile commands for Fortran are the same two lines as for C, with gfortran for the first:

R CMD SHLIB loan_totals.f90
#> gfortran -fPIC -O2 -c loan_totals.f90 -o loan_totals.o
#> gcc -std=gnu2x -shared -o loan_totals.so loan_totals.o -lgfortran -lm -L"<R_HOME>/lib" -lR

(Flags shortened, and the first line, which names the Fortran compiler and the system, omitted. The link line still uses the C driver, with -lgfortran -lm added by R’s FLIBS.)

A subroutine for .Fortran

loan_totals adds up the days beyond the loan period and counts the late loans. Every argument is declared with its intent, which is how Fortran says "input" and "output", and the sizes come from the array lengths that R passes in. The -Wall -pedantic flags of the personal Makevars compile it without warnings.

! Total and count of the days beyond the loan period, for .Fortran.
subroutine loan_totals(days, n, limit, late_total, late_count)
  implicit none
  integer, intent(in) :: n
  double precision, intent(in) :: days(n), limit
  double precision, intent(out) :: late_total
  integer, intent(out) :: late_count
  integer :: i

  late_total = 0.0d0
  late_count = 0
  do i = 1, n
    if (days(i) > limit) then
      late_total = late_total + (days(i) - limit)
      late_count = late_count + 1
    end if
  end do
end subroutine loan_totals

Printing, warnings, errors and random numbers from Fortran

Fortran print and write statements to the default unit are not guaranteed to appear in the R console, may not interleave with C output, and on Windows interfere with C’s own output (the manual describes how gfortran’s initialisation changes the line endings of C’s stdout and how to undo it in R_init_<pkg>). R therefore provides subroutines that print through R’s own channels: labelpr(label, nchar) prints a label, intpr1(label, nchar, var), realpr1() and dblepr1() print a labelled scalar, and intpr(), realpr() and dblepr() print a labelled array (label, nchar, data, ndata); nchar is the length of the label, or -1 for all of it. There is no check on the type: a real passed to dblepr prints nonsense. For conditions, rexit(message) signals an error and rwarn(message) a warning (messages are cut to 255 characters), and rchkusr() is the interrupt check. fortran_io.f90 uses all of them.

! Printing, a warning and an error from Fortran through R's own routines.
subroutine summarise(days, n)
  implicit none
  integer, intent(in) :: n
  double precision, intent(in) :: days(n)
  double precision :: mean

  if (n < 1) call rexit("summarise: no loans")
  if (n == 1) call rwarn("summarise: only one loan")
  mean = sum(days) / n
  call labelpr("loan summary", 12)
  call intpr1("loans", 5, n)
  call dblepr1("mean days", 9, mean)
  call rchkusr()
end subroutine summarise

Random numbers are the other thing Fortran code must not do itself. The Fortran 90 random_number and random_seed, GNU’s rand, and the C library’s rand all keep a state that is private to the library: packages cannot initialise it safely, two packages can interfere, and the user’s set.seed() does not reach it. The manual’s rule is to use R’s generator, which a routine reaches by bracketing the draws with GetRNGstate() and PutRNGstate() and calling unif_rand() (or norm_rand(), exp_rand()), and it shows how to bind those C entry points from Fortran with iso_c_binding (Writing R Extensions: Random-number generation from Fortran). rngdemo.f90 does this without a line of C.

! Random loan lengths drawn from R's own generator (so set.seed() applies).
subroutine random_loans(n, longest, out)
  use iso_c_binding, only: c_double
  implicit none
  interface
    subroutine get_rng_state() bind(C, name = "GetRNGstate")
    end subroutine get_rng_state
    subroutine put_rng_state() bind(C, name = "PutRNGstate")
    end subroutine put_rng_state
    function unif_rand() bind(C, name = "unif_rand") result(u)
      import :: c_double
      real(c_double) :: u
    end function unif_rand
  end interface
  integer, intent(in) :: n
  double precision, intent(in) :: longest
  double precision, intent(out) :: out(n)
  integer :: i

  call get_rng_state()
  do i = 1, n
    out(i) = ceiling(longest * unif_rand())
  end do
  call put_rng_state()
end subroutine random_loans

Modern Fortran through .C and bind©

For Fortran 90 and later code the help page of .Fortran() and the manual give the same advice: the most portable way is to give the subroutine a C interface with iso_c_binding and call it with .C(). With bind(C, name = "isbn_check_digit") the routine has exactly the C name given, the kinds c_int and c_double say exactly what C types the arguments are, and the compiler can check the declaration. isbn_check_digit computes the 13th digit of an ISBN from the first twelve.

! ISBN-13 check digit from the first twelve digits, callable through .C.
subroutine isbn_check_digit(digits, check) bind(C, name = "isbn_check_digit")
  use iso_c_binding, only: c_int
  implicit none
  integer(c_int), intent(in) :: digits(12)
  integer(c_int), intent(out) :: check
  integer :: i, total

  total = 0
  do i = 1, 12
    if (mod(i, 2) == 1) then
      total = total + digits(i)
    else
      total = total + 3 * digits(i)
    end if
  end do
  check = mod(10 - mod(total, 10), 10)
end subroutine isbn_check_digit

The script calls all four Fortran files. In the results, late_total is 49 and late_count is 3 for five loans of 14, 35, 28, 21 and 49 days against a limit of 21; summarise prints through R (the label lines and the values), warns for a single loan with the message summarise: only one loan, and stops with summarise: no loans for none (the mean of 14, 35 and 28 appears as 25.7, not 25.66667: dblepr1 uses R’s internal real-vector printer, and in this script the preceding str(res) left that printer’s number of significant digits at 3, as format() with a digits argument does too; a print() call resets it to getOption("digits"), after which the same .Fortran() call prints 25.66667, which is what was observed when the three variants were run); the draws made in Fortran are identical to ceiling(28 * runif(5)) after the same set.seed(); and the ISBN of the Le Guin entry in the catalogue has check digit 7.

# Built with `R CMD SHLIB loan_totals.f90` and the other three .f90 files.
for (name in c("loan_totals", "fortran_io", "rngdemo", "check_digit")) {
  dyn.load(paste0(name, .Platform$dynlib.ext))
}
message_of <- function(expr) tryCatch(expr, error = conditionMessage)

# .Fortran: lower-case name, arguments by reference, results in a list.
res <- .Fortran("loan_totals",
  days = c(14, 35, 28, 21, 49), n = 5L, limit = 21,
  late_total = 0, late_count = 0L,
  PACKAGE = "loan_totals"
)
res$late_total
#> [1] 49
res$late_count
#> [1] 3
str(res)
#> List of 5
#>  $ days      : num [1:5] 14 35 28 21 49
#>  $ n         : int 5
#>  $ limit     : num 21
#>  $ late_total: num 49
#>  $ late_count: int 3

# Printing, warnings and errors go through R, not through Fortran I/O.
invisible(.Fortran("summarise", c(14, 35, 28), 3L, PACKAGE = "fortran_io"))
#> loan summary
#> loans
#> [1] 3
#> mean days
#> [1] 25.7
tryCatch(
  .Fortran("summarise", 7, 1L, PACKAGE = "fortran_io"),
  warning = conditionMessage
)
#> [1] "summarise: only one loan"
message_of(.Fortran("summarise", numeric(), 0L, PACKAGE = "fortran_io"))
#> [1] "summarise: no loans"

# R's generator from Fortran: the same stream as runif() after set.seed().
set.seed(2026)
from_fortran <- .Fortran("random_loans", 5L, 28, out = double(5),
  PACKAGE = "rngdemo"
)$out
set.seed(2026)
from_r <- ceiling(28 * runif(5))
from_fortran
#> [1] 20 16  4  9 16
identical(from_fortran, from_r)
#> [1] TRUE

# Modern Fortran: bind(C) makes the routine an ordinary C symbol for .C.
check_digit <- function(first12) {
  .C("isbn_check_digit",
    digits = as.integer(first12), check = 0L,
    PACKAGE = "check_digit"
  )$check
}
check_digit(c(9, 7, 8, 0, 3, 0, 6, 4, 0, 6, 1, 5))
#> [1] 7

Registering a Fortran routine

A registered .Fortran() routine needs a prototype in C, because the registration table is C. The prototype uses F77_NAME(loan_totals) for the name, with every argument a pointer; and a Fortran LOGICAL is declared int * in C, or better void *, because gfortran reports int_least32_t * and link-time optimisation would otherwise warn. The entry goes in an R_FortranMethodDef table, with the argument types, as the manual’s example for .C() did. R_init_<name> is named after the shared object, which is why the object is built as loantot.so.

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

/* The Fortran routine as C sees it: lower-case name, every argument a pointer. */
extern void F77_NAME(loan_totals)(double *days, int *n, double *limit,
                                  double *late_total, int *late_count);

static R_NativePrimitiveArgType loan_totals_types[] = {
  REALSXP, INTSXP, REALSXP, REALSXP, INTSXP
};

static const R_FortranMethodDef fortran_methods[] = {
  {"loan_totals", (DL_FUNC) &F77_NAME(loan_totals), 5, loan_totals_types},
  {NULL, NULL, 0, NULL}
};

void R_init_loantot(DllInfo *dll) {
  R_registerRoutines(dll, NULL, NULL, fortran_methods, NULL);
  R_useDynamicSymbols(dll, FALSE);
}

The registration table makes the same checks as for .C(), here with Fortran’s type names in the message. The script loads the object built with R CMD SHLIB -o loantot.so loan_totals.f90 fortran_init.c.

# Built with `R CMD SHLIB -o loantot.so loan_totals.f90 fortran_init.c`.
dyn.load(paste0("loantot", .Platform$dynlib.ext))
routines <- getDLLRegisteredRoutines("loantot")
routines$.Fortran
#>             numParameters
#> loan_totals             5

# Registered with argument types, so R checks them on every call.
message_of <- function(expr) tryCatch(expr, error = conditionMessage)
call_totals <- function(...) {
  .Fortran("loan_totals", ..., PACKAGE = "loantot")
}
res <- call_totals(c(14, 35, 28), 3L, 21, late_total = 0, late_count = 0L)
res$late_total
#> [1] 21
res$late_count
#> [1] 2
message_of(call_totals(c(14, 35, 28), 3, 21, late_total = 0, late_count = 0L))
#> [1] "wrong type for argument 2 in call to loan_totals"
message_of(call_totals(c(14, 35, 28), 3L, 21))
#> [1] "Incorrect number of arguments (3), expecting 5 for 'loan_totals'"

Character strings and logicals between C and Fortran

Two kinds of argument are not portable and need a special mention, and both are covered in the manual’s section on calling C from Fortran and vice versa.

  • Character strings. A Fortran string is an array of bytes plus a hidden length argument; a C string is NUL-terminated. Since 2019 gfortran passes the hidden length at the end of the argument list as a size_t (backported to gfortran 7 and 8), and code that ignored it broke. R defines the macro USE_FC_LEN_T and the type FC_LEN_T in Rconfig.h to match: a C routine called from Fortran declares the extra FC_LEN_T msg_len argument, and C code that calls a BLAS or LAPACK routine with character arguments (such as dgemm("N", "T", ...)) appends FCONE for every character argument, with no comma. USE_FC_LEN_T became the default in R 4.3.0, and packages that call BLAS or LAPACK with character arguments fail to install without the adjustment. The alternative is to avoid the problem, by passing integer codes instead of strings or by writing the interface with iso_c_binding (Writing R Extensions: Fortran character strings).

  • Logicals. Fortran LOGICAL has no guaranteed size. In practice it is a 32-bit integer, and int * is the traditional C equivalent, but recent gfortran says int_least32_t *. The simplest design is to pass integers and convert in a Fortran wrapper (Writing R Extensions: Fortran LOGICAL).

Passing C functions to Fortran as arguments (callbacks) depends on the two compilers agreeing on the calling convention and has failed in practice; the manual’s most portable suggestion is to convert the Fortran to C. For everything about the Fortran language and about calling it from C, C++ and Python, the Fortran Reference section added by issue #277 has the pages Interoperability with C, Calling Fortran from C, C++ and Python and BLAS, LAPACK and Numerical Libraries, and the C build and tooling page explains the compilers and linkers involved.

.External: arguments as a pairlist

.External() differs from .Call() in one respect: the C function receives a single SEXP, a pairlist whose first cell holds the name of the routine and whose remaining cells are the arguments. That removes the limit of 65 arguments and gives the C code the argument names (the "tags"), which is what makes a function with ... natural to write. The walk over the list uses the Lisp-style macros: CDR(args) is the rest of the list, CAR(args) the current argument, TAG(args) its name as a symbol (or R_NilValue), and PRINTNAME() turns a symbol into a CHARSXP. CADR, CADDR, CADDDR and so on read the first few arguments by position, and SETCAR() and its relatives modify a cell. The first cell is skipped with args = CDR(args). When registering .External() routines, the argument count is -1, which tells R not to check it.

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

/* .External: the arguments arrive as one pairlist; the first cell is the name. */
SEXP total_days_ext(SEXP args) {
  args = CDR(args);
  double total = 0.0;
  for (; args != R_NilValue; args = CDR(args)) {
    SEXP el = CAR(args);
    if (!Rf_isNumeric(el)) {
      Rf_error("every argument must be numeric");
    }
    SEXP v = PROTECT(Rf_coerceVector(el, REALSXP));
    for (R_xlen_t i = 0; i < XLENGTH(v); i++) {
      total += REAL(v)[i];
    }
    UNPROTECT(1);
  }
  return Rf_ScalarReal(total);
}

/* The tags (argument names) of the call, "" for unnamed arguments. */
SEXP arg_names(SEXP args) {
  args = CDR(args);
  int n = Rf_length(args);
  SEXP out = PROTECT(Rf_allocVector(STRSXP, n));
  for (int i = 0; i < n; i++, args = CDR(args)) {
    if (TAG(args) == R_NilValue) {
      SET_STRING_ELT(out, i, Rf_mkChar(""));
    } else {
      SET_STRING_ELT(out, i, PRINTNAME(TAG(args)));
    }
  }
  UNPROTECT(1);
  return out;
}

The R wrappers pass ... straight through. total_days() adds up any number of numeric arguments, and arg_names() returns their tags, with "" for unnamed arguments.

# Built with `R CMD SHLIB external.c`.
dyn.load(paste0("external", .Platform$dynlib.ext))
total_days <- function(...) {
  .External("total_days_ext", ..., PACKAGE = "external")
}
arg_names <- function(...) {
  .External("arg_names", ..., PACKAGE = "external")
}

total_days(14, 21, 35)
#> [1] 70
total_days(1:3, c(0.5, 0.25), TRUE)
#> [1] 7.75
total_days()
#> [1] 0
arg_names(first = 1, 2, third = "x")
#> [1] "first" ""      "third"
tryCatch(total_days("a"), error = conditionMessage)
#> [1] "every argument must be numeric"

The same job with .Call() is a one-liner in R: .Call(sym, list(...)) and a loop over the list in C. Writing R Extensions says that the .External() style is convenient but not necessary. Its value is when the C code wants the arguments and their tags without building a list first.

C++ without Rcpp

C++ code reaches R through the same C interfaces, with three differences: the entry points need C linkage, the R headers need care, and a C++ exception must never cross the boundary. The manual’s own example is a C++ library of two classes that is made callable by writing a wrapper function inside extern "C" { } and building with R CMD SHLIB (which links with the C++ compiler because there is a C++ source file); the same page warns that C++ iostreams should not be used for output, because nothing guarantees that the text reaches the R console, and that R’s own entry points (Rprintf()) should be used instead (Writing R Extensions: Interfacing C++ code). The C++ Reference page on program structure and compilation explains extern "C" and name mangling.

The three points in practice:

  • Linkage. .Call() looks up a C symbol, but the C++ compiler mangles names. A function called from R must be declared extern "C". The header files of R can be included from C++ but not inside an extern "C" block, because they include system headers.

  • Header order and R_NO_REMAP. Include the C++ standard headers before the R headers, and use the Rf_ names, because since R 4.5.0 R_NO_REMAP is always defined for C++ (an earlier section). Several R short names (length, error) collide with identifiers in the standard library.

  • Exceptions and longjmp. Rf_error() unwinds the stack with a longjmp, which skips the destructors of C++ objects between the call and the catching R frame: a std::vector or std::string alive at that moment leaks. A C++ exception must not be allowed to propagate out of an extern "C" function into R’s C code. The safe pattern is to catch all exceptions inside the C++ code, copy the message into a plain character buffer, let every C++ object go out of scope, and only then call Rf_error(). For C++ code that must run destructors when an R error unwinds the stack, R offers R_UnwindProtect() with a continuation token (R_MakeUnwindCont() and R_ContinueUnwind()), which the manual describes as suitable for C++ stack unwinding (Writing R Extensions: Condition handling and cleanup code).

cxx_median.cpp follows the safe pattern. The vector, the sort and the exception live in an anonymous-namespace function that returns a status; the extern "C" function holds only plain C data when it calls Rf_error().

#define R_NO_REMAP
#include <algorithm>
#include <cstdio>
#include <stdexcept>
#include <vector>
#include <R.h>
#include <Rinternals.h>

namespace {

// All the C++ lives here; no exception leaves this function.
// Returns 0 on success, 1 after copying an error message into msg.
int median_impl(const double *x, R_xlen_t n, double *result, char *msg,
                std::size_t msg_size) {
  try {
    std::vector<double> v;
    for (R_xlen_t i = 0; i < n; ++i) {
      if (!ISNAN(x[i])) {
        v.push_back(x[i]);
      }
    }
    if (v.empty()) {
      throw std::invalid_argument("no non-missing values");
    }
    std::sort(v.begin(), v.end());
    std::size_t m = v.size();
    *result = m % 2 == 1 ? v[m / 2] : 0.5 * (v[m / 2 - 1] + v[m / 2]);
    return 0;
  } catch (const std::exception &e) {
    std::snprintf(msg, msg_size, "%s", e.what());
    return 1;
  }
}

}  // namespace

// R calls C symbols, so the entry point has C linkage.
extern "C" SEXP median_cxx(SEXP days) {
  SEXP d = PROTECT(Rf_coerceVector(days, REALSXP));
  double result = NA_REAL;
  char msg[256];
  int status = 0;
  if (XLENGTH(d) == 0) {
    std::snprintf(msg, sizeof msg, "%s", "no non-missing values");
    status = 1;
  } else {
    status = median_impl(REAL(d), XLENGTH(d), &result, msg, sizeof msg);
  }
  UNPROTECT(1);
  if (status != 0) {
    Rf_error("%s", msg);  // only plain C data is alive here: nothing to leak
  }
  return Rf_ScalarReal(result);
}

The build is the same command with a .cpp file, and R links with g++; the C++20 option is the default of R 4.6.1. The transcript shows the two lines with the flags shortened.

R CMD SHLIB cxx_median.cpp
#> g++ -std=gnu++20 -I"<R_HOME>/include" -fPIC -O2 -c cxx_median.cpp -o cxx_median.o
#> g++ -std=gnu++20 -shared -o cxx_median.so cxx_median.o -L"<R_HOME>/lib" -lR
# Built with `R CMD SHLIB cxx_median.cpp`.
dyn.load(paste0("cxx_median", .Platform$dynlib.ext))
median_cxx <- function(days) .Call("median_cxx", days, PACKAGE = "cxx_median")

median_cxx(c(14, 35, NA, 21, 28))
#> [1] 24.5
median_cxx(1:4)
#> [1] 2.5

# A C++ exception was turned into an R error, with the stack unwound cleanly.
tryCatch(median_cxx(c(NA, NaN)), error = conditionMessage)
#> [1] "no non-missing values"
tryCatch(median_cxx(numeric()), error = conditionMessage)
#> [1] "no non-missing values"

The first error is the std::invalid_argument thrown inside the C++ function (a vector with no usable value), the second the explicit test for an empty input; R sees only the message in both cases, and no C++ object was alive when Rf_error() jumped.

Libraries are the other problem. Header-only C++ libraries (most of Boost, as packaged in the BH package; most of Armadillo, as RcppArmadillo) compile with the package and cause few problems; a library with a compiled C++ interface has a name-mangled ABI that depends on the compiler, its version and even preprocessor defines, so the package would have to be compiled exactly as the library was; and a library that uses C++ internally but offers a C interface needs the C++ runtime linked, which is done by including a dummy C++ file in src/ so that R links with the C++ compiler. The manual’s advice for libraries with both is to use the C interface (Writing R Extensions: External C++ code).

Rcpp: R objects as C++ classes

Rcpp (version 1.1.2 here) is a package of C++ headers and a small runtime that wraps R’s C API in classes. A numeric vector is a NumericVector, a list a List, a data frame a DataFrame, an R function a Function; conversion between R and C++ types happens at the boundary, PROTECT() is done by the object’s constructor and destructor, and a C++ exception thrown in your code becomes an R error. The project’s own site describes it as an interface that maps the key R data types to C++ data structures, says that it makes it easy to create a first example with cppFunction(), and reports that it is used by over 3000 packages on CRAN (Rcpp website). Rcpp is community-maintained software outside the R core, not a base package: the reference below is the package’s own documentation, the CRAN package page and the vignettes that come with it (among them the introduction, attributes, sugar and package vignettes cited below); the Rcpp Gallery (https://gallery.rcpp.org/) is community material with examples of a wide range of tasks, and is named here only as such.

What Rcpp removes

The .Call() routines of the earlier sections have a lot of ceremony that is the same each time: the SEXP signature, the coercion, the allocation, the PROTECT() and UNPROTECT() bookkeeping, the loop over REAL(x), the wrapper that attaches the attributes, the registration table and the R_init function, and the R function that calls .Call(). Rcpp attributes generate all of them. A C++ function whose signature uses Rcpp types and which is marked with // [[Rcpp::export]] is turned into an R function with the same name and arguments, including default values; the glue is generated into a pair of files in a package (RcppExports.cpp and RcppExports.R) or compiled on the fly by cppFunction() and sourceCpp().

Types, missing values and sugar

Rcpp class R object Notes

NumericVector, IntegerVector, LogicalVector, CharacterVector, ComplexVector, RawVector

the matching atomic vector

Indexed with x[i] (0-based); x.size(); x.names() or x.attr("names").

NumericMatrix, IntegerMatrix, and so on

the vector with a dim attribute

m(i, j), m.nrow(), m.ncol().

List, DataFrame

list, data frame

x["name"] or x[i] for an element; List::create(_["n"] = 1) builds a named list; DataFrame::create() builds a data frame.

Function, Environment

function, environment

f(arg) calls an R function from C++; env["name"] reads a variable.

NA_REAL, NA_INTEGER, NA_LOGICAL, NA_STRING

the missing values

NumericVector::is_na(x) tests one element; sugar’s is_na(v) is vectorised; do not compare to NA_REAL with ==.

Rcpp::stop(), Rcpp::warning(), Rcout, Rcerr

stop(), warning(), the output and error connections

stop("%d bad rows", n) takes a format; Rcout << x << std::endl is safe to use, unlike std::cout.

Rcpp also provides "sugar": vectorised expressions that look like R and run as C++ loops. sum(x), mean(x), ifelse(cond, a, b), is_na(x), any(...), cumsum(x), rev(x), x > limit, x - limit and many more are defined for the vector classes, so a loop can often be replaced by an expression; the sugar vignette lists them (Rcpp package documentation: Rcpp-sugar vignette).

evalCpp() and cppFunction()

evalCpp("2 + 2") compiles one C++ expression and returns its value, which is the quickest check that the toolchain works. cppFunction(code) takes the source of one function as a string, adds #include <Rcpp.h> and using namespace Rcpp;, compiles it with the C++ standard of the running R, and returns an R function. The arguments depends, plugins and includes add compile-time dependencies, plugin settings and extra includes; rebuild forces recompilation, and showOutput and verbose show what the compiler does (Rcpp package documentation: cppFunction). The script defines the two running examples, the overdue loop and the ISBN-13 check, from strings. The result of the second is compared with the C version above on the same five inputs.

library(Rcpp)
packageVersion("Rcpp")
#> [1] ‘1.1.2’

# evalCpp() compiles and runs one C++ expression.
evalCpp("2 + 2")
#> [1] 4
evalCpp("std::numeric_limits<int>::max()")
#> [1] 2147483647
evalCpp("sizeof(double)")
#> [1] 8

# cppFunction() compiles a function from a string and returns an R function.
overdue_days <- cppFunction("
NumericVector overdue_days(NumericVector days, double limit = 21) {
  NumericVector out(days.size());
  for (R_xlen_t i = 0; i < days.size(); ++i) {
    out[i] = NumericVector::is_na(days[i]) ? days[i]
                                           : std::max(days[i] - limit, 0.0);
  }
  return out;
}")
overdue_days(c(14, 35, NA, 21, 28.5))
#> [1]  0.0 14.0   NA  0.0  7.5
overdue_days(c(14, 35), limit = 7)
#> [1]  7 28
class(overdue_days)
#> [1] "function"
args(overdue_days)
#> function (days, limit = 21)
#> NULL

# The ISBN-13 check; cppFunction() adds #include <Rcpp.h> and the namespace.
isbn13_cpp <- cppFunction("
LogicalVector isbn13_cpp(CharacterVector x) {
  LogicalVector out(x.size());
  for (R_xlen_t i = 0; i < x.size(); ++i) {
    if (CharacterVector::is_na(x[i])) {
      continue;
    }
    std::string s = as<std::string>(x[i]);
    int n = 0, sum = 0;
    bool ok = true;
    for (char c : s) {
      if (c >= '0' && c <= '9') {
        if (n == 13) {
          ok = false;
          break;
        }
        sum += (c - '0') * (n % 2 == 0 ? 1 : 3);
        ++n;
      } else if (c == 'X' || c == 'x') {
        ok = false;
        break;
      }
    }
    out[i] = ok && n == 13 && sum % 10 == 0;
  }
  return out;
}")
isbn13_cpp(c("978-0-306-40615-7", "9780001000100", "978-0-01-100112-9", "", NA))
#> [1]  TRUE  TRUE FALSE FALSE FALSE

The default argument of the C++ function (double limit = 21) is carried over to the R function, which args() shows. Compilation is slow the first time, because the Rcpp headers are large, and the result is cached for the session: the same code string compiles once.

sourceCpp() and a file of exports

sourceCpp("file.cpp") does the same for a file that may define many functions and helpers. It parses the file for // [[Rcpp::export]] attributes (and for RCPP_MODULE declarations), builds a shared library in a temporary directory (the build is cached, and only redone when the file changes unless rebuild = TRUE), and defines the exported functions in the calling environment. /*** R blocks in the file are R code that runs after compilation, which turns the C++ file into a self-testing script. Attributes also exist for dependencies (// [[Rcpp::depends(RcppArmadillo)]] adds the include path and flags of a package) and for plugins (// [[Rcpp::plugins(cpp17)]] requests a standard; the installed plugins include cpp11 to cpp26, openmp and unwindProtect). Its help page lists the other arguments: embeddedR, cacheDir, showOutput, verbose, dryRun and echo (Rcpp package documentation: sourceCpp). The help page also says that sourceCpp() is for one file whose only dependencies are R packages; for more files or external libraries, make a package.

loans.cpp has five exported functions. overdue_days_loop() is the C++ version of the loop of vectors.c; overdue_days_sugar() is the same job in one sugar expression; loan_stats() builds the classed list of structures.c in one expression; flag_late() takes a data frame and returns a data frame with a logical column; and mean_days() writes to Rcout, warns and stops. Nothing in the file mentions SEXP, PROTECT or .Call.

#include <Rcpp.h>
using namespace Rcpp;

// Days beyond the loan period, written as a loop; NA and NaN pass through.
// [[Rcpp::export]]
NumericVector overdue_days_loop(NumericVector days, double limit = 21) {
  R_xlen_t n = days.size();
  NumericVector out(n);
  for (R_xlen_t i = 0; i < n; ++i) {
    if (NumericVector::is_na(days[i])) {
      out[i] = days[i];
    } else {
      out[i] = days[i] > limit ? days[i] - limit : 0.0;
    }
  }
  out.attr("names") = days.attr("names");
  return out;
}

// The same result from Rcpp sugar: vectorised expressions, as in R.
// [[Rcpp::export]]
NumericVector overdue_days_sugar(NumericVector days, double limit = 21) {
  return ifelse(days > limit, days - limit, 0.0);
}

// A classed, named list, like the C version in structures.c.
// [[Rcpp::export]]
List loan_stats(NumericVector days) {
  NumericVector seen = days[!is_na(days)];
  List out = List::create(
    _["n"] = seen.size(),
    _["mean"] = seen.size() > 0 ? mean(seen) : NA_REAL,
    _["max"] = seen.size() > 0 ? max(seen) : NA_REAL
  );
  out.attr("class") = "loan_stats";
  return out;
}

// A data frame in, a data frame out: a logical column is added.
// [[Rcpp::export]]
DataFrame flag_late(DataFrame loans, double limit = 21) {
  CharacterVector member = loans["member"];
  NumericVector days = loans["days"];
  LogicalVector late(days.size());
  for (R_xlen_t i = 0; i < days.size(); ++i) {
    late[i] = NumericVector::is_na(days[i]) ? NA_LOGICAL : days[i] > limit;
  }
  return DataFrame::create(
    _["member"] = member, _["days"] = days, _["late"] = late
  );
}

// Output through Rcout, a warning and an error, all as R conditions.
// [[Rcpp::export]]
double mean_days(NumericVector days) {
  if (days.size() == 0) {
    stop("no loans");
  }
  if (is_true(any(is_na(days)))) {
    warning("missing days are ignored");
  }
  NumericVector seen = days[!is_na(days)];
  Rcout << "averaging " << seen.size() << " loans" << std::endl;
  return mean(seen);
}

/*** R
overdue_days_loop(c(loan1 = 14, loan2 = 35, loan3 = NA), 21)
*/

A few lines of the script deserve a reading. The first printed block is the output of the /*** R block, echoed because echo = TRUE is the default, and printed by R as soon as the compilation finishes. The loop version keeps the names of the input (it copies the attribute) and the sugar version drops them, which is why only the first of the two printed vectors has a header line. loan_stats() returns what the C version returned, with the class attribute set from C++. flag_late() shows the data-frame round trip. And the last lines show what an error and a warning look like, which the next subsection explains.

library(Rcpp)

# Compile loans.cpp, load its exported functions, and run its /*** R block.
sourceCpp("loans.cpp")
#>
#> > overdue_days_loop(c(loan1 = 14, loan2 = 35, loan3 = NA),
#> +     21)
#> loan1 loan2 loan3
#>     0    14    NA

days <- c(loan1 = 14, loan2 = 35, loan3 = NA, loan4 = 28.5)
overdue_days_loop(days)
#> loan1 loan2 loan3 loan4
#>   0.0  14.0    NA   7.5
overdue_days_sugar(days)
#> [1]  0.0 14.0   NA  7.5
identical(overdue_days_loop(unname(days)), overdue_days_sugar(unname(days)))
#> [1] TRUE
overdue_days_loop(days, limit = 14)
#> loan1 loan2 loan3 loan4
#>   0.0  21.0    NA  14.5

stats <- loan_stats(days)
class(stats)
#> [1] "loan_stats"
unclass(stats)
#> $n
#> [1] 3
#>
#> $mean
#> [1] 25.83333
#>
#> $max
#> [1] 35
#>

loans <- data.frame(
  member = c("Ana Ruiz", "Ben Okafor", "Dev Patel"),
  days = c(14, NA, 35)
)
flag_late(loans)
#>       member days  late
#> 1   Ana Ruiz   14 FALSE
#> 2 Ben Okafor   NA    NA
#> 3  Dev Patel   35  TRUE
class(flag_late(loans))
#> [1] "data.frame"

mean_days(c(14, 35))
#> averaging 2 loans
#> [1] 24.5
withCallingHandlers(
  mean_days(c(14, NA, 35)),
  warning = \(w) {
    message("caught: ", conditionMessage(w))
    invokeRestart("muffleWarning")
  }
)
#> caught: missing days are ignored
#> averaging 2 loans
#> [1] 24.5
err <- tryCatch(mean_days(numeric()), error = identity)
conditionMessage(err)
#> [1] "no loans"
class(err)
#> [1] "Rcpp::exception" "C++Error"        "error"           "condition"

Errors, warnings and output

Rcpp::stop("no loans") throws a C++ exception, the wrapper that Rcpp generates around every exported function catches it and calls R’s error machinery, and the result is an ordinary R error whose message is the text. It is a condition of class Rcpp::exception, C++Error, error, condition, as the last line of the output of the script shows, so tryCatch(..., error = ) handles it and conditionMessage() returns no loans. A C++ exception derived from std::exception is converted the same way, with the text of what(). Rcpp::warning() signals an R warning that withCallingHandlers() can handle, and Rcout writes through R’s output, so capture.output() and sink() see it. In the script, mean_days(c(14, 35)) prints averaging 2 loans through Rcout and returns 24.5; with c(14, NA, 35) the calling handler prints caught: missing days are ignored to the message stream and muffles the warning, and the function carries on and averages the two known values; and with numeric() the error is caught. Rcpp can also use R_UnwindProtect() so that C++ destructors run when an R error unwinds the stack, and has a plugin named unwindProtect for it.

When it does not compile

Compiler errors come out as the compiler wrote them, and R adds its own short error. broken.cpp misspells days as day inside a loop.

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
double total_days(NumericVector days) {
  double total = 0;
  for (R_xlen_t i = 0; i < days.size(); ++i) {
    total += day[i];
  }
  return total;
}

The script runs sourceCpp() under tryCatch(). The compiler’s lines are written straight to the console, and the transcript keeps the part that matters: the command line is shortened (and its first line, which names the compiler and the system, is omitted), and <R_HOME> replaces the directory in the make line.

library(Rcpp)

# broken.cpp misspells a variable. The compiler's messages go to the console;
# R then signals an error whose message is short and has no paths in it.
result <- tryCatch(sourceCpp("broken.cpp"), error = identity)
#> g++ -std=gnu++20 -I"<R_HOME>/include" -I"<Rcpp>/include" -fPIC -O2 -c broken.cpp -o broken.o
#> broken.cpp: In function ‘double total_days(Rcpp::NumericVector)’:
#> broken.cpp:8:14: error: ‘day’ was not declared in this scope; did you mean ‘days’?
#>     8 |     total += day[i];
#>       |              ^~~
#>       |              days
#> make: *** [<R_HOME>/etc/Makeconf:199: broken.o] Error 1
class(result)
#> [1] "simpleError" "error"       "condition"
conditionMessage(result)
#> [1] "Error 1 occurred building shared library."

# Nothing was defined.
exists("total_days")
#> [1] FALSE

The R error is the short message that the final lines show, Error 1 occurred building shared library., and the information is in the compiler text before it; the function was not defined. Template errors can fill pages, and the useful line is nearly always the first one that contains error:. With cppFunction() the file name in the messages is a generated temporary file, since the code is a string.

Rcpp in a package: the second copy of shelf

For a package the same function is written in src/isbn.cpp, and compileAttributes() generates the glue. The steps are the ones of the C copy of shelf, with Rcpp in place of init.c. usethis::use_rcpp("isbn") adds Rcpp to the LinkingTo and Imports fields of DESCRIPTION, creates src/ with its .gitignore, writes a template src/isbn.cpp, and prints the two roxygen tags to paste. LinkingTo puts the Rcpp headers on the include path when the package is compiled. Imports makes sure the Rcpp namespace is loaded before the package’s shared object. The manual describes LinkingTo as the way to use the header files of other packages, and says that those packages must also be in Imports or Depends so that they are loaded first (Writing R Extensions: Linking to other packages). The transcript is the output of the call in a fresh copy of shelf, with the path shortened.

> usethis::use_rcpp("isbn")
✔ Setting active project to "~/shelf".
✔ Adding Rcpp to 'LinkingTo' field in DESCRIPTION.
✔ Adding Rcpp to 'Imports' field in DESCRIPTION.
☐ Copy and paste the following lines into 'R/shelf-package.R':
  ## usethis namespace: start
  #' @importFrom Rcpp sourceCpp
  ## usethis namespace: end
  NULL
✔ Creating 'src/'.
✔ Adding "*.o", "*.so", and "*.dll" to 'src/.gitignore'.
☐ Copy and paste the following lines into 'R/shelf-package.R':
  ## usethis namespace: start
  #' @useDynLib shelf, .registration = TRUE
  ## usethis namespace: end
  NULL
✔ Writing 'src/isbn.cpp'.
☐ Edit 'src/isbn.cpp'.

The DESCRIPTION of the copy differs from the earlier shelf package only by the two fields that were added at the end.

Package: shelf
Title: Tools for a Small Library Catalogue
Version: 0.1.0
Authors@R:
    person("Shelf", "Maintainers", , "maintainer@example.com", role = c("aut", "cre", "cph"))
Description: Cleans a small library catalogue and its loans table,
    validates ISBN-13 strings, and reports overdue loans and overdue rates
    by genre. Ships the example 'books' and 'loans' datasets used by the
    documentation.
License: MIT + file LICENSE
Config/roxygen2/version: 8.1.1
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
Suggests:
    knitr,
    rmarkdown,
    testthat (>= 3.0.0),
    withr
Config/testthat/edition: 3
Depends:
    R (>= 4.1.0)
VignetteBuilder: knitr
LazyData: true
LinkingTo:
    Rcpp
Imports:
    Rcpp

The two roxygen tags go into R/shelf-package.R. @importFrom Rcpp sourceCpp is the tag that usethis prints; it makes roxygen2 write importFrom(Rcpp,sourceCpp) into NAMESPACE, which loads the Rcpp namespace with the package. @useDynLib shelf, .registration = TRUE is the tag already met.

## usethis namespace: start
#' @importFrom Rcpp sourceCpp
## usethis namespace: end
NULL

## usethis namespace: start
#' @useDynLib shelf, .registration = TRUE
## usethis namespace: end
NULL

src/isbn.cpp is the ISBN check as an exported C++ function. The roxygen comments are written with //' (the C++ form of #'), and compileAttributes() copies them into the generated R file, so the function is documented like any other. @export makes roxygen2 export the generated R function. The file is shorter than the C version because Rcpp supplies the loop over the vector, the allocation and the missing-value test.

#include <Rcpp.h>
using namespace Rcpp;

// Keep the digits, reject an X, weight 1 and 3 alternately.
static bool valid_isbn(const std::string& s) {
  int n = 0, sum = 0;
  for (char c : s) {
    if (c >= '0' && c <= '9') {
      if (n == 13) {
        return false;
      }
      sum += (c - '0') * (n % 2 == 0 ? 1 : 3);
      ++n;
    } else if (c == 'X' || c == 'x') {
      return false;
    }
  }
  return n == 13 && sum % 10 == 0;
}

//' Check ISBN-13 strings with C++
//'
//' `is_valid_isbn13_cpp()` gives the same answers as [is_valid_isbn13()], but
//' the check runs in C++ through Rcpp.
//'
//' @param x A character vector of ISBNs; `NA` and empty strings are allowed.
//' @returns A logical vector of the same length as `x`, `FALSE` for `NA` and
//'   for the empty string.
//' @examples
//' is_valid_isbn13_cpp(c("978-0-306-40615-7", "978-0-01-100112-9", NA))
//' @export
// [[Rcpp::export]]
LogicalVector is_valid_isbn13_cpp(CharacterVector x) {
  R_xlen_t n = x.size();
  LogicalVector out(n);
  for (R_xlen_t i = 0; i < n; ++i) {
    out[i] = !CharacterVector::is_na(x[i]) && valid_isbn(as<std::string>(x[i]));
  }
  return out;
}

src/Makevars contains a single line, the C++ standard. R’s own default would be C++20 on R 4.6; the line is shown because it is the form that a package states when it depends on a specific standard (and the SystemRequirements: C++17 field would carry the same requirement).

CXX_STD = CXX17

Rcpp::compileAttributes() scans src/*.cpp for attributes and writes src/RcppExports.cpp and R/RcppExports.R. It prints what it found. This block is a fragment, not a script: it is run in the package directory and the call has no other lines.

Rcpp::compileAttributes()
#> Exports from ~/shelf/src/isbn.cpp:
#>    LogicalVector is_valid_isbn13_cpp(CharacterVector x)
#>
#> ~/shelf/src/RcppExports.cpp updated.
#> ~/shelf/R/RcppExports.R updated.

(Paths shortened.) Both files are marked "do not edit by hand", and they hold what a hand-written package would have in init.c and in the R wrapper. RcppExports.R is the R function, with the roxygen block copied from the C++ comments and a .Call() to a registered symbol whose name starts with an underscore and the package name:

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

#' Check ISBN-13 strings with C++
#'
#' `is_valid_isbn13_cpp()` gives the same answers as [is_valid_isbn13()], but
#' the check runs in C++ through Rcpp.
#'
#' @param x A character vector of ISBNs; `NA` and empty strings are allowed.
#' @returns A logical vector of the same length as `x`, `FALSE` for `NA` and
#'   for the empty string.
#' @examples
#' is_valid_isbn13_cpp(c("978-0-306-40615-7", "978-0-01-100112-9", NA))
#' @export
is_valid_isbn13_cpp <- function(x) {
    .Call(`_shelf_is_valid_isbn13_cpp`, x)
}

RcppExports.cpp holds the C-callable wrapper (_shelf_is_valid_isbn13_cpp), which converts the argument with Rcpp::traits::input_parameter, calls your function inside BEGIN_RCPP and END_RCPP (the macros that catch exceptions), and the registration: a CallEntries table and an R_init_shelf() that calls R_registerRoutines() and R_useDynamicSymbols(dll, FALSE). That is the registration section of this page, written for you.

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

#include <Rcpp.h>

using namespace Rcpp;

#ifdef RCPP_USE_GLOBAL_ROSTREAM
Rcpp::Rostream<true>&  Rcpp::Rcout = Rcpp::Rcpp_cout_get();
Rcpp::Rostream<false>& Rcpp::Rcerr = Rcpp::Rcpp_cerr_get();
#endif

// is_valid_isbn13_cpp
LogicalVector is_valid_isbn13_cpp(CharacterVector x);
RcppExport SEXP _shelf_is_valid_isbn13_cpp(SEXP xSEXP) {
BEGIN_RCPP
    Rcpp::RObject rcpp_result_gen;
    Rcpp::RNGScope rcpp_rngScope_gen;
    Rcpp::traits::input_parameter< CharacterVector >::type x(xSEXP);
    rcpp_result_gen = Rcpp::wrap(is_valid_isbn13_cpp(x));
    return rcpp_result_gen;
END_RCPP
}

static const R_CallMethodDef CallEntries[] = {
    {"_shelf_is_valid_isbn13_cpp", (DL_FUNC) &_shelf_is_valid_isbn13_cpp, 1},
    {NULL, NULL, 0}
};

RcppExport void R_init_shelf(DllInfo *dll) {
    R_registerRoutines(dll, NULL, CallEntries, NULL, NULL);
    R_useDynamicSymbols(dll, FALSE);
}

After devtools::document() the NAMESPACE has the two directives that were put in the roxygen file, the export of the new function, and nothing else about Rcpp.

# Generated by roxygen2: do not edit by hand

S3method(format,book)
S3method(print,book)
S3method(print,summary.book)
S3method(summary,book)
export(book)
export(clean_books)
export(clean_loans)
export(is_valid_isbn13)
export(is_valid_isbn13_cpp)
export(loan_summary)
export(loans_per_month)
export(new_book)
export(normalise_isbn)
export(overdue_loans)
export(validate_book)
importFrom(Rcpp,sourceCpp)
useDynLib(shelf, .registration = TRUE)

devtools::document() loads (compiles) the package, so the first build is part of it; the transcript is the same as for the C copy with C++ compiler lines, with specified C++17 and using C++17 lines from the CXX_STD setting. The compiler lines show the include path of the Rcpp headers, which LinkingTo provides.

> devtools::document()
ℹ Updating shelf documentation
Writing 'NAMESPACE'
ℹ Loading shelf
Exports from ~/shelf/src/isbn.cpp:
   LogicalVector is_valid_isbn13_cpp(CharacterVector x)

~/shelf/src/RcppExports.cpp updated.
~/shelf/R/RcppExports.R updated.
ℹ Re-compiling shelf (debug build)
── R CMD INSTALL ───────────────────────────────────────────────────────────────
* installing *source* package ‘shelf’ ...
** this is package ‘shelf’ version ‘0.1.0’
** using staged installation
** libs
specified C++17
using C++17
g++ -std=gnu++17 -I"<R_HOME>/include" -I"<Rcpp>/include" -fPIC -UNDEBUG -Wall -pedantic -g -O0 -c RcppExports.cpp -o RcppExports.o
g++ -std=gnu++17 -I"<R_HOME>/include" -I"<Rcpp>/include" -fPIC -UNDEBUG -Wall -pedantic -g -O0 -c isbn.cpp -o isbn.o
g++ -std=gnu++17 -shared -o shelf.so RcppExports.o isbn.o -L"<R_HOME>/lib" -lR
** checking absolute paths in shared objects and dynamic libraries
* DONE (shelf)
shelf 0.1.0: loan period is 21 days
Writing 'NAMESPACE'
Writing 'is_valid_isbn13_cpp.Rd'

(Here <Rcpp> stands for the Rcpp directory in the library; the temporary-library line and the line that names the compiler are omitted, and the flags are shortened.) The script that follows runs after R CMD INSTALL shelf of this copy. It shows that the C++ function agrees with the R function on the data set, that the registration table lists the one routine, and that DESCRIPTION has Rcpp in both Imports and LinkingTo.

# The shelf copy with src/isbn.cpp, installed by `R CMD INSTALL shelf`.
library(shelf)
#> shelf 0.1.0: loan period is 21 days

is_valid_isbn13_cpp(c("978-0-306-40615-7", "978-0-01-100112-9", NA, ""))
#> [1]  TRUE FALSE FALSE FALSE
identical(is_valid_isbn13_cpp(books$isbn), is_valid_isbn13(books$isbn))
#> [1] TRUE

# Rcpp registered one routine; its R name starts with an underscore.
getDLLRegisteredRoutines("shelf")$.Call
#>                            numParameters
#> _shelf_is_valid_isbn13_cpp             1
body(is_valid_isbn13_cpp)
#> {
#>     .Call(`_shelf_is_valid_isbn13_cpp`, x)
#> }

# DESCRIPTION: Rcpp is both linked to (headers) and imported (runtime).
read.dcf(system.file("DESCRIPTION", package = "shelf"),
  fields = c("Imports", "LinkingTo")
)
#>      Imports LinkingTo
#> [1,] "Rcpp"  "Rcpp"

R CMD check on this copy passed the same compiled-code checks as the C copy (foreign function calls, compilation flags in Makevars, compiled code), and added a line of its own for the C++ standard, which reads:

* checking C++ specification ... INFO
  specified C++17

Two habits keep an Rcpp package healthy. Commit the generated RcppExports files, because CRAN and users compile the package without Rcpp’s tooling, and run compileAttributes() after every change of an exported signature (devtools::document() runs it as part of building, as the transcript shows); a stale RcppExports.cpp can fail to compile or to load. And do not define a second R_init_shelf() next to the generated one: two functions of the same name cannot link, so a package that uses Rcpp attributes and also has hand-written routines must register them from the generated initialiser (the attributes vignette documents the // [[Rcpp::init]] attribute for running your own initialisation code).

cpp11 and the other relatives

cpp11 (0.5.5 is installed; it was not run) is a header-only alternative in the r-lib family, with similar goals and a different design: C++11-style attributes ([[cpp11::register]]), LinkingTo: cpp11 only, copy-on-write semantics, UTF-8 everywhere, and no shared runtime library; its site describes the differences from Rcpp and how to convert (cpp11 package documentation). RcppArmadillo and RcppEigen add linear-algebra libraries to Rcpp, and RcppParallel adds threading; none of them is installed here, so they are only named. All build on the same R CMD SHLIB and LinkingTo machinery described above.

How much faster? A measurement

Whether any of this pays is a question of numbers. The script compares four implementations of the overdue-days function on a million simulated loan durations: an R for loop, the vectorised R expression pmax(days - limit, 0), the .Call() routine from vectors.c and a cppFunction() loop. bench::mark() checks that all four return the same result before timing them. Timings depend on the machine, the compiler flags and what else is running, so the script prints only the order of magnitude of each median relative to the fastest, as round(log10(ratio)); the exact ratios, which vary from run to run, are not shown. Performance, Profiling and Benchmarking explains how to measure with bench and microbenchmark.

# Built with `R CMD SHLIB vectors.c`. The same job four ways on a million
# simulated loan lengths: compare the ratios, not the seconds.
library(Rcpp)
dyn.load(paste0("vectors", .Platform$dynlib.ext))

set.seed(2026)
days <- round(runif(1e6, 1, 60))

overdue_r_loop <- function(days, limit = 21) {
  out <- numeric(length(days))
  for (i in seq_along(days)) {
    out[[i]] <- if (days[[i]] > limit) days[[i]] - limit else 0
  }
  out
}
overdue_r_vector <- function(days, limit = 21) pmax(days - limit, 0)
overdue_c <- function(days, limit = 21) {
  .Call("overdue_days", days, limit, PACKAGE = "vectors")
}
overdue_cpp <- cppFunction("
NumericVector overdue_cpp(NumericVector days, double limit = 21) {
  NumericVector out(days.size());
  for (R_xlen_t i = 0; i < days.size(); ++i) {
    out[i] = days[i] > limit ? days[i] - limit : 0.0;
  }
  return out;
}")

b <- bench::mark(
  r_loop = overdue_r_loop(days),
  r_vector = overdue_r_vector(days),
  c_call = overdue_c(days),
  rcpp = overdue_cpp(days),
  min_iterations = 20
)
rel <- summary(b, relative = TRUE)
data.frame(
  expression = as.character(rel$expression),
  orders_of_magnitude_slower = round(log10(rel$median / min(rel$median)))
)
#>   expression orders_of_magnitude_slower
#> 1     r_loop                          1
#> 2   r_vector                          0
#> 3     c_call                          0
#> 4       rcpp                          0

The result is the useful lesson. The plain R for loop is about one order of magnitude slower than compiled code here. It is byte-compiled by the JIT (see Vectorisation and Iteration), which is why the gap is a factor of ten or twenty and not larger. The vectorised R expression, which runs in R’s own compiled code, is within a factor of about three of the .Call() routine and the Rcpp function, which is the resolution of this measurement. Compiled code won over the loop and not over the right R expression. That is the usual outcome for work that a vectorised built-in can already do, and the reason to reach for C, C++ or Fortran for a loop that cannot be vectorised, and not for one that can.

Debugging compiled code

The R debugger of Debugging sees interpreted code only: browser() and debug() stop at R lines, and a .Call() is a single step. Debugging compiled code uses the tools of the language, and the manual’s chapter on debugging collects the R-specific tricks (Writing R Extensions: Debugging compiled code).

  • A crash prints a traceback. On Unix-alikes R installs a signal handler. A segmentation fault or illegal instruction prints *** caught segfault ***, the address, the R-level traceback and a menu of possible actions. Since the process may be damaged, the manual says the only really safe choices are to abort or to exit without saving the workspace.

  • Run R under the debugger. R -d gdb --vanilla starts R under gdb; on macOS the debugger is lldb (R -d lldb), and the manual has a separate section on its hurdles there. A breakpoint cannot be set in code that is not loaded yet, so the manual’s recipe is: start R under the debugger, dyn.load() or library() the shared object, send an interrupt to return to the debugger prompt, set the breakpoints, and continue with signal 0. Build with -g -O0 (in a personal Makevars) so that the debugger can step through the source; pkgload::load_all() does so by default. gdb -p <pid> attaches to a running R, and Sys.getpid() gives the pid.

  • Look at R objects from the debugger. Rf_PrintValue(s) prints an object with R’s own printing; R_PV(s) prints only objects. From gdb, p R_PV(ab) prints the object ab (Writing R Extensions: Inspecting R objects when debugging). The R FAQ has two short entries on exactly this: how to debug dynamically loaded code and how to inspect R objects when debugging (R FAQ 8.2: How can I debug dynamically loaded code? and R FAQ 8.3: How can I inspect R objects when debugging?).

  • Catch memory errors early. gctorture(TRUE) finds missing PROTECT() calls; R -d valgrind --vanilla < script.R (and --tool=memcheck --leak-check=full) checks reads of uninitialised memory and writes outside allocated blocks, with false positives that the manual explains; and R can be built with the address sanitizer and the undefined-behaviour sanitizer, which catch the same errors and some more (Writing R Extensions: Checking memory access). options(CBoundsCheck = TRUE) is the cheap test for .C() and .Fortran() overruns, shown above. Fortran has its own array bounds checking: put gfortran’s -fbounds-check in FFLAGS in the personal Makevars while checking.

  • Rprintf and the other print debugging. Still the most used. Rprintf() and Rf_PrintValue() insert into the code; a C++ std::cout should not be used for it.

  • Profiling compiled code. The R profiler (Rprof) sees a .Call() as one line. Where time goes inside it is a question for the system profilers (perf on Linux, Instruments on macOS, Very Sleepy on Windows), which the manual’s section on profiling compiled code describes, with the caveat that it is based on user reports and may not be current (Writing R Extensions: Profiling compiled code).

On Windows the equivalent is gdb from Rtools with a debug build of R and of the package (Rcmd INSTALL --debug); the details are in the R for Windows FAQ (R for Windows FAQ 8.4: How do I debug code that I have compiled and dyn.load-ed?).

Portability rules for compiled code

The manual’s chapter on writing portable packages has a section for C and C++ code, and one for Fortran, whose rules are the difference between a package that installs everywhere and one that works on the author’s machine. The points that concern code written for this page’s interfaces:

  • Never end the process. Compiled code must not call abort() or exit(), which kill the user’s R session and its unsaved work. This includes assert(), which is active unless NDEBUG is defined; R CMD INSTALL defines it, which is why the debug flags of load_all() start with -UNDEBUG. Fortran code must not call STOP or EXIT.

  • Do not write to stdout or stderr. Use Rprintf() and REprintf(), or the Fortran print routines of R. C++ and Fortran I/O should not be used.

  • Do not use the system random number generators (rand, drand48, random, Fortran random_number, the C++11 <random> library, or third-party generators such as GSL’s). Use R’s through GetRNGstate(), unif_rand() and PutRNGstate(), so that set.seed() works and packages do not interfere.

  • Mind the sizes of types. long is 32 bits on some R platforms (64-bit Windows) and 64 on most Unix. Use int, R_xlen_t, size_t or a typedef chosen by a configure test; use intptr_t for pointers stored in integers. A Fortran integer is a C int on all R platforms; a Fortran logical is not guaranteed to be.

  • Avoid -march=native and other flags that produce code for the build machine’s CPU only, and avoid sprintf() and vsprintf(), which R CMD check reports; use snprintf().

  • Include the R headers in the right order (system headers first) and never inside extern "C".

The two sections on portable code in the same chapter are Writing R Extensions: Portable C and C++ code and Writing R Extensions: Portable Fortran code. For the C standard library itself, C program structure and the C Reference pages on pointers and dynamic allocation explain the language rules that these R rules rest on.

The C API: what a package may call

R has a very large number of entry points in its executable and shared library, and most of them are not for packages. The manual divides them into three classes. The API is the set that is documented in Writing R Extensions and declared in an installed header file: packages can use it, and it is changed only after deprecation. Public entry points are declared in an installed header and exported on all platforms, but are not documented and can change without notice: they must not be used in distributed code, and their declarations will eventually be moved out of the installed headers. Private entry points are used to build R and are not declared in the installed headers at all. What R CMD check calls "non-API" is everything outside the first class.

The header files follow the same division. R.h includes the commonly used headers from R_ext/ (Arith.h, Error.h, Memory.h, Print.h, Random.h, RS.h and Utils.h); Rinternals.h declares the SEXP machinery used by .Call(); Rmath.h is R’s standalone math library; Rdynload.h (in R_ext/) is what registration needs; R_ext/Altrep.h is for packages that implement their own alternate representation of vectors, like R’s compact integer sequences; R_ext/BLAS.h and R_ext/Lapack.h declare the linear algebra routines. Headers not listed there are not part of the API, and the manual notes that Rdefines.h, an S-style macro layer, is no longer maintained (Writing R Extensions: Organization of header files).

What changed in R 4.5.0 and 4.6.0

R has been tightening the API in steps, and the NEWS files of R 4.5.0 and 4.6.0 list them. The facts that matter for a package author, in the order they happened:

  • R 4.5.0 added a section to Writing R Extensions, "Moving into C API compliance", to help authors move from non-API entry points to API ones; it will keep being updated. It made strict R headers the default, so the legacy definitions PI, Calloc, Realloc and Free are gone. It made R_NO_REMAP the default for C++. It added many functions that replace non-API ones (R_mkClosure, R_ClosureFormals, R_ClosureBody, R_ClosureEnv, R_ParentEnv, R_getVar and R_getVarEx, Rf_isDataFrame, Rf_charIsASCII and more, Rf_asRboolean). It lengthened the list of non-API entry points that R CMD check reports, and upgraded some of the NOTEs to WARNINGs, as preparation for removing the declarations and hiding the entry points where possible. A C23 compiler is selected by default where available.

  • R 4.6.0 removed declarations from the installed headers: the functions LOGICAL0, INTEGER0, REAL0, COMPLEX0 and RAW0, DATAPTR, VECTOR_PTR, Rf_isFrame (use Rf_isDataFrame), and the header R_ext/PrtUtil.h is no longer copied to the installed include directory. It marked a long list of further entry points as hidden and without declarations (ENVFLAGS, FRAME, ENCLOS, HASHTAB, IS_S4_OBJECT, LEVELS, NAMED, OBJECT, SETLENGTH, TRUELENGTH, SET_BODY and others). It upgraded to WARNINGs the check results for further non-API functions (including BODY, FORMALS, CLOENV, SET_TYPEOF, STRING_PTR, ATTRIB, SET_ATTRIB, Rf_findVarInFrame3, R_nchar and Rf_GetOption), and made the use of any non-API variable a check NOTE. And it added replacements: R_mapAttrib, R_getAttributes, R_class, R_nrow, R_ncol and a new experimental API for variable bindings.

  • R-devel (4.7.0) has no C-level entry in its NEWS section at the time of writing.

The table in the manual maps the non-API names to replacements. A selection:

Non-API use Replacement

ATTRIB(x), SET_ATTRIB, SET_OBJECT

Rf_getAttrib() and Rf_setAttrib() for one attribute; ANY_ATTRIB(x) to test for any (4.5.0); R_mapAttrib() to iterate (4.6.0); DUPLICATE_ATTRIB, SHALLOW_DUPLICATE_ATTRIB and CLEAR_ATTRIB to copy or clear.

Rf_findVar(), Rf_findVarInFrame()

R_getVar() and R_getVarEx() (4.5.0); R_existsVarInFrame() to test for existence.

BODY(), FORMALS(), CLOENV()

R_ClosureBody(), R_ClosureFormals(), R_ClosureEnv() (4.5.0).

ENCLOS()

R_ParentEnv() (4.5.0).

STRING_PTR(), DATAPTR(), STDVEC_DATAPTR()

STRING_PTR_RO() and DATAPTR_RO(); writable pointers to these are unsupported.

REAL0(), COMPLEX0()

REAL() and COMPLEX().

OBJECT(), IS_S4_OBJECT()

Rf_isObject() and Rf_isS4().

Rf_allocSExp(ENVSXP) with SET_ENCLOS

R_NewEnv(); and R_mkClosure() for closures.

R_GetCurrentEnv()

environment() in R, passed as an argument.

PRVALUE, PRCODE, PRENV, R_PromiseExpr

the binding access API of 4.6.0.

What happens to code that uses a non-API function

Two experiments on R 4.6.1 show the result. The first file calls ATTRIB() and Rf_findVar(), two of the entry points that the NEWS of 4.5.0 and 4.6.0 single out.

#define R_NO_REMAP
#include <R.h>
#include <Rinternals.h>

/* Both entry points are exported by libR but not declared by R 4.6's headers. */
SEXP all_attributes(SEXP x) {
  return ATTRIB(x);
}

SEXP lookup(SEXP name, SEXP rho) {
  return Rf_findVar(Rf_installChar(STRING_ELT(name, 0)), rho);
}

Neither is declared by the installed headers any more, so the compiler does not know them. On gcc 13 with the C23 option this is a warning (an implicit declaration, which C99 already disallowed), and the call is compiled as a call to a function returning int, which is wrong for a pointer. Other compilers can reject the file outright.

R CMD SHLIB nonapi.c 2>&1 | grep -E ": (warning|error):"
#> nonapi.c:7:10: warning: implicit declaration of function ‘ATTRIB’ [-Wimplicit-function-declaration]
#> nonapi.c:7:10: warning: returning ‘int’ from a function with return type ‘SEXP’ {aka ‘struct SEXPREC *’} makes pointer from integer without a cast [-Wint-conversion]
#> nonapi.c:11:10: warning: implicit declaration of function ‘Rf_findVar’; did you mean ‘Rf_findFun’? [-Wimplicit-function-declaration]
#> nonapi.c:11:10: warning: returning ‘int’ from a function with return type ‘SEXP’ {aka ‘struct SEXPREC *’} makes pointer from integer without a cast [-Wint-conversion]

The second experiment is a package. Package authors whose code stopped compiling because the declarations went away sometimes add the prototype themselves, which makes the file compile and hides nothing from R CMD check. probe is a deliberately wrong package of one R function and one C file, which declares Rf_findVar() by hand, does not register its routine, and sets PKG_CFLAGS = -Wall -O3.

#define R_NO_REMAP
#include <R.h>
#include <Rinternals.h>

/* Not declared by the installed headers any more: declared by hand. */
extern SEXP Rf_findVar(SEXP symbol, SEXP rho);

SEXP probe_lookup(SEXP name, SEXP rho) {
  return Rf_findVar(Rf_installChar(STRING_ELT(name, 0)), rho);
}
PKG_CFLAGS = -Wall -O3

The relevant part of R CMD check --as-cran --no-manual --no-tests on this package, with the other checks (all passing) left out, is below. It contains the text of the three findings. The compiled code check names the non-API call and the missing registration, says the entry point may be removed soon, and points to the two sections of the manual that matter.

* checking compilation flags in Makevars ... WARNING
Non-portable flags in variable 'PKG_CFLAGS':
  -Wall -O3
* checking compiled code ... WARNING
File ‘probe/libs/probe.so’:
  Found non-API call to R: ‘Rf_findVar’
File ‘probe/libs/probe.so’:
  Found no calls to: ‘R_registerRoutines’, ‘R_useDynamicSymbols’
This entry point may be removed soon:
‘Rf_findVar’

Compiled code should not call non-API entry points in R.
It is good practice to register native routines and to disable symbol
search.

See ‘Writing portable packages’ in the ‘Writing R Extensions’ manual,
and section ‘Moving into C API compliance’ for issues with the use of
non-API entry points.

(The platform header lines of the check log are not shown, and Rf_findVar was chosen because the NEWS names it among the non-API functions that R_getVar replaces; the replacement call is R_getVar(sym, rho, TRUE).) A WARNING from R CMD check does not stop a package from installing, but a package submitted to CRAN is expected to pass without WARNINGs (Publishing to CRAN covers the policy), and since R 4.6.0 the list of non-API entry points is long. The practical advice is to run R CMD check with the newest R, including R-devel, and to treat every line of "checking compiled code" as something to fix with the replacement table above. For an entry point that has no replacement, the manual’s section is the place to look first, since it is updated as the API is clarified.

Toolchain notes that affect compliance

Two toolchain facts interact with all this. First, the default C standard is C23 on R 4.5.0 and later where the compiler supports it, which rejects implicit function declarations and K&R-style definitions, so older C code needs fixing or USE_C17; and the default C++ standard is C++20 from R 4.6.0, which has removed some library features that C++17 still has (the manual gives std::uncaught_exception as an example and warns that some compilers accept removed features in C++17 mode). Second, Rtools is versioned with R: Rtools45 serves R 4.5.0 and every later R including R-devel, so the Windows toolchain of this page is the same GCC 14.2 for R 4.5 and 4.6; the version history is in the next section. R CMD config prints the compiler settings that R will use, and a CI job that checks packages on Windows, macOS and Linux with the current release and R-devel finds toolchain differences before CRAN’s checks do. How packages are checked and submitted is the subject of Publishing to CRAN.

Version notes

The entries come from the NEWS files of the corresponding releases (R NEWS and R 3.x NEWS); the dates of the Rtools releases are the R releases that adopted them.

Version Change

R 3.2.0

.C(DUP = FALSE) and .Fortran(DUP = FALSE) are ignored: the arguments are copied as DUP = TRUE would do. The help page keeps the argument for old code and says that .Call() is much preferred.

R 3.4.0

R_Calloc(), R_Realloc() and R_Free() introduced. tools::package_native_routine_registration_skeleton() added. R CMD check --as-cran notes a package that neither registers its native routines nor declares its intentions on symbol search. The styles member of R_CMethodDef and R_FortranMethodDef removed; packages that register routines for .C() or .Fortran() had to be reinstalled.

R 4.0.0

Reference counting replaces the NAMED mechanism (use MAYBE_SHARED() and related macros). The Fortran print subroutines labelpr, dblepr1, realpr1 and intpr1 added. Rf_installChar() and Rf_installTrChar() become identical.

R 4.2.0

Makevars.ucrt, Makefile.ucrt, configure.ucrt and cleanup.ucrt supported and preferred to the .win files on Windows; Rtools42 (a UCRT toolchain based on gcc 10.3) recommended. R can be built with -DSTRICT_R_HEADERS; the S-compatibility macros PROBLEM, ERROR, WARN and the like are no longer defined in R_ext/RS.h; R_Calloc and friends are preferred and used in error messages.

R 4.3.0

The default C++ standard becomes C++17 where available (C++14 or C++11 otherwise). USE_FC_LEN_T becomes the default, so C and C++ calls to BLAS and LAPACK routines with character arguments must append FCONE. Initial C++23 support. Rtools43 (gcc 12).

R 4.4.0

Rtools44 (gcc 13). The environment variable _R_CXX_USE_NO_REMAP_ makes R CMD INSTALL compile C++ with -DR_NO_REMAP. The manual dates the change that only a C++17 compiler is selected by default to this release. S4SXP becomes OBJSXP.

R 4.5.0

A C23 compiler is selected by default where available. Strict R headers are the default: PI, Calloc, Realloc and Free are removed (use M_PI, R_Calloc, R_Realloc, R_Free). R_NO_REMAP is the default for C++. The manual gains the section "Moving into C API compliance"; many API replacements are added (R_mkClosure, R_getVar, Rf_isDataFrame, Rf_asRboolean, …​); the list of non-API entry points that R CMD check reports grows, and some NOTEs become WARNINGs. C++26 support begins (from R 4.5.3 on Windows). Rtools45 (gcc 14.2).

R 4.6.0

The default C++ standard becomes C++20 where available, otherwise C++17; C++11 and C++14 are no longer supported. Declarations of LOGICAL0, INTEGER0, REAL0, COMPLEX0, RAW0, DATAPTR and VECTOR_PTR removed from the installed headers; Rf_isFrame removed; a long list of internal entry points hidden and undeclared; more non-API functions become check WARNINGs and non-API variables a check NOTE. New: R_mapAttrib, R_getAttributes, R_class, R_nrow, R_ncol, an experimental API for variable bindings, and an experimental way to allocate resizable vectors.

R-devel (4.7.0)

No C-level entry in the NEWS section at the time of writing. (Upcoming; not run.)

Registration itself is older (the manual’s example converts the splines package, which was converted in 2001), but R 3.4.0 is when R added the skeleton generator and a check NOTE for packages that did not register, and registration has been expected practice for packages with compiled code since then. The environment variable _R_CHECK_NATIVE_ROUTINE_REGISTRATION_, which makes R CMD check report packages that do not register, is documented in R Internals (R Internals: Tools).

Where older tutorials differ

Older tutorial writes Current practice Why Since

.C() as the main interface, with every vector passed as double *

.Call() with SEXP, or Rcpp

.C() copies every argument, cannot return an object other than the list of arguments, has no NA for strings and no attributes; the manual lists .Call() as the default and .C() for plain numbers

always

dyn.load("foo.so") and .Call("foo", x) in a script, or in .First.lib

A package with useDynLib(pkg, .registration = TRUE) and .Call(symbol, ...)

The package loads the right object by itself, on every platform, and the symbol is looked up once

R 3.4.0 (check NOTE)

.Call("foo", x, PACKAGE = "pkg") with a string

.Call(foo, x) or .Call(C_foo, x) with R_forceSymbols()

Registration makes the call faster, checks the arity and the types, and removes the chance of calling another package’s foo

R 3.4.0

A package without R_init_pkg()

R_registerRoutines() and R_useDynamicSymbols(dll, FALSE)

R CMD check --as-cran reports its absence

R 3.4.0

Calloc(), Free(), PI, error(), length()

R_Calloc(), R_Free(), M_PI, Rf_error(), Rf_length() and R_NO_REMAP

Strict R headers are the default and the short names clash with other libraries

R 3.4.0 for R_Calloc; R 4.5.0 for the removal

NAMED(x), SET_NAMED(x) to decide whether to copy

MAYBE_SHARED(x) and the reference-counting macros

NAMED is hidden and the mechanism was replaced

R 4.0.0 (replaced), R 4.6.0 (hidden)

ATTRIB(x), STRING_PTR(x), DATAPTR(x), SET_TYPEOF(), Rf_findVar()

Rf_getAttrib(), STRING_PTR_RO(), DATAPTR_RO(), R_getVar() and the replacement table

Non-API entry points are no longer declared, and R CMD check warns

R 4.5.0 and R 4.6.0

.C(..., DUP = FALSE) to avoid copying

.Call(), which does not copy

DUP is ignored

R 3.2.0

R CMD SHLIB and source() or dyn.load() as the development workflow

A package with pkgload::load_all(), or Rcpp::sourceCpp() for one file

The package keeps the build, the registration and the tests together; load_all() recompiles

always

The inline package and Rcpp::cxxfunction()

Rcpp::cppFunction(), Rcpp::sourceCpp() and // [[Rcpp::export]]

Attributes generate the glue and the wrapper; inline is a separate package that is not needed

Rcpp attributes

Rcpp modules (RCPP_MODULE) for exposing functions

Attributes for functions; modules for exposing C++ classes

Attributes are shorter and more robust for functions; sourceCpp() still loads modules

always

sourceCpp() called inside package code to compile when loaded

src/ with compileAttributes(), compiled at install time

A package is compiled once, at install time; the help page of sourceCpp() says to use a package for more than one file

always

CXX_STD = CXX11 in Makevars

Nothing, or CXX_STD = CXX17 (or CXX20) when the code needs it

C++11 and C++14 are no longer supported; the default is C++17 or C++20

R 4.3.0, R 4.6.0

-fopenmp in PKG_CXXFLAGS

$(SHLIB_OPENMP_CXXFLAGS) in the flags and PKG_LIBS

The flag is compiler-specific; Apple’s clang has no OpenMP

always

PKG_CFLAGS = -Wall -O3 -march=native in a distributed package

Those flags in ~/.R/Makevars

R CMD check warns about non-portable flags in Makevars

always

Fortran print * and write(6, *), and character*255 arguments

dblepr(), intpr(), rexit(); integer codes or iso_c_binding and .C()

Fortran I/O may not reach the console; character passing depends on the compiler and USE_FC_LEN_T

R 4.0.0 (dblepr1), R 4.3.0

rand(), srand(), Fortran random_number

GetRNGstate(), unif_rand(), PutRNGstate()

set.seed() must work, and packages must not interfere

always

Where to go next

References