-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
Use case
Currently, getCallbackHandle's implementation in Flutter Engine persists the library and name of the callback to disk for future lookup. This has several issues:
- Can fail between application updates:
- Sensitive to source changes like:
- Changing the name of the callback
- Moving the callback to another package or file (changing the library name)
- Changing the outer class of the callback (or moving in/out of a class)
- Unknown interactions with obfuscation.
- Sensitive to source changes like:
- Requires the use of File I/O as part of the cache + lookup mechanism. (See Support for Custom Embedders with Non-standard File I/O #93838)
To allow callbacks to be more robust, a different implementation should be explored.
Proposal
Callbacks should be registered in source via an annotation. Then a Kernel Transformer (which would live in the Engine repository) would be in charge of generating a lookup table from identifiers to callbacks.
This implementation would also be compatible with Flutter Web, which currently doesn't have an implementation of PluginUtilities.getCallbackHandle. See #33615
// This is an annotation used to mark callbacks.
class PluginCallback {
/// Global name of the callback.
///
/// Packages and applications should prefix this with a stable identifier.
/// For example, package:foo could use `foo:MyCallback`.
final String name;
const PluginCallback(this.name);
}// Example usage of @PluginCallback in user code.
@PluginCallback("my_cool_app:AlarmCallback")
void alarmCallback() {
print('Alarm fired!');
}
// Registration and usage of the callback is like-normal:
void registerAlarm() {
SomeAlarmPlugin.registerAlarm(const Duration(days: 3), PluginUtilities.getCallbackHandle(alarmCallback));
}And ultimately, the Kernel Transform would generate the following (after subjecting unused callbacks to treeshaking):
// GENERATED CODE (As an example, generated Kernel IR would look very different)
import 'package:my_cool_app/alarm_stuff.dart';
const _callbackToHandle = <Function, CallbackHandle>{
alarmCallback: const CallbackHandle(<32 or 64 bit FNV hash of callback name>),
};
const _handleToCallback = <int, Function>{
<32 or 64 bit FNV hash of callback name>: alarmCallback,
};
CallbackHandle _getCallbackHandle(Function func) => _callbackToHandle[func] ?? throw ArgumentError("Function not registered as callback, add @PluginCallback annotation on the function definition");
Function _getCallbackFromHandle(CallbackHandle handle) => _handleToCallback[handle.toRawHandle()];
// The implementation of PluginUtilities.getCallbackHandle & getCallbackFromHandle would be changed by the transform to call these generated functions.Transitionary Period
To allow packages and apps to transition over to the new registration annotation, we can fall back to the original callback registration code in the engine, while printing a warning in debug mode. Removing the transitionary code would be a breaking change. On Web & Custom Embedders where the original callback registration code does not exist or is stubbed, this would not be a breaking change in behavior.
Alternatives
The concept of a "Raw Handle" could be deprecated and replaced with a string-based lookup. While 64-bit collisions are unlikely, they may be more likely for 32-bit identifiers used on web. We would still require the use of an API like PluginUtilities.getCallbackHandle to allow for proper treeshaking of callbacks.