Add PlatformDispatcher.engineId (#163476)

Fixes https://github.com/flutter/flutter/issues/163430.

This PR adds `engineId` field to `PlatformDispatcher`. When provided by
the engine, this can be used to retrieve the engine instance from native
code.

Dart code:
```dart
final identifier = PlatformDispatcher.instance.engineId!;
```

macOS, iOS: 
```objc
FlutterEngine *engine = [FlutterEngine engineForIdentifier: identifier];
```

Android:
```java
FlutterEngine engine = FlutterEngine.engineForId(identifier);
```

Linux
```cpp
FlEngine *engine = fl_engine_for_id(identifier);
```

Windows
```cpp
FlutterDesktopEngineRef engine = FlutterDesktopEngineForId(identifier);
```

*If you had to change anything in the [flutter/tests] repo, include a
link to the migration guide as per the [breaking change policy].*

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Matej Knopp 2025-02-28 23:20:11 +01:00 committed by GitHub
parent 600ee30696
commit b831b269c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 551 additions and 38 deletions

View File

@ -0,0 +1,23 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'package:android_driver_extensions/extension.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'src/allow_list_devices.dart';
void main() {
ensureAndroidDevice();
enableFlutterDriverExtension(
handler: (String? command) async {
return json.encode(<String, Object?>{'engineId': PlatformDispatcher.instance.engineId});
},
commands: <CommandExtension>[nativeDriverCommands],
);
runApp(const SizedBox());
}

View File

@ -0,0 +1,37 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'package:android_driver_extensions/native_driver.dart';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
late final FlutterDriver flutterDriver;
late final NativeDriver nativeDriver;
void main() async {
setUpAll(() async {
flutterDriver = await FlutterDriver.connect();
nativeDriver = await AndroidNativeDriver.connect(flutterDriver);
});
tearDownAll(() async {
await nativeDriver.close();
await flutterDriver.close();
});
// TODO(matanlurey): Convert to use package:integration_test
test('verify that engineId is set and works', () async {
final Map<String, Object?> response =
json.decode(await flutterDriver.requestData('')) as Map<String, Object?>;
expect(
response['engineId'],
1,
// Valid engine ids start at 1 to make detecting uninitialized
// values easier.
reason: 'engineId of first engine instance should be 1',
);
}, timeout: Timeout.none);
}

View File

@ -68,6 +68,11 @@ void _sendViewFocusEvent(int viewId, int viewFocusState, int viewFocusDirection)
PlatformDispatcher.instance._sendViewFocusEvent(viewFocusEvent);
}
@pragma('vm:entry-point')
void _setEngineId(int engineId) {
PlatformDispatcher.instance._engineId = engineId;
}
@pragma('vm:entry-point')
void _updateDisplays(
List<int> ids,

View File

@ -305,6 +305,13 @@ class PlatformDispatcher {
_invoke1<ViewFocusEvent>(onViewFocusChange, _onViewFocusChangeZone, event);
}
/// Opaque engine identifier for the engine running current isolate. Can be used
/// in native code to retrieve the engine instance.
/// The identifier is valid while the isolate is running.
int? get engineId => _engineId;
int? _engineId;
// Called from the engine, via hooks.dart.
//
// Updates the available displays.

View File

@ -49,6 +49,8 @@ void PlatformConfiguration::DidCreateIsolate() {
send_view_focus_event_.Set(
tonic::DartState::Current(),
Dart_GetField(library, tonic::ToDart("_sendViewFocusEvent")));
set_engine_id_.Set(tonic::DartState::Current(),
Dart_GetField(library, tonic::ToDart("_setEngineId")));
update_window_metrics_.Set(
tonic::DartState::Current(),
Dart_GetField(library, tonic::ToDart("_updateWindowMetrics")));
@ -168,6 +170,20 @@ bool PlatformConfiguration::SendFocusEvent(const ViewFocusEvent& event) {
return true;
}
bool PlatformConfiguration::SetEngineId(int64_t engine_id) {
std::shared_ptr<tonic::DartState> dart_state =
set_engine_id_.dart_state().lock();
if (!dart_state) {
return false;
}
tonic::DartState::Scope scope(dart_state);
tonic::CheckAndHandleError(
tonic::DartInvoke(set_engine_id_.Get(), {
tonic::ToDart(engine_id),
}));
return true;
}
bool PlatformConfiguration::UpdateViewMetrics(
int64_t view_id,
const ViewportMetrics& view_metrics) {

View File

@ -356,6 +356,14 @@ class PlatformConfiguration final {
/// @return Whether the focus event was sent.
bool SendFocusEvent(const ViewFocusEvent& event);
/// @brief Sets the opaque identifier of the engine.
///
/// The identifier can be passed from Dart to native code to
/// retrieve the engine instance.
///
/// @return Whether the identifier was set.
bool SetEngineId(int64_t engine_id);
//----------------------------------------------------------------------------
/// @brief Update the view metrics for the specified view.
///
@ -548,6 +556,7 @@ class PlatformConfiguration final {
tonic::DartPersistentValue add_view_;
tonic::DartPersistentValue remove_view_;
tonic::DartPersistentValue send_view_focus_event_;
tonic::DartPersistentValue set_engine_id_;
tonic::DartPersistentValue update_window_metrics_;
tonic::DartPersistentValue update_displays_;
tonic::DartPersistentValue update_locales_;

View File

@ -37,6 +37,8 @@ abstract class PlatformDispatcher {
FlutterView? get implicitView;
int? get engineId;
VoidCallback? get onMetricsChanged;
set onMetricsChanged(VoidCallback? callback);

View File

@ -140,6 +140,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
@override
EngineFlutterWindow? get implicitView => viewManager[kImplicitViewId] as EngineFlutterWindow?;
@override
int? get engineId => null;
/// A callback that is invoked whenever the platform's [devicePixelRatio],
/// [physicalSize], [padding], [viewInsets], or [systemGestureInsets]
/// values change, for example when the device is rotated or when the

View File

@ -537,7 +537,8 @@ bool RuntimeController::LaunchRootIsolate(
std::optional<std::string> dart_entrypoint_library,
const std::vector<std::string>& dart_entrypoint_args,
std::unique_ptr<IsolateConfiguration> isolate_configuration,
std::shared_ptr<NativeAssetsManager> native_assets_manager) {
std::shared_ptr<NativeAssetsManager> native_assets_manager,
std::optional<int64_t> engine_id) {
if (root_isolate_.lock()) {
FML_LOG(ERROR) << "Root isolate was already running.";
return false;
@ -586,6 +587,11 @@ bool RuntimeController::LaunchRootIsolate(
if (!FlushRuntimeStateToIsolate()) {
FML_DLOG(ERROR) << "Could not set up initial isolate state.";
}
if (engine_id) {
if (!platform_configuration->SetEngineId(*engine_id)) {
FML_DLOG(ERROR) << "Could not set engine identifier.";
}
}
} else {
FML_DCHECK(false) << "RuntimeController created without window binding.";
}

View File

@ -156,6 +156,8 @@ class RuntimeController : public PlatformConfigurationClient,
/// @param[in] dart_entrypoint_args Arguments passed as a List<String>
/// to Dart's entrypoint function.
/// @param[in] isolate_configuration The isolate configuration
/// @param[in] engine_id. Engine identifier to be passed to the
/// platform dispatcher.
///
/// @return If the isolate could be launched and guided to the
/// `DartIsolate::Phase::Running` phase.
@ -167,7 +169,8 @@ class RuntimeController : public PlatformConfigurationClient,
std::optional<std::string> dart_entrypoint_library,
const std::vector<std::string>& dart_entrypoint_args,
std::unique_ptr<IsolateConfiguration> isolate_configuration,
std::shared_ptr<NativeAssetsManager> native_assets_manager);
std::shared_ptr<NativeAssetsManager> native_assets_manager,
std::optional<int64_t> engine_id);
//----------------------------------------------------------------------------
/// @brief Clone the runtime controller. Launching an isolate with a

View File

@ -245,8 +245,9 @@ Engine::RunStatus Engine::Run(RunConfiguration configuration) {
configuration.GetEntrypointLibrary(), //
configuration.GetEntrypointArgs(), //
configuration.TakeIsolateConfiguration(), //
native_assets_manager_) //
) {
native_assets_manager_, //
configuration.GetEngineId())) //
{
return RunStatus::Failure;
}

View File

@ -640,3 +640,11 @@ void testSendViewFocusEvent() {
};
notifyNative();
}
@pragma('vm:external-name', 'ReportEngineId')
external void _reportEngineId(int? identifier);
@pragma('vm:entry-point')
void providesEngineId() {
_reportEngineId(PlatformDispatcher.instance.engineId);
}

View File

@ -105,6 +105,15 @@ const std::vector<std::string>& RunConfiguration::GetEntrypointArgs() const {
return entrypoint_args_;
}
void RunConfiguration::SetEngineId(int64_t engine_id) {
engine_id_ = engine_id;
}
// Engine identifier to be passed to the platform dispatcher.
std::optional<int64_t> RunConfiguration::GetEngineId() const {
return engine_id_;
}
std::unique_ptr<IsolateConfiguration>
RunConfiguration::TakeIsolateConfiguration() {
return std::move(isolate_configuration_);

View File

@ -194,12 +194,22 @@ class RunConfiguration {
///
std::unique_ptr<IsolateConfiguration> TakeIsolateConfiguration();
//----------------------------------------------------------------------------
/// @brief Sets the engine identifier to be passed to the platform
/// dispatcher.
void SetEngineId(int64_t engine_id);
///----------------------------------------------------------------------------
/// @return Engine identifier to be passed to the platform dispatcher.
std::optional<int64_t> GetEngineId() const;
private:
std::unique_ptr<IsolateConfiguration> isolate_configuration_;
std::shared_ptr<AssetManager> asset_manager_;
std::string entrypoint_ = "main";
std::string entrypoint_library_ = "";
std::vector<std::string> entrypoint_args_;
std::optional<int64_t> engine_id_;
FML_DISALLOW_COPY_AND_ASSIGN(RunConfiguration);
};

View File

@ -71,6 +71,7 @@ class ShellTest : public FixtureTest {
// Defaults to calling ShellTestPlatformView::Create with the provided
// arguments.
Shell::CreateCallback<PlatformView> platform_view_create_callback;
std::optional<int64_t> engine_id;
};
ShellTest();

View File

@ -5003,7 +5003,69 @@ TEST_F(ShellTest, SendViewFocusEvent) {
latch.Wait();
ASSERT_EQ(last_event,
"2 ViewFocusState.unfocused ViewFocusDirection.backward");
DestroyShell(std::move(shell), task_runners);
}
TEST_F(ShellTest, ProvidesEngineId) {
Settings settings = CreateSettingsForFixture();
TaskRunners task_runners = GetTaskRunnersForFixture();
fml::AutoResetWaitableEvent latch;
std::optional<int> reported_handle = std::nullopt;
AddNativeCallback(
"ReportEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
Dart_Handle arg = Dart_GetNativeArgument(args, 0);
if (Dart_IsNull(arg)) {
reported_handle = std::nullopt;
} else {
reported_handle = tonic::DartConverter<int64_t>::FromDart(arg);
}
latch.Signal();
}));
fml::AutoResetWaitableEvent check_latch;
std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);
ASSERT_TRUE(shell->IsSetup());
auto configuration = RunConfiguration::InferFromSettings(settings);
configuration.SetEngineId(99);
configuration.SetEntrypoint("providesEngineId");
RunEngine(shell.get(), std::move(configuration));
latch.Wait();
ASSERT_EQ(reported_handle, 99);
DestroyShell(std::move(shell), task_runners);
}
TEST_F(ShellTest, ProvidesNullEngineId) {
Settings settings = CreateSettingsForFixture();
TaskRunners task_runners = GetTaskRunnersForFixture();
fml::AutoResetWaitableEvent latch;
std::optional<int> reported_handle = std::nullopt;
AddNativeCallback(
"ReportEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
Dart_Handle arg = Dart_GetNativeArgument(args, 0);
if (Dart_IsNull(arg)) {
reported_handle = std::nullopt;
} else {
reported_handle = tonic::DartConverter<int64_t>::FromDart(arg);
}
latch.Signal();
}));
fml::AutoResetWaitableEvent check_latch;
std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);
ASSERT_TRUE(shell->IsSetup());
auto configuration = RunConfiguration::InferFromSettings(settings);
configuration.SetEntrypoint("providesEngineId");
RunEngine(shell.get(), std::move(configuration));
latch.Wait();
ASSERT_EQ(reported_handle, std::nullopt);
DestroyShell(std::move(shell), task_runners);
}

View File

@ -222,7 +222,8 @@ std::unique_ptr<AndroidShellHolder> AndroidShellHolder::Spawn(
const std::string& entrypoint,
const std::string& libraryUrl,
const std::string& initial_route,
const std::vector<std::string>& entrypoint_args) const {
const std::vector<std::string>& entrypoint_args,
int64_t engine_id) const {
FML_DCHECK(shell_ && shell_->IsSetup())
<< "A new Shell can only be spawned "
"if the current Shell is properly constructed";
@ -269,6 +270,7 @@ std::unique_ptr<AndroidShellHolder> AndroidShellHolder::Spawn(
// Fail the whole thing.
return nullptr;
}
config->SetEngineId(engine_id);
std::unique_ptr<flutter::Shell> shell =
shell_->Spawn(std::move(config.value()), initial_route,
@ -284,7 +286,8 @@ void AndroidShellHolder::Launch(
std::unique_ptr<APKAssetProvider> apk_asset_provider,
const std::string& entrypoint,
const std::string& libraryUrl,
const std::vector<std::string>& entrypoint_args) {
const std::vector<std::string>& entrypoint_args,
int64_t engine_id) {
if (!IsValid()) {
return;
}
@ -294,6 +297,7 @@ void AndroidShellHolder::Launch(
if (!config) {
return;
}
config->SetEngineId(engine_id);
UpdateDisplayMetrics();
shell_->RunEngine(std::move(config.value()));
}

View File

@ -79,12 +79,14 @@ class AndroidShellHolder {
const std::string& entrypoint,
const std::string& libraryUrl,
const std::string& initial_route,
const std::vector<std::string>& entrypoint_args) const;
const std::vector<std::string>& entrypoint_args,
int64_t engine_id) const;
void Launch(std::unique_ptr<APKAssetProvider> apk_asset_provider,
const std::string& entrypoint,
const std::string& libraryUrl,
const std::vector<std::string>& entrypoint_args);
const std::vector<std::string>& entrypoint_args,
int64_t engine_id);
const flutter::Settings& GetSettings() const;

View File

@ -44,8 +44,10 @@ import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.plugin.platform.PlatformViewsController2;
import io.flutter.plugin.text.ProcessTextPlugin;
import io.flutter.util.ViewUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@ -115,6 +117,20 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
// Engine Lifecycle.
@NonNull private final Set<EngineLifecycleListener> engineLifecycleListeners = new HashSet<>();
// Unique handle for this engine.
@NonNull private final long engineId;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static void resetNextEngineId() {
nextEngineId = 1;
}
// Handle to assign to the next engine created.
private static long nextEngineId = 1;
// Map of engine identifiers to engines.
private static final Map<Long, FlutterEngine> idToEngine = new HashMap<>();
@NonNull
private final EngineLifecycleListener engineLifecycleListener =
new EngineLifecycleListener() {
@ -312,6 +328,10 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
boolean automaticallyRegisterPlugins,
boolean waitForRestorationData,
@Nullable FlutterEngineGroup group) {
this.engineId = nextEngineId++;
idToEngine.put(engineId, this);
AssetManager assetManager;
try {
assetManager = context.createPackageContext(context.getPackageName(), 0).getAssets();
@ -326,7 +346,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
}
this.flutterJNI = flutterJNI;
this.dartExecutor = new DartExecutor(flutterJNI, assetManager);
this.dartExecutor = new DartExecutor(flutterJNI, assetManager, engineId);
this.dartExecutor.onAttachedToJNI();
DeferredComponentManager deferredComponentManager =
@ -451,7 +471,8 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
dartEntrypoint.dartEntrypointFunctionName,
dartEntrypoint.dartEntrypointLibrary,
initialRoute,
dartEntrypointArgs);
dartEntrypointArgs,
nextEngineId);
return new FlutterEngine(
context, // Context.
null, // FlutterLoader. A null value passed here causes the constructor to get it from the
@ -486,6 +507,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
FlutterInjector.instance().deferredComponentManager().destroy();
deferredComponentChannel.setDeferredComponentManager(null);
}
idToEngine.remove(engineId);
}
/**
@ -679,6 +701,25 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
return pluginRegistry;
}
/** Returns unique identifier for this engine. */
public long getEngineId() {
return engineId;
}
/**
* Returns engine for the given identifier or null if identifier is not valid. The handle can be
* obtained through
*
* <pre>PlatformDispatcher.instance.engineId</pre>
*
* <p>Must be called on the UI thread.
*/
@Nullable
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static FlutterEngine engineForId(long handle) {
return idToEngine.get(handle);
}
/** Lifecycle callbacks for Flutter engine lifecycle events. */
public interface EngineLifecycleListener {
/** Lifecycle callback invoked before a hot restart of the Flutter engine. */

View File

@ -460,7 +460,8 @@ public class FlutterJNI {
@Nullable String entrypointFunctionName,
@Nullable String pathToEntrypointFunction,
@Nullable String initialRoute,
@Nullable List<String> entrypointArgs) {
@Nullable List<String> entrypointArgs,
long engineId) {
ensureRunningOnMainThread();
ensureAttachedToNative();
FlutterJNI spawnedJNI =
@ -469,7 +470,8 @@ public class FlutterJNI {
entrypointFunctionName,
pathToEntrypointFunction,
initialRoute,
entrypointArgs);
entrypointArgs,
engineId);
Preconditions.checkState(
spawnedJNI.nativeShellHolderId != null && spawnedJNI.nativeShellHolderId != 0,
"Failed to spawn new JNI connected shell from existing shell.");
@ -482,7 +484,8 @@ public class FlutterJNI {
@Nullable String entrypointFunctionName,
@Nullable String pathToEntrypointFunction,
@Nullable String initialRoute,
@Nullable List<String> entrypointArgs);
@Nullable List<String> entrypointArgs,
long engineId);
/**
* Detaches this {@code FlutterJNI} instance from Flutter's native engine, which precludes any
@ -491,7 +494,7 @@ public class FlutterJNI {
* <p>This method must not be invoked if {@code FlutterJNI} is not already attached to native.
*
* <p>Invoking this method will result in the release of all native-side resources that were set
* up during {@link #attachToNative()} or {@link #spawn(String, String, String, List)}, or
* up during {@link #attachToNative()} or {@link #spawn(String, String, String, List, long)}, or
* accumulated thereafter.
*
* <p>It is permissible to re-attach this instance to native after detaching it from native.
@ -1002,7 +1005,8 @@ public class FlutterJNI {
@Nullable String entrypointFunctionName,
@Nullable String pathToEntrypointFunction,
@NonNull AssetManager assetManager,
@Nullable List<String> entrypointArgs) {
@Nullable List<String> entrypointArgs,
long engineId) {
ensureRunningOnMainThread();
ensureAttachedToNative();
nativeRunBundleAndSnapshotFromLibrary(
@ -1011,7 +1015,8 @@ public class FlutterJNI {
entrypointFunctionName,
pathToEntrypointFunction,
assetManager,
entrypointArgs);
entrypointArgs,
engineId);
}
private native void nativeRunBundleAndSnapshotFromLibrary(
@ -1020,7 +1025,8 @@ public class FlutterJNI {
@Nullable String entrypointFunctionName,
@Nullable String pathToEntrypointFunction,
@NonNull AssetManager manager,
@Nullable List<String> entrypointArgs);
@Nullable List<String> entrypointArgs,
long engineId);
// ------ End Dart Execution Support -------
// --------- Start Platform Message Support ------

View File

@ -8,6 +8,7 @@ import android.content.res.AssetManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import io.flutter.FlutterInjector;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterJNI;
@ -40,6 +41,7 @@ public class DartExecutor implements BinaryMessenger {
@NonNull private final FlutterJNI flutterJNI;
@NonNull private final AssetManager assetManager;
private final long engineId;
@NonNull private final DartMessenger dartMessenger;
@NonNull private final BinaryMessenger binaryMessenger;
private boolean isApplicationRunning = false;
@ -57,9 +59,16 @@ public class DartExecutor implements BinaryMessenger {
}
};
@VisibleForTesting
public DartExecutor(@NonNull FlutterJNI flutterJNI, @NonNull AssetManager assetManager) {
this(flutterJNI, assetManager, 0);
}
public DartExecutor(
@NonNull FlutterJNI flutterJNI, @NonNull AssetManager assetManager, long engineId) {
this.flutterJNI = flutterJNI;
this.assetManager = assetManager;
this.engineId = engineId;
this.dartMessenger = new DartMessenger(flutterJNI);
dartMessenger.setMessageHandler("flutter/isolate", isolateChannelMessageHandler);
this.binaryMessenger = new DefaultBinaryMessenger(dartMessenger);
@ -148,7 +157,8 @@ public class DartExecutor implements BinaryMessenger {
dartEntrypoint.dartEntrypointFunctionName,
dartEntrypoint.dartEntrypointLibrary,
assetManager,
dartEntrypointArgs);
dartEntrypointArgs,
engineId);
isApplicationRunning = true;
}
@ -174,7 +184,8 @@ public class DartExecutor implements BinaryMessenger {
dartCallback.callbackHandle.callbackName,
dartCallback.callbackHandle.callbackLibraryPath,
dartCallback.androidAssetManager,
null);
null,
engineId);
isApplicationRunning = true;
}

View File

@ -195,7 +195,8 @@ static jobject SpawnJNI(JNIEnv* env,
jstring jEntrypoint,
jstring jLibraryUrl,
jstring jInitialRoute,
jobject jEntrypointArgs) {
jobject jEntrypointArgs,
jlong engineId) {
jobject jni = env->NewObject(g_flutter_jni_class->obj(), g_jni_constructor);
if (jni == nullptr) {
FML_LOG(ERROR) << "Could not create a FlutterJNI instance";
@ -211,8 +212,9 @@ static jobject SpawnJNI(JNIEnv* env,
auto initial_route = fml::jni::JavaStringToString(env, jInitialRoute);
auto entrypoint_args = fml::jni::StringListToVector(env, jEntrypointArgs);
auto spawned_shell_holder = ANDROID_SHELL_HOLDER->Spawn(
jni_facade, entrypoint, libraryUrl, initial_route, entrypoint_args);
auto spawned_shell_holder =
ANDROID_SHELL_HOLDER->Spawn(jni_facade, entrypoint, libraryUrl,
initial_route, entrypoint_args, engineId);
if (spawned_shell_holder == nullptr || !spawned_shell_holder->IsValid()) {
FML_LOG(ERROR) << "Could not spawn Shell";
@ -279,7 +281,8 @@ static void RunBundleAndSnapshotFromLibrary(JNIEnv* env,
jstring jEntrypoint,
jstring jLibraryUrl,
jobject jAssetManager,
jobject jEntrypointArgs) {
jobject jEntrypointArgs,
jlong engineId) {
auto apk_asset_provider = std::make_unique<flutter::APKAssetProvider>(
env, // jni environment
jAssetManager, // asset manager
@ -290,7 +293,7 @@ static void RunBundleAndSnapshotFromLibrary(JNIEnv* env,
auto entrypoint_args = fml::jni::StringListToVector(env, jEntrypointArgs);
ANDROID_SHELL_HOLDER->Launch(std::move(apk_asset_provider), entrypoint,
libraryUrl, entrypoint_args);
libraryUrl, entrypoint_args, engineId);
}
static jobject LookupCallbackInformation(JNIEnv* env,
@ -687,7 +690,7 @@ bool RegisterApi(JNIEnv* env) {
{
.name = "nativeSpawn",
.signature = "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/"
"String;Ljava/util/List;)Lio/flutter/"
"String;Ljava/util/List;J)Lio/flutter/"
"embedding/engine/FlutterJNI;",
.fnPtr = reinterpret_cast<void*>(&SpawnJNI),
},
@ -695,7 +698,7 @@ bool RegisterApi(JNIEnv* env) {
.name = "nativeRunBundleAndSnapshotFromLibrary",
.signature = "(JLjava/lang/String;Ljava/lang/String;"
"Ljava/lang/String;Landroid/content/res/"
"AssetManager;Ljava/util/List;)V",
"AssetManager;Ljava/util/List;J)V",
.fnPtr = reinterpret_cast<void*>(&RunBundleAndSnapshotFromLibrary),
},
{

View File

@ -59,6 +59,7 @@ public class FlutterEngineGroupComponentTest {
when(mockFlutterJNI.isAttached()).thenAnswer(invocation -> jniAttached);
doAnswer(invocation -> jniAttached = true).when(mockFlutterJNI).attachToNative();
GeneratedPluginRegistrant.clearRegisteredEngines();
FlutterEngine.resetNextEngineId();
when(mockFlutterLoader.findAppBundlePath()).thenReturn("some/path/to/flutter_assets");
@ -190,7 +191,8 @@ public class FlutterEngineGroupComponentTest {
eq("other entrypoint"),
isNull(),
any(AssetManager.class),
nullable(List.class));
nullable(List.class),
eq(1l));
}
@Test
@ -213,14 +215,20 @@ public class FlutterEngineGroupComponentTest {
nullable(String.class),
nullable(String.class),
nullable(String.class),
nullable(List.class));
nullable(List.class),
eq(2l));
FlutterEngine secondEngine =
engineGroupUnderTest.createAndRunEngine(ctx, mock(DartEntrypoint.class), "/bar");
assertEquals(2, engineGroupUnderTest.activeEngines.size());
verify(mockFlutterJNI, times(1))
.spawn(nullable(String.class), nullable(String.class), eq("/bar"), nullable(List.class));
.spawn(
nullable(String.class),
nullable(String.class),
eq("/bar"),
nullable(List.class),
eq(2l));
}
@Test
@ -238,7 +246,8 @@ public class FlutterEngineGroupComponentTest {
nullable(String.class),
isNull(),
any(AssetManager.class),
eq(firstDartEntrypointArgs));
eq(firstDartEntrypointArgs),
eq(1l));
when(mockFlutterJNI.isAttached()).thenReturn(true);
jniAttached = false;
@ -251,7 +260,8 @@ public class FlutterEngineGroupComponentTest {
nullable(String.class),
nullable(String.class),
nullable(String.class),
nullable(List.class));
nullable(List.class),
eq(2l));
List<String> secondDartEntrypointArgs = new ArrayList<String>();
FlutterEngine secondEngine =
engineGroupUnderTest.createAndRunEngine(
@ -265,7 +275,8 @@ public class FlutterEngineGroupComponentTest {
nullable(String.class),
nullable(String.class),
nullable(String.class),
eq(secondDartEntrypointArgs));
eq(secondDartEntrypointArgs),
eq(2l));
}
@Test
@ -309,7 +320,8 @@ public class FlutterEngineGroupComponentTest {
nullable(String.class),
isNull(),
any(AssetManager.class),
nullable(List.class));
nullable(List.class),
eq(1l));
when(mockFlutterJNI.isAttached()).thenReturn(true);
jniAttached = false;
@ -322,7 +334,8 @@ public class FlutterEngineGroupComponentTest {
nullable(String.class),
nullable(String.class),
nullable(String.class),
nullable(List.class));
nullable(List.class),
eq(2l));
PlatformViewsController controller = new PlatformViewsController();
boolean waitForRestorationData = false;

View File

@ -79,6 +79,7 @@ public class FlutterEngineTest {
.when(flutterJNI)
.attachToNative();
GeneratedPluginRegistrant.clearRegisteredEngines();
FlutterEngine.resetNextEngineId();
}
@After
@ -130,6 +131,23 @@ public class FlutterEngineTest {
.dispatchPlatformMessage(eq("flutter/localization"), any(), anyInt(), anyInt());
}
@Test
public void itCanBeRetrievedByHandle() {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
when(mockFlutterJNI.isAttached()).thenReturn(true);
FlutterLoader mockFlutterLoader = mock(FlutterLoader.class);
when(mockFlutterLoader.automaticallyRegisterPlugins()).thenReturn(true);
FlutterEngine flutterEngine1 = new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJNI);
FlutterEngine flutterEngine2 = new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJNI);
assertEquals(flutterEngine1, FlutterEngine.engineForId(1));
assertEquals(flutterEngine2, FlutterEngine.engineForId(2));
flutterEngine1.destroy();
assertEquals(null, FlutterEngine.engineForId(1));
assertEquals(flutterEngine2, FlutterEngine.engineForId(2));
flutterEngine2.destroy();
assertEquals(null, FlutterEngine.engineForId(2));
}
// Helps show the root cause of MissingPluginException type errors like
// https://github.com/flutter/flutter/issues/78625.
@Test

View File

@ -60,7 +60,7 @@ public class DeferredComponentChannelTest {
public void deferredComponentChannel_installCompletesResults() {
MethodChannel rawChannel = mock(MethodChannel.class);
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = new DartExecutor(mockFlutterJNI, mock(AssetManager.class));
DartExecutor dartExecutor = new DartExecutor(mockFlutterJNI, mock(AssetManager.class), 0);
TestDeferredComponentManager testDeferredComponentManager = new TestDeferredComponentManager();
DeferredComponentChannel fakeDeferredComponentChannel =
new DeferredComponentChannel(dartExecutor);

View File

@ -160,6 +160,10 @@ static constexpr int kNumProfilerSamplesPerSec = 5;
std::unique_ptr<flutter::ConnectionCollection> _connections;
}
- (int64_t)engineIdentifier {
return reinterpret_cast<int64_t>((__bridge void*)self);
}
- (instancetype)init {
return [self initWithName:@"FlutterEngine" project:nil allowHeadlessExecution:YES];
}
@ -241,6 +245,11 @@ static constexpr int kNumProfilerSamplesPerSec = 5;
return self;
}
+ (FlutterEngine*)engineForIdentifier:(int64_t)identifier {
NSAssert([[NSThread currentThread] isMainThread], @"Must be called on the main thread.");
return (__bridge FlutterEngine*)reinterpret_cast<void*>(identifier);
}
- (void)setUpSceneLifecycleNotifications:(NSNotificationCenter*)center API_AVAILABLE(ios(13.0)) {
[center addObserver:self
selector:@selector(sceneWillEnterForeground:)
@ -704,9 +713,13 @@ static constexpr int kNumProfilerSamplesPerSec = 5;
libraryURI:(NSString*)libraryOrNil
entrypointArgs:(NSArray<NSString*>*)entrypointArgs {
// Launch the Dart application with the inferred run configuration.
self.shell.RunEngine([self.dartProject runConfigurationForEntrypoint:entrypoint
flutter::RunConfiguration configuration =
[self.dartProject runConfigurationForEntrypoint:entrypoint
libraryOrNil:libraryOrNil
entrypointArgs:entrypointArgs]);
entrypointArgs:entrypointArgs];
configuration.SetEngineId(self.engineIdentifier);
self.shell.RunEngine(std::move(configuration));
}
- (void)setUpShell:(std::unique_ptr<flutter::Shell>)shell
@ -1451,6 +1464,8 @@ static void SetEntryPoint(flutter::Settings* settings, NSString* entrypoint, NSS
libraryOrNil:libraryURI
entrypointArgs:entrypointArgs];
configuration.SetEngineId(result.engineIdentifier);
fml::WeakPtr<flutter::PlatformView> platform_view = _shell->GetPlatformView();
FML_DCHECK(platform_view);
// Static-cast safe since this class always creates PlatformViewIOS instances.

View File

@ -273,6 +273,20 @@ FLUTTER_ASSERT_ARC
XCTAssertNotNil(spawn);
}
- (void)testEngineId {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
[engine run];
int64_t id1 = engine.engineIdentifier;
XCTAssertTrue(id1 != 0);
FlutterEngine* spawn = [engine spawnWithEntrypoint:nil
libraryURI:nil
initialRoute:nil
entrypointArgs:nil];
int64_t id2 = spawn.engineIdentifier;
XCTAssertEqual([FlutterEngine engineForIdentifier:id1], engine);
XCTAssertEqual([FlutterEngine engineForIdentifier:id2], spawn);
}
- (void)testSetHandlerAfterRun {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
XCTestExpectation* gotMessage = [self expectationWithDescription:@"gotMessage"];

View File

@ -87,6 +87,22 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, readonly) FlutterDartProject* project;
/**
* Returns the engine handle. Used in FlutterEngineTest.
*/
- (int64_t)engineIdentifier;
/**
* Returns engine for the identifier. The identifier must be valid for an engine
* that is currently running, otherwise the behavior is undefined.
*
* The identifier can be obtained in Dart code through
* `PlatformDispatcher.instance.engineId`.
*
* This function must be called on the main thread.
*/
+ (nullable FlutterEngine*)engineForIdentifier:(int64_t)identifier;
@end
NS_ASSUME_NONNULL_END

View File

@ -642,6 +642,8 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
std::cout << message << std::endl;
};
flutterArguments.engine_id = reinterpret_cast<int64_t>((__bridge void*)self);
static size_t sTaskRunnerIdentifiers = 0;
const FlutterTaskRunnerDescription cocoa_task_runner_description = {
.struct_size = sizeof(FlutterTaskRunnerDescription),
@ -1159,6 +1161,11 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
_engine = nullptr;
}
+ (FlutterEngine*)engineForIdentifier:(int64_t)identifier {
NSAssert([[NSThread currentThread] isMainThread], @"Must be called on the main thread.");
return (__bridge FlutterEngine*)reinterpret_cast<void*>(identifier);
}
- (void)setUpPlatformViewChannel {
_platformViewsChannel =
[FlutterMethodChannel methodChannelWithName:@"flutter/platform_views"

View File

@ -863,6 +863,31 @@ TEST_F(FlutterEngineTest, ResponseFromBackgroundThread) {
}
}
TEST_F(FlutterEngineTest, CanGetEngineForId) {
FlutterEngine* engine = GetFlutterEngine();
fml::AutoResetWaitableEvent latch;
std::optional<int64_t> engineId;
AddNativeCallback("NotifyEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
const auto argument = Dart_GetNativeArgument(args, 0);
if (!Dart_IsNull(argument)) {
const auto id = tonic::DartConverter<int64_t>::FromDart(argument);
engineId = id;
}
latch.Signal();
}));
EXPECT_TRUE([engine runWithEntrypoint:@"testEngineId"]);
latch.Wait();
EXPECT_TRUE(engineId.has_value());
if (!engineId.has_value()) {
return;
}
EXPECT_EQ(engine, [FlutterEngine engineForIdentifier:*engineId]);
ShutDownEngine();
}
TEST_F(FlutterEngineTest, ThreadSynchronizerNotBlockingRasterThreadAfterShutdown) {
FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init];
[threadSynchronizer shutdown];

View File

@ -222,6 +222,17 @@ typedef NS_ENUM(NSInteger, FlutterAppExitResponse) {
* Returns an array of screen objects representing all of the screens available on the system.
*/
- (NSArray<NSScreen*>*)screens;
/**
* Returns engine for the identifier. The identifier must be valid for an engine
* that is currently running, otherwise the behavior is undefined.
*
* The identifier can be obtained in Dart code through
* `PlatformDispatcher.instance.engineId`.
*
* This function must be called on the main thread.
*/
+ (nullable FlutterEngine*)engineForIdentifier:(int64_t)identifier;
@end
@interface FlutterEngine (Tests)

View File

@ -85,3 +85,11 @@ void backgroundTest() {
void sendFooMessage() {
PlatformDispatcher.instance.sendPlatformMessage('foo', null, (ByteData? result) {});
}
@pragma('vm:external-name', 'NotifyEngineId')
external void notifyEngineId(int? engineId);
@pragma('vm:entry-point')
void testEngineId() {
notifyEngineId(PlatformDispatcher.instance.engineId);
}

View File

@ -2410,6 +2410,10 @@ FlutterEngineResult FlutterEngineInitialize(size_t version,
run_configuration.SetEntrypointArgs(std::move(arguments));
}
if (SAFE_ACCESS(args, engine_id, 0) != 0) {
run_configuration.SetEngineId(args->engine_id);
}
if (!run_configuration.IsValid()) {
return LOG_EMBEDDER_ERROR(
kInvalidArguments,

View File

@ -2631,6 +2631,11 @@ typedef struct {
/// the native view. The callback is invoked from a task posted to the
/// platform thread.
FlutterViewFocusChangeRequestCallback view_focus_change_request_callback;
/// Opaque identifier provided by the engine. Accessible in Dart code through
/// `PlatformDispatcher.instance.engineId`. Can be used in native code to
/// retrieve the engine instance that is running the Dart code.
int64_t engine_id;
} FlutterProjectArgs;
#ifndef FLUTTER_ENGINE_NO_PROTOTYPES

View File

@ -545,6 +545,7 @@ static FlEngine* fl_engine_new_full(FlDartProject* project,
g_return_val_if_fail(FL_IS_RENDERER(renderer), nullptr);
FlEngine* self = FL_ENGINE(g_object_new(fl_engine_get_type(), nullptr));
self->project = FL_DART_PROJECT(g_object_ref(project));
self->renderer = FL_RENDERER(g_object_ref(renderer));
if (binary_messenger != nullptr) {
@ -563,6 +564,12 @@ static FlEngine* fl_engine_new_full(FlDartProject* project,
return self;
}
FlEngine* fl_engine_for_id(int64_t id) {
void* engine = reinterpret_cast<void*>(id);
g_return_val_if_fail(FL_IS_ENGINE(engine), nullptr);
return FL_ENGINE(engine);
}
FlEngine* fl_engine_new_with_renderer(FlDartProject* project,
FlRenderer* renderer) {
g_return_val_if_fail(FL_IS_DART_PROJECT(project), nullptr);
@ -651,6 +658,7 @@ gboolean fl_engine_start(FlEngine* self, GError** error) {
dart_entrypoint_args != nullptr ? g_strv_length(dart_entrypoint_args) : 0;
args.dart_entrypoint_argv =
reinterpret_cast<const char* const*>(dart_entrypoint_args);
args.engine_id = reinterpret_cast<int64_t>(self);
FlutterCompositor compositor = {};
compositor.struct_size = sizeof(FlutterCompositor);

View File

@ -588,6 +588,20 @@ FlTextInputHandler* fl_engine_get_text_input_handler(FlEngine* engine);
*/
FlMouseCursorHandler* fl_engine_get_mouse_cursor_handler(FlEngine* engine);
/**
* fl_engine_for_id:
* @handle: an engine identifier obtained through
* PlatformDispatcher.instance.engineId.
*
* Returns Flutter engine associated with the identifier. The identifier
* must be valid and for a running engine otherwise the behavior is
* undefined.
* Must be called from the main thread.
*
* Returns: a #FlEngine or NULL.
*/
FlEngine* fl_engine_for_id(int64_t handle);
G_END_DECLS
#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_ENGINE_PRIVATE_H_

View File

@ -416,6 +416,29 @@ TEST(FlEngineTest, DartEntrypointArgs) {
EXPECT_TRUE(called);
}
TEST(FlEngineTest, EngineId) {
g_autoptr(FlDartProject) project = fl_dart_project_new();
g_autoptr(FlEngine) engine = fl_engine_new(project);
int64_t engine_id;
fl_engine_get_embedder_api(engine)->Initialize = MOCK_ENGINE_PROC(
Initialize,
([&engine_id](size_t version, const FlutterRendererConfig* config,
const FlutterProjectArgs* args, void* user_data,
FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) {
engine_id = args->engine_id;
return kSuccess;
}));
fl_engine_get_embedder_api(engine)->RunInitialized =
MOCK_ENGINE_PROC(RunInitialized, ([](auto engine) { return kSuccess; }));
g_autoptr(GError) error = nullptr;
EXPECT_TRUE(fl_engine_start(engine, &error));
EXPECT_EQ(error, nullptr);
EXPECT_TRUE(engine_id != 0);
EXPECT_EQ(fl_engine_for_id(engine_id), engine);
}
TEST(FlEngineTest, Locales) {
g_autofree gchar* initial_language = g_strdup(g_getenv("LANGUAGE"));
g_setenv("LANGUAGE", "de:en_US", TRUE);

View File

@ -396,3 +396,11 @@ void onMetricsChangedSignalViewIds() {
void mergedUIThread() {
signal();
}
@pragma('vm:external-name', 'NotifyEngineId')
external void notifyEngineId(int? handle);
@pragma('vm:entry-point')
void testEngineId() {
notifyEngineId(ui.PlatformDispatcher.instance.engineId);
}

View File

@ -197,6 +197,12 @@ bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine_ref) {
return result;
}
FLUTTER_EXPORT FlutterDesktopEngineRef FlutterDesktopEngineForId(
int64_t engine_id) {
return HandleForEngine(
flutter::FlutterWindowsEngine::GetEngineForId(engine_id));
}
bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine,
const char* entry_point) {
std::string_view entry_point_view{""};

View File

@ -233,6 +233,10 @@ FlutterWindowsEngine::~FlutterWindowsEngine() {
Stop();
}
FlutterWindowsEngine* FlutterWindowsEngine::GetEngineForId(int64_t engine_id) {
return reinterpret_cast<FlutterWindowsEngine*>(engine_id);
}
void FlutterWindowsEngine::SetSwitches(
const std::vector<std::string>& switches) {
project_->SetSwitches(switches);
@ -309,6 +313,7 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) {
args.icu_data_path = icu_path_string.c_str();
args.command_line_argc = static_cast<int>(argv.size());
args.command_line_argv = argv.empty() ? nullptr : argv.data();
args.engine_id = reinterpret_cast<int64_t>(this);
// Fail if conflicting non-default entrypoints are specified in the method
// argument and the project.

View File

@ -96,6 +96,12 @@ class FlutterWindowsEngine {
virtual ~FlutterWindowsEngine();
// Returns the engine associated with the given identifier.
// The engine_id must be valid and for a running engine, otherwise
// the behavior is undefined.
// Must be called on the platform thread.
static FlutterWindowsEngine* GetEngineForId(int64_t engine_id);
// Starts running the entrypoint function specifed in the project bundle. If
// unspecified, defaults to main().
//

View File

@ -65,6 +65,15 @@ FLUTTER_EXPORT void FlutterDesktopEngineRegisterPlatformViewType(
const char* view_type_name,
FlutterPlatformViewTypeEntry view_type);
// Returns the engine associated with the given identifier. Engine identifier
// must be valid and for a running engine, otherwise the behavior is undefined.
//
// Identifier can be obtained from PlatformDispatcher.instance.engineId.
//
// This method must be called from the platform thread.
FLUTTER_EXPORT FlutterDesktopEngineRef FlutterDesktopEngineForId(
int64_t engine_id);
#if defined(__cplusplus)
}
#endif

View File

@ -652,5 +652,34 @@ TEST_F(WindowsTest, AddRemoveView) {
}
}
TEST_F(WindowsTest, EngineId) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
builder.SetDartEntrypoint("testEngineId");
fml::AutoResetWaitableEvent latch;
std::optional<int64_t> engineId;
context.AddNativeFunction(
"NotifyEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
const auto argument = Dart_GetNativeArgument(args, 0);
if (!Dart_IsNull(argument)) {
const auto handle = tonic::DartConverter<int64_t>::FromDart(argument);
engineId = handle;
}
latch.Signal();
}));
// Create the implicit view.
ViewControllerPtr first_controller{builder.Run()};
ASSERT_NE(first_controller, nullptr);
latch.Wait();
EXPECT_TRUE(engineId.has_value());
if (!engineId.has_value()) {
return;
}
auto engine = FlutterDesktopViewControllerGetEngine(first_controller.get());
EXPECT_EQ(engine, FlutterDesktopEngineForId(*engineId));
}
} // namespace testing
} // namespace flutter