Bug 1287298 - Add an API to give up ownership of ArrayBuffer data; r=Waldo

This is similar to stealing the buffer, except that the ArrayBuffer
won't be detached.  The caller is still responsible for freeing the
buffer.
This commit is contained in:
Ehsan Akhgari 2016-07-16 17:52:39 -04:00
parent 652779a674
commit 556af42028
4 changed files with 157 additions and 9 deletions

View File

@ -158,3 +158,70 @@ bool hasDetachedBuffer(JS::HandleObject obj) {
}
END_TEST(testArrayBuffer_bug720949_viewList)
BEGIN_TEST(testArrayBuffer_externalize)
{
if (!testWithSize(cx, 2)) // ArrayBuffer data stored inline in the object.
return false;
if (!testWithSize(cx, 2000)) // ArrayBuffer data stored out-of-line in a separate heap allocation.
return false;
return true;
}
bool testWithSize(JSContext* cx, size_t n)
{
JS::RootedObject buffer(cx, JS_NewArrayBuffer(cx, n));
CHECK(buffer != nullptr);
JS::RootedObject view(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, 0, -1));
CHECK(view != nullptr);
void* contents = JS_ExternalizeArrayBufferContents(cx, buffer);
CHECK(contents != nullptr);
uint32_t actualLength;
CHECK(hasExpectedLength(cx, view, &actualLength));
CHECK(actualLength == n);
CHECK(!JS_IsDetachedArrayBufferObject(buffer));
CHECK(JS_GetArrayBufferByteLength(buffer) == uint32_t(n));
uint8_t* uint8Contents = static_cast<uint8_t*>(contents);
CHECK(*uint8Contents == 0);
uint8_t randomByte(rand() % UINT8_MAX);
*uint8Contents = randomByte;
JS::RootedValue v(cx);
CHECK(JS_GetElement(cx, view, 0, &v));
CHECK(v.toInt32() == randomByte);
view = nullptr;
GC(cx);
CHECK(JS_DetachArrayBuffer(cx, buffer));
GC(cx);
CHECK(*uint8Contents == randomByte);
JS_free(cx, contents);
GC(cx);
buffer = nullptr;
GC(cx);
return true;
}
static void GC(JSContext* cx)
{
JS_GC(cx);
JS_GC(cx); // Trigger another to wait for background finalization to end
}
static bool
hasExpectedLength(JSContext* cx, JS::HandleObject obj, uint32_t* len)
{
JS::RootedValue v(cx);
if (!JS_GetProperty(cx, obj, "byteLength", &v))
return false;
*len = v.toInt32();
return true;
}
END_TEST(testArrayBuffer_externalize)

View File

@ -3446,6 +3446,25 @@ JS_NewArrayBufferWithExternalContents(JSContext* cx, size_t nbytes, void* conten
extern JS_PUBLIC_API(void*)
JS_StealArrayBufferContents(JSContext* cx, JS::HandleObject obj);
/**
* Returns a pointer to the ArrayBuffer |obj|'s data. |obj| and its views will store and expose
* the data in the returned pointer: assigning into the returned pointer will affect values exposed
* by views of |obj| and vice versa.
*
* The caller must ultimately deallocate the returned pointer to avoid leaking. The memory is
* *not* garbage-collected with |obj|. These steps must be followed to deallocate:
*
* 1. The ArrayBuffer |obj| must be detached using JS_DetachArrayBuffer.
* 2. The returned pointer must be freed using JS_free.
*
* To perform step 1, callers *must* hold a reference to |obj| until they finish using the returned
* pointer. They *must not* attempt to let |obj| be GC'd, then JS_free the pointer.
*
* If |obj| isn't an ArrayBuffer, this function returns null and reports an error.
*/
extern JS_PUBLIC_API(void*)
JS_ExternalizeArrayBufferContents(JSContext* cx, JS::HandleObject obj);
/**
* Create a new mapped array buffer with the given memory mapped contents. It
* must be legal to free the contents pointer by unmapping it. On success,

View File

@ -316,21 +316,21 @@ ArrayBufferObject::detach(JSContext* cx, Handle<ArrayBufferObject*> buffer,
}
if (newContents.data() != buffer->dataPointer())
buffer->setNewOwnedData(cx->runtime()->defaultFreeOp(), newContents);
buffer->setNewData(cx->runtime()->defaultFreeOp(), newContents, OwnsData);
buffer->setByteLength(0);
buffer->setIsDetached();
}
void
ArrayBufferObject::setNewOwnedData(FreeOp* fop, BufferContents newContents)
ArrayBufferObject::setNewData(FreeOp* fop, BufferContents newContents, OwnsState ownsState)
{
if (ownsData()) {
MOZ_ASSERT(newContents.data() != dataPointer());
releaseData(fop);
}
setDataPointer(newContents, OwnsData);
setDataPointer(newContents, ownsState);
}
// This is called *only* from changeContents(), below.
@ -361,14 +361,15 @@ ArrayBufferObject::changeViewContents(JSContext* cx, ArrayBufferViewObject* view
// BufferContents is specific to ArrayBuffer, hence it will not represent shared memory.
void
ArrayBufferObject::changeContents(JSContext* cx, BufferContents newContents)
ArrayBufferObject::changeContents(JSContext* cx, BufferContents newContents,
OwnsState ownsState)
{
MOZ_RELEASE_ASSERT(!isWasm());
MOZ_ASSERT(!forInlineTypedObject());
// Change buffer contents.
uint8_t* oldDataPointer = dataPointer();
setNewOwnedData(cx->runtime()->defaultFreeOp(), newContents);
setNewData(cx->runtime()->defaultFreeOp(), newContents, ownsState);
// Update all views.
auto& innerViews = cx->compartment()->innerViews;
@ -741,7 +742,7 @@ ArrayBufferObject::prepareForAsmJS(JSContext* cx, Handle<ArrayBufferObject*> buf
// Swap the new elements into the ArrayBufferObject. Mark the
// ArrayBufferObject so we don't do this again.
buffer->changeContents(cx, BufferContents::create<WASM>(data));
buffer->changeContents(cx, BufferContents::create<WASM>(data), OwnsData);
buffer->setIsPreparedForAsmJS();
MOZ_ASSERT(data == buffer->dataPointer());
cx->zone()->updateMallocCounter(wasmBuf->mappedSize());
@ -760,7 +761,7 @@ ArrayBufferObject::prepareForAsmJS(JSContext* cx, Handle<ArrayBufferObject*> buf
if (!contents)
return false;
memcpy(contents.data(), buffer->dataPointer(), buffer->byteLength());
buffer->changeContents(cx, contents);
buffer->changeContents(cx, contents, OwnsData);
}
buffer->setIsPreparedForAsmJS();
@ -1096,6 +1097,32 @@ ArrayBufferObject::createDataViewForThis(JSContext* cx, unsigned argc, Value* vp
return CallNonGenericMethod<IsArrayBuffer, createDataViewForThisImpl>(cx, args);
}
/* static */ ArrayBufferObject::BufferContents
ArrayBufferObject::externalizeContents(JSContext* cx, Handle<ArrayBufferObject*> buffer,
bool hasStealableContents)
{
MOZ_ASSERT(buffer->isPlain(), "Only support doing this on plain ABOs");
MOZ_ASSERT(!buffer->isDetached(), "must have contents to externalize");
MOZ_ASSERT_IF(hasStealableContents, buffer->hasStealableContents());
BufferContents contents(buffer->dataPointer(), buffer->bufferKind());
if (hasStealableContents) {
buffer->setOwnsData(DoesntOwnData);
return contents;
}
// Create a new chunk of memory to return since we cannot steal the
// existing contents away from the buffer.
BufferContents newContents = AllocateArrayBufferContents(cx, buffer->byteLength());
if (!newContents)
return BufferContents::createPlain(nullptr);
memcpy(newContents.data(), contents.data(), buffer->byteLength());
buffer->changeContents(cx, newContents, DoesntOwnData);
return newContents;
}
/* static */ ArrayBufferObject::BufferContents
ArrayBufferObject::stealContents(JSContext* cx, Handle<ArrayBufferObject*> buffer,
bool hasStealableContents)
@ -1655,6 +1682,38 @@ js::UnwrapSharedArrayBuffer(JSObject* obj)
return nullptr;
}
JS_PUBLIC_API(void*)
JS_ExternalizeArrayBufferContents(JSContext* cx, HandleObject obj)
{
AssertHeapIsIdle(cx);
CHECK_REQUEST(cx);
assertSameCompartment(cx, obj);
if (!obj->is<ArrayBufferObject>()) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_BAD_ARGS);
return nullptr;
}
Handle<ArrayBufferObject*> buffer = obj.as<ArrayBufferObject>();
if (!buffer->isPlain()) {
// This operation isn't supported on mapped or wsm ArrayBufferObjects.
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_BAD_ARGS);
return nullptr;
}
if (buffer->isDetached()) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED);
return nullptr;
}
// The caller assumes that a plain malloc'd buffer is returned.
// hasStealableContents is true for mapped buffers, so we must additionally
// require that the buffer is plain. In the future, we could consider
// returning something that handles releasing the memory.
bool hasStealableContents = buffer->hasStealableContents();
return ArrayBufferObject::externalizeContents(cx, buffer, hasStealableContents).data();
}
JS_PUBLIC_API(void*)
JS_StealArrayBufferContents(JSContext* cx, HandleObject objArg)
{

View File

@ -276,6 +276,9 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared
static void trace(JSTracer* trc, JSObject* obj);
static void objectMoved(JSObject* obj, const JSObject* old);
static BufferContents externalizeContents(JSContext* cx,
Handle<ArrayBufferObject*> buffer,
bool hasStealableContents);
static BufferContents stealContents(JSContext* cx,
Handle<ArrayBufferObject*> buffer,
bool hasStealableContents);
@ -297,8 +300,8 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared
bool addView(JSContext* cx, JSObject* view);
void setNewOwnedData(FreeOp* fop, BufferContents newContents);
void changeContents(JSContext* cx, BufferContents newContents);
void setNewData(FreeOp* fop, BufferContents newContents, OwnsState ownsState);
void changeContents(JSContext* cx, BufferContents newContents, OwnsState ownsState);
// Detach this buffer from its original memory. (This necessarily makes
// views of this buffer unusable for modifying that original memory.)