// sum_c.c
#include <R.h>
#include <Rinternals.h>
SEXP sum_c(SEXP x) {
int n = length(x);
double *px = REAL(x);
double total = 0.0;
for (int i = 0; i < n; i++) {
total += px[i];
}
return ScalarReal(total);
}31 Connecting to other languages
Call sum() in R and you are calling C. Call qr() and you are calling LAPACK Fortran routines that predate R by decades. The numerical core of the language, from matrix multiplication to sorting to random number generation, lives in compiled code that R orchestrates from above. R was designed as a glue language. So what happens when you need compiled code of your own?
This chapter covers the practical interfaces: how to call C, C++, Rust, Python, and Fortran from R, with a complete working example for each. Section 28.6 covered when compiled code is worth the trouble; here the question is how, starting with the interface that underlies all the others.
31.1 C via .Call()
.Call() is R’s native foreign function interface, the third of the three roads in Section 29.10 and the one every other approach rests on. When Rcpp generates a wrapper or extendr produces a binding, what R loads and dispatches is a .Call() entry point.
A C function callable from R takes SEXP arguments and returns a SEXP, and any R object it allocates has to be shielded from the garbage collector with PROTECT and released with UNPROTECT before returning (Section 29.6).
Here is a complete example: a C function that computes the sum of a numeric vector.
Compile and load it from R:
system("R CMD SHLIB sum_c.c")
dyn.load("sum_c.so") # sum_c.dll on Windows
.Call("sum_c", as.numeric(1:1000))R CMD SHLIB invokes the system C compiler with the correct flags and include paths, dyn.load() loads the shared library into R’s process, and .Call() dispatches to the function by name.
Inside the function, REAL(x) extracts the underlying C double* array from a numeric SEXP; ScalarReal() wraps a C double back into a SEXP. Because this function does not allocate any new R objects, there is nothing to PROTECT. If it did allocate (say, a result vector via allocVector(REALSXP, n)), you would need to PROTECT that allocation and UNPROTECT(1) before returning.
Getting that count wrong is the classic bug in hand-written extensions: one PROTECT too few and the collector frees your object mid-computation, one UNPROTECT too few and the protection stack overflows. The higher-level interfaces exist to take this bookkeeping away, but the accessor macros underneath them are still worth knowing.
Other accessors follow the same pattern: INTEGER(x) for integer vectors, LOGICAL(x) for logical vectors, STRING_ELT(x, i) for string vectors (which are arrays of CHARSXP pointers, not C strings), VECTOR_ELT(x, i) for list elements.
Writing raw C against R’s API is rarely the right choice for new code. The reason to learn it is to read existing code: base R, data.table, and hundreds of CRAN packages use .Call() directly, and knowing the interface makes their source legible in a way that no amount of documentation can substitute for.
Exercises
- Write a C function that takes an integer vector and returns its maximum value. Compile it with
R CMD SHLIB, load it, and test it from R. UseINTEGER()instead ofREAL(). - Modify the
sum_cfunction to return a length-1 numeric vector allocated withallocVector(REALSXP, 1)instead of usingScalarReal(). You will needPROTECTandUNPROTECT. Verify it produces the same result.
31.2 C++ via Rcpp
What if something else handled the PROTECT bookkeeping? Rcpp wraps R’s C API in C++ classes that manage type conversion and memory: no protection macros, no SEXP arithmetic, no accessor juggling. You write ordinary C++ and Rcpp generates the .Call() machinery underneath.
The same sum function, rewritten:
// sum_rcpp.cpp
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
double sum_rcpp(NumericVector x) {
int n = x.size();
double total = 0.0;
for (int i = 0; i < n; i++) {
total += x[i];
}
return total;
}The // [[Rcpp::export]] attribute tells Rcpp::sourceCpp() to generate the .Call() wrapper automatically. From R:
Rcpp::sourceCpp("sum_rcpp.cpp")
sum_rcpp(as.numeric(1:1000))One function call compiles, links, loads, and registers the function; the turnaround from edit to test is seconds. The convenience extends beyond compilation, too, because Rcpp handles type conversion on both sides of the boundary.
Rcpp provides wrapper classes for all common R types: NumericVector, IntegerVector, CharacterVector, LogicalVector, List, DataFrame, NumericMatrix. These proxy the underlying SEXP without copying, so you pay no conversion cost on the way in, and return values are converted back to R objects automatically.
Rcpp also mirrors R’s vectorized functions in C++, a layer it calls sugar:
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
NumericVector abs_diff(NumericVector x, NumericVector y) {
return abs(x - y); // sugar: vectorized, no explicit loop
}
// [[Rcpp::export]]
LogicalVector above_threshold(NumericVector x, double threshold) {
return x > threshold; // sugar: vectorized comparison
}Sugar covers abs, sum, mean, min, max, ifelse, which, any, all, pow, sqrt, and many more. When sugar is sufficient, your C++ code looks almost identical to R but runs at compiled speed.
For linear algebra, RcppArmadillo adds the Armadillo library:
// [[Rcpp::depends(RcppArmadillo)]]
#include <RcppArmadillo.h>
// [[Rcpp::export]]
arma::vec solve_system(arma::mat A, arma::vec b) {
return arma::solve(A, b);
}Armadillo provides matrix decompositions (QR, SVD, Cholesky, eigenvalues), sparse matrix support, and an expression template engine that fuses operations to avoid temporaries. If your bottleneck is linear algebra beyond what R’s BLAS provides, RcppArmadillo is the natural tool.
Rcpp pays off for tight loops over vector elements, element-wise operations that resist vectorization in R, recursive algorithms like tree traversal or dynamic programming, and anything that needs fine-grained control over iteration. It cannot help with I/O-bound code or with code that already calls optimized C: sum() is already C, and rewriting it in Rcpp will not make it faster. So what about a language that offers the same low-level performance but catches your memory bugs before they happen?
Exercises
- Write an Rcpp function that computes the running maximum of a numeric vector (each element is the max of all elements up to that index). Compare its speed with
cummax()from base R usingbench::mark(). - Write an Rcpp function that takes a numeric vector and returns the indices of all values greater than the mean. Use a loop, not sugar. Then rewrite it using sugar (
which(x > mean(x))). Which version is faster? - Using RcppArmadillo, write a function that computes the ordinary least squares coefficients (X^T X)^{-1} X^T y for a matrix X and vector y. Compare with R’s
lm.fit().
31.3 Rust via extendr
C and C++ give you PROTECT/UNPROTECT and hope you get the count right. Rust’s type system prevents memory errors at compile time: the compiler flatly rejects code that could produce dangling pointers or use-after-free bugs. No garbage collector needed, no protection stack to manage, no segfaults lurking in code that “usually works.”
The extendr crate bridges Rust and R, and the R package rextendr provides the interactive workflow:
rextendr::rust_function("
fn sum_rust(x: &[f64]) -> f64 {
x.iter().sum()
}
")
sum_rust(as.numeric(1:1000))rust_function() compiles a single Rust function and loads it into R, similar to Rcpp::cppFunction(). For larger projects, rust_source() compiles an entire Rust file.
A slightly more involved example: computing the nth Fibonacci number iteratively.
rextendr::rust_function("
fn fib(n: i32) -> i32 {
if n <= 1 { return n; }
let mut a = 0i32;
let mut b = 1i32;
for _ in 2..=n {
let tmp = a + b;
a = b;
b = tmp;
}
b
}
")
fib(10)
#> [1] 55No PROTECT, no UNPROTECT, no SEXP. The type conversion between R and Rust is handled by extendr’s #[extendr] macro (used in source files) or inferred by rust_function().
Rust’s ownership model says every value has exactly one owner; when ownership transfers, the old name becomes invalid. R’s copy-on-modify (Section 29.5) shares an object until someone modifies it, then copies. Rust enforces its rule at compile time and R at run time, and both guarantee the same thing: a value will not change while someone else is looking at it. That shared guarantee is why a Rust data frame engine can hand data to R through extendr without defensive copying.
But does the safety justify the ecosystem trade-off?
Rcpp has about 3,300 CRAN packages depending on it as of this writing and has been on CRAN since 2008; its documentation, Stack Overflow coverage, and library of examples are unmatched in the R world. extendr is younger, with a smaller ecosystem and fewer production examples. What it buys is the compile-time memory safety described above, which matters more the larger the compiled codebase becomes.
For package development, rextendr::use_extendr() sets up the directory structure and build configuration. The polars package (R bindings to the Polars data frame library, distributed through R-universe) is the largest extendr-based package; prqlr, b64, and heck are smaller ones on CRAN.
If you are starting a new package today and the compiled code is non-trivial (more than a few hundred lines), Rust is worth serious consideration. The upfront cost of learning ownership semantics pays for itself in bugs you never have to debug. For quick one-off functions or small performance patches, Rcpp’s lower friction and larger community still win.
Exercises
- Install
rextendrand write a Rust function that counts the number of values in a numeric vector that exceed a given threshold. Call it from R and verify the result. - Compare the compile time of
Rcpp::cppFunction()andrextendr::rust_function()for equivalent simple functions. Which has faster turnaround?
31.4 Python via reticulate
Calling Python from R is not about speed. Python’s interpreter is slower than R’s for numerical work. The reason to call Python is access: scikit-learn, PyTorch, TensorFlow, Hugging Face Transformers, spaCy, and hundreds of other libraries either have no R equivalent or their R equivalents lag behind by months or years. When a new deep learning architecture appears, the Python implementation comes first.
The reticulate package embeds a Python interpreter inside R’s process, with no inter-process communication overhead; R and Python share the same memory space. Importing a module gives you an object whose attributes you reach with $:
library(reticulate)
np <- import("numpy")
pd <- import("pandas")
x <- np$array(c(1, 2, 3, 4, 5))
np$mean(x)
#> [1] 3R vectors convert to NumPy arrays automatically, and NumPy arrays convert back to R vectors. A whole Python file can be sourced:
# helpers.py contains:
# def normalize(x):
# return (x - x.mean()) / x.std()
source_python("helpers.py")
normalize(c(1, 2, 3, 4, 5))source_python() executes a Python file and makes its top-level functions available in R’s global environment as regular R functions.
The conversions happen automatically: numeric vectors become NumPy arrays, data frames become pandas DataFrames, named lists become dicts and unnamed lists become Python lists, TRUE/FALSE become True/False, and NULL becomes None. A scikit-learn model fits the same way:
sklearn <- import("sklearn")
linear_model <- import("sklearn.linear_model")
X <- matrix(rnorm(200), ncol = 2)
y <- X[, 1] * 3 + X[, 2] * -1 + rnorm(100, sd = 0.5)
model <- linear_model$LinearRegression()
model$fit(X, y)
model$coef_
#> [1] 2.98 -1.02The model object lives in Python, but you interact with it from R using $. Predictions, coefficients, scores: all accessible through the same operator.
reticulate can use the system Python, a virtualenv, or a conda environment. Say which before importing anything:
Sys.setenv(RETICULATE_PYTHON = "/usr/bin/python3")
# or
reticulate::use_virtualenv("myproject")
# or
reticulate::use_condaenv("myenv")Call this before import(). Once the Python interpreter starts, you cannot switch to a different one within the same R session.
Exercises
- Use reticulate to import Python’s
collectionsmodule and callCounteron a character vector. Verify the result matches R’stable(). - Create a NumPy array of 1 million random values with
np$random$standard_normal(), then pass it to an R function (e.g.,mean()). Does reticulate copy the data or share it? Usebench::mark()with varying sizes to find out.
31.5 Fortran
Every linear model you have ever fitted in R ultimately ran Fortran code. Every call to svd(), chol(), and matrix multiplication (%*%) dispatches to BLAS and LAPACK routines written in a language that predates C by over a decade, and those routines have been optimized by numerical analysts for longer than most programming languages have existed. Fortran is also the oldest language in R’s foreign function toolkit. Understanding its interface explains something that puzzles people who benchmark R against “faster” languages: R’s numerical performance is competitive precisely because R does not do the numerical work itself.
The .Fortran() interface passes R vectors to Fortran subroutines by copying them in and out:
! dot_product.f90
subroutine dotprod(x, y, n, result)
implicit none
integer, intent(in) :: n
double precision, intent(in) :: x(n), y(n)
double precision, intent(out) :: result
integer :: i
result = 0.0d0
do i = 1, n
result = result + x(i) * y(i)
end do
end subroutinesystem("R CMD SHLIB dot_product.f90")
dyn.load("dot_product.so")
result <- .Fortran("dotprod",
x = as.double(1:5),
y = as.double(6:10),
n = 5L,
result = double(1))
result$result
#> [1] 130.Fortran() returns a named list with all arguments, including outputs. This copy-in-copy-out semantics is straightforward but wasteful for large data; the newer .Call() interface with C wrappers around Fortran code avoids the copies.
You will rarely write new Fortran for R. The interface matters for two reasons: reading legacy code (many statistical packages on CRAN have Fortran backends dating to the 1990s), and understanding why R’s numerical performance is strong despite its interpreted overhead. When someone says “R is slow,” they are talking about R’s interpreter loop, not the compiled Fortran that does the actual linear algebra. But with four modern interfaces available (C, C++, Rust, Python) plus Fortran, which one should you actually reach for?
31.6 When to use what
The answer depends on why you need another language in the first place.
If you need speed in a tight loop, profile first (Section 28.1). If the bottleneck is a loop that cannot be vectorized, use Rcpp (largest ecosystem, fastest iteration cycle) or Rust via extendr (memory safety, better for large codebases), or raw C via .Call() if you want zero dependencies.
If you need a library that only exists in Python, use reticulate. This includes deep learning (PyTorch, TensorFlow), NLP (spaCy, Transformers), and computer vision. Do not rewrite Python libraries in R; call them.
If you need numerical linear algebra beyond base R, RcppArmadillo covers dense matrices and RcppEigen sparse ones, or call LAPACK directly via .Fortran() for a specific routine.
If you are building a package with substantial compiled code, consider Rust if the team knows it or is willing to learn. The compile-time checks catch bugs that would otherwise surface as sporadic segfaults in users’ R sessions. For smaller amounts of compiled code, Rcpp is fine.
If you have legacy Fortran, wrap it with .Fortran() or write a thin C wrapper and use .Call().
The ordering from Section 28.7 still applies: pure R first, vectorize, pre-allocate, switch engines (data.table, Arrow, DuckDB), then compiled code. Calling another language is a cost, one that adds build dependencies, complicates installation, and makes debugging harder. Pay that cost only when the benefit is clear.
R was designed from the start to sit on top of compiled code. The foreign function interface is the architecture.
31.7 Packages worth studying
Real-world packages show how these interfaces work at scale. Each of the following is open source, and reading its src/ directory teaches more than any tutorial.
Two C backends first. data.table’s grouping, joining, and sorting engine is C, and its fread() and fwrite() are C implementations of CSV reading and writing that outperform most alternatives; the src/ directory is a course in high-performance C against R’s API. stringi wraps the ICU (International Components for Unicode) C library for Unicode normalization, collation, regular expressions, and transliteration.
Among C++ backends, arrow binds the Apache Arrow C++ library, a columnar in-memory format for zero-copy exchange between systems; torch binds LibTorch, PyTorch’s C++ backend, with no Python dependency; and dplyr’s grouped operations run through C++ (via cpp11) on top of vctrs, whose own core is C.
polars, R bindings to the Polars data frame engine written in Rust, is built with extendr and exposed through generated .Call() wrappers. gifski (GIF encoding) calls a Rust crate through a hand-written C interface instead, a small example of the plain FFI route.
The Python bridges include tensorflow and keras3, which use reticulate to call TensorFlow and Keras with an R API that mirrors the Python one, and spacyr, which wraps spaCy.
The pattern across all of these is the same: R provides the user-facing API (function names, argument handling, documentation, S3 dispatch), while the compiled backend provides the computation. The interface layer is thin.
Chapter 1 described two traditions, Church’s expressions and Turing’s instructions. R descends from Church and calls into Turing’s world every time it invokes C, Fortran, or Rust: you write the logic in R, composing functions, passing closures, piping transformations, and the inner loops run in a language that manipulates memory directly. .Call() is the boundary between the two, and R was designed with that boundary in mind.
The next chapter turns from a single compiled function to a whole project: how R code, compiled or not, is packaged so that other people can install it.
31.8 References and sources
C (the foundation):
- R Core Team, Writing R Extensions, chapter 5 (“System and foreign language interfaces”). The official guide to
.C(),.Call(),.External(). .Call()is the modern interface: pass SEXPs, return SEXPs, full control..C()is the old interface (copies data, limited types)..External()is rarely used.R_RegisterCCallable()/R_GetCCallable(): sharing C functions between packages without linking.
C++ via Rcpp:
- Dirk Eddelbuettel, Seamless R and C++ Integration with Rcpp (2013). The standard reference.
- Dirk Eddelbuettel & Romain Francois, “Rcpp: Seamless R and C++ Integration” (2011, JSS).
- Hadley Wickham, Advanced R (2e), chapter 25.
Rust via extendr:
- extendr project (extendr.github.io). Rust extensions for R, inspired by PyO3.
- The
rextendrpackage vignettes cover both interactive use and package integration.
Python via reticulate:
- Kevin Ushey et al., “reticulate: Interface to Python” (CRAN). Full documentation at rstudio.github.io/reticulate.
Fortran:
- R Core Team, Writing R Extensions, section 5.2. The
.Fortran()interface. - LAPACK Users’ Guide (netlib.org). The linear algebra library that R calls internally.