Skip to content

Commit

Permalink
Convert TypedArray, ArrayBuffer and ArrayBufferView JS objects …
Browse files Browse the repository at this point in the history
…to R raw atomic vectors (#363)

* Convert ArrayBuffer and ArrayBufferView objects to RRaw

* Update docs for ArrayBuffer conversion

* Add tests for converting ArrayBuffer to RRaw

* Update NEWS.md
  • Loading branch information
georgestagg authored Feb 27, 2024
1 parent 4951221 commit 9de9ad9
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 19 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

* The `RCall` and `RFunction` classes now have a `capture(options, ...args)` method that captures output during evaluation.

* JavaScript objects of type `TypedArray`, `ArrayBuffer`, and `ArrayBufferView` (e.g. `Uint8Array`) may now be used with the `RRaw` R object constructor. The generic `RObject` constructor now converts objects of this type to R raw atomic vectors by default.

## Breaking changes

* The `captureR()` method now captures plots generated by the canvas graphics device by default. Captured plots are returned as an array of `ImageBitmap` objects in the property `images`. The previous behaviour may be restored either by manually starting a non-capturing `webr::canvas()` device during execution, or by including `captureGraphics: false` as part of the `options` argument. The default options for `evalR()` are set so that plotting is not captured, retaining the current behaviour.
Expand Down
21 changes: 11 additions & 10 deletions src/docs/convert-js-to-r.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,17 @@ The [`WebR` proxy classes](api/js/classes/WebR.WebR.md#properties) take a single

The resulting R object type is chosen based on the contents of the JavaScript argument provided. When there is ambiguity, the following conversion rules are used,

| Constructor Argument | R Type |
|---------------------------------|---------------------------------------|
| `null` | Logical `NA` |
| `boolean` | Logical atomic vector |
| `number` | Double atomic vector |
| `{ re: 1, im: 2 }` | Complex atomic vector |
| `string` | Character atomic vector |
| JavaScript array | An atomic vector with type based on the above rules and following the coercion rules of R's `c()` function |
| [Object of type `WebRDataJs`](convert-r-to-js.qmd#serialising-r-objects) | An R object type given by the `type` property in the provided object |
| Other JavaScript object | Reserved for future use |
| Constructor Argument | R Type |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
| `null` | Logical `NA` |
| `boolean` | Logical atomic vector |
| `number` | Double atomic vector |
| `{ re: 1, im: 2 }` | Complex atomic vector |
| `string` | Character atomic vector |
| `TypedArray`, `ArrayBuffer`, `ArrayBufferView` | Raw atomic vector |
| `Array` | An atomic vector of type following the coercion rules of R's `c()` function |
| [Object of type `WebRDataJs`](convert-r-to-js.qmd#serialising-r-objects) | Given by the `type` property in the provided object |
| Other JavaScript object | Reserved for future use |

In the case of R lists provided as an [`WebRDataJs`](api/js/modules/RObject.md#webrdatajs), the above rules are applied recursively to also construct the objects provided within the list.

Expand Down
46 changes: 40 additions & 6 deletions src/tests/webR/webr-main.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { WebR } from '../../webR/webr-main';
import { Message } from '../../webR/chan/message';
import {
RDouble,
RLogical,
RCall,
RCharacter,
RComplex,
RList,
RPairlist,
RDouble,
REnvironment,
RInteger,
RFunction,
RCall,
RInteger,
RList,
RLogical,
RPairlist,
RRaw,
} from '../../webR/robj-main';

const webR = new WebR({
Expand Down Expand Up @@ -375,6 +376,27 @@ describe('Create R vectors from JS arrays using RObject constructor', () => {
})
);
});

test('Create a raw atomic vector', async () => {
const jsObj = new Uint8Array([0, 2, 4, 8, 16, 30, 52, 84, 128, 186]);
let rObj = (await new webR.RObject(jsObj)) as RRaw;
expect(await rObj.type()).toEqual('raw');
expect(await rObj.toJs()).toEqual(
expect.objectContaining({
values: Array.from(jsObj),
names: null,
})
);

rObj = (await new webR.RObject(jsObj.buffer)) as RRaw;
expect(await rObj.type()).toEqual('raw');
expect(await rObj.toJs()).toEqual(
expect.objectContaining({
values: Array.from(jsObj),
names: null,
})
);
});
});

describe('Create R objects from JS objects using proxy constructors', () => {
Expand Down Expand Up @@ -456,6 +478,18 @@ describe('Create R objects from JS objects using proxy constructors', () => {
);
});

test('Create a raw atomic vector using an ArrayBuffer', async () => {
const jsObj = new Uint8Array([1, 2, 6, 140, 16456, 8390720]);
const rObj = await new webR.RRaw(jsObj.buffer);
expect(await rObj.type()).toEqual('raw');
expect(await rObj.toJs()).toEqual(
expect.objectContaining({
values: Array.from(jsObj),
names: null,
})
);
});

test('Create a list containing both a logical NA and R NULL', async () => {
const jsObj = [true, 2, null, webR.objs.null];
const rObj = await new webR.RList(jsObj);
Expand Down
8 changes: 8 additions & 0 deletions src/webR/robj-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ function newObjectFromData(obj: WebRData): RObject {
if (isComplex(obj)) {
return new RComplex(obj);
}

// JS Array-like objects
if (ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer) {
return new RRaw(obj);
}
if (Array.isArray(obj)) {
return newObjectFromArray(obj);
}
Expand Down Expand Up @@ -1076,6 +1081,9 @@ export class RCharacter extends RVectorAtomic<string> {

export class RRaw extends RVectorAtomic<number> {
constructor(val: WebRDataAtomic<number>) {
if (val instanceof ArrayBuffer) {
val = new Uint8Array(val);
}
super(val, 'raw', RRaw.#newSetter);
}

Expand Down
8 changes: 5 additions & 3 deletions src/webR/robj.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export type WebRData =
| WebRDataRaw
| WebRDataJs
| WebRData[]
| ArrayBuffer
| ArrayBufferView
| { [key: string]: WebRData };

/**
Expand All @@ -87,13 +89,13 @@ export type WebRData =
*/
export type WebRDataAtomic<T> =
| WebRDataScalar<T>
| (T | null)[]
| WebRDataJsAtomic<T>
| NamedObject<T | null>;
| NamedObject<T | null>
| (T extends number ? ArrayBuffer | ArrayBufferView | (number | null)[] : (T | null)[]);

/**
* `WebRDataJs` objects form a tree structure, used when serialising R objects
* into a JavaScript respresentation.
* into a JavaScript representation.
*
* Nested R objects are serialised using the {@link WebRDataJsNode} type,
* forming branches in the resulting tree structure, with leaves formed by the
Expand Down
3 changes: 3 additions & 0 deletions src/webR/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export function replaceInObject<T>(
if (obj === null || typeof obj !== 'object' || isImageBitmap(obj) ) {
return obj;
}
if (obj instanceof ArrayBuffer) {
return new Uint8Array(obj) as T;
}
if (test(obj)) {
return replacer(obj, ...replacerArgs) as T;
}
Expand Down

0 comments on commit 9de9ad9

Please sign in to comment.