0

Support associated interfaces in Mojo JS modules

Adds associated interface support to modern Mojo JS bindings and
generated JS modules.

Fixed: 914165
Bug: 1004256
Change-Id: I5421ace585ad129374526bc7237980333b141b1e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2611647
Commit-Queue: Ken Rockot <rockot@google.com>
Reviewed-by: Peter Beverloo <peter@chromium.org>
Reviewed-by: Oksana Zhuravlova <oksamyt@chromium.org>
Auto-Submit: Ken Rockot <rockot@google.com>
Cr-Commit-Position: refs/heads/master@{#841192}
This commit is contained in:
Ken Rockot
2021-01-07 21:26:55 +00:00
committed by Chromium LUCI CQ
parent b8a846a4a0
commit 54b704a56c
12 changed files with 667 additions and 68 deletions

@ -61,3 +61,26 @@ struct StructVersionTest {
interface InterfaceVersionTest {
Foo(int32 x, [MinVersion=1] int32 y) => (int32 z, [MinVersion=1] int32 w);
};
interface Counter {
// Two different varieties of observer addition to exercise sending remotes
// and receiving receivers.
AddObserver(pending_associated_remote<CounterObserver> observer);
AddNewObserver() => (pending_associated_receiver<CounterObserver> receiver);
RemoveAllObservers();
// Two different varieties of cloning to exercise sending receivers and
// receiving remotes.
Clone(pending_associated_receiver<Counter> receiver);
CloneToNewRemote() => (pending_associated_remote<Counter> remote);
// Increments the counter, notifies all observers, then replies. Because
// observers are associated with this interface, they are therefore guaranteed
// to observe an increment before the caller observes its corresponding reply.
Increment() => (int32 count);
};
interface CounterObserver {
OnCountChanged(int32 count);
OnCloneDisconnected();
};

@ -164,6 +164,7 @@ static_library("web_test_browser") {
"//content/public/browser", # For component builds.
"//content/shell:content_shell_lib",
"//content/test:blink_test_browser_support",
"//content/test:mojo_bindings_web_test_mojom",
"//content/test:mojo_web_test_bindings",
"//content/test:test_support",
"//device/bluetooth:fake_bluetooth",

@ -30,6 +30,7 @@
#include "content/public/common/content_switches.h"
#include "content/shell/browser/shell_browser_context.h"
#include "content/shell/browser/shell_content_browser_client.h"
#include "content/test/data/mojo_bindings_web_test.test-mojom.h"
#include "content/test/data/mojo_web_test_helper_test.mojom.h"
#include "content/test/mock_badge_service.h"
#include "content/test/mock_clipboard_host.h"
@ -52,8 +53,11 @@
#include "device/bluetooth/public/mojom/test/fake_bluetooth.mojom.h"
#include "device/bluetooth/test/fake_bluetooth.h"
#include "gpu/config/gpu_switches.h"
#include "mojo/public/cpp/bindings/associated_receiver_set.h"
#include "mojo/public/cpp/bindings/binder_map.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/remote_set.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/net_buildflags.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "services/service_manager/public/cpp/manifest.h"
@ -142,6 +146,65 @@ void CreateChildProcessCrashWatcher() {
static base::NoDestructor<ChildProcessCrashWatcher> watcher;
}
class MojoWebTestCounterImpl : public mojo_bindings_test::mojom::Counter {
public:
using CounterObserver = mojo_bindings_test::mojom::CounterObserver;
MojoWebTestCounterImpl() {
additional_receivers_.set_disconnect_handler(base::BindRepeating(
&MojoWebTestCounterImpl::OnCloneDisconnected, base::Unretained(this)));
}
~MojoWebTestCounterImpl() override = default;
static void Bind(mojo::PendingReceiver<Counter> receiver) {
mojo::MakeSelfOwnedReceiver(std::make_unique<MojoWebTestCounterImpl>(),
std::move(receiver));
}
// mojo_bindings_test::mojom::Counter:
void AddObserver(
mojo::PendingAssociatedRemote<CounterObserver> observer) override {
observers_.Add(std::move(observer));
}
void AddNewObserver(AddNewObserverCallback callback) override {
mojo::PendingAssociatedRemote<CounterObserver> observer;
std::move(callback).Run(observer.InitWithNewEndpointAndPassReceiver());
observers_.Add(std::move(observer));
}
void RemoveAllObservers() override { observers_.Clear(); }
void Clone(mojo::PendingAssociatedReceiver<Counter> receiver) override {
additional_receivers_.Add(this, std::move(receiver));
}
void CloneToNewRemote(CloneToNewRemoteCallback callback) override {
mojo::PendingAssociatedRemote<Counter> new_remote;
additional_receivers_.Add(this,
new_remote.InitWithNewEndpointAndPassReceiver());
std::move(callback).Run(std::move(new_remote));
}
void Increment(IncrementCallback callback) override {
++count_;
for (const auto& observer : observers_)
observer->OnCountChanged(count_);
std::move(callback).Run(count_);
}
private:
void OnCloneDisconnected() {
for (const auto& observer : observers_)
observer->OnCloneDisconnected();
}
int count_ = 0;
mojo::AssociatedReceiverSet<Counter> additional_receivers_;
mojo::AssociatedRemoteSet<CounterObserver> observers_;
};
} // namespace
WebTestContentBrowserClient::WebTestContentBrowserClient() {
@ -205,6 +268,8 @@ void WebTestContentBrowserClient::ExposeInterfacesToRenderer(
RenderProcessHost* render_process_host) {
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner =
content::GetUIThreadTaskRunner({});
registry->AddInterface(base::BindRepeating(&MojoWebTestCounterImpl::Bind),
ui_task_runner);
registry->AddInterface(base::BindRepeating(&MojoEcho::Bind), ui_task_runner);
registry->AddInterface(
base::BindRepeating(&WebTestBluetoothFakeAdapterSetterImpl::Create),

@ -103,6 +103,7 @@ concatenate_files("bindings_uncompiled_module") {
"bindings_uncompiled_module_preamble.js.part",
"bindings_lite.js",
"$root_gen_dir/mojo/public/interfaces/bindings/interface_control_messages.mojom-lite.js",
"$root_gen_dir/mojo/public/interfaces/bindings/pipe_control_messages.mojom-lite.js",
"interface_support.js",
"bindings_uncompiled_module_export.js.part",
]
@ -146,6 +147,7 @@ if (enable_mojom_closure_compile || enable_js_type_check) {
inputs = [
"$target_gen_dir/mojo_internal.js",
"$root_gen_dir/mojo/public/interfaces/bindings/interface_control_messages.mojom-lite-for-compile.js",
"$root_gen_dir/mojo/public/interfaces/bindings/pipe_control_messages.mojom-lite-for-compile.js",
"$target_gen_dir/interface_support.js",
]
script = "//mojo/public/tools/bindings/concatenate_and_replace_closure_exports.py"

@ -20,6 +20,9 @@ mojo.internal.kMessageV0HeaderSize = 24;
/** @const {number} */
mojo.internal.kMessageV1HeaderSize = 32;
/** @const {number} */
mojo.internal.kMessageV2HeaderSize = 48;
/** @const {number} */
mojo.internal.kMapDataSize = 24;
@ -32,6 +35,9 @@ mojo.internal.kMessageFlagExpectsResponse = 1 << 0;
/** @const {number} */
mojo.internal.kMessageFlagIsResponse = 1 << 1;
/** @const {number} */
mojo.internal.kInterfaceNamespaceBit = 0x80000000;
/** @const {boolean} */
mojo.internal.kHostLittleEndian = (function() {
const wordBytes = new Uint8Array(new Uint16Array([1]).buffer);
@ -137,6 +143,14 @@ mojo.internal.getUint64 = function(dataView, byteOffset) {
return (BigInt(high) << BigInt(32)) | BigInt(low);
};
/**
* @typedef {{
* size: number,
* numInterfaceIds: (number|undefined),
* }}
*/
mojo.internal.MessageDimensions;
/**
* This computes the total amount of buffer space required to hold a struct
* value and all its fields, including indirect objects like arrays, structs,
@ -144,32 +158,41 @@ mojo.internal.getUint64 = function(dataView, byteOffset) {
*
* @param {!mojo.internal.StructSpec} structSpec
* @param {!Object} value
* @return {number}
* @return {!mojo.internal.MessageDimensions}
*/
mojo.internal.computeTotalStructSize = function(structSpec, value) {
mojo.internal.computeStructDimensions = function(structSpec, value) {
let size = structSpec.packedSize;
let numInterfaceIds = 0;
for (const field of structSpec.fields) {
const fieldValue = value[field.name];
if (field.type.$.computePayloadSize &&
!mojo.internal.isNullOrUndefined(fieldValue)) {
size += mojo.internal.align(
field.type.$.computePayloadSize(fieldValue, field.nullable), 8);
if (mojo.internal.isNullOrUndefined(fieldValue)) {
continue;
}
if (field.type.$.computeDimensions) {
const fieldDimensions =
field.type.$.computeDimensions(fieldValue, field.nullable);
size += mojo.internal.align(fieldDimensions.size, 8);
numInterfaceIds += fieldDimensions.numInterfaceIds;
} else if (field.type.$.hasInterfaceId) {
numInterfaceIds++;
}
}
return size;
return {size, numInterfaceIds};
};
/**
* @param {!mojo.internal.UnionSpec} unionSpec
* @param {!Object} value
* @return {number}
* @return {!mojo.internal.MessageDimensions}
*/
mojo.internal.computeTotalUnionSize = function(unionSpec, nullable, value) {
mojo.internal.computeUnionDimensions = function(unionSpec, nullable, value) {
// Unions are normally inlined since they're always a fixed width of 16
// bytes, but nullable union-typed fields require indirection. Hence this
// unique special case where a union field requires additional storage
// beyond the struct's own packed field data only when it's nullable.
let size = nullable ? mojo.internal.kUnionDataSize : 0;
let numInterfaceIds = 0;
const keys = Object.keys(value);
if (keys.length !== 1) {
@ -183,19 +206,20 @@ mojo.internal.computeTotalUnionSize = function(unionSpec, nullable, value) {
const field = unionSpec.fields[tag];
const fieldValue = value[tag];
if (!mojo.internal.isNullOrUndefined(fieldValue)) {
if (field['type'].$.unionSpec) {
// Nested unions are always encoded with indirection, which we induce by
// claiming the field is nullable even if it's not.
size += mojo.internal.align(
field['type'].$.computePayloadSize(fieldValue, true /* nullable */),
8);
} else if (field['type'].$.computePayloadSize) {
size += mojo.internal.align(
field['type'].$.computePayloadSize(fieldValue, field['nullable']), 8);
// Nested unions are always encoded with indirection, which we induce by
// claiming the field is nullable even if it's not.
if (field['type'].$.computeDimensions) {
const nullable = !!field['type'].$.unionSpec || field['nullable'];
const fieldDimensions =
field['type'].$.computeDimensions(fieldValue, nullable);
size += mojo.internal.align(fieldDimensions.size, 8);
numInterfaceIds += fieldDimensions.numInterfaceIds;
} else if (field['type'].$.hasInterfaceId) {
numInterfaceIds++;
}
}
return size;
return {size, numInterfaceIds};
};
/**
@ -220,15 +244,16 @@ mojo.internal.computeInlineArraySize = function(arraySpec, value) {
*/
mojo.internal.computeTotalArraySize = function(arraySpec, value) {
const inlineSize = mojo.internal.computeInlineArraySize(arraySpec, value);
if (!arraySpec.elementType.$.computePayloadSize)
if (!arraySpec.elementType.$.computeDimensions)
return inlineSize;
let totalSize = inlineSize;
for (let elementValue of value) {
if (!mojo.internal.isNullOrUndefined(elementValue)) {
totalSize += mojo.internal.align(
arraySpec.elementType.$.computePayloadSize(
elementValue, !!arraySpec.elementNullable),
arraySpec.elementType.$
.computeDimensions(elementValue, !!arraySpec.elementNullable)
.size,
8);
}
}
@ -239,6 +264,7 @@ mojo.internal.computeTotalArraySize = function(arraySpec, value) {
/** Owns an outgoing message buffer and facilitates serialization. */
mojo.internal.Message = class {
/**
* @param {?mojo.internal.interfaceSupport.Endpoint} sender
* @param {number} interfaceId
* @param {number} flags
* @param {number} ordinal
@ -247,9 +273,17 @@ mojo.internal.Message = class {
* @param {!Object} value
* @public
*/
constructor(interfaceId, flags, ordinal, requestId, paramStructSpec, value) {
constructor(
sender, interfaceId, flags, ordinal, requestId, paramStructSpec, value) {
const dimensions =
mojo.internal.computeStructDimensions(paramStructSpec, value);
let headerSize, version;
if ((flags &
if (dimensions.numInterfaceIds > 0) {
headerSize = mojo.internal.kMessageV2HeaderSize;
version = 2;
} else if (
(flags &
(mojo.internal.kMessageFlagExpectsResponse |
mojo.internal.kMessageFlagIsResponse)) == 0) {
headerSize = mojo.internal.kMessageV0HeaderSize;
@ -259,8 +293,12 @@ mojo.internal.Message = class {
version = 1;
}
const totalMessageSize = headerSize +
mojo.internal.computeTotalStructSize(paramStructSpec, value);
const headerWithPayloadSize = headerSize + dimensions.size;
const interfaceIdsSize = dimensions.numInterfaceIds > 0 ?
mojo.internal.kArrayHeaderSize + dimensions.numInterfaceIds * 4 :
0;
const paddedInterfaceIdsSize = mojo.internal.align(interfaceIdsSize, 8);
const totalMessageSize = headerWithPayloadSize + paddedInterfaceIdsSize;
/** @public {!ArrayBuffer} */
this.buffer = new ArrayBuffer(totalMessageSize);
@ -275,14 +313,38 @@ mojo.internal.Message = class {
header.setUint32(12, ordinal, mojo.internal.kHostLittleEndian);
header.setUint32(16, flags, mojo.internal.kHostLittleEndian);
header.setUint32(20, 0); // Padding
if (version > 0)
if (version >= 1) {
mojo.internal.setUint64(header, 24, requestId);
if (version >= 2) {
mojo.internal.setUint64(header, 32, BigInt(16));
mojo.internal.setUint64(header, 40, BigInt(headerWithPayloadSize - 40));
header.setUint32(
headerWithPayloadSize, interfaceIdsSize,
mojo.internal.kHostLittleEndian);
header.setUint32(
headerWithPayloadSize + 4, dimensions.numInterfaceIds || 0,
mojo.internal.kHostLittleEndian);
}
}
/** @private {number} */
this.nextInterfaceIdIndex_ = 0;
/** @private {?Uint32Array} */
this.interfaceIds_ = null;
if (dimensions.numInterfaceIds) {
this.interfaceIds_ = new Uint32Array(
this.buffer, headerWithPayloadSize + mojo.internal.kArrayHeaderSize,
dimensions.numInterfaceIds);
}
/** @private {number} */
this.nextAllocationOffset_ = headerSize;
const paramStructData = this.allocate(paramStructSpec.packedSize);
const encoder = new mojo.internal.Encoder(this, paramStructData);
const encoder =
new mojo.internal.Encoder(this, paramStructData, {endpoint: sender});
encoder.encodeStructInline(paramStructSpec, value);
}
@ -299,6 +361,15 @@ mojo.internal.Message = class {
}
};
/**
* Additional context to aid in encoding and decoding of message data.
*
* @typedef {{
* endpoint: ?mojo.internal.interfaceSupport.Endpoint,
* }}
*/
mojo.internal.MessageContext;
/**
* Helps encode outgoing messages. Encoders may be created recursively to encode
* parial message fragments indexed by indirect message offsets, as with encoded
@ -308,9 +379,13 @@ mojo.internal.Encoder = class {
/**
* @param {!mojo.internal.Message} message
* @param {!DataView} data
* @param {?mojo.internal.MessageContext=} context
* @public
*/
constructor(message, data) {
constructor(message, data, context = null) {
/** @const {?mojo.internal.MessageContext} */
this.context_ = context;
/** @private {!mojo.internal.Message} */
this.message_ = message;
@ -371,6 +446,16 @@ mojo.internal.Encoder = class {
this.message_.handles.push(value);
}
encodeAssociatedEndpoint(offset, endpoint) {
console.assert(
endpoint.isPendingAssociation, 'expected unbound associated endpoint');
const sender = this.context_.endpoint;
const id = sender.associatePeerOfOutgoingEndpoint(endpoint);
const index = this.message_.nextInterfaceIdIndex_++;
this.encodeUint32(offset, index);
this.message_.interfaceIds_[index] = id;
}
encodeString(offset, value) {
if (typeof value !== 'string')
throw new Error('Unxpected non-string value for string field.');
@ -391,7 +476,8 @@ mojo.internal.Encoder = class {
encodeArray(arraySpec, offset, value) {
const arraySize = mojo.internal.computeInlineArraySize(arraySpec, value);
const arrayData = this.message_.allocate(arraySize);
const arrayEncoder = new mojo.internal.Encoder(this.message_, arrayData);
const arrayEncoder =
new mojo.internal.Encoder(this.message_, arrayData, this.context_);
this.encodeOffset(offset, arrayData.byteOffset);
arrayEncoder.encodeUint32(0, arraySize);
@ -442,7 +528,8 @@ mojo.internal.Encoder = class {
}
const mapData = this.message_.allocate(mojo.internal.kMapDataSize);
const mapEncoder = new mojo.internal.Encoder(this.message_, mapData);
const mapEncoder =
new mojo.internal.Encoder(this.message_, mapData, this.context_);
this.encodeOffset(offset, mapData.byteOffset);
mapEncoder.encodeUint32(0, mojo.internal.kMapDataSize);
@ -463,7 +550,8 @@ mojo.internal.Encoder = class {
*/
encodeStruct(structSpec, offset, value) {
const structData = this.message_.allocate(structSpec.packedSize);
const structEncoder = new mojo.internal.Encoder(this.message_, structData);
const structEncoder =
new mojo.internal.Encoder(this.message_, structData, this.context_);
this.encodeOffset(offset, structData.byteOffset);
structEncoder.encodeStructInline(structSpec, value);
}
@ -513,7 +601,8 @@ mojo.internal.Encoder = class {
*/
encodeUnionAsPointer(unionSpec, offset, value) {
const unionData = this.message_.allocate(mojo.internal.kUnionDataSize);
const unionEncoder = new mojo.internal.Encoder(this.message_, unionData);
const unionEncoder =
new mojo.internal.Encoder(this.message_, unionData, this.context_);
this.encodeOffset(offset, unionData.byteOffset);
unionEncoder.encodeUnion(unionSpec, /*offset=*/0, value);
}
@ -571,8 +660,12 @@ mojo.internal.Decoder = class {
/**
* @param {!DataView} data
* @param {!Array<MojoHandle>} handles
* @param {?mojo.internal.MessageContext=} context
*/
constructor(data, handles) {
constructor(data, handles, context = null) {
/** @private {?mojo.internal.MessageContext} */
this.context_ = context;
/** @private {!DataView} */
this.data_ = data;
@ -664,7 +757,8 @@ mojo.internal.Decoder = class {
return null;
const arrayDecoder = new mojo.internal.Decoder(
new DataView(this.data_.buffer, arrayOffset), this.handles_);
new DataView(this.data_.buffer, arrayOffset), this.handles_,
this.context_);
const size = arrayDecoder.decodeUint32(0);
const numElements = arrayDecoder.decodeUint32(4);
@ -700,7 +794,8 @@ mojo.internal.Decoder = class {
return null;
const mapDecoder = new mojo.internal.Decoder(
new DataView(this.data_.buffer, mapOffset), this.handles_);
new DataView(this.data_.buffer, mapOffset), this.handles_,
this.context_);
const mapStructSize = mapDecoder.decodeUint32(0);
const mapStructVersion = mapDecoder.decodeUint32(4);
if (mapStructSize != mojo.internal.kMapDataSize || mapStructVersion != 0)
@ -739,7 +834,8 @@ mojo.internal.Decoder = class {
return null;
const decoder = new mojo.internal.Decoder(
new DataView(this.data_.buffer, structOffset), this.handles_);
new DataView(this.data_.buffer, structOffset), this.handles_,
this.context_);
return decoder.decodeStructInline(structSpec);
}
@ -815,7 +911,8 @@ mojo.internal.Decoder = class {
return null;
const decoder = new mojo.internal.Decoder(
new DataView(this.data_.buffer, unionOffset), this.handles_);
new DataView(this.data_.buffer, unionOffset), this.handles_,
this.context_);
return decoder.decodeUnion(unionSpec, 0);
}
@ -870,6 +967,25 @@ mojo.internal.Decoder = class {
return null;
return new type(mojo.internal.interfaceSupport.createEndpoint(handle));
}
decodeAssociatedEndpoint(offset) {
if (!this.context_ || !this.context_.endpoint) {
throw new Error('cannot deserialize associated endpoint without context');
}
const receivingEndpoint = this.context_.endpoint;
const message = new DataView(this.data_.buffer);
const interfaceIdsOffset = Number(mojo.internal.getUint64(message, 40));
const numInterfaceIds = message.getUint32(
interfaceIdsOffset + 44, mojo.internal.kHostLittleEndian);
const interfaceIds = new Uint32Array(
message.buffer,
interfaceIdsOffset + mojo.internal.kArrayHeaderSize + 40,
numInterfaceIds);
const index = this.decodeUint32(offset);
const interfaceId = interfaceIds[index];
return new mojo.internal.interfaceSupport.Endpoint(
receivingEndpoint.router, interfaceId);
}
};
/** @type {TextDecoder} */
@ -918,8 +1034,10 @@ mojo.internal.deserializeMessageHeader = function(data) {
* encode: function(*, !mojo.internal.Encoder, number, number, boolean),
* encodeNull: ((function(!mojo.internal.Encoder, number))|undefined),
* decode: function(!mojo.internal.Decoder, number, number, boolean):*,
* computePayloadSize: ((function(*, boolean):number)|undefined),
* computeDimensions:
* ((function(*, boolean):!mojo.internal.MessageDimensions)|undefined),
* isValidObjectKeyType: boolean,
* hasInterfaceId: (boolean|undefined),
* arrayElementSize: ((function(boolean):number)|undefined),
* arraySpec: (!mojo.internal.ArraySpec|undefined),
* mapSpec: (!mojo.internal.MapSpec|undefined),
@ -1217,10 +1335,11 @@ mojo.internal.String = {
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeString(byteOffset);
},
computePayloadSize: function(value, nullable) {
return mojo.internal.computeTotalArraySize(
computeDimensions: function(value, nullable) {
const size = mojo.internal.computeTotalArraySize(
{elementType: mojo.internal.Uint8},
mojo.internal.Encoder.stringToUtf8Bytes(value));
return {size};
},
arrayElementSize: nullable => 8,
isValidObjectKeyType: true,
@ -1249,8 +1368,8 @@ mojo.internal.Array = function(elementType, elementNullable) {
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeArray(arraySpec, byteOffset);
},
computePayloadSize: function(value, nullable) {
return mojo.internal.computeTotalArraySize(arraySpec, value);
computeDimensions: function(value, nullable) {
return {size: mojo.internal.computeTotalArraySize(arraySpec, value)};
},
arrayElementSize: nullable => 8,
isValidObjectKeyType: false,
@ -1282,13 +1401,13 @@ mojo.internal.Map = function(keyType, valueType, valueNullable) {
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeMap(mapSpec, byteOffset);
},
computePayloadSize: function(value, nullable) {
computeDimensions: function(value, nullable) {
const keys = (value instanceof Map) ? Array.from(value.keys()) :
Object.keys(value);
const values = (value instanceof Map) ? Array.from(value.values()) :
keys.map(k => value[k]);
return mojo.internal.kMapDataSize +
const size = mojo.internal.kMapDataSize +
mojo.internal.computeTotalArraySize({elementType: keyType}, keys) +
mojo.internal.computeTotalArraySize(
{
@ -1296,6 +1415,7 @@ mojo.internal.Map = function(keyType, valueType, valueNullable) {
elementNullable: valueNullable,
},
values);
return {size};
},
arrayElementSize: nullable => 8,
isValidObjectKeyType: false,
@ -1371,8 +1491,8 @@ mojo.internal.Struct = function(
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeStruct(structSpec, byteOffset);
},
computePayloadSize: function(value, nullable) {
return mojo.internal.computeTotalStructSize(structSpec, value);
computeDimensions: function(value, nullable) {
return mojo.internal.computeStructDimensions(structSpec, value);
},
arrayElementSize: nullable => 8,
isValidObjectKeyType: false,
@ -1416,8 +1536,8 @@ mojo.internal.Union = function(objectToBlessAsUnion, name, fields) {
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeUnion(unionSpec, byteOffset);
},
computePayloadSize: function(value, nullable) {
return mojo.internal.computeTotalUnionSize(unionSpec, nullable, value);
computeDimensions: function(value, nullable) {
return mojo.internal.computeUnionDimensions(unionSpec, nullable, value);
},
arrayElementSize: nullable => (nullable ? 8 : 16),
isValidObjectKeyType: false,
@ -1490,12 +1610,27 @@ mojo.internal.AssociatedInterfaceProxy = function(type) {
$: {
type: type,
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
throw new Error('Associated interfaces not supported yet.');
console.assert(
value instanceof type,
`unexpected object in place of ${type.name}: `, value);
console.assert(
value.proxy.endpoint && value.proxy.endpoint.isPendingAssociation,
`expected ${type.name} to be associated and unbound`);
encoder.encodeAssociatedEndpoint(byteOffset, value.proxy.endpoint);
encoder.encodeUint32(byteOffset + 4, 0);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeUint32(byteOffset, 0xffffffff);
encoder.encodeUint32(byteOffset + 4, 0);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
throw new Error('Associated interfaces not supported yet.');
return new type(decoder.decodeAssociatedEndpoint(byteOffset));
},
arrayElementSize: _ => {
throw new Error('Arrays of associated endpoints are not yet supported');
},
isValidObjectKeyType: false,
hasInterfaceId: true,
},
};
};
@ -1509,12 +1644,26 @@ mojo.internal.AssociatedInterfaceRequest = function(type) {
$: {
type: type,
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
throw new Error('Associated interfaces not supported yet.');
console.assert(
value instanceof type,
`unexpected object in place of ${type.name}: `, value);
console.assert(
value.handle && value.handle.isPendingAssociation,
`expected ${type.name} to be associated and unbound`);
encoder.encodeAssociatedEndpoint(byteOffset, value.handle);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeUint32(byteOffset, 0xffffffff);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
throw new Error('Associated interfaces not supported yet.');
return new type(decoder.decodeAssociatedEndpoint(byteOffset));
},
arrayElementSize: _ => {
throw new Error('Arrays of associated endpoints are not yet supported');
},
isValidObjectKeyType: false,
hasInterfaceId: true,
},
};
};

@ -1,5 +1,5 @@
const mojoTmp = self['mojo'];
export {mojoTmp as mojo};
if (preservedGlobalMojo) {
window.mojo = preservedGlobalMojo;
self.mojo = preservedGlobalMojo;
}

@ -1 +1 @@
const preservedGlobalMojo = window && window.mojo;
const preservedGlobalMojo = self && self.mojo;

@ -1,4 +1,5 @@
const mojo = {
internal: { interfaceSupport: {} },
interfaceControl: {}
interfaceControl: {},
pipeControl: {},
};

@ -9,9 +9,10 @@
mojo.internal.interfaceSupport.Router = class {
/**
* @param {!MojoHandle} pipe
* @param {boolean} setNamespaceBit
* @public
*/
constructor(pipe) {
constructor(pipe, setNamespaceBit) {
/** @const {!MojoHandle} */
this.pipe_ = pipe;
@ -22,6 +23,18 @@ mojo.internal.interfaceSupport.Router = class {
/** @const {!Map<number, !mojo.internal.interfaceSupport.Endpoint>} */
this.endpoints_ = new Map();
/** @private {number} */
this.nextInterfaceId_ = 1;
/** @const {number} */
this.interfaceIdNamespace_ =
setNamespaceBit ? mojo.internal.kInterfaceNamespaceBit : 0;
/** @const {!mojo.internal.interfaceSupport.PipeControlMessageHandler} */
this.pipeControlHandler_ =
new mojo.internal.interfaceSupport.PipeControlMessageHandler(
this, this.onPeerEndpointClosed_.bind(this));
}
/** @return {!MojoHandle} */
@ -29,6 +42,11 @@ mojo.internal.interfaceSupport.Router = class {
return this.pipe_;
}
/** @return {number} */
generateInterfaceId() {
return (this.nextInterfaceId_++ | this.interfaceIdNamespace_) >>> 0;
}
/**
* @param {!mojo.internal.interfaceSupport.Endpoint} endpoint
* @param {number} interfaceId
@ -57,6 +75,16 @@ mojo.internal.interfaceSupport.Router = class {
this.reader_.stopAndCloseHandle();
}
/** @param {number} interfaceId */
closeEndpoint(interfaceId) {
this.removeEndpoint(interfaceId);
if (interfaceId === 0) {
this.close();
} else {
this.pipeControlHandler_.notifyEndpointClosed(interfaceId);
}
}
/** @return {boolean} */
isReading() {
return !this.reader_.isStopped();
@ -79,6 +107,10 @@ mojo.internal.interfaceSupport.Router = class {
}
const header = mojo.internal.deserializeMessageHeader(new DataView(buffer));
if (this.pipeControlHandler_.maybeHandleMessage(header, buffer)) {
return;
}
const endpoint = this.endpoints_.get(header.interfaceId);
if (!endpoint) {
console.error(
@ -95,6 +127,14 @@ mojo.internal.interfaceSupport.Router = class {
}
this.endpoints_.clear();
}
/** @param {number} id */
onPeerEndpointClosed_(id) {
const endpoint = this.endpoints_.get(id);
if (endpoint) {
endpoint.onError();
}
}
};
/**
@ -144,6 +184,28 @@ mojo.internal.interfaceSupport.Endpoint = class {
/** @private {number} */
this.nextRequestId_ = 0;
/** @private {mojo.internal.interfaceSupport.Endpoint} */
this.localPeer_ = null;
}
/**
* @return {{
* endpoint0: !mojo.internal.interfaceSupport.Endpoint,
* endpoint1: !mojo.internal.interfaceSupport.Endpoint,
* }}
*/
static createAssociatedPair() {
const endpoint0 = new mojo.internal.interfaceSupport.Endpoint();
const endpoint1 = new mojo.internal.interfaceSupport.Endpoint();
endpoint1.localPeer_ = endpoint0;
endpoint0.localPeer_ = endpoint1;
return {endpoint0, endpoint1};
}
/** @return {mojo.internal.interfaceSupport.Router} */
get router() {
return this.router_;
}
/** @return {boolean} */
@ -157,6 +219,11 @@ mojo.internal.interfaceSupport.Endpoint = class {
return this.router_.pipe;
}
/** @return {boolean} */
get isPendingAssociation() {
return this.localPeer_ !== null;
}
/**
* @param {string} interfaceName
* @param {string} scope
@ -168,6 +235,24 @@ mojo.internal.interfaceSupport.Endpoint = class {
Mojo.bindInterface(interfaceName, this.router_.pipe, scope);
}
/**
* @param {!mojo.internal.interfaceSupport.Endpoint} endpoint
* @return {number}
*/
associatePeerOfOutgoingEndpoint(endpoint) {
console.assert(this.router_, 'cannot associate with unbound endpoint');
const peer = endpoint.localPeer_;
endpoint.localPeer_ = peer.localPeer_ = null;
const id = this.router_.generateInterfaceId();
peer.router_ = this.router_;
peer.interfaceId_ = id;
if (peer.client_) {
this.router_.addEndpoint(peer, id);
}
return id;
}
/** @return {number} */
generateRequestId() {
const id = this.nextRequestId_++;
@ -186,7 +271,7 @@ mojo.internal.interfaceSupport.Endpoint = class {
*/
send(ordinal, requestId, flags, paramStruct, value) {
const message = new mojo.internal.Message(
this.interfaceId_, flags, ordinal, requestId,
this, this.interfaceId_, flags, ordinal, requestId,
/** @type {!mojo.internal.StructSpec} */ (paramStruct.$.structSpec),
value);
console.assert(
@ -196,10 +281,11 @@ mojo.internal.interfaceSupport.Endpoint = class {
/** @param {mojo.internal.interfaceSupport.EndpointClient} client */
start(client) {
console.assert(this.router_, 'starting unassociated secondary endpoint');
console.assert(!this.client_, 'endpoint already started');
this.client_ = client;
this.router_.addEndpoint(this, this.interfaceId_);
if (this.router_) {
this.router_.addEndpoint(this, this.interfaceId_);
}
}
/** @return {boolean} */
@ -216,10 +302,11 @@ mojo.internal.interfaceSupport.Endpoint = class {
}
close() {
this.stop();
if (this.isPrimary()) {
this.router_.close();
if (this.router_) {
this.router.closeEndpoint(this.interfaceId_);
}
this.client_ = null;
this.controlMessageHandler_ = null;
}
async flushForTesting() {
@ -253,11 +340,13 @@ mojo.internal.interfaceSupport.Endpoint = class {
* Creates a new Endpoint wrapping a given pipe handle.
*
* @param {!MojoHandle} pipe
* @param {boolean=} setNamespaceBit
* @return {!mojo.internal.interfaceSupport.Endpoint}
*/
mojo.internal.interfaceSupport.createEndpoint = function(pipe) {
mojo.internal.interfaceSupport.createEndpoint = function(
pipe, setNamespaceBit = false) {
return new mojo.internal.interfaceSupport.Endpoint(
new mojo.internal.interfaceSupport.Router(pipe), 0);
new mojo.internal.interfaceSupport.Router(pipe, setNamespaceBit), 0);
};
/**
@ -288,6 +377,62 @@ mojo.internal.interfaceSupport.bind = function(endpoint, interfaceName, scope) {
endpoint.bindInBrowser(interfaceName, scope);
};
mojo.internal.interfaceSupport.PipeControlMessageHandler = class {
/**
* @param {!mojo.internal.interfaceSupport.Router} router
* @param {function(number)} onDisconnect
*/
constructor(router, onDisconnect) {
/** @const {!mojo.internal.interfaceSupport.Router} */
this.router_ = router;
/** @const {function(number)} */
this.onDisconnect_ = onDisconnect;
}
/**
* @param {!mojo.pipeControl.RunOrClosePipeInput} input
*/
send(input) {
const spec = /** @type {!mojo.internal.StructSpec} */ (
mojo.pipeControl.RunOrClosePipeMessageParamsSpec.$.$.structSpec);
const message = new mojo.internal.Message(
null, 0xffffffff, 0, mojo.pipeControl.RUN_OR_CLOSE_PIPE_MESSAGE_ID, 0,
/** @type {!mojo.internal.StructSpec} */
(mojo.pipeControl.RunOrClosePipeMessageParamsSpec.$.$.structSpec),
{'input': input});
this.router_.send(message);
}
/**
* @param {!mojo.internal.MessageHeader} header
* @param {!ArrayBuffer} buffer
* @return {boolean}
*/
maybeHandleMessage(header, buffer) {
if (header.ordinal !== mojo.pipeControl.RUN_OR_CLOSE_PIPE_MESSAGE_ID) {
return false;
}
const data = new DataView(buffer, header.headerSize);
const decoder = new mojo.internal.Decoder(data, []);
const spec = /** @type {!mojo.internal.StructSpec} */ (
mojo.pipeControl.RunOrClosePipeMessageParamsSpec.$.$.structSpec);
const input = decoder.decodeStructInline(spec)['input'];
if (input.hasOwnProperty('peerAssociatedEndpointClosedEvent')) {
this.onDisconnect_(input['peerAssociatedEndpointClosedEvent']['id']);
return true;
}
return true;
}
/**@param {number} interfaceId */
notifyEndpointClosed(interfaceId) {
this.send({'peerAssociatedEndpointClosedEvent': {'id': interfaceId}});
}
};
/**
* Handles incoming interface control messages on an interface endpoint.
*/
@ -467,6 +612,11 @@ mojo.internal.interfaceSupport.InterfaceRemoteBase = class {
}
}
/** @return {mojo.internal.interfaceSupport.Endpoint} */
get endpoint() {
return this.endpoint_;
}
/**
* @return {!mojo.internal.interfaceSupport.PendingReceiver}
*/
@ -484,13 +634,23 @@ mojo.internal.interfaceSupport.InterfaceRemoteBase = class {
bindHandle(handle) {
console.assert(!this.endpoint_, 'already bound');
if (handle instanceof MojoHandle) {
handle = mojo.internal.interfaceSupport.createEndpoint(handle);
handle = mojo.internal.interfaceSupport.createEndpoint(
handle, /* setNamespaceBit */ true);
}
this.endpoint_ = handle;
this.endpoint_.start(this);
this.pendingResponses_ = new Map;
}
/** @export */
associateAndPassReceiver() {
console.assert(!this.endpoint_, 'cannot associate when already bound');
const {endpoint0, endpoint1} =
mojo.internal.interfaceSupport.Endpoint.createAssociatedPair();
this.bindHandle(endpoint0);
return new this.requestType_(endpoint1);
}
/**
* @return {?mojo.internal.interfaceSupport.Endpoint}
* @export
@ -575,7 +735,7 @@ mojo.internal.interfaceSupport.InterfaceRemoteBase = class {
if (!pendingResponse)
return this.onError(endpoint, 'Received unexpected response message');
const decoder = new mojo.internal.Decoder(
new DataView(buffer, header.headerSize), handles);
new DataView(buffer, header.headerSize), handles, {endpoint});
const responseValue = decoder.decodeStructInline(
/** @type {!mojo.internal.StructSpec} */ (
pendingResponse.responseStruct.$.structSpec));
@ -633,6 +793,14 @@ mojo.internal.interfaceSupport.InterfaceRemoteBaseWrapper = class {
return this.remote_.bindNewPipeAndPassReceiver();
}
/**
* @return {!T}
* @export
*/
associateAndPassReceiver() {
return this.remote_.associateAndPassReceiver();
}
/** @export */
close() {
this.remote_.close();
@ -831,6 +999,17 @@ mojo.internal.interfaceSupport.InterfaceReceiverHelperInternal = class {
return remote;
}
/**
* @return {!T}
* @export
*/
associateAndPassRemote() {
const {endpoint0, endpoint1} =
mojo.internal.interfaceSupport.Endpoint.createAssociatedPair();
this.bindHandle(endpoint0);
return new this.remoteType_(endpoint1);
}
/** @export */
closeBindings() {
for (const endpoint of this.endpoints_) {
@ -855,7 +1034,7 @@ mojo.internal.interfaceSupport.InterfaceReceiverHelperInternal = class {
if (!handler)
throw new Error('Received unknown message');
const decoder = new mojo.internal.Decoder(
new DataView(buffer, header.headerSize), handles);
new DataView(buffer, header.headerSize), handles, {endpoint});
const request = decoder.decodeStructInline(
/** @type {!mojo.internal.StructSpec} */ (
handler.paramStruct.$.structSpec));
@ -945,6 +1124,14 @@ mojo.internal.interfaceSupport.InterfaceReceiverHelper = class {
return this.helper_internal_.bindNewPipeAndPassRemote();
}
/**
* @return {!T}
* @export
*/
associateAndPassRemote() {
return this.helper_internal_.associateAndPassRemote();
}
/** @export */
close() {
this.helper_internal_.closeBindings();

@ -4,5 +4,9 @@ goog.require('mojo.interfaceControl.RUN_MESSAGE_ID');
goog.require('mojo.interfaceControl.RunResponseMessageParamsSpec');
goog.require('mojo.internal');
goog.require('mojo.pipeControl.RUN_OR_CLOSE_PIPE_MESSAGE_ID');
goog.require('mojo.pipeControl.RunOrClosePipeMessageParamsSpec');
goog.require('mojo.pipeControl.RunOrClosePipeInput');
goog.provide('mojo.internal.interfaceSupport');

@ -0,0 +1,4 @@
<!DOCTYPE html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./associated-interfaces.js" type="module"></script>

@ -0,0 +1,163 @@
import {Counter, CounterObserverReceiver, CounterRemote} from '/gen/content/test/data/mojo_bindings_web_test.test-mojom.m.js';
class ObserverImpl {
constructor() {
this.count_ = null;
this.disconnectResolvers_ = [];
}
get count() {
return this.count_;
}
async nextCloneDisconnect() {
return new Promise(r => this.disconnectResolvers_.push(r));
}
onCountChanged(count) {
this.count_ = count;
}
onCloneDisconnected() {
let resolvers = [];
[resolvers, this.disconnectResolvers_] =
[this.disconnectResolvers_, resolvers];
resolvers.forEach(r => r());
}
}
function getCounterRemote() {
const {handle0, handle1} = Mojo.createMessagePipe();
const remote = new CounterRemote(handle1);
Mojo.bindInterface(Counter.$interfaceName, handle0, 'process');
return remote;
}
async function waitForDisconnect(receiver) {
return new Promise(r => receiver.onConnectionError.addListener(r));
}
promise_test(async () => {
const counter = getCounterRemote();
counter.increment();
counter.increment();
const {count} = await counter.increment();
assert_equals(count, 3);
}, 'basic validity check for browser-side support of these tests');
promise_test(async () => {
const counter = getCounterRemote();
const observerA = new ObserverImpl;
const receiverA = new CounterObserverReceiver(observerA);
const observerB = new ObserverImpl;
const receiverB = new CounterObserverReceiver(observerB);
counter.addObserver(receiverA.$.associateAndPassRemote());
counter.addObserver(receiverB.$.associateAndPassRemote());
counter.increment();
const {count} = await counter.increment();
assert_equals(count, 2);
// The observers should always observe changes before the caller of increment
// gets a reply, so the above await should guarantee that the observers' count
// values are up-to-date.
assert_equals(observerA.count, 2);
assert_equals(observerB.count, 2);
}, 'associated remotes can be created and passed');
promise_test(async () => {
const counter = getCounterRemote();
const observerA = new ObserverImpl;
const receiverA = new CounterObserverReceiver(observerA);
const observerB = new ObserverImpl;
const receiverB = new CounterObserverReceiver(observerB);
receiverA.$.bindHandle((await counter.addNewObserver()).receiver.handle);
receiverB.$.bindHandle((await counter.addNewObserver()).receiver.handle);
counter.increment();
const {count} = await counter.increment();
assert_equals(count, 2);
assert_equals(observerA.count, 2);
assert_equals(observerB.count, 2);
}, 'associated receivers can be deserialized and receive messages');
promise_test(async () => {
const counterA = getCounterRemote();
const counterB = new CounterRemote;
const counterC = new CounterRemote;
const counterD = new CounterRemote;
counterA.clone(counterB.$.associateAndPassReceiver());
counterA.clone(counterC.$.associateAndPassReceiver());
counterB.clone(counterD.$.associateAndPassReceiver());
// Increment operations among the three interfaces should be strictly ordered.
const increments = [
counterA.increment(),
counterB.increment(),
counterC.increment(),
counterD.increment(),
counterA.increment(),
counterB.increment(),
counterC.increment(),
counterD.increment(),
];
const replies = await Promise.all(increments);
const results = replies.map(reply => reply.count);
assert_array_equals([1, 2, 3, 4, 5, 6, 7, 8], results);
}, 'associated receivers can be created and passed, and message ordering is preserved among endpoints');
promise_test(async () => {
const counterA = getCounterRemote();
const {remote: counterB} = await counterA.cloneToNewRemote();
const {remote: counterC} = await counterA.cloneToNewRemote();
const {remote: counterD} = await counterC.cloneToNewRemote();
const increments = [
counterA.increment(),
counterB.increment(),
counterC.increment(),
counterD.increment(),
counterA.increment(),
counterB.increment(),
counterC.increment(),
counterD.increment(),
];
const replies = await Promise.all(increments);
const results = replies.map(reply => reply.count);
assert_array_equals([1, 2, 3, 4, 5, 6, 7, 8], results);
}, 'associated remotes can be deserialized and used to send messages, and message ordering is preserved among endpoints');
promise_test(async () => {
const counter = getCounterRemote();
const observer = new ObserverImpl;
const receiver = new CounterObserverReceiver(observer);
counter.addObserver(receiver.$.associateAndPassRemote());
counter.increment();
counter.increment();
counter.increment();
await counter.$.flushForTesting();
assert_equals(observer.count, 3);
}, 'associated endpoints can use flushForTesting');
promise_test(async () => {
const counter = getCounterRemote();
const {remote: clone} = await counter.cloneToNewRemote();
const observer = new ObserverImpl;
const receiver = new CounterObserverReceiver(observer);
counter.addObserver(receiver.$.associateAndPassRemote());
clone.$.close();
observer.nextCloneDisconnect();
}, 'closing an associated endpoint from JavaScript will signal its peer');
promise_test(async () => {
const counter = getCounterRemote();
const observer = new ObserverImpl;
const receiver = new CounterObserverReceiver(observer);
counter.addObserver(receiver.$.associateAndPassRemote());
counter.removeAllObservers();
await waitForDisconnect(receiver);
}, 'JavaScript associated endpoints are notified when their peers close');