Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit efcfc04

Browse files
committed
Refactor platform message logic
This laids the groundwork for sending messages through ChannelBuffers with channel-specific callbacks rather than a single onPlatformMessage callback. This allows us to remove the logic from the framework that puts data back into the channel buffers. Right now (before this PR) the logic for messages from plugins to the framework is bidirectional: ``` ** * Plugins -> Engine -> ChannelBuffers <- Framework <---+-. | | | | | '------> via drain ----' | | | '----------------- onPlatformMessage ---' * = when the listener is null on the framework side ** = when onPlatformMessage is null ``` This ends up with weird race conditions and is generally less than completely clear. With this PR, we lay the groundwork for eventually reaching this model: ``` Plugins -> Engine -> ChannelBuffers -> Framework ``` ...which is significantly simpler.
1 parent 042c7f0 commit efcfc04

15 files changed

Lines changed: 710 additions & 376 deletions

lib/ui/channel_buffers.dart

Lines changed: 298 additions & 127 deletions
Large diffs are not rendered by default.

lib/ui/hooks.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ void _updateAccessibilityFeatures(int values) {
143143
// ignore: unused_element
144144
void _dispatchPlatformMessage(String name, ByteData? data, int responseId) {
145145
if (name == ChannelBuffers.kControlChannelName) {
146+
// TODO(ianh): move this logic into ChannelBuffers once we remove onPlatformMessage
146147
try {
147148
channelBuffers.handleMessage(data!);
148149
} catch (ex) {
@@ -266,6 +267,22 @@ void _invoke1<A>(void callback(A a)?, Zone zone, A arg) {
266267
}
267268
}
268269

270+
/// Invokes [callback] inside the given [zone] passing it [arg1] and [arg2].
271+
void _invoke2<A1, A2>(void callback(A1 a1, A2 a2)?, Zone zone, A1 arg1, A2 arg2) {
272+
if (callback == null)
273+
return;
274+
275+
assert(zone != null); // ignore: unnecessary_null_comparison
276+
277+
if (identical(zone, Zone.current)) {
278+
callback(arg1, arg2);
279+
} else {
280+
zone.runGuarded(() {
281+
callback(arg1, arg2);
282+
});
283+
}
284+
}
285+
269286
/// Invokes [callback] inside the given [zone] passing it [arg1], [arg2], and [arg3].
270287
void _invoke3<A1, A2, A3>(void callback(A1 a1, A2 a2, A3 a3)?, Zone zone, A1 arg1, A2 arg2, A3 arg3) {
271288
if (callback == null)

lib/ui/text.dart

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2273,11 +2273,18 @@ final ByteData _fontChangeMessage = utf8.encoder.convert(
22732273
).buffer.asByteData();
22742274

22752275
FutureOr<void> _sendFontChangeMessage() async {
2276-
window.onPlatformMessage?.call(
2277-
'flutter/system',
2278-
_fontChangeMessage,
2279-
(_) {},
2280-
);
2276+
const String kSystemChannelName = 'flutter/system';
2277+
if (window.onPlatformMessage != null) {
2278+
_invoke3<String, ByteData?, PlatformMessageResponseCallback>(
2279+
window.onPlatformMessage,
2280+
window._onPlatformMessageZone,
2281+
kSystemChannelName,
2282+
_fontChangeMessage,
2283+
(ByteData? responseData) { },
2284+
);
2285+
} else {
2286+
channelBuffers.push(kSystemChannelName, _fontChangeMessage, (ByteData? responseData) { });
2287+
}
22812288
}
22822289

22832290
String _loadFontFromList(Uint8List list, _Callback<void> callback, String? fontFamily) native 'loadFontFromList';

lib/ui/window.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ typedef SemanticsActionCallback = void Function(int id, SemanticsAction action,
3838
typedef PlatformMessageResponseCallback = void Function(ByteData? data);
3939

4040
/// Signature for [Window.onPlatformMessage].
41+
// TODO(ianh): deprecate once framework uses [ChannelBuffers.setListener].
4142
typedef PlatformMessageCallback = void Function(String name, ByteData? data, PlatformMessageResponseCallback? callback);
4243

4344
// Signature for _setNeedsReportTimings.
@@ -1205,6 +1206,7 @@ class Window {
12051206
///
12061207
/// The framework invokes this callback in the same zone in which the
12071208
/// callback was set.
1209+
// TODO(ianh): deprecate once framework uses [ChannelBuffers.setListener].
12081210
PlatformMessageCallback? get onPlatformMessage => _onPlatformMessage;
12091211
PlatformMessageCallback? _onPlatformMessage;
12101212
Zone _onPlatformMessageZone = Zone.root;

lib/web_ui/lib/src/engine.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ library engine;
88

99
import 'dart:async';
1010
import 'dart:collection'
11-
show ListBase, IterableBase, DoubleLinkedQueue, DoubleLinkedQueueEntry;
11+
show ListBase, ListQueue, IterableBase, DoubleLinkedQueue, DoubleLinkedQueueEntry;
1212
import 'dart:convert' hide Codec;
1313
import 'dart:developer' as developer;
1414
import 'dart:html' as html;
@@ -57,6 +57,7 @@ part 'engine/canvaskit/util.dart';
5757
part 'engine/canvaskit/vertices.dart';
5858
part 'engine/canvaskit/viewport_metrics.dart';
5959
part 'engine/canvas_pool.dart';
60+
part 'engine/channel_buffers.dart';
6061
part 'engine/clipboard.dart';
6162
part 'engine/color_filter.dart';
6263
part 'engine/dom_canvas.dart';
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.10
6+
7+
part of engine;
8+
9+
class _ChannelCallbackRecord {
10+
_ChannelCallbackRecord(this.callback) : zone = Zone.current;
11+
final ui.ChannelCallback callback;
12+
final Zone zone;
13+
14+
void invoke(ByteData? dataArg, ui.PlatformMessageResponseCallback callbackArg) {
15+
_invoke2<ByteData?, ui.PlatformMessageResponseCallback>(callback, zone, dataArg, callbackArg);
16+
}
17+
}
18+
19+
class _StoredMessage {
20+
const _StoredMessage(this.data, this.callback);
21+
22+
final ByteData? data;
23+
24+
final ui.PlatformMessageResponseCallback callback;
25+
}
26+
27+
class _Channel {
28+
_Channel(this._capacity)
29+
: _queue = ListQueue<_StoredMessage>(_capacity);
30+
31+
final ListQueue<_StoredMessage> _queue;
32+
33+
int get length => _queue.length;
34+
35+
int _capacity;
36+
int get capacity => _capacity;
37+
38+
int resize(int newSize) {
39+
_capacity = newSize;
40+
return _dropOverflowMessages(newSize);
41+
}
42+
43+
bool _draining = false;
44+
45+
bool push(_StoredMessage message) {
46+
if (!_draining && _channelCallbackRecord != null) {
47+
assert(_queue.isEmpty);
48+
_channelCallbackRecord!.invoke(message.data, message.callback);
49+
return false;
50+
}
51+
if (_capacity <= 0) {
52+
return true;
53+
}
54+
final int overflowCount = _dropOverflowMessages(_capacity - 1);
55+
_queue.addLast(message);
56+
return overflowCount > 0;
57+
}
58+
59+
_StoredMessage pop() => _queue.removeFirst();
60+
61+
int _dropOverflowMessages(int lengthLimit) {
62+
int result = 0;
63+
while (_queue.length > lengthLimit) {
64+
final _StoredMessage message = _queue.removeFirst();
65+
message.callback(null); // send empty reply to the plugin side
66+
result += 1;
67+
}
68+
return result;
69+
}
70+
71+
_ChannelCallbackRecord? _channelCallbackRecord;
72+
73+
void setListener(ui.ChannelCallback callback) {
74+
final bool needDrain = _channelCallbackRecord == null;
75+
_channelCallbackRecord = _ChannelCallbackRecord(callback);
76+
if (needDrain && !_draining)
77+
_drain();
78+
}
79+
80+
void clearListener() {
81+
_channelCallbackRecord = null;
82+
}
83+
84+
void _drain() {
85+
assert(!_draining);
86+
_draining = true;
87+
scheduleMicrotask(_drainStep);
88+
}
89+
90+
void _drainStep() {
91+
assert(_draining);
92+
if (_queue.isNotEmpty && _channelCallbackRecord != null) {
93+
final _StoredMessage message = pop();
94+
_channelCallbackRecord!.invoke(message.data, message.callback);
95+
scheduleMicrotask(_drainStep);
96+
} else {
97+
_draining = false;
98+
}
99+
}
100+
}
101+
102+
class EngineChannelBuffers extends ui.ChannelBuffers {
103+
final Map<String, _Channel> _channels = <String, _Channel>{};
104+
105+
@override
106+
bool push(String name, ByteData? data, ui.PlatformMessageResponseCallback callback) {
107+
final _Channel channel = _channels.putIfAbsent(name, () => _Channel(ui.ChannelBuffers.kDefaultBufferSize));
108+
return channel.push(_StoredMessage(data, callback));
109+
}
110+
111+
@override
112+
void setListener(String name, ui.ChannelCallback callback) {
113+
final _Channel channel = _channels.putIfAbsent(name, () => _Channel(ui.ChannelBuffers.kDefaultBufferSize));
114+
channel.setListener(callback);
115+
}
116+
117+
@override
118+
void clearListener(String name) {
119+
final _Channel? channel = _channels[name];
120+
if (channel != null)
121+
channel.clearListener();
122+
}
123+
124+
@override
125+
Future<void> drain(String name, ui.DrainChannelCallback callback) async {
126+
final _Channel? channel = _channels[name];
127+
while (channel != null && !channel._queue.isEmpty) {
128+
final _StoredMessage message = channel.pop();
129+
await callback(message.data, message.callback);
130+
}
131+
}
132+
133+
String _getString(ByteData data) {
134+
final ByteBuffer buffer = data.buffer;
135+
final Uint8List list = buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
136+
return utf8.decode(list);
137+
}
138+
139+
@override
140+
void handleMessage(ByteData data) {
141+
final List<String> command = _getString(data).split('\r');
142+
if (command.length == 1 + /*arity=*/2 && command[0] == 'resize') {
143+
_resize(command[1], int.parse(command[2]));
144+
} else {
145+
throw Exception('Unrecognized command $command sent to ${ui.ChannelBuffers.kControlChannelName}.');
146+
}
147+
}
148+
149+
void _resize(String name, int newSize) {
150+
_Channel? channel = _channels[name];
151+
if (channel == null) {
152+
channel = _Channel(newSize);
153+
_channels[name] = channel;
154+
} else {
155+
channel.resize(newSize);
156+
}
157+
}
158+
}
159+
160+
final ui.ChannelBuffers channelBuffers = EngineChannelBuffers();

lib/web_ui/lib/src/engine/history.dart

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -159,18 +159,16 @@ class MultiEntriesBrowserHistory extends BrowserHistory {
159159
currentPath);
160160
}
161161
_lastSeenSerialCount = _currentSerialCount;
162-
if (window._onPlatformMessage != null) {
163-
window.invokeOnPlatformMessage(
164-
'flutter/navigation',
165-
const JSONMethodCodec().encodeMethodCall(
166-
MethodCall('pushRouteInformation', <dynamic, dynamic>{
167-
'location': currentPath,
168-
'state': event.state?['state'],
169-
})
170-
),
171-
(_) {},
172-
);
173-
}
162+
window.invokeOnPlatformMessage(
163+
'flutter/navigation',
164+
const JSONMethodCodec().encodeMethodCall(
165+
MethodCall('pushRouteInformation', <dynamic, dynamic>{
166+
'location': currentPath,
167+
'state': event.state?['state'],
168+
})
169+
),
170+
(_) {},
171+
);
174172
}
175173

176174
@override
@@ -263,13 +261,11 @@ class SingleEntryBrowserHistory extends BrowserHistory {
263261
_setupFlutterEntry(_locationStrategy!);
264262

265263
// 2. Send a 'popRoute' platform message so the app can handle it accordingly.
266-
if (window._onPlatformMessage != null) {
267-
window.invokeOnPlatformMessage(
268-
'flutter/navigation',
269-
const JSONMethodCodec().encodeMethodCall(_popRouteMethodCall),
270-
(_) {},
271-
);
272-
}
264+
window.invokeOnPlatformMessage(
265+
'flutter/navigation',
266+
const JSONMethodCodec().encodeMethodCall(_popRouteMethodCall),
267+
(_) {},
268+
);
273269
} else if (_isFlutterEntry(event.state)) {
274270
// We get into this scenario when the user changes the url manually. It
275271
// causes a new entry to be pushed on top of our "flutter" one. When this
@@ -282,15 +278,13 @@ class SingleEntryBrowserHistory extends BrowserHistory {
282278
_userProvidedRouteName = null;
283279

284280
// Send a 'pushRoute' platform message so the app handles it accordingly.
285-
if (window._onPlatformMessage != null) {
286-
window.invokeOnPlatformMessage(
287-
'flutter/navigation',
288-
const JSONMethodCodec().encodeMethodCall(
289-
MethodCall('pushRoute', newRouteName),
290-
),
291-
(_) {},
292-
);
293-
}
281+
window.invokeOnPlatformMessage(
282+
'flutter/navigation',
283+
const JSONMethodCodec().encodeMethodCall(
284+
MethodCall('pushRoute', newRouteName),
285+
),
286+
(_) {},
287+
);
294288
} else {
295289
// The user has pushed a new entry on top of our flutter entry. This could
296290
// happen when the user modifies the hash part of the url directly, for

lib/web_ui/lib/src/engine/keyboard.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,6 @@ class Keyboard {
8181

8282
final html.KeyboardEvent keyboardEvent = event;
8383

84-
if (window._onPlatformMessage == null) {
85-
return;
86-
}
87-
8884
if (_shouldPreventDefault(event)) {
8985
event.preventDefault();
9086
}

0 commit comments

Comments
 (0)