Repository: flutter
Issue: Change `PluginUtilities.getCallbackHandle` to require callbacks to be registered
Labels: c: new feature,engine,c: proposal,P3,team-engine,triaged-engine

## 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 #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

```dart
// 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);
}
```

```dart
// 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):

```dart
// 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