Skip to content

Change PluginUtilities.getCallbackHandle to require callbacks to be registered #94571

@ds84182

Description

@ds84182

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.
  • 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.

cc @chinmaygarde @jonahwilliams

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projectc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterengineflutter/engine related. See also e: labels.team-engineOwned by Engine teamtriaged-engineTriaged by Engine team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions