feat: remove deprecated base::Value-based serialization (#21560)

* feat: remove deprecated base::Value-based serialization

* add note to breaking-changes
This commit is contained in:
Jeremy Apthorp 2019-12-17 22:24:50 -08:00 committed by Cheng Zhao
parent cc8bf12351
commit 1cac62f0a2
5 changed files with 40 additions and 120 deletions

View file

@ -28,6 +28,22 @@ Electron 8.x, and has been removed in Electron 9.x. The layout zoom level limits
are now fixed at a minimum of 0.25 and a maximum of 5.0, as defined are now fixed at a minimum of 0.25 and a maximum of 5.0, as defined
[here](https://chromium.googlesource.com/chromium/src/+/938b37a6d2886bf8335fc7db792f1eb46c65b2ae/third_party/blink/common/page/page_zoom.cc#11). [here](https://chromium.googlesource.com/chromium/src/+/938b37a6d2886bf8335fc7db792f1eb46c65b2ae/third_party/blink/common/page/page_zoom.cc#11).
### Sending non-JS objects over IPC now throws an exception
In Electron 8.0, IPC was changed to use the Structured Clone Algorithm,
bringing significant performance improvements. To help ease the transition, the
old IPC serialization algorithm was kept and used for some objects that aren't
serializable with Structured Clone. In particular, DOM objects (e.g. `Element`,
`Location` and `DOMMatrix`), Node.js objects backed by C++ classes (e.g.
`process.env`, some members of `Stream`), and Electron objects backed by C++
classes (e.g. `WebContents`, `BrowserWindow` and `WebFrame`) are not
serializable with Structured Clone. Whenever the old algorithm was invoked, a
deprecation warning was printed.
In Electron 9.0, the old serialization algorithm has been removed, and sending
such non-serializable objects will now throw an "object could not be cloned"
error.
## Planned Breaking API Changes (8.0) ## Planned Breaking API Changes (8.0)
### Values sent over IPC are now serialized with Structured Clone Algorithm ### Values sent over IPC are now serialized with Structured Clone Algorithm

View file

@ -12,8 +12,6 @@
#include "base/strings/string_util.h" #include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "gin/converter.h" #include "gin/converter.h"
#include "mojo/public/cpp/base/values_mojom_traits.h"
#include "mojo/public/mojom/base/values.mojom.h"
#include "shell/common/deprecate_util.h" #include "shell/common/deprecate_util.h"
#include "shell/common/gin_converters/value_converter.h" #include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h" #include "shell/common/gin_helper/dictionary.h"
@ -510,42 +508,23 @@ bool Converter<network::mojom::ReferrerPolicy>::FromV8(
} }
namespace { namespace {
constexpr uint8_t kNewSerializationTag = 0;
constexpr uint8_t kOldSerializationTag = 1;
class V8Serializer : public v8::ValueSerializer::Delegate { class V8Serializer : public v8::ValueSerializer::Delegate {
public: public:
explicit V8Serializer(v8::Isolate* isolate, explicit V8Serializer(v8::Isolate* isolate)
bool use_old_serialization = false) : isolate_(isolate), serializer_(isolate, this) {}
: isolate_(isolate),
serializer_(isolate, this),
use_old_serialization_(use_old_serialization) {}
~V8Serializer() override = default; ~V8Serializer() override = default;
bool Serialize(v8::Local<v8::Value> value, blink::CloneableMessage* out) { bool Serialize(v8::Local<v8::Value> value, blink::CloneableMessage* out) {
serializer_.WriteHeader(); serializer_.WriteHeader();
if (use_old_serialization_) {
WriteTag(kOldSerializationTag);
if (!WriteBaseValue(value)) {
isolate_->ThrowException(
StringToV8(isolate_, "An object could not be cloned."));
return false;
}
} else {
WriteTag(kNewSerializationTag);
bool wrote_value; bool wrote_value;
v8::TryCatch try_catch(isolate_);
if (!serializer_.WriteValue(isolate_->GetCurrentContext(), value) if (!serializer_.WriteValue(isolate_->GetCurrentContext(), value)
.To(&wrote_value)) { .To(&wrote_value)) {
try_catch.Reset(); isolate_->ThrowException(v8::Exception::Error(
if (!V8Serializer(isolate_, true).Serialize(value, out)) { StringToV8(isolate_, "An object could not be cloned.")));
try_catch.ReThrow();
return false; return false;
} }
return true;
}
DCHECK(wrote_value); DCHECK(wrote_value);
}
std::pair<uint8_t*, size_t> buffer = serializer_.Release(); std::pair<uint8_t*, size_t> buffer = serializer_.Release();
DCHECK_EQ(buffer.first, data_.data()); DCHECK_EQ(buffer.first, data_.data());
@ -555,29 +534,6 @@ class V8Serializer : public v8::ValueSerializer::Delegate {
return true; return true;
} }
bool WriteBaseValue(v8::Local<v8::Value> object) {
node::Environment* env = node::Environment::GetCurrent(isolate_);
if (env) {
electron::EmitDeprecationWarning(
env,
"Passing functions, DOM objects and other non-cloneable JavaScript "
"objects to IPC methods is deprecated and will throw an exception "
"beginning with Electron 9.",
"DeprecationWarning");
}
base::Value value;
if (!ConvertFromV8(isolate_, object, &value)) {
return false;
}
mojo::Message message = mojo_base::mojom::Value::SerializeAsMessage(&value);
serializer_.WriteUint32(message.data_num_bytes());
serializer_.WriteRawBytes(message.data(), message.data_num_bytes());
return true;
}
void WriteTag(uint8_t tag) { serializer_.WriteRawBytes(&tag, 1); }
// v8::ValueSerializer::Delegate // v8::ValueSerializer::Delegate
void* ReallocateBufferMemory(void* old_buffer, void* ReallocateBufferMemory(void* old_buffer,
size_t size, size_t size,
@ -601,7 +557,6 @@ class V8Serializer : public v8::ValueSerializer::Delegate {
v8::Isolate* isolate_; v8::Isolate* isolate_;
std::vector<uint8_t> data_; std::vector<uint8_t> data_;
v8::ValueSerializer serializer_; v8::ValueSerializer serializer_;
bool use_old_serialization_;
}; };
class V8Deserializer : public v8::ValueDeserializer::Delegate { class V8Deserializer : public v8::ValueDeserializer::Delegate {
@ -620,55 +575,12 @@ class V8Deserializer : public v8::ValueDeserializer::Delegate {
if (!deserializer_.ReadHeader(context).To(&read_header)) if (!deserializer_.ReadHeader(context).To(&read_header))
return v8::Null(isolate_); return v8::Null(isolate_);
DCHECK(read_header); DCHECK(read_header);
uint8_t tag;
if (!ReadTag(&tag))
return v8::Null(isolate_);
switch (tag) {
case kNewSerializationTag: {
v8::Local<v8::Value> value; v8::Local<v8::Value> value;
if (!deserializer_.ReadValue(context).ToLocal(&value)) { if (!deserializer_.ReadValue(context).ToLocal(&value)) {
return v8::Null(isolate_); return v8::Null(isolate_);
} }
return scope.Escape(value); return scope.Escape(value);
} }
case kOldSerializationTag: {
v8::Local<v8::Value> value;
if (!ReadBaseValue(&value)) {
return v8::Null(isolate_);
}
return scope.Escape(value);
}
default:
NOTREACHED() << "Invalid tag: " << tag;
return v8::Null(isolate_);
}
}
bool ReadTag(uint8_t* tag) {
const void* tag_bytes;
if (!deserializer_.ReadRawBytes(1, &tag_bytes))
return false;
*tag = *reinterpret_cast<const uint8_t*>(tag_bytes);
return true;
}
bool ReadBaseValue(v8::Local<v8::Value>* value) {
uint32_t length;
const void* data;
if (!deserializer_.ReadUint32(&length) ||
!deserializer_.ReadRawBytes(length, &data)) {
return false;
}
mojo::Message message(
base::make_span(reinterpret_cast<const uint8_t*>(data), length), {});
base::Value out;
if (!mojo_base::mojom::Value::DeserializeFromMessage(std::move(message),
&out)) {
return false;
}
*value = ConvertToV8(isolate_, out);
return true;
}
private: private:
v8::Isolate* isolate_; v8::Isolate* isolate_;

View file

@ -52,22 +52,15 @@ describe('ipcRenderer module', () => {
expect(Buffer.from(data).equals(received)).to.be.true() expect(Buffer.from(data).equals(received)).to.be.true()
}) })
// TODO(nornagon): Change this test to expect an exception to be thrown in it('throws when sending objects with DOM class prototypes', async () => {
// Electron 9. await expect(w.webContents.executeJavaScript(`{
it('can send objects with DOM class prototypes', async () => {
w.webContents.executeJavaScript(`{
const { ipcRenderer } = require('electron') const { ipcRenderer } = require('electron')
ipcRenderer.send('message', document.location) ipcRenderer.send('message', document.location)
}`) }`)).to.eventually.be.rejected()
const [, value] = await emittedOnce(ipcMain, 'message')
expect(value.protocol).to.equal('about:')
expect(value.hostname).to.equal('')
}) })
// TODO(nornagon): Change this test to expect an exception to be thrown in
// Electron 9.
it('does not crash when sending external objects', async () => { it('does not crash when sending external objects', async () => {
w.webContents.executeJavaScript(`{ await expect(w.webContents.executeJavaScript(`{
const { ipcRenderer } = require('electron') const { ipcRenderer } = require('electron')
const http = require('http') const http = require('http')
@ -75,10 +68,7 @@ describe('ipcRenderer module', () => {
const stream = request.agent.sockets['127.0.0.1:5000:'][0]._handle._externalStream const stream = request.agent.sockets['127.0.0.1:5000:'][0]._handle._externalStream
ipcRenderer.send('message', stream) ipcRenderer.send('message', stream)
}`) }`)).to.eventually.be.rejected()
const [, externalStreamValue] = await emittedOnce(ipcMain, 'message')
expect(externalStreamValue).to.be.null()
}) })
it('can send objects that both reference the same object', async () => { it('can send objects that both reference the same object', async () => {

View file

@ -48,11 +48,12 @@ function makeWindow () {
before(async () => { before(async () => {
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, enableRemoteModule: true } }) w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, enableRemoteModule: true } })
await w.loadURL('about:blank') await w.loadURL('about:blank')
await w.webContents.executeJavaScript(` await w.webContents.executeJavaScript(`{
const chai_1 = window.chai_1 = require('chai') const chai_1 = window.chai_1 = require('chai')
chai_1.use(require('chai-as-promised')) chai_1.use(require('chai-as-promised'))
chai_1.use(require('dirty-chai')) chai_1.use(require('dirty-chai'))
`) null
}`)
}) })
after(closeAllWindows) after(closeAllWindows)
return () => w return () => w
@ -63,11 +64,12 @@ function makeEachWindow () {
beforeEach(async () => { beforeEach(async () => {
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, enableRemoteModule: true } }) w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, enableRemoteModule: true } })
await w.loadURL('about:blank') await w.loadURL('about:blank')
await w.webContents.executeJavaScript(` await w.webContents.executeJavaScript(`{
const chai_1 = window.chai_1 = require('chai') const chai_1 = window.chai_1 = require('chai')
chai_1.use(require('chai-as-promised')) chai_1.use(require('chai-as-promised'))
chai_1.use(require('dirty-chai')) chai_1.use(require('dirty-chai'))
`) null
}`)
}) })
afterEach(closeAllWindows) afterEach(closeAllWindows)
return () => w return () => w

View file

@ -20,7 +20,7 @@ describe('modules support', () => {
it('can be required in renderer', async () => { it('can be required in renderer', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
w.loadURL('about:blank') w.loadURL('about:blank')
await expect(w.webContents.executeJavaScript(`{ require('echo') }`)).to.be.fulfilled() await expect(w.webContents.executeJavaScript(`{ require('echo'); null }`)).to.be.fulfilled()
}) })
ifit(features.isRunAsNodeEnabled())('can be required in node binary', function (done) { ifit(features.isRunAsNodeEnabled())('can be required in node binary', function (done) {