Skip to content

Commit

Permalink
transport JavaScript standard builtin objects (#151)
Browse files Browse the repository at this point in the history
* transport-fallback one pager

* refine the design doc

* fix typos

* prototype of v8-transport-helper

* builtin types transporter

* resolve some comments

* Update transport-js-builtins.md

* Update transport-js-builtins.md

* resolve comments

* add NAPA_API for  transportutils methods

* in order to retrieve v8::Value(De)Serilaizer::Delegate symbols correctly, make SerializeValue / DesrializeValue non-static

* Disable rtti for compatibility with node and v8

* make v8-extensions a static lib to use -fno-rtti

* restrict v8-extensions lib building by v8 version

* add -fPIC to build v8-extensions for linux

* include stdlib.h explicitly for malloc/free/realloc

* .cc to .cpp under src/v8-extensions
  • Loading branch information
helloshuangzi committed Dec 20, 2017
1 parent 38410ec commit 1bf7fed
Show file tree
Hide file tree
Showing 29 changed files with 1,140 additions and 34 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ test/module/test-dir
test/module/test-file
!test/module/node_modules
!test/module/**/*.js
!test/transport-builtin-test.js

!unittest/module/test-files/node_modules

Expand Down
135 changes: 135 additions & 0 deletions docs/design/transport-js-builtins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Transport JavaScript standard built-in objects

## Incentives
The abstraction of 'Transportable' lies in the center of napa.js to efficiently share objects between JavaScript VMs (napa workers). Except JavaScript primitive types, an object needs to implement 'Transportable' interface to make it transportable. It means [Javascript standard built-in objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects) are not transportable unless wrappers or equivalent implementations for them are implemented by extending 'Transportable' interface. Developing cost for all those objects is not trivial, and new abstraction layer (wrappers or equivalent implementations) will bring barriers for users to learn and adopt these new stuffs. Moreover, developers also need to deal with the interaction between JavaScript standards objects and those wrappers or equivalent implementations.

The incentive of this design is to provide a solution to make JavaScript standard built-in objects transportable with requirements listed in the Goals section.

At the first stage, we will focus on an efficient solution to share data between napa workers. Basically, it is about making SharedArrayBuffer / TypedArray / DataView transportable.

## Goals
Make Javascript standard built-in objects transportable with
- an efficient way to share structured data, like SharedArrayBuffer, among napa workers
- consistent APIs with ECMA standards
- no new abstraction layers for the simplest usage
- the least new concepts for advanced usage
- a scalable solution to make all Javascript standard built-in objects transportable, avoiding to make them transportable one by one.

## Example
The below example shows how SharedArrayBuffer object is transported across multiple napa workers. It will print the TypedArray 'ta' created from a SharedArrayBuffer, with all its elements set to 100 from different napa workers.
```js
var napa = require("napajs");
var zone = napa.zone.create('zone', { workers: 4 });

function foo(sab, i) {
var ta = new Uint8Array(sab);
ta[i] = 100;
return i;
}

function run() {
var promises = [];
var sab = new SharedArrayBuffer(4);
for (var i = 0; i < 4; i++) {
promises[i] = zone.execute(foo, [sab, i]);
}

return Promise.all(promises).then(values => {
var ta = new Uint8Array(sab);
console.log(ta);
});
}

run();

```

## Solution
Here we just give a high level description of the solution. Its api will go to docs/api/transport.
- V8 provides its value-serialization mechanism by ValueSerializer and ValueDeserializer, which is compatible with the HTML structured clone algorithm. It is a horizontal solution to serialize / deserialize JavaScript objects. ValueSerializer::Delegate and ValueDeserializer::Delegate are their inner class. They work as base classes from which developers can deprive to customize some special handling of external / shared resources, like memory used by a SharedArrayBuffer object.

- napa::v8_extensions::ExternalizedContents
> 1. It holds the externalized contents (memory) of a SharedArrayBuffer instance once it is serialized via napa::v8_extensions::Utils::SerializeValue().
> 2. Only 1 instance of ExternalizedContents wil be generated for each SharedArrayBuffer. If a SharedArrayBuffer had been externalized, it will reuse the ExternalizedContents instance created before in napa::v8_extensions::Utils::SerializeValue()
- napa::v8_extensions::SerializedData
> 1. It is generated by napa::v8_extensions::Utils::SerializeValue(). It holds the serialized data of a JavaScript object, which is required during its deserialization.
- BuiltInObjectTransporter
> 1. napa::v8_extensions::Serializer, derived from v8::ValueSerializer::Delegate
> 2. napa::v8_extensions::Deserializer, derived from v8::ValueDeserializer::Delegate
> 3. static std::shared_ptr<SerializedData> v8_extensions::Utils::SerializeValue(Isolate* isolate, Local<Value> value);
>>> Generate the SerializedData instance given an input value.
>>> If any SharedArrayBuffer instances exist in the input value, their ExternalizedContents instances will be generated and attached to the ShareArrayBuffer instances respectively.
> 4. static MaybeLocal<Value> v8_extensions::Utils::DeserializeValue(Isolate* isolate, std::shared_ptr<SerializedData> data);
>>> Restore a JavaScript value from its SerializedData instance generated by v8_extensions::Utils::SerializeValue() before.
- Currently, napa relies on Transportable API and a registered constructor to make an object transportable. In [marshallTransform](https://github.com/Microsoft/napajs/blob/master/lib/transport/transport.ts), when a JavaScript object is detected to have a registered constructor, it will go with napa way to marshall this object with the help of a TransportContext object, otherwise a non-transportable error is thrown.

- Instead of throwing an Error when no registered constructor is detected, the above mentioned BuiltInObjectTransporter can jump in to help marshall this object. We can use a whitelist of object types to restrict this solution to those verified types at first.
```js
export function marshallTransform(jsValue: any, context: transportable.TransportContext): any {
if (jsValue != null && typeof jsValue === 'object' && !Array.isArray(jsValue)) {
let constructorName = Object.getPrototypeOf(jsValue).constructor.name;
if (constructorName !== 'Object') {
if (typeof jsValue['cid'] === 'function') {
return <transportable.Transportable>(jsValue).marshall(context);
} else if (_builtInTypeWhitelist.has(constructorName)) {
let serializedData = builtinObjectTransporter.serializeValue(jsValue);
if (serializedData) {
return { _serialized : serializedData };
} else {
throw new Error(`Failed to serialize object with type of \"${constructorName}\".`);
}
} else {
throw new Error(`Object type \"${constructorName}\" is not transportable.`);
}
}
}
return jsValue;
}
```
- The reverse process will be invoked in [unmarshallTransform](https://github.com/Microsoft/napajs/edit/master/lib/transport/transport.ts) if the payload is detected to have '_serialized' property.
```js
function unmarshallTransform(payload: any, context: transportable.TransportContext): any {
if (payload != null && payload._cid !== undefined) {
let cid = payload._cid;
if (cid === 'function') {
return functionTransporter.load(payload.hash);
}
let subClass = _registry.get(cid);
if (subClass == null) {
throw new Error(`Unrecognized Constructor ID (cid) "${cid}". Please ensure @cid is applied on the class or transport.register is called on the class.`);
}
let object = new subClass();
object.unmarshall(payload, context);
return object;
} else if (payload.hasOwnProperty('_serialized')) {
return builtinObjectTransporter.deserializeValue(payload['_serialized']);
}
return payload;
}
```

- Life cycle of SharedArrayBuffer (SAB)
> 1. When a SAB participates transportation among napa workers, its life cycle will be extended till the last reference this SAB. The reference of a SAB could be
>>> 1) a SAB object in its original isolate.
>>> 2) a received SAB transported from another napa workers, including node zone of napa.
>>> 3) a TypedArray or DataView created from the original SAB or a received SAB.
> 2. The life cycle extension during transportation is achieved through the ExternalizedContents SharedPtrWrap of the SAB.
>>> 1) When a SAB is transported for the first time, it will be externalized and its ExternalizedContents will be stored in its SerializedData. At the same time, the SharedPtrWrap of the ExternalizedContents will be set to the '_externalized' property of the original SAB.
>>> 2) When a SAB is transported for the second time or later, it wil skip externalization and find its ExternalizedContents from its '_externalized' property, and store it to its SerializedData.
>>> 3) When a napa worker try to restored a transported SAB, it will find the pre-stored ExternalizedContents, and create a SharedPtrWrap for it, then set it to the to-be-restored SAB.
>>> 4) The life cycle of the SharedArrayBuffer is extended by the SharedPtrWrap of its ExternalizedContents.

## Constraints
The above solution is based on the serialization / deserialization mechanism of V8. It may have the following constraints.
- Not all [JavaScripts standard built-in objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects) are supported by Node (as a dependency of Napa in node mode) or V8 of a given version. We only provide transporting solution for those mature object types.
- Up to present, Node does not explicitly support multiple V8 isolates. There might be inconsistency to transport objects between node zone and napa zones. Extra effort might be required to make it consistent.
27 changes: 27 additions & 0 deletions inc/napa/module/binding/basic-wraps.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

#pragma once

#include <napa/module/binding.h>
#include <napa/module/shareable-wrap.h>

#include <v8.h>

namespace napa {
namespace module {
namespace binding {
/// <summary> It creates a new instance of wrapType with a shared_ptr<T>. </summary>
/// <param name="object"> shared_ptr of object. </summary>
/// <param name="wrapType"> wrap type from napa-binding, which extends napa::module::Sharable. </param>
/// <returns> V8 object of wrapType. </summary>

template <typename T>
inline v8::Local<v8::Object> CreateShareableWrap(std::shared_ptr<T> object, const char* wrapType = "SharedPtrWrap") {
auto instance = NewInstance(wrapType, 0, nullptr).ToLocalChecked();
ShareableWrap::Set(instance, std::move(object));
return instance;
}
}
}
}
13 changes: 1 addition & 12 deletions inc/napa/module/binding/wraps.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

#pragma once

#include <napa/module/binding.h>
#include <napa/module/shareable-wrap.h>
#include <napa/module/binding/basic-wraps.h>
#include <napa/memory/allocator.h>
#include <napa/memory/allocator-debugger.h>

Expand All @@ -13,16 +12,6 @@
namespace napa {
namespace module {
namespace binding {
/// <summary> It creates a new instance of wrapType with a shared_ptr<T>. </summary>
/// <param name="object"> shared_ptr of object. </summary>
/// <param name="wrapType"> wrap type from napa-binding, which extends napa::module::Sharable. </param>
/// <returns> V8 object of wrapType. </summary>
template <typename T>
inline v8::Local<v8::Object> CreateShareableWrap(std::shared_ptr<T> object, const char* wrapType = "SharedPtrWrap") {
auto instance = NewInstance(wrapType, 0, nullptr).ToLocalChecked();
ShareableWrap::Set(instance, std::move(object));
return instance;
}

/// <summary> It creates a new instance of AllocatorWrap. </summary>
/// <param name="allocator"> shared_ptr of allocator. </summary>
Expand Down
18 changes: 18 additions & 0 deletions lib/transport/builtin-object-transporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import { Shareable } from '../memory/shareable';

/// <summary>
/// ShareableWrap of SerializedData.
/// </summary>
export interface SerializedData extends Shareable {
}

export function serializeValue(jsValue: any): SerializedData {
return require('../binding').serializeValue(jsValue);
}

export function deserializeValue(serializedData: SerializedData): any {
return require('../binding').deserializeValue(serializedData);
}
26 changes: 25 additions & 1 deletion lib/transport/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,26 @@

import * as transportable from './transportable';
import * as functionTransporter from './function-transporter';
import * as builtinObjectTransporter from './builtin-object-transporter';
import * as path from 'path';

/// <summary> Per-isolate cid => constructor registry. </summary>
let _registry: Map<string, new(...args: any[]) => transportable.Transportable>
= new Map<string, new(...args: any[]) => transportable.Transportable>();
= new Map<string, new(...args: any[]) => transportable.Transportable>();

let _builtInTypeWhitelist = new Set();
[
'ArrayBuffer',
'Float32Array',
'Float64Array',
'Int16Array',
'Int32Array',
'Int8Array',
'SharedArrayBuffer',
'Uint16Array',
'Uint32Array',
'Uint8Array'
].forEach((type) => { _builtInTypeWhitelist.add(type); });

/// <summary> Register a TransportableObject sub-class with a Constructor ID (cid). </summary>
export function register(subClass: new(...args: any[]) => any) {
Expand All @@ -33,6 +48,13 @@ export function marshallTransform(jsValue: any, context: transportable.Transport
if (constructorName !== 'Object') {
if (typeof jsValue['cid'] === 'function') {
return <transportable.Transportable>(jsValue).marshall(context);
} else if (_builtInTypeWhitelist.has(constructorName)) {
let serializedData = builtinObjectTransporter.serializeValue(jsValue);
if (serializedData) {
return { _serialized : serializedData };
} else {
throw new Error(`Failed to serialize object with type of \"${constructorName}\".`);
}
} else {
throw new Error(`Object type \"${constructorName}\" is not transportable.`);
}
Expand All @@ -58,6 +80,8 @@ function unmarshallTransform(payload: any, context: transportable.TransportConte
let object = new subClass();
object.unmarshall(payload, context);
return object;
} else if (payload.hasOwnProperty('_serialized')) {
return builtinObjectTransporter.deserializeValue(payload['_serialized']);
}
return payload;
}
Expand Down
25 changes: 23 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
set(CMAKE_CXX_FLAGS_ORG ${CMAKE_CXX_FLAGS})

# Use -fno-rtti and -fPIC to build v8-extensions as a static library for compatibility with node and v8.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fPIC")
add_subdirectory(v8-extensions)

# Restore CMAKE_CXX_FLAGS from CMAKE_CXX_FLAGS_ORG.
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS_ORG})

# Files to compile
file(GLOB_RECURSE SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
file(GLOB SOURCE_FILES_0 ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
file(GLOB_RECURSE SOURCE_FILES_1
${CMAKE_CURRENT_SOURCE_DIR}/api/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/memory/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/module/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platform/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/providers/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/settings/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/store/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/utils/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/zone/*.cpp
)
set(SOURCE_FILES ${SOURCE_FILES_0} ${SOURCE_FILES_1})

# The target name
set(TARGET_NAME ${PROJECT_NAME})
Expand All @@ -19,7 +40,7 @@ target_include_directories(${TARGET_NAME}
target_compile_definitions(${TARGET_NAME} PRIVATE NAPA_EXPORTS NAPA_BINDING_EXPORTS BUILDING_NAPA_EXTENSION)

# Link libraries
target_link_libraries(${TARGET_NAME} PRIVATE)
target_link_libraries(${TARGET_NAME} PRIVATE "v8-extensions")

if(CMAKE_JS_VERSION)
# Building Napa as an npm package for node usage (using exported v8 from node.exe)
Expand Down
2 changes: 1 addition & 1 deletion src/api/capi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#include <providers/providers.h>
#include <settings/settings-parser.h>
#include <utils/debug.h>
#include <v8/v8-common.h>
#include <v8-extensions/v8-common.h>
#include <zone/napa-zone.h>
#include <zone/node-zone.h>
#include <zone/worker-context.h>
Expand Down

0 comments on commit 1bf7fed

Please sign in to comment.