Compiling R for iOS

Cross-compiling R 4.5.2 for iPhone, and why dynamic symbol loading is the real problem.

r
ios
cross-compilation
Author

James Balamuta

Published

January 17, 2026

Abstract

Can you compile R natively for iOS? We explored this over a few weeks, cross-compiling R 4.5.2 for ARM64, rendering plots with Core Graphics, and running base R in an iPhone app. The experiment revealed a fundamental blocker: iOS’s prohibition on dynamic symbol loading (dlopen()) breaks R’s package system in a way that’s hard to work around. This post documents the exploration: what it took to get R running, where we hit walls, and what we learned about the boundaries of iOS’s security model.

Can you compile R to run natively on an iPhone? Not through a server, not through WebAssembly, but as a real ARM64 binary linked into an iOS app?

We spent a few weeks exploring this. The short answer: yes, with caveats. This post documents what we found.

Cross-Compiling R for ARM64

R’s build system is autoconf-based, which means cross-compilation is supported but not exactly plug-and-play. The basic setup:

  • Compiler: Xcode’s clang targeting the iOS SDK
  • Target: arm64-apple-ios15.0 (Simulator first, then device)
  • Fortran: R traditionally depends on Fortran for BLAS/LAPACK. iOS has no Fortran compiler. We replaced this entirely with Apple’s Accelerate framework, which provides highly optimized BLAS/LAPACK routines already built for iOS.
  • Dependencies: zlib, libbz2, and iconv come from the iOS SDK. We cross-compiled liblzma and PCRE2 separately.
  • Disabled: readline, X11, Tcl/Tk, Aqua, Java, and recommended packages

After patching a few configure checks and stubbing some system calls (more on that below), R compiles as a static library. You link it into an Objective-C or Swift app, set up R_HOME in the app’s sandbox, and call Rf_initEmbeddedR(). It starts. 1 + 1 returns 2. mean(rnorm(1000)) works. Matrix operations go through Accelerate and are fast.

iPhone Simulator screenshot showing a minimal test app running native R with output from 1+1, sum(1:100), c(1,2,3,4,5), and mean(1:10).

Early prototype: basic R expressions running natively on iOS.

iPhone Simulator screenshot showing a more advanced test app with ggplot2 loaded, LAPACK tests passing, and a rendered scatter plot with regression line.

Later version: ggplot2 loaded, graphics device active, and a plot rendered through Core Graphics.

Here’s a quick demo of the prototype in action:

What Had to Be Stubbed

iOS is not a general-purpose Unix. Several POSIX functions that R depends on are either missing or prohibited:

fork() and exec() are completely blocked. iOS apps run as a single process; spawning child processes is not allowed. This means:

  • system() doesn’t work
  • mclapply() and the parallel package fall back to serial execution
  • Piped connections (pipe()) fail

We stubbed these to return errors gracefully rather than crash. R handles the fallbacks reasonably well for interactive use.

Dynamic library loading is the bigger problem. R loads packages by calling dlopen() on .so files (shared objects) at runtime. On macOS this works fine. On iOS, dlopen() is restricted to system frameworks and libraries that ship with the app bundle. You cannot load arbitrary compiled code at runtime. This is a security policy enforced by the kernel, not something you can work around.

The dlopen() Problem

This is where the experiment hit a wall.

R’s package installation system assumes it can compile C/C++/Fortran source code into a shared library and load it with dlopen(). On a desktop, you run install.packages("dplyr"), R downloads the source, compiles it with your system compiler, and loads the resulting .so. On iOS, every step of that pipeline is blocked:

  1. No compiler on device. There’s no gcc or clang available at runtime.
  2. No dlopen() for user code. Even if you could compile a package, iOS won’t let you load it.
  3. No fork()/exec(). R’s build system shells out to make, which requires process spawning.

Pure R packages (no compiled code) can still be installed at runtime, since they don’t need compilation or dlopen(). But most of the packages people actually want (the tidyverse, data.table, Rcpp, stringi, jsonlite) have native code.

The Pre-Compilation Workaround

The only path forward for native packages is to pre-compile them on a Mac, targeting iOS, and bundle the resulting static libraries with the app. We built a system for this with three tiers:

  1. Base packages (base, stats, graphics, utils, methods, etc.) bundled at build time as part of R itself.
  2. Popular packages (dplyr, ggplot2, tidyr, readr, etc.) pre-compiled as iOS static libraries and shipped in a downloadable cache.
  3. User packages (pure R only) installable at runtime via install.packages().

This works, but the maintenance burden is significant. Every time R releases a new version, every pre-compiled package needs to be rebuilt. Package updates require a new cache build. And users can’t install any native package that isn’t already in the cache.

We got a working prototype with base R, a graphics device (rendering through Core Graphics with Retina support), and a handful of pre-compiled packages. But the package story was always going to be the bottleneck.

The Graphics Device

One thing that worked well: we built a custom R graphics device on top of iOS’s Core Graphics framework. It supports lines, rectangles, circles, polygons, text with full font access via Core Text, raster images with rotation, clipping, transparency, and Retina resolution. Plots render natively without any web view.

This was satisfying to build, and the output quality is excellent. But it doesn’t matter much if users can’t install ggplot2.

Where This Leaves Us

This was always an exploration, not a product plan. We wanted to know: can R run natively on iOS? The answer is yes, with real constraints.

The experiment proved a few things worth knowing:

  • R’s autoconf build system can target iOS ARM64 with manageable patching.
  • Apple’s Accelerate framework is a drop-in replacement for Fortran-based BLAS/LAPACK, and it’s fast.
  • Core Graphics makes a solid backend for an R graphics device.
  • Base R and pure-R packages work fine in the iOS sandbox.

But the dlopen() restriction is fundamental. It’s not a bug to work around; it’s an iOS security policy enforced at the kernel level. Any approach that relies on loading compiled package code at runtime is blocked. Pre-compiling packages is technically possible but creates a maintenance burden that scales poorly.

Next Up: webR

There’s another path to R on iOS that sidesteps dlopen() entirely: webR, which compiles R to WebAssembly. WebAssembly has its own module loading system that doesn’t touch iOS’s dynamic linking restrictions, and the webR repository already hosts hundreds of CRAN packages compiled to Wasm, maintained by George Stagg and others at Posit.

The trade-off is performance and memory (WebAssembly is slower than native and limited to ~300 MB), but for interactive use on a phone that may be exactly the right trade-off. We’re going to find out.