Managing R Objects
References to R objects
When webR provides a reference to an R object on the main thread, such as the result of R code evaluation with WebR.evalR()
, it is provided in the form of an RObject
proxy.
Technically, an RObject
is a JavaScript object that holds a reference to an R object in WebAssembly memory and forwards requests relating to that object to the webR worker thread. The forwarding between the main and worker threads is implemented with the JavaScript Proxy
mechanism, which is why RObject
instances print as objects of type Proxy
in the console:
Proxy {obj: { type: 'double', ptr: 2184504, ... }, payloadType: 'ptr'}
RObject
proxys can be passed to webR API calls that manipulate or access R objects. For instance, an RObject
representing an environment can be passed to WebR.evalR()
to evaluate code in a particular environment.
A proxy is parameterised by an R type, e.g. a proxy that wraps a character vector is implemented as RProxy<RWorker.RCharacter>
. The RMain
module provides convenient type aliases for each R type. For instance, RCharacter
stands for RProxy<RWorker.RCharacter>
. Each of these types supports specific methods related to the kind of data they represent. For instance RCharacter
supports conversion to a JS string with toString()
whereas RDouble
supports toNumber()
.
Under the hood
Each type of RObject
proxy is associated with a corresponding subclass of RWorker.RObject
with the same name, e.g. RMain.RCharacter
has a corresponding RWorker.RCharacter
type that is it proxied to. The RWorker
module implements the methods for interacting with R objects on the webR worker thread, and the methods are made available to the main thread through the RObject
proxies.
Invoking a method on an RObject
proxy is automatically handled by webR by issuing a request to the worker thread over the established communication channel. RProxy
method invocation returns a JavaScript Promise that resolves to the result of invoking the corresponding RWorker.RObject
method for the R object associated with the proxy.
Memory management
Both the R interpreter and the JS environment include a built-in garbage collector (GC) that deletes objects from memory when they are no longer required. Unfortunately, there is no way to integrate these garbage collectors in an automated way 1. This is why a Javascript wrapper can’t keep an R object alive on its own and it needs some manual work from the developer who must ensure that any R objects targeted by an RObject
reference are not deleted by the R GC while they are still in use.
It is the user’s responsibility to signal to webR when they have finished working with an RObject
reference.
Sheltering R object references
WebR solves the problem of keeping R objects alive through a sheltering mechanism. When an RObject
reference is created on the main thread, the object referenced is automatically sheltered from R’s GC. The object will not be deleted by R until it has been removed from the shelter.
Once an R object is no longer protected by a shelter, there are no longer any guarantees about the state of the referenced object – it may have already been deleted by the GC. In effect the RObject
reference is no longer valid, and so the process of removing an object from the shelter is referred to as destroying the reference.
An RObject
reference must not be used after it has been destroyed. Using a destroyed RObject
results in undefined behaviour in the worker thread running R, most likely a crash.
The global shelter
As webR initialises it creates a default shelter for R objects. When R object references are created without specifying a particular shelter, when running WebR.evalR()
for example, they are sheltered by the global shelter.
Unlike normal shelters, the global shelter can’t be purged all at once. A purge would not generally be safe because this shelter is likely shared among different concurrent routines. Instead, objects sheltered globally must be individually destroyed by passing them to the WebR.destroy()
method.
Using try
/finally
with the global shelter
To avoid memory leaks, all RObject
instances should be destroyed once they are no longer in use. This requires precautions to ensure destruction when a JavaScript error is thrown and interrupts execution. A useful pattern is to wrap code manipulating an RObject
within a JavaScript try
/finally
block to ensure that the R object reference is always destroyed once the work is complete.
const obj = await webR.evalR('42 + 123');
try {
const result = await obj.toNumber();
console.log(result);
finally {
} .destroy(obj);
webR }
import type { RDouble } from 'webr';
const obj = await webR.evalR('42 + 123') as RDouble;
try {
const result = await obj.toNumber();
console.log(result);
finally {
} .destroy(obj);
webR }
However this pattern quickly shows its limits when multiple objects are transferred from R because each object requires its own try
/ finally
blocks.
const obj1 = await webR.evalR('42 + 123');
try {
let result = await obj1.toNumber();
const obj2 = await webR.evalR('42 * 123');
try {
= result + await obj2.toNumber();
result finally {
} .destroy(obj2);
webR
}
console.log(result);
finally {
} .destroy(obj1);
webR }
Using try
/finally
with a local shelter
To avoid increasingly nested blocks, it is often preferable to work with a shelter instead. A new shelter can be created using the Shelter
class. An asynchronous constructor for this class is available in the WebR.Shelter
property for convenient instantiation, e.g. const myShelter = await new myWebR.Shelter()
.
The objects referenced in a shelter can be destroyed all at once using the
purge()
method. They can also be destroyed individually with thedestroy()
method, just like you would do with the global shelter.The
Shelter.evalR()
andShelter.captureR()
methods automatically protect resultingRObject
references within the shelter instance instead of the default shelter.
The nested try
/ finally
blocks example can be refactored as:
const myShelter = await new webR.Shelter();
try {
const obj1 = await myShelter.evalR('42 + 123');
const obj2 = await myShelter.evalR('42 * 123');
const result = await obj1.toNumber() + await obj2.toNumber();
console.log(result);
finally {
} .purge();
myShelter }
import type { RDouble } from 'webr';
const myShelter = await new webR.Shelter();
try {
const obj1 = await myShelter.evalR('42 + 123') as RDouble;
const obj2 = await myShelter.evalR('42 * 123') as RDouble;
const result = await obj1.toNumber() + await obj2.toNumber();
console.log(result);
finally {
} .purge();
myShelter }
A list of available methods can be found in the methods section of the Shelter
class reference. This includes similar methods for interacting with R or evaluating code to those provided on the WebR
class.
Create new objects protected by a shelter
Shelters also provide a range of R object class proxies which can be used to create a new R object by providing a JavaScript object. This works in the same way as when creating R objects protected by the global shelter, except that the resulting R objects will automatically be protected using this shelter, rather than the default shelter.
const obj = await new myShelter.RDouble([1, 1.5, 2, 2.5, 3]);
Typing an RObject
When working with TypeScript, it becomes important to pay attention to the typing for a given R object reference since the methods available depends on the kind of data referenced. Unfortunately, functions such as WebR.evalR()
may run arbitrary R code and so it is not clear what type of R object will be returned. As such, they are typed to return R object references as a generic RObject
instance.
If you are sure of the type of data referenced by the returned R object, it is possible to use the TypeScript as
keyword to assert the type that you are expecting. We make use of this method throughout the TypeScript examples in this documentation, so that the relevant methods can be invoked without error.
import type { RDouble } from 'webr';
let result = await webR.evalR('rnorm(10,5,1)') as RDouble;
let output = await result.toTypedArray();
Converting types using RObject
constructors
An alternative to the TypeScript as
keyword is to construct a new RObject
with a specific type, passing an existing RObject
reference as the constructor argument. The returned object will be a reference to the same point in WebAssembly memory as the source object, but narrowed to the specific type. After conversion, methods can be invoked without error.
const foo = await webR.evalR('123');
console.log(await foo.toJs());
// The next line causes a "missing property" error in TypeScript
// await foo.toNumber();
// Construct an `RDouble` referencing the same R object
const bar = await new webR.RDouble(foo);
console.log(await bar.toNumber());
{ type: 'double', names: null, values: [123] }
123
If the R object is unable be converted into the selected type, a WebRError
will be thrown,
const baz = await new webR.RLogical(foo);
console.log(await baz.toBoolean());
Uncaught Error: Unexpected object type "double" when expecting type "logical"
Type predicate functions
For the case where type assertion is not appropriate, such as when the data referenced by an R object is unknown, webR provides a selection of type predicate functions. The functions return boolean true
if the argument’s R object type matches, and so different actions can be taken at runtime depending on the type. In addition, the TypeScript compiler is able to use the result of the functions to narrow the RObject
type.
A useful pattern is to check the type of a returned R object and throwing an Error
early if it is not as expected. After the check, Typescript will narrow the R object type automatically while it remains in scope.
import { isREnvironment } from 'webr';
let result = await webR.evalR('as.environment(list(foo=1, bar=2, baz=3))');
if (!isREnvironment(result)) {
throw new Error("Unexpected type of result from evalR evaluation.");
}
let output = await result.ls();
console.log('The result is an environment containing: ', output);
Notice that TypeScript is able to deduce that the ls()
method is indeed valid for use with the result
object, despite the method not existing on the generic RObject
class. Typescript has successfully narrowed the object type to an REnvironment
.
Typing of JavaScript conversions
Explicitly converting an RObject
to JavaScript can be done by invoking the toJs()
method, which returns a JavaScript representation of the referenced R object data. In general, the JavaScript object returned by toJs()
is of type WebRDataJs
, a type union covering all possible R object data.
Subclasses of RObject
will return different types from the WebRDataJs
type union. For example, an RNull
object will return a JavaScript representation with type WebRDataJsNull
.
Further information about converting R objects to JavaScript can be found in the next section.
Footnotes
While the JavaScript specification does provide a finaliser mechanism to signal when objects are no longer in use, it cannot be relied upon. Even a fully conforming JavaScript implementation is not required to call the cleanup callbacks.↩︎