diff --git a/README.md b/README.md index 64eda74..f6c6c81 100644 --- a/README.md +++ b/README.md @@ -225,8 +225,13 @@ can then be quickly copied into any isolate. large object. This only applies to ArrayBuffer and TypedArray instances. Primitive values can be copied exactly as they are. Date objects will be copied as as Dates. -ArrayBuffers, TypedArrays, and DataViews will be copied in an efficient format. All other objects -will be copied in seralized form using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). +ArrayBuffers, TypedArrays, and DataViews will be copied in an efficient format. SharedArrayBuffers +will simply copy a reference to the existing memory and when copied into another isolate the new +SharedArrayBuffer will point to the same underlying data. After passing a SharedArrayBuffer to +ExternalCopy for the first time isolated-vm will take over management of the underlying memory +block, so a "copied" SharedArrayBuffer can outlive the isolate that created the memory originally. + +All other objects will be copied in seralized form using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). ##### `ExternalCopy.totalExternalSize` *[number]* diff --git a/src/external_copy.cc b/src/external_copy.cc index d4cc0cf..36ac35e 100644 --- a/src/external_copy.cc +++ b/src/external_copy.cc @@ -35,11 +35,12 @@ unique_ptr ExternalCopy::Copy(const Local& value, bool tran if (transfer_out) { return ExternalCopyArrayBuffer::Transfer(array_buffer); } else { - ArrayBuffer::Contents contents(array_buffer->GetContents()); - return make_unique(contents.Data(), contents.ByteLength()); + return make_unique(array_buffer); } + } else if (value->IsSharedArrayBuffer()) { + return make_unique(value.As()); } else if (value->IsArrayBufferView()) { - Local view(Local::Cast(value)); + Local view = value.As(); using ViewType = ExternalCopyArrayBufferView::ViewType; ViewType type; if (view->IsUint8Array()) { @@ -65,14 +66,25 @@ unique_ptr ExternalCopy::Copy(const Local& value, bool tran } else { assert(false); } - if (transfer_out) { - Local array_buffer = view->Buffer(); + // `Buffer()` returns a Local but it may be a Local + Local tmp = view->Buffer(); + if (tmp->IsArrayBuffer()) { + Local array_buffer = tmp.As(); + if (transfer_out) { + if (view->ByteOffset() != 0 || view->ByteLength() != array_buffer->ByteLength()) { + throw js_generic_error("Cannot transfer sliced TypedArray (this.byteOffset != 0 || this.byteLength != this.buffer.byteLength)"); + } + return make_unique(ExternalCopyArrayBuffer::Transfer(array_buffer), type); + } else { + return make_unique(make_unique(array_buffer), type); + } + } else { + assert(tmp->IsSharedArrayBuffer()); + Local array_buffer = tmp.As(); if (view->ByteOffset() != 0 || view->ByteLength() != array_buffer->ByteLength()) { throw js_generic_error("Cannot transfer sliced TypedArray (this.byteOffset != 0 || this.byteLength != this.buffer.byteLength)"); } - return make_unique(ExternalCopyArrayBuffer::Transfer(array_buffer), type); - } else { - return make_unique(view, type); + return make_unique(make_unique(array_buffer), type); } } else if (value->IsObject()) { Isolate* isolate = Isolate::GetCurrent(); @@ -442,14 +454,12 @@ ExternalCopyArrayBuffer::ExternalCopyArrayBuffer(shared_ptr ptr, size_t le value(std::move(ptr)), length(length) {} -ExternalCopyArrayBuffer::ExternalCopyArrayBuffer(const Local& handle) : +ExternalCopyArrayBuffer::ExternalCopyArrayBuffer(const Local& handle) : ExternalCopyBytes(handle->ByteLength() + sizeof(ExternalCopyArrayBuffer)), value(malloc(handle->ByteLength()), std::free), length(handle->ByteLength()) { - if (handle->CopyContents(value.get(), length) != length) { - throw js_generic_error("Failed to copy array contents"); - } + std::memcpy(value.get(), handle->GetContents().Data(), length); } unique_ptr ExternalCopyArrayBuffer::Transfer(const Local& handle) { @@ -523,47 +533,103 @@ size_t ExternalCopyArrayBuffer::Length() const { } /** - * ExternalCopyArrayBufferView implementation + * ExternalCopySharedArrayBuffer implementation */ -ExternalCopyArrayBufferView::ExternalCopyArrayBufferView(const Local& handle, ViewType type) : - ExternalCopy(sizeof(ExternalCopyArrayBufferView)), - buffer(std::make_unique(handle)), - type(type) {} +ExternalCopySharedArrayBuffer::ExternalCopySharedArrayBuffer(const v8::Local& handle) : + ExternalCopyBytes(handle->ByteLength() + sizeof(ExternalCopySharedArrayBuffer)), + length(handle->ByteLength()) +{ + // Similar to ArrayBuffer::Transfer but different enough to make it not worth abstracting out.. + size_t length = handle->ByteLength(); + if (length == 0) { + throw js_generic_error("Array buffer is invalid"); + } + if (handle->IsExternal()) { + // Buffer lifespan is not handled by v8.. attempt to recover from isolated-vm + Holder* ptr = reinterpret_cast(handle->GetAlignedPointerFromInternalField(0)); + if (ptr == nullptr || ptr->magic != Holder::kMagic) { // dangerous pointer dereference + throw js_generic_error("Array buffer cannot be externalized"); + } + // No race conditions here because only one thread can access `Holder` + value = ptr->cc_ptr; + return; + } + // In this case the buffer is internal and should be externalized + SharedArrayBuffer::Contents contents = handle->Externalize(); + value = shared_ptr(contents.Data(), std::free); + new Holder(handle, value, length); + // Adjust allocator memory down, and `Holder` will adjust memory back up + auto allocator = dynamic_cast(IsolateEnvironment::GetCurrent()->GetAllocator()); + if (allocator != nullptr) { + allocator->AdjustAllocatedSize(-static_cast(length)); + } +} + +Local ExternalCopySharedArrayBuffer::CopyInto(bool /*transfer_in*/) { + auto ptr = GetSharedPointer(); + Local array_buffer = SharedArrayBuffer::New(Isolate::GetCurrent(), ptr.get(), length); + new Holder(array_buffer, std::move(ptr), length); + return array_buffer; +} + +uint32_t ExternalCopySharedArrayBuffer::WorstCaseHeapSize() const { + return length; +} + +shared_ptr ExternalCopySharedArrayBuffer::GetSharedPointer() const { + auto ptr = std::atomic_load(&value); + if (!ptr) { + throw js_generic_error("Array buffer is invalid"); + } + return ptr; +} -ExternalCopyArrayBufferView::ExternalCopyArrayBufferView(std::unique_ptr buffer, ViewType type) : +/** + * ExternalCopyArrayBufferView implementation + */ +ExternalCopyArrayBufferView::ExternalCopyArrayBufferView(std::unique_ptr buffer, ViewType type) : ExternalCopy(sizeof(ExternalCopyArrayBufferView)), buffer(std::move(buffer)), type(type) {} -Local ExternalCopyArrayBufferView::CopyInto(bool transfer_in) { - Local buffer = Local::Cast(this->buffer->CopyInto(transfer_in)); +template +Local NewTypedArrayView(Local buffer, ExternalCopyArrayBufferView::ViewType type) { size_t length = buffer->ByteLength(); switch (type) { - case ViewType::Uint8: + case ExternalCopyArrayBufferView::ViewType::Uint8: return Uint8Array::New(buffer, 0, length); - case ViewType::Uint8Clamped: + case ExternalCopyArrayBufferView::ViewType::Uint8Clamped: return Uint8ClampedArray::New(buffer, 0, length); - case ViewType::Int8: + case ExternalCopyArrayBufferView::ViewType::Int8: return Int8Array::New(buffer, 0, length); - case ViewType::Uint16: + case ExternalCopyArrayBufferView::ViewType::Uint16: return Uint16Array::New(buffer, 0, length); - case ViewType::Int16: + case ExternalCopyArrayBufferView::ViewType::Int16: return Int16Array::New(buffer, 0, length); - case ViewType::Uint32: + case ExternalCopyArrayBufferView::ViewType::Uint32: return Uint32Array::New(buffer, 0, length); - case ViewType::Int32: + case ExternalCopyArrayBufferView::ViewType::Int32: return Int32Array::New(buffer, 0, length); - case ViewType::Float32: + case ExternalCopyArrayBufferView::ViewType::Float32: return Float32Array::New(buffer, 0, length); - case ViewType::Float64: + case ExternalCopyArrayBufferView::ViewType::Float64: return Float64Array::New(buffer, 0, length); - case ViewType::DataView: + case ExternalCopyArrayBufferView::ViewType::DataView: return DataView::New(buffer, 0, length); default: throw std::exception(); } } +Local ExternalCopyArrayBufferView::CopyInto(bool transfer_in) { + Local buffer = this->buffer->CopyInto(transfer_in); + if (buffer->IsArrayBuffer()) { + return NewTypedArrayView(buffer.As(), type); + } else { + return NewTypedArrayView(buffer.As(), type); + } +} + uint32_t ExternalCopyArrayBufferView::WorstCaseHeapSize() const { // This includes the overhead of the ArrayBuffer return 208 + buffer->WorstCaseHeapSize(); diff --git a/src/external_copy.h b/src/external_copy.h index 90305a5..127582a 100644 --- a/src/external_copy.h +++ b/src/external_copy.h @@ -217,7 +217,7 @@ class ExternalCopyArrayBuffer : public ExternalCopyBytes { public: ExternalCopyArrayBuffer(const void* data, size_t length); ExternalCopyArrayBuffer(std::shared_ptr ptr, size_t length); - explicit ExternalCopyArrayBuffer(const v8::Local& handle); + explicit ExternalCopyArrayBuffer(const v8::Local& handle); static std::unique_ptr Transfer(const v8::Local& handle); v8::Local CopyInto(bool transfer_in = false) final; @@ -227,6 +227,22 @@ class ExternalCopyArrayBuffer : public ExternalCopyBytes { size_t Length() const; }; +/** + * SharedArrayBuffer instances + */ +class ExternalCopySharedArrayBuffer : public ExternalCopyBytes { + private: + std::shared_ptr value; + const size_t length; + + public: + explicit ExternalCopySharedArrayBuffer(const v8::Local& handle); + + v8::Local CopyInto(bool transfer_in = false) final; + uint32_t WorstCaseHeapSize() const final; + std::shared_ptr GetSharedPointer() const; +}; + /** * All types of TypedArray views w/ underlying buffer handle */ @@ -235,12 +251,11 @@ class ExternalCopyArrayBufferView : public ExternalCopy { enum class ViewType { Uint8, Uint8Clamped, Int8, Uint16, Int16, Uint32, Int32, Float32, Float64, DataView }; private: - std::unique_ptr buffer; + std::unique_ptr buffer; ViewType type; public: - ExternalCopyArrayBufferView(const v8::Local& handle, ViewType type); - ExternalCopyArrayBufferView(std::unique_ptr buffer, ViewType type); + ExternalCopyArrayBufferView(std::unique_ptr buffer, ViewType type); v8::Local CopyInto(bool transfer_in = false) final; uint32_t WorstCaseHeapSize() const final; }; diff --git a/src/isolate/environment.h b/src/isolate/environment.h index 923d764..95c9137 100644 --- a/src/isolate/environment.h +++ b/src/isolate/environment.h @@ -22,9 +22,10 @@ class Runnable; * Wrapper around Isolate with helpers to make working with multiple isolates easier. */ class IsolateEnvironment { - // These are here so they can adjust `extra_allocated_memory` + // These are here so they can adjust `extra_allocated_memory`. TODO: Make this a method friend class ExternalCopyBytes; friend class ExternalCopyArrayBuffer; + friend class ExternalCopySharedArrayBuffer; friend class ExternalCopyString; friend class InspectorAgent; diff --git a/test.js b/test.js index a7b008d..eb64e6b 100644 --- a/test.js +++ b/test.js @@ -5,16 +5,25 @@ let path = require('path'); let ret = 0; function runTest(test, cb) { + // Copy env variables let env = {}; for (let ii in process.env) { env[ii] = process.env[ii]; } env.NODE_PATH = __dirname; - let proc = spawn( - process.execPath, - [ path.join('tests', test) ], - { env: env } - ); + + // Get extra args + let args = []; + let testPath = path.join('tests', test); + let content = fs.readFileSync(testPath, 'utf8'); + let match = /node-args: *(.+)/.exec(content); + if (match) { + args = match[1].split(/ /g); + } + args.push(testPath); + + // Launch process + let proc = spawn(process.execPath, args, { env }); proc.stdout.setEncoding('utf8'); proc.stderr.setEncoding('utf8'); @@ -27,6 +36,7 @@ function runTest(test, cb) { }); proc.stdin.end(); + // Wait for completion process.stderr.write(`${test}: `); proc.on('exit', function(code) { if (stdout !== 'pass\n' || stderr !== '') { @@ -50,7 +60,7 @@ function runTest(test, cb) { let cb = function() { process.exit(ret); }; -fs.readdirSync('./tests').reverse().forEach(function(file) { +fs.readdirSync('./tests').sort().reverse().forEach(function(file) { cb = new function(cb) { return function(err) { if (err) return cb(err); diff --git a/tests/shared-array-buffer.js b/tests/shared-array-buffer.js new file mode 100644 index 0000000..69f83d2 --- /dev/null +++ b/tests/shared-array-buffer.js @@ -0,0 +1,67 @@ +'use strict'; +// node-args: --harmony_sharedarraybuffer +let ivm = require('isolated-vm'); + +let buffer = new SharedArrayBuffer(1024 * 1024); +let array = new Uint8Array(buffer); + +function runSync(env, fn) { + env.isolate.compileScriptSync('new '+ fn).runSync(env.context); +} + +function makeIsolate() { + let isolate = new ivm.Isolate; + let context = isolate.createContextSync(); + let global = context.globalReference(); + return { isolate, context, global }; +} +let env = makeIsolate(); + +// Check initial transfer +env.global.setSync('buffer', new ivm.ExternalCopy(buffer).copyInto({ release: true })); +env.global.setSync('ivm', ivm); + +if (env.isolate.getHeapStatisticsSync().externally_allocated_size !== 1024 * 1024) { + console.log('size wrong'); +} + +// Check that buffer transfer + externalize size adjustments work +let env2 = makeIsolate(); +env.global.setSync('global2', env2.global); +env2.global.setSync('ivm', ivm); +runSync(env, function() { + global2.setSync('buffer', new ivm.ExternalCopy(buffer).copyInto({ release: true })); +}); + +if ( + env.isolate.getHeapStatisticsSync().externally_allocated_size !== 1024 * 1024 || + env2.isolate.getHeapStatisticsSync().externally_allocated_size !== 1024 * 1024 +) { + console.log('size wrong2'); +} + +// Make sure we're using the same array +env.global.setSync('array', new ivm.ExternalCopy(array).copyInto({ release: true })); +runSync(env, function() { + array[0] = 123; +}); +runSync(env2, function() { + let foo = new Uint8Array(buffer); + foo[1] = 234; + buf2 = new SharedArrayBuffer(1024 * 1024); + new ivm.ExternalCopy(buf2); // externalize +}); + +if (array[0] !== 123 || array[1] !== 234) { + console.log('not shared'); +} + +if (env.isolate.getHeapStatisticsSync().externally_allocated_size !== 1024 * 1024 * 2) { + // This isn't really a good thing but I can't really think of an efficient way to fix it + console.log('size wrong3'); +} +if (env2.isolate.getHeapStatisticsSync().externally_allocated_size !== 1024 * 1024 * 2) { + console.log('size wrong4'); +} + +console.log('pass');