webR 0.3.1

  wasm, webassembly, webr

  George Stagg

We’re delighted to announce the release of webR 0.3.1. This release brings bug fixes, infrastructure upgrades, and exciting improvements to webR’s API for creating R objects and evaluating R code from JavaScript. These new features make integrating webR with existing JavaScript frameworks such as Observable a breeze.

You can install the latest release from npm with the command:

npm i webr@0.3.1

or if you’re using JavaScript modules to import webR directly from CDN:

import { WebR } from 'https://webr.r-wasm.org/v0.3.1/webr.mjs';

A summary of changes is described below, with the full release notes on GitHub.

Evaluating R code from JavaScript

The underlying interpreter powering webR is built from the same source code as R itself, with patches applied so that it can run in the WebAssembly environment. With this release, we have rebased our patches on the latest stable version of R1. By keeping our source in sync, improvements and bug fixes made by the R Core Team also benefit any project making use of webR.

WebR’s core functionality is to evaluate R code from a JavaScript environment. As such, it is imperative that this works well, even with large and complex scripts. The webR app has been updated to better handle large R scripts, and scripts longer than 4096 characters should no longer cause strange issues in the R console.

Loading WebAssembly packages

The package management functions provided by webR have been expanded and improved. We set up webR with shims (interceptors) for install.packages(), library(), and require() so that installing or loading R packages automatically downloads WebAssembly binaries from the webR package repository. Also, it is no longer required to run the library() command a second time to subsequently load the package.

In this interactive example, webR is configured to automatically install WebAssembly packages. Click “Run code” to download the packages listed in the R script.

See the documentation for more details on how to control this behaviour in your own webR-powered applications, including optionally showing an interactive download menu to the user.

Error handling and reporting

Improvements have been made to how webR raises R conditions as JavaScript exceptions. Exceptions now include the offending source R call in the error message text, better matching what is shown in a traditional R console.

await webR.evalR("sin('abc')");
Uncaught Error: Error in sin("abc"): non-numeric argument to mathematical function

Conditions raised when invoking function objects are now also re-thrown as JavaScript exceptions, rather than a generic UnwindProtectException error. Compare the error messages shown below from the previous and latest versions of webR to see the useful context added by this change.

// webR 0.2.2
const do_calc = await webR.evalR(`function (n) { rnorm(n) }`)
do_calc(-10)
Uncaught (in promise) UnwindProtectException: A non-local transfer of control occured during evaluation
// webR 0.3.1
const do_calc = await webR.evalR(`function (n) { rnorm(n) }`)
do_calc(-10)
Uncaught (in promise) Error: Error in rnorm(n): invalid arguments

Some base R features can be problematic when running R under WebAssembly. For example, in the constrained WebAssembly sandbox the base R function system() does not work. The latest release of webR now handles these cases more consistently, raising R stop() conditions rather than incorrectly returning an empty result.

# webR 0.3.1
system()
Error in webr_hook_system(command) : 
  The "system()" function is unsupported under Emscripten.

Capturing HTML canvas graphics output

The captureR() function is designed to capture output generated when evaluating R code. In addition to capturing standard text output, details about errors and other R conditions are also captured. With this release, plots drawn using webR’s HTML canvas graphics device, webr::canvas(), are also captured and returned by default.

// Evaluate R code, capturing all output
const capture = await webR.globalShelter.captureR(`
  x <- rnorm(10000)
  print(x[1])
  hist(x)
`);
console.log(capture);
{
  result: Proxy(Object),
  output: [
    { type: 'stdout', data: '[1] 0.7612882' },
  ],
  images: [ ImageBitmap ],
}

Captured plots are returned as an array of ImageBitmap JavaScript objects in the images property. This interface represents a bitmap image in a way that can be efficiently drawn to a HTML <canvas> element.

This change makes plotting consistent with other forms of R output and simplifies the process when working with multiple independent R code blocks and output images. See the webR documentation on evaluating R code for further details, and this Observable notebook for an example of capturing R plots from JavaScript.

Graphics device bug fixes

In addition to adding the ability to capture graphics output, the webr::canvas() graphics device has also had various bug fixes made to better implement R base graphics. The easiest way to demonstrate is probably by example:

The R object interface

The R object interface provided by webR has been expanded to support the conversion of more types of JavaScript objects into R objects. Such conversions are automatically applied when interacting with the R environment from JavaScript.

Raw vectors

JavaScript objects of type TypedArray, ArrayBuffer, and ArrayBufferView (e.g.  Uint8Array) may now be used to construct R objects. By default, objects of this type are converted to R raw atomic vectors. This simplifies the transfer of binary data to R.

const data = new Uint8Array([4, 12, 8, 24, 15, 12]);
// Print data's R object class and an example byte
await webR.evalR(`
  class(data)
  data[2]
`, { withAutoprint: true, env: { data } });
[1] "raw"
[1] 0c

R data.frame

JavaScript objects of shape { x: [...], y: [...] }, with data in a “long” column-based form, can now be used to construct R objects. In previous versions of webR, this object shape was reserved for future use. However, with this release webR now constructs an R data.frame by taking the source object’s properties as column vectors. The resulting data.frame can then be manipulated from R in the usual way:

const data = { column_x: ["foo", "bar", "baz"], column_y: [1, 3, 7] }
await webR.evalR(`
  class(data)
  colnames(data)
  data[2:3,]
`, { withAutoprint: true, env: { data } });
[1] "data.frame"
[1] "column_x" "column_y"
        column_x column_y
 2      bar        3
 3      baz        7

Similarly, an R data.frame can be converted back into a JavaScript object of this form:

const cars = await webR.evalR(`mtcars`);
await cars.toObject();
{
  am: [1, 1, 1, ..., 1],
  carb: [4, 4, 1, ..., 2],
  cyl: [6, 6, 4, ..., 4]
  ...,
  wt: [2.62, 2.875, 2.32, ..., 2.78],
}

D3 “wide” format

In JavaScript, particularly when using frameworks built upon D3, it is typical to work with data in a “wide” form: an array of objects per row, each including all the column names and values. With this release, webR can also convert JavaScript objects in this form into an R data.frame.

The following example loads the same data as shown in the previous example but expressed in the “wide” form.

const data = [
  { column_x: "foo", column_y: 1 },
  { column_x: "bar", column_y: 3 },
  { column_x: "baz", column_y: 7 },
];
await webR.evalR(`
  class(data)
  colnames(data)
  data[2:3,]
`, { withAutoprint: true, env: { data } });
[1] "data.frame"
[1] "column_x" "column_y"
        column_x column_y
 2      bar        3
 3      baz        7

An R data.frame can also be converted into a D3 compatible JavaScript object:

const cars = await webR.evalR(`mtcars`);
await cars.toD3();
[
  { mpg: 21, cyl: 6, disp: 160, ... },
  { mpg: 21, cyl: 6, disp: 160, ... },
  { mpg: 22.8, cyl: 4, disp: 108, ...},
  ...
  { mpg: 21.4, cyl: 4, disp: 121, ...},
]

WebAssembly toolchain upgrades

We have updated our WebAssembly build system, upgrading the Emscripten C/C++ compiler to version 3.1.47 and the LLVM Flang Fortran compiler to be based on LLVM 18.1.1. As part of the work, webR now supports building under Nix using flakes, suggested and largely implemented by @wch.

With this, source-code level reproducible builds of the webR WebAssembly binaries can be made, strengthening the argument for webR as a potential future platform for reproducible data science.

LLVM Flang

To compile Fortran sources in the R source code2 for webR, we require a Fortran compiler that supports outputting WebAssembly objects. This is a surprisingly tricky business, and our current solution is to maintain a patched version of LLVM’s flang-new compiler frontend.

In recent months, the patches we must make to LLVM Flang have become smaller and easier to manage as the LLVM team continues to improve the Flang frontend. While too long for this post, for those interested in exactly what changes we make to enable WebAssembly output, I have written a deep-dive blog post, Fortran on WebAssembly.

Additional system libraries and Rust support

Thanks to some great work by @jeroen and @yutannihilation, this release of webR includes some additional WebAssembly system libraries and software in the webR Docker container. This includes numerical libraries such as GSL and GMP, image manipulation tools such as ImageMagick, and a Rust compiler configured to build WebAssembly R packages containing Rust source code.

A demonstration R package containing Rust code, compatible with webR, can be found at https://github.com/yutannihilation/savvy-webr-test/.

An example Shiny app making use of the WebAssembly compiled ImageMagick library is shown below, with the source code at https://github.com/jeroen/shinymagick.

WebAssembly R package binaries

With the introduction of additional system libraries and changes to the WebAssembly toolchain, the default webR package repository has also been refreshed. The repository tends to follow CRAN package releases, though is updated less frequently. 19452 WebAssembly R packages have been recompiled from source for this release, with 12969 packages, about 63% of CRAN, fully available3 for use in webR.

As my usual caveat goes, we have not been able to test all the available packages. Feel free to try your favourite package in the webR app and let us know in a GitHub issue if there is a problem.

The package repository index contains further information and a searchable list of WebAssembly R packages. In addition, R-Universe also builds webR-compatible binaries and so can be used as an alternative repository for access to even more R packages.

Building custom WebAssembly R packages

If you’d like to build your own R packages for webR, the rwasm package provides functions to help compile R packages for WebAssembly, manage repositories, and prepare webR-compatible filesystem images.

We’ve also started building reusable workflows for GitHub Actions. If you have an R package with source code hosted on GitHub, an action can be added to your repository such that a WebAssembly version of your package will be built automatically by a GitHub runner on package release.

Acknowledgements

Thank you, as always, to the users and developers contributing to webR in the form of discussion in issues, bug reports, and pull requests.

@adrianolszewski, @christianp, @coatless, @ColinFay, @drgomulka, @erex, @gitdemont, @gorkang, @isbool, @JeremyPasco, @jeroen, @JosiahParry, @Luke-Symes-Tsy, @maek-ies, @MaybeJustJames, @ravinder387, @StaffanBetner, @SugarRayLua, @takahser, @tim-newans, @timelyportfolio, @tstubbs-evolution, @yhm-amber, @yii-iiy, @yutannihilation, and @zhangwenda0518.


  1. The latest stable release at the time of writing: R 4.3.3 — “Angel Food Cake” ↩︎

  2. There are also many R packages containing Fortran source code. ↩︎

  3. Here “available” means that both a binary build of an R package and all of its dependencies can be downloaded from the repository. ↩︎