Serialize and restore typed arrays too

This also makes it so that if you pass the same ArrayBuffer twice in an object, we use a single object on the receiver side too
This commit is contained in:
Matt Bierner 2021-02-05 18:45:48 -08:00
parent 671ccf261c
commit ba8fa699dd
5 changed files with 211 additions and 34 deletions

View file

@ -450,6 +450,67 @@ suite.skip('vscode API - webview', () => {
}
}
});
test('webviews should transfer Typed arrays to and from webviews', async () => {
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true }));
const ready = getMessage(webview);
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
<script>
const vscode = acquireVsCodeApi();
window.addEventListener('message', (message) => {
switch (message.data.type) {
case 'add1':
const uint8Array = message.data.array1;
// This should update both buffers since they use the same ArrayBuffer storage
const uint16Array = message.data.array2;
for (let i = 0; i < uint16Array.length; ++i) {
uint16Array[i] = uint16Array[i] + 1;
}
vscode.postMessage({ array1: uint8Array, array2: uint16Array, }, [uint16Array.buffer]);
break;
}
});
vscode.postMessage({ type: 'ready' });
</script>`);
await ready;
const responsePromise = getMessage(webview);
const bufferLen = 100;
{
const arrayBuffer = new ArrayBuffer(bufferLen);
const uint8Array = new Uint8Array(arrayBuffer);
const uint16Array = new Uint16Array(arrayBuffer);
for (let i = 0; i < uint16Array.length; ++i) {
uint16Array[i] = i;
}
webview.webview.postMessage({
type: 'add1',
array1: uint8Array,
array2: uint16Array,
});
}
{
const response = await responsePromise;
assert.ok(response.array1 instanceof Uint8Array);
assert.ok(response.array2 instanceof Uint16Array);
assert.ok(response.array1.buffer === response.array2.buffer);
const uint8Array = response.array1;
for (let i = 0; i < bufferLen; ++i) {
if (i % 2 === 0) {
assert.strictEqual(uint8Array[i], Math.floor(i / 2) + 1);
} else {
assert.strictEqual(uint8Array[i], 0);
}
}
}
});
});
function createHtmlDocumentWithBody(body: string): string {

View file

@ -15,6 +15,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IProductService } from 'vs/platform/product/common/productService';
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
import { serializeMessage } from 'vs/workbench/api/common/extHostWebview';
import { deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging';
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape {
@ -62,23 +63,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
public async $postMessage(handle: extHostProtocol.WebviewHandle, jsonMessage: string, ...buffers: VSBuffer[]): Promise<boolean> {
const webview = this.getWebview(handle);
const arrayBuffers: ArrayBuffer[] = buffers.map(buffer => {
const arrayBuffer = new ArrayBuffer(buffer.byteLength);
const uint8Array = new Uint8Array(arrayBuffer);
uint8Array.set(buffer.buffer);
return arrayBuffer;
});
const reviver = !buffers.length ? undefined : (_key: string, value: any) => {
if (typeof value === 'object' && (value as extHostProtocol.WebviewMessageArrayBufferReference).$$vscode_array_buffer_reference$$) {
const { index } = value;
return arrayBuffers[index];
}
return value;
};
const message = JSON.parse(jsonMessage, reviver);
const { message, arrayBuffers } = deserializeWebviewMessage(jsonMessage, buffers);
webview.postMessage(message, arrayBuffers);
return true;
}

View file

@ -668,10 +668,33 @@ export interface CustomTextEditorCapabilities {
readonly supportsMove?: boolean;
}
export const enum WebviewMessageArrayBufferViewType {
Int8Array = 1,
Uint8Array = 2,
Uint8ClampedArray = 3,
Int16Array = 4,
Uint16Array = 5,
Int32Array = 6,
Uint32Array = 7,
Float32Array = 8,
Float64Array = 9,
BigInt64Array = 10,
BigUint64Array = 11,
}
export interface WebviewMessageArrayBufferReference {
readonly $$vscode_array_buffer_reference$$: true,
readonly index: number;
/**
* Tracks if the reference is to a view instead of directly to an ArrayBuffer.
*/
readonly view?: {
readonly type: WebviewMessageArrayBufferViewType;
readonly byteLength: number;
readonly byteOffset: number;
};
}
export interface MainThreadWebviewsShape extends IDisposable {

View file

@ -10,6 +10,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'
import { normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
import { deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview';
import type * as vscode from 'vscode';
@ -186,23 +187,8 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
): void {
const webview = this.getWebview(handle);
if (webview) {
const arrayBuffers: ArrayBuffer[] = buffers.map(buffer => {
const newBuffer = new ArrayBuffer(buffer.byteLength);
const uint8buffer = new Uint8Array(newBuffer);
uint8buffer.set(buffer.buffer);
return newBuffer;
});
const reviver = !buffers.length ? undefined : (_key: string, value: any) => {
if (typeof value === 'object' && (value as extHostProtocol.WebviewMessageArrayBufferReference).$$vscode_array_buffer_reference$$) {
const { index } = (value as extHostProtocol.WebviewMessageArrayBufferReference);
return arrayBuffers[index];
}
return value;
};
const revivedMessage = JSON.parse(jsonMessage, reviver);
webview._onMessageEmitter.fire(revivedMessage);
const { message } = deserializeWebviewMessage(jsonMessage, buffers);
webview._onMessageEmitter.fire(message);
}
}

View file

@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBuffer } from 'vs/base/common/buffer';
import * as extHostProtocol from './extHost.protocol';
class ArrayBufferSet {
public readonly buffers: ArrayBuffer[] = [];
public add(buffer: ArrayBuffer): number {
let index = this.buffers.indexOf(buffer);
if (index < 0) {
index = this.buffers.length;
this.buffers.push(buffer);
}
return index;
}
}
export function serializeWebviewMessage(
message: any,
transfer?: readonly ArrayBuffer[]
): { message: string, buffers: VSBuffer[] } {
if (transfer) {
// Extract all ArrayBuffers from the message and replace them with references.
const arrayBuffers = new ArrayBufferSet();
const replacer = (_key: string, value: any) => {
if (value instanceof ArrayBuffer) {
const index = arrayBuffers.add(value);
return <extHostProtocol.WebviewMessageArrayBufferReference>{
$$vscode_array_buffer_reference$$: true,
index,
};
} else if (ArrayBuffer.isView(value)) {
const type = getTypedArrayType(value);
if (type) {
const index = arrayBuffers.add(value.buffer);
return <extHostProtocol.WebviewMessageArrayBufferReference>{
$$vscode_array_buffer_reference$$: true,
index,
view: {
type: type,
byteLength: value.byteLength,
byteOffset: value.byteOffset,
}
};
}
}
return value;
};
const serializedMessage = JSON.stringify(message, replacer);
const buffers = arrayBuffers.buffers.map(arrayBuffer => {
const bytes = new Uint8Array(arrayBuffer);
return VSBuffer.wrap(bytes);
});
return { message: serializedMessage, buffers };
} else {
return { message: JSON.stringify(message), buffers: [] };
}
}
function getTypedArrayType(value: ArrayBufferView): extHostProtocol.WebviewMessageArrayBufferViewType | undefined {
switch (value.constructor.name) {
case 'Int8Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Int8Array;
case 'Uint8Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Uint8Array;
case 'Uint8ClampedArray': return extHostProtocol.WebviewMessageArrayBufferViewType.Uint8ClampedArray;
case 'Int16Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Int16Array;
case 'Uint16Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Uint16Array;
case 'Int32Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Int32Array;
case 'Uint32Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Uint32Array;
case 'Float32Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Float32Array;
case 'Float64Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Float64Array;
case 'BigInt64Array': return extHostProtocol.WebviewMessageArrayBufferViewType.BigInt64Array;
case 'BigUint64Array': return extHostProtocol.WebviewMessageArrayBufferViewType.BigUint64Array;
}
return undefined;
}
export function deserializeWebviewMessage(jsonMessage: string, buffers: VSBuffer[]): { message: any, arrayBuffers: ArrayBuffer[] } {
const arrayBuffers: ArrayBuffer[] = buffers.map(buffer => {
const arrayBuffer = new ArrayBuffer(buffer.byteLength);
const uint8Array = new Uint8Array(arrayBuffer);
uint8Array.set(buffer.buffer);
return arrayBuffer;
});
const reviver = !buffers.length ? undefined : (_key: string, value: any) => {
if (typeof value === 'object' && (value as extHostProtocol.WebviewMessageArrayBufferReference).$$vscode_array_buffer_reference$$) {
const ref = value as extHostProtocol.WebviewMessageArrayBufferReference;
const { index } = ref;
const arrayBuffer = arrayBuffers[index];
if (ref.view) {
switch (ref.view.type) {
case extHostProtocol.WebviewMessageArrayBufferViewType.Int8Array: return new Int8Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Int8Array.BYTES_PER_ELEMENT);
case extHostProtocol.WebviewMessageArrayBufferViewType.Uint8Array: return new Uint8Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Uint8Array.BYTES_PER_ELEMENT);
case extHostProtocol.WebviewMessageArrayBufferViewType.Uint8ClampedArray: return new Uint8ClampedArray(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Uint8ClampedArray.BYTES_PER_ELEMENT);
case extHostProtocol.WebviewMessageArrayBufferViewType.Int16Array: return new Int16Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Int16Array.BYTES_PER_ELEMENT);
case extHostProtocol.WebviewMessageArrayBufferViewType.Uint16Array: return new Uint16Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Uint16Array.BYTES_PER_ELEMENT);
case extHostProtocol.WebviewMessageArrayBufferViewType.Int32Array: return new Int32Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Int32Array.BYTES_PER_ELEMENT);
case extHostProtocol.WebviewMessageArrayBufferViewType.Uint32Array: return new Uint32Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Uint32Array.BYTES_PER_ELEMENT);
case extHostProtocol.WebviewMessageArrayBufferViewType.Float32Array: return new Float32Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Float32Array.BYTES_PER_ELEMENT);
case extHostProtocol.WebviewMessageArrayBufferViewType.Float64Array: return new Float64Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Float64Array.BYTES_PER_ELEMENT);
case extHostProtocol.WebviewMessageArrayBufferViewType.BigInt64Array: return new BigInt64Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / BigInt64Array.BYTES_PER_ELEMENT);
case extHostProtocol.WebviewMessageArrayBufferViewType.BigUint64Array: return new BigUint64Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / BigUint64Array.BYTES_PER_ELEMENT);
default: throw new Error('Unknown array buffer view type');
}
}
return arrayBuffer;
}
return value;
};
const message = JSON.parse(jsonMessage, reviver);
return { message, arrayBuffers };
}