Commit 4844d315 authored by Ken Rockot's avatar Ken Rockot Committed by Chromium LUCI CQ

Implement versioning support in Mojo JS modules

Bug: 1004256
Fixed: 914161
Change-Id: I0effc175311f9c5f4ae87ab57e7fffddb70c1c31
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2577883Reviewed-by: default avatarOksana Zhuravlova <oksamyt@chromium.org>
Commit-Queue: Ken Rockot <rockot@google.com>
Cr-Commit-Position: refs/heads/master@{#835385}
parent 97cb85ea
......@@ -846,6 +846,8 @@ mojom("mojo_bindings_web_test_mojom") {
"data/mojo_bindings_web_test.test-mojom",
"data/mojo_bindings_web_test_types.test-mojom",
]
scramble_message_ids = false
js_generate_struct_deserializers = true
}
if (is_android) {
......
......@@ -51,3 +51,13 @@ interface SubinterfaceClient {
DidFlush(array<int32> values);
};
struct StructVersionTest {
string a;
string b;
[MinVersion=1] string? c;
[MinVersion=2] string? d;
};
interface InterfaceVersionTest {
Foo(int32 x, [MinVersion=1] int32 y) => (int32 z, [MinVersion=1] int32 w);
};
......@@ -147,7 +147,7 @@ mojo.internal.getUint64 = function(dataView, byteOffset) {
* @return {number}
*/
mojo.internal.computeTotalStructSize = function(structSpec, value) {
let size = mojo.internal.kStructHeaderSize + structSpec.packedSize;
let size = structSpec.packedSize;
for (const field of structSpec.fields) {
const fieldValue = value[field.name];
if (field.type.$.computePayloadSize &&
......@@ -280,8 +280,7 @@ mojo.internal.Message = class {
/** @private {number} */
this.nextAllocationOffset_ = headerSize;
const paramStructData = this.allocate(
mojo.internal.kStructHeaderSize + paramStructSpec.packedSize);
const paramStructData = this.allocate(paramStructSpec.packedSize);
const encoder = new mojo.internal.Encoder(this, paramStructData);
encoder.encodeStructInline(paramStructSpec, value);
}
......@@ -462,8 +461,7 @@ mojo.internal.Encoder = class {
* @param {!Object} value
*/
encodeStruct(structSpec, offset, value) {
const structData = this.message_.allocate(
mojo.internal.kStructHeaderSize + structSpec.packedSize);
const structData = this.message_.allocate(structSpec.packedSize);
const structEncoder = new mojo.internal.Encoder(this.message_, structData);
this.encodeOffset(offset, structData.byteOffset);
structEncoder.encodeStructInline(structSpec, value);
......@@ -474,9 +472,9 @@ mojo.internal.Encoder = class {
* @param {!Object} value
*/
encodeStructInline(structSpec, value) {
this.encodeUint32(
0, mojo.internal.kStructHeaderSize + structSpec.packedSize);
this.encodeUint32(4, 0); // TODO: Support versioning.
const versions = structSpec.versions;
this.encodeUint32(0, structSpec.packedSize);
this.encodeUint32(4, versions[versions.length - 1].version);
for (const field of structSpec.fields) {
const byteOffset = mojo.internal.kStructHeaderSize + field.packedOffset;
......@@ -744,6 +742,36 @@ mojo.internal.Decoder = class {
return decoder.decodeStructInline(structSpec);
}
/**
* @param {!mojo.internal.StructSpec} structSpec
* @param {number} size
* @param {number} version
* @return {boolean}
*/
isStructHeaderValid(structSpec, size, version) {
const versions = structSpec.versions;
for (let i = versions.length - 1; i >= 0; --i) {
const info = versions[i];
if (version > info.version) {
// If it's newer than the next newest version we know about, the only
// requirement is that it's at least large enough to decode that next
// newest version.
return size >= info.packedSize;
}
if (version == info.version) {
// If it IS the next newest version we know about, expect an exact size
// match.
return size == info.packedSize;
}
}
// This should be effectively unreachable, because we always generate info
// for version 0, and the `version` parameter here is guaranteed in practice
// to be a non-negative value.
throw new Error(
`Impossible version ${version} for struct ${structSpec.name}`);
}
/**
* @param {!mojo.internal.StructSpec} structSpec
* @return {!Object}
......@@ -751,9 +779,19 @@ mojo.internal.Decoder = class {
decodeStructInline(structSpec) {
const size = this.decodeUint32(0);
const version = this.decodeUint32(4);
if (!this.isStructHeaderValid(structSpec, size, version)) {
throw new Error(
`Received ${structSpec.name} of invalid size (${size}) and/or ` +
`version (${version})`);
}
const result = {};
for (const field of structSpec.fields) {
const byteOffset = mojo.internal.kStructHeaderSize + field.packedOffset;
if (field.minVersion > version) {
result[field.name] = field.defaultValue;
continue;
}
const value = field.type.$.decode(
this, byteOffset, field.packedBitOffset, !!field.nullable);
if (value === null && !field.nullable) {
......@@ -933,15 +971,25 @@ mojo.internal.MapSpec;
* type: !mojo.internal.MojomType,
* defaultValue: *,
* nullable: boolean,
* minVersion: number,
* }}
*/
mojo.internal.StructFieldSpec;
/**
* @typedef {{
* version: number,
* packedSize: number,
* }}
*/
mojo.internal.StructVersionInfo;
/**
* @typedef {{
* name: string,
* packedSize: number,
* fields: !Array<!mojo.internal.StructFieldSpec>,
* versions: !Array<!mojo.internal.StructVersionInfo>,
* }}
*/
mojo.internal.StructSpec;
......@@ -1295,11 +1343,13 @@ mojo.internal.Enum = function() {
* @param {!mojo.internal.MojomType} type
* @param {*} defaultValue
* @param {boolean} nullable
* @param {number=} minVersion
* @return {!mojo.internal.StructFieldSpec}
* @export
*/
mojo.internal.StructField = function(
name, packedOffset, packedBitOffset, type, defaultValue, nullable) {
name, packedOffset, packedBitOffset, type, defaultValue, nullable,
minVersion = 0) {
return {
name: name,
packedOffset: packedOffset,
......@@ -1307,23 +1357,22 @@ mojo.internal.StructField = function(
type: type,
defaultValue: defaultValue,
nullable: nullable,
minVersion: minVersion,
};
};
/**
* @param {!Object} objectToBlessAsType
* @param {string} name
* @param {number} packedSize
* @param {!Array<!mojo.internal.StructFieldSpec>} fields
* @param {Array<!Array<number>>=} versionData
* @export
*/
mojo.internal.Struct = function(objectToBlessAsType, name, packedSize, fields) {
/** @type {!mojo.internal.StructSpec} */
const structSpec = {
name: name,
packedSize: packedSize,
fields: fields,
};
mojo.internal.Struct =
function(objectToBlessAsType, name, fields, versionData) {
const versions = versionData.map(v => ({version: v[0], packedSize: v[1]}));
const packedSize = versions[versions.length - 1].packedSize;
const structSpec = {name, packedSize, fields, versions};
objectToBlessAsType.$ = {
structSpec: structSpec,
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
......
......@@ -16,7 +16,6 @@
mojo.internal.Struct(
{{module.namespace}}.{{struct.name}}Spec.$,
'{{struct.name}}',
{{struct.packed|payload_size}},
[
{%- for packed_field in struct.packed.packed_fields_in_ordinal_order %}
mojo.internal.StructField(
......@@ -31,6 +30,11 @@ mojo.internal.Struct(
false /* nullable */),
{%- endif %}
{%- endfor %}
],
[
{%- for info in struct.versions -%}
[{{info.version}}, {{info.num_bytes}}],
{%- endfor -%}
]);
{% if generate_struct_deserializers %}
......
......@@ -14,7 +14,6 @@ export const {{struct.name}}_{{constant.name}} =
mojo.internal.Struct(
{{struct.name}}Spec.$,
'{{struct.name}}',
{{struct.packed|payload_size}},
[
{%- for packed_field in struct.packed.packed_fields_in_ordinal_order %}
mojo.internal.StructField(
......@@ -24,11 +23,17 @@ mojo.internal.Struct(
{{packed_field.field.kind|spec_type_in_js_module}},
{{packed_field.field|default_value_in_js_module}},
{%- if packed_field.field.kind.is_nullable %}
true /* nullable */),
true /* nullable */,
{%- else %}
false /* nullable */),
false /* nullable */,
{%- endif %}
{{packed_field.field.min_version or 0}}),
{%- endfor %}
],
[
{%- for info in struct.versions -%}
[{{info.version}}, {{info.num_bytes}}],
{%- endfor -%}
]);
{% if generate_struct_deserializers %}
......
<!DOCTYPE html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/gen/layout_test_data/mojo/public/js/mojo_bindings_lite.js"></script>
<script src="/gen/mojo/public/interfaces/bindings/tests/deserializer.test-mojom-lite.js"></script>
<script>
'use strict';
<script type="module">
import {TestStruct_Deserialize} from '/gen/mojo/public/interfaces/bindings/tests/deserializer.test-mojom.m.js';
test(() => {
const data = new Uint32Array([
0, 0, // header
24, // header (size)
0, // header (version)
8899, // v1
9988, // v2
7777, 0 // v3
]);
7777, 0, // v3
]);
const dataview = new DataView(data.buffer);
const s = deserializer.TestStruct_Deserialize(dataview);
const s = TestStruct_Deserialize(dataview);
assert_equals(s.v1, 8899);
assert_equals(s.v2, 9988);
assert_equals(s.v3, 7777n);
}, 'deserializer');
}, 'deserializers are generated and exported as requested');
</script>
<!DOCTYPE html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./versioning.js" type="module"></script>
import {InterfaceVersionTest, InterfaceVersionTestReceiver, InterfaceVersionTestRemote, StructVersionTest_Deserialize} from '/gen/content/test/data/mojo_bindings_web_test.test-mojom.m.js';
async function waitForMessage(handle) {
let watcher;
await new Promise(r => {
watcher = handle.watch({readable: true}, r);
});
watcher.cancel();
}
promise_test(async () => {
// A manually serialized encoding of the StructVersionTest struct at version
// 0, with just two string fields `a` and `b`. Assumes a little-endian host.
const kTestStructV0 = new Uint32Array([
24, 0, // size and version
16, 0, 24, 0, // offset of string fields `a` and `b`
9, 1, 120, 0, // string `a` header and value 'x'
9, 1, 121, 0, // string `b` header and value 'y'
]);
assert_object_equals(
StructVersionTest_Deserialize(new DataView(kTestStructV0.buffer)),
{a: 'x', b: 'y', c: null, d: null});
// Adding field `c` as a version 1 struct
const kTestStructV1 = new Uint32Array([
32, 1, // size and version
24, 0, 32, 0, 40, 0, // offsets of string fields
9, 1, 120, 0, // string `a` header and value 'x'
9, 1, 121, 0, // string `b` header and value 'y'
9, 1, 122, 0, // string `c` header and value 'z'
]);
assert_object_equals(
StructVersionTest_Deserialize(new DataView(kTestStructV1.buffer)),
{a: 'x', b: 'y', c: 'z', d: null});
// Adding field `d` as a version 2 struct
const kTestStructV2 = new Uint32Array([
40, 2, // size and version
32, 0, 40, 0, 48, 0, 56, 0, // offsets of string fields
9, 1, 120, 0, // string `a` header and value 'x'
9, 1, 121, 0, // string `b` header and value 'y'
9, 1, 122, 0, // string `c` header and value 'z'
9, 1, 119, 0, // string `d` header and value 'w'
]);
assert_object_equals(
StructVersionTest_Deserialize(new DataView(kTestStructV2.buffer)),
{a: 'x', b: 'y', c: 'z', d: 'w'});
}, 'current and older versions can be deserialized');
promise_test(async () => {
// A manually serialized encoding of the StructVersionTest struct at an
// imaginary version newer than the one defined in the committed mojom file we
// have. This version has an extra string field unknown to our generated
// bindings.
const kTestStructV7 = new Uint32Array([
48, 7, // size and version
40, 0, 48, 0, 56, 0, 64, 0, 72, 0, // offset of string fields
9, 1, 120, 0, // string `a` header and value 'x'
9, 1, 121, 0, // string `b` header and value 'y'
9, 1, 122, 0, // string `c` header and value 'z'
9, 1, 119, 0, // string `d` header and value 'w'
11, 3, 0x6c6f6c, 0, // unknown new string field header and contents
]);
// The `e` field is not present because it is not part of our local mojom
// definition and is therefore not known to our generated deserializer. We can
// still deserialize what we know about though.
assert_object_equals(
StructVersionTest_Deserialize(new DataView(kTestStructV7.buffer)),
{a: 'x', b: 'y', c: 'z', d: 'w'});
}, 'newer versions can be partially deserialized');
promise_test(async () => {
const kTestTooSmall = new Uint32Array([8, 0]);
assert_throws_js(
Error,
() => StructVersionTest_Deserialize(new DataView(kTestTooSmall.buffer)));
}, 'reject struct encoding that is too small even for version 0');
promise_test(async () => {
const kTestWrongSize = new Uint32Array([
32, 2, // size and version. size should be 40 but isn't.
16, 0, 24, 0, // offsets of string fields
9, 1, 120, 0, // string `a` header and value 'x'
9, 1, 121, 0, // string `b` header and value 'y'
]);
assert_throws_js(
Error,
() => StructVersionTest_Deserialize(new DataView(kTestWrongSize.buffer)));
}, 'reject struct encoding whose size does not match the version');
class InterfaceVersionTestImpl {
constructor() {
const {handle0, handle1} = Mojo.createMessagePipe();
this.remoteHandle_ = handle0;
this.receiver_ = new InterfaceVersionTestReceiver(this);
this.receiver_.$.bindHandle(handle1);
this.nextFooResolvers_ = [];
}
sendRawMessage(message) {
this.remoteHandle_.writeMessage(message, []);
}
receiveNextFoo() {
return new Promise(resolve => {
this.nextFooResolvers_.push(resolve);
});
}
foo(x, y) {
const resolvers = this.nextFooResolvers_;
this.nextFooResolvers = [];
for (const r of resolvers) {
r({z: y, w: x});
}
return {z: y, w: x};
}
}
promise_test(async () => {
const impl = new InterfaceVersionTestImpl;
const kTestMessageV0 = new Uint32Array([
// Mojo message header. This is request 0 of message ID 0 on interface 0,
// expecting a reply. See the definition of MessageHeaderV1 within
// mojo/public/cpp/bindings/lib/message_internal.h.
32, 1, 0, 0, 1, 0, 0, 0,
16, 0, // Foo params struct size and version
42, 0, // value of `x` and padding
]);
// The implementation returns the inputs x,y swapped, so z=y and w=x. The
// message was v0 and does not include y, so we should have z=0 and w=42.
impl.sendRawMessage(kTestMessageV0);
const {z: z0, w: w0} = await impl.receiveNextFoo();
assert_equals(z0, 0);
assert_equals(w0, 42);
// Current version (1)
const kTestMessageV1 = new Uint32Array([
32, 1, 0, 0, 1, 0, 1, 0, // Mojo message header. See above.
16, 1, // Foo params struct size and version
42, 37, // `x` and `y` values
]);
impl.sendRawMessage(kTestMessageV1);
const {z: z1, w: w1} = await impl.receiveNextFoo();
assert_equals(z1, 37);
assert_equals(w1, 42);
// Imaginary future version 2.
const kTestMessageV2 = new Uint32Array([
32, 1, 0, 0, 1, 0, 2, 0, // Mojo message header
24, 2, // Foo params struct size and version
42, 37, // `x` and `y` values
19, 0, // new unknown field value
]);
impl.sendRawMessage(kTestMessageV2);
const {z: z2, w: w2} = await impl.receiveNextFoo();
assert_equals(z2, 37);
assert_equals(w2, 42);
}, 'interface receivers can handle requests from all versions old and new');
promise_test(async () => {
const {handle0, handle1} = Mojo.createMessagePipe();
const remote = new InterfaceVersionTestRemote(handle1);
const kTestReplyV0 = new Uint32Array([
// Mojo message header. This is reply 0 for message ID 0 on interface 0.
// See the definition of MessageHeaderV1 within
// mojo/public/cpp/bindings/lib/message_internal.h.
32, 1, 0, 0, 2, 0, 0, 0,
16, 0, // Foo reply struct size and version
19, 0, // `z` value and padding
]);
// Send the expected reply before we actually send the corresponding request.
// The receiver won't see it until after the request happens below.
handle0.writeMessage(kTestReplyV0, []);
const {z: z0, w: w0} = await remote.foo(1, 2);
assert_equals(z0, 19);
assert_equals(w0, 0);
const kTestReplyV1 = new Uint32Array([
32, 1, 0, 0, 2, 0, 1, 0, // Mojo message header
16, 1, // Foo reply struct size and version
19, 7, // `z` and `w` values
]);
// Send the expected reply before we actually send the corresponding request.
// The receiver won't see it until after the request happens below.
handle0.writeMessage(kTestReplyV1, []);
const {z: z1, w: w1} = await remote.foo(1, 2);
assert_equals(z1, 19);
assert_equals(w1, 7);
const kTestReplyV2 = new Uint32Array([
32, 1, 0, 0, 2, 0, 2, 0, // Mojo message header
24, 2, // Foo reply struct size and version
19, 7, // `z` and `w` values
0x12345678, // new unknown field value
]);
// Send the expected reply before we actually send the corresponding request.
// The receiver won't see it until after the request happens below.
handle0.writeMessage(kTestReplyV2, []);
const {z: z2, w: w2} = await remote.foo(1, 2);
assert_equals(z2, 19);
assert_equals(w2, 7);
}, 'interface remotes can handle responses from all versions old and new');
promise_test(async () => {
const {handle0, handle1} = Mojo.createMessagePipe();
const remote = new InterfaceVersionTestRemote(handle0);
remote.foo(1, 2);
await waitForMessage(handle1);
const {result, buffer} = handle1.readMessage();
assert_equals(result, Mojo.RESULT_OK);
// First some quick general validity checks.
const data = new DataView(buffer);
assert_equals(data.getUint32(0, true), 32); // message header size
assert_equals(data.getUint32(4, true), 1); // message header version
// Ensure the parameter structure reflects version 1 of the Foo message, since
// that's the current version in the mojom we're using.
assert_equals(data.getUint32(32, true), 16); // Foo request size
assert_equals(data.getUint32(36, true), 1); // Foo request version
}, 'structures properly encode their current version in outgoing messages');
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment