Chapter 18 The V8 Engine

V8 is an R interface to Google’s open-source JavaScript engine of the same name; it powers Google Chrome, Node.js, and many other things. It is the last integration of JavaScript with R that is covered in this book. Both the V8 package and the engine it wraps are straightforward yet amazingly powerful.

18.1 Installation

First, install the V8 engine itself, instructions to do so are well detailed on V8’s README and below.

On Debian or Ubuntu use the code below from the terminal to install libv8.

On Centos install v8-devel, which requires the EPEL tools.

On Mac OS use Homebrew.

Then install the R package from CRAN.

18.2 Basics

V8 provides an JavaScript execution environment through returning a closure-based object with v8(); Each of such environments is independent of another.

The eval method allows running JavaScript code from R.

Two observations are worth making on the above snippet of code. First, the variable we got back in R is a character vector when it should have been either an integer or a numeric. This is because we used the eval method, which returns what is printed in the V8 console, but get is more appropriate; it converts the output to its appropriate R equivalent.

Second, while creating a scalar with eval("var x = 1;") appears painless, imagine if you will the horror of having to convert a data frame to a JavaScript array via jsonlite then flatten it to character string so it can be used with the eval method. Horrid. Thankfully V8 comes with a method assign, complimentary to get, which declares R objects as JavaScript variables. It takes two arguments, first the name of the variable to create, second the object to assign to it.

All of the conversion is handled by V8 internally with jsonlite, as demonstrated in the previous chapter. We can confirm that the data frame was converted to a list row-wise, using JSON.stringify to display how the object is stored in V8.

However this reveals a tedious cyclical loop: 1) creating an object in JavaScript to 2) run a function on the aforementioned object, 3) get the results back in R, and repeat. So V8 also allows calling JavaScript functions on R objects directly with the call method and obtains the results back in R.

#> [1] "Sun Oct 18 2020 18:34:45 GMT+0200 
  (Central European Summer Time)"
#> [1] "Sun Oct 18 2020 18:34:45 GMT+0200 
  (Central European Summer Time)"

Finally, one can run code interactively rather than as strings by calling the console from the engine with engine$console(). You can then exit the console by typing exit or hitting the ESC key.

18.3 External Libraries

V8 is quite bare in and of itself; there is, for instance, no functionalities built in to read or write files from disk. It thus becomes truly interesting when you can leverage JavaScript libraries. We’ll demonstrate this using fuse.js a fuzzy-search library.

The very first step of integrating any external library is to look at the code (often examples) to grasp an idea of what is to be achieved from R. Below is an example from the official documentation. First, an array of two books is defined; this is later used to test the search. Then another array of options is defined. This should at the very least include the key(s) that should be searched; here it is set to search through the title and authors. Then, the fuse object is initialised based on the array of books and the options. Finally, the search method is used to retrieve all books, the title or author of which partially match the term tion.

With some understanding of what is to be reproduced in R, we can import the library with the source method, which takes a file argument that will accept a path or URL to a JavaScript file to source. Below we use the handy CDN (Content Delivery Network) to avoid downloading a file.

You can think of it as using the script tag in HTML to source (src) said file from disk or CDN.

Now onto replicating the array (list) which we want to search through, the books object used in a previous example. As already observed, this is in essence, how V8 stores data frames in the environment. Below we define a data frame of books that looks similar and load it into the engine.

Then again, we can make sure that the data frame was turned into a row-wise JSON object.

Now we can define options for the search; we don’t get into the details of fuse.js here as this is not the purpose of this book. You can read more about the options in the examples section of the site. We can mimic the format of the JSON options shown on the website with a simple list and assign that to a new variable in the engine. Note that we wrap the title in a list to ensure it is converted to an array of length 1: list("title") should be converted to a ["title"] array and not a "title" scalar.

Then we can finish the second step of the online examples, instantiate a fuse.js object with the books and options objects, then do a search, the result of which is assigned to an object which is retrieved in R with get.

A search for “sense” returns a vector of ids where the term “sense” was found; c and d or the books Common Sense, Sense and Sensibility. We could perhaps make that last code simpler using the call method.

18.4 NPM Packages

We can also use npm packages, though not all will work. NPM is Node’s package manager, or in a sense Node’s equivalent of CRAN.

To use NPM packages we need browserify, a node library to bundle all dependencies of an NPM package into a single file, which can subsequently be imported in V8. Browserify is itself an NPM package, and therefore requires Node.js to be installed. The reason browserify is required will be covered in more depth in chapter 20, in essence, NPM assumes disk access to load dependencies in require() (JavaScript) statements. This will not work with V8. Browserify will bundle all the files that comprise an NPM module into a single file that does not require disk access.

You can install browserify globally with the following the g flag. Once Node.js installed, browserify can be installed from the terminal (not R console) with the npm command.

We can now “browserify” an npm package. To demonstrate, we will use ms, which converts various time formats to milliseconds. First, we install the npm package.

Then we browserify it. From the terminal, the first line creates a file called in.js which contains global.ms = require('ms'); we then call browserify on that file specifying ms.js as output file. The require function in JavaScript is used to import files, require('ms') imports ms.js, it’s to some extend like source("ms.R").

We can now source ms.js with V8. Before we do so we ought to look at example code to see what has to be reproduced using V8. Luckily the library is very straightforward: it includes a single function for all conversions, e.g.: ms('2 days') to convert two days in milliseconds.

Then using the library simply consists of using eval or preferably call (for cleaner code and data interpretation to R).

18.5 Use in Packages

In this section, we detail how one should go about using V8 in an R package. If you are not familiar with package development you can skip ahead. We start by creating a package called “ms” that will hold functionalities we explored in the previous section on NPM packages.

The package is going to rely on V8 so it needs to be added under Imports in the DESCRIPTION file, then again this can be done with usethis as shown below.

The package should also include the external library ms.js browserified from the NPM package, which should be placed it in the inst directory. Create it and place the ms.js file within the latter.

As explored, the core of the V8 package is the execution environment(s) that are spawned using the v8 function. One could perhaps provide a function that returns the object created by v8, but it would not be convenient: this function would need to be called explicitly by the users of the package, and the output of it would need to be passed to every subsequent function. Thankfully there is a better way.

Instead, we can use the function .onLoad, to create the execution environment and import the dependency when the package is loaded by the user.

You can read more about this function in Hadley Wickham’s Advanced R book. This is, in effect, very similar to how the Python integration of R, reticulate (Ushey, Allaire, and Tang 2020), is used in packages. This function is often placed in a zzz.R file.

At this stage the package’s directory structure should look similar to the tree below.

Now the dependency can be sourced in the .onLoad function. We can locate the files in the inst directory with the system.file function.

We can then create a to_ms function. It will have access to the ms object we instantiated in .onLoad.

After running devtools::document() and installing the package with devtools::install(), it’s ready to be used.

References

Ushey, Kevin, JJ Allaire, and Yuan Tang. 2020. Reticulate: Interface to ’Python’. https://CRAN.R-project.org/package=reticulate.