diff --git a/dev/integration_tests/android_engine_test/lib/engine_handle.dart b/dev/integration_tests/android_engine_test/lib/engine_handle.dart new file mode 100644 index 0000000000..a13d3202aa --- /dev/null +++ b/dev/integration_tests/android_engine_test/lib/engine_handle.dart @@ -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({'engineId': PlatformDispatcher.instance.engineId}); + }, + commands: [nativeDriverCommands], + ); + runApp(const SizedBox()); +} diff --git a/dev/integration_tests/android_engine_test/test_driver/engine_handle_test.dart b/dev/integration_tests/android_engine_test/test_driver/engine_handle_test.dart new file mode 100644 index 0000000000..32fb36f1e9 --- /dev/null +++ b/dev/integration_tests/android_engine_test/test_driver/engine_handle_test.dart @@ -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 response = + json.decode(await flutterDriver.requestData('')) as Map; + 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); +} diff --git a/engine/src/flutter/lib/ui/hooks.dart b/engine/src/flutter/lib/ui/hooks.dart index 4404d97bcf..c053eb27e8 100644 --- a/engine/src/flutter/lib/ui/hooks.dart +++ b/engine/src/flutter/lib/ui/hooks.dart @@ -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 ids, diff --git a/engine/src/flutter/lib/ui/platform_dispatcher.dart b/engine/src/flutter/lib/ui/platform_dispatcher.dart index d8a55ffbae..80997e29e0 100644 --- a/engine/src/flutter/lib/ui/platform_dispatcher.dart +++ b/engine/src/flutter/lib/ui/platform_dispatcher.dart @@ -305,6 +305,13 @@ class PlatformDispatcher { _invoke1(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. diff --git a/engine/src/flutter/lib/ui/window/platform_configuration.cc b/engine/src/flutter/lib/ui/window/platform_configuration.cc index d87b16a636..654ac77c9f 100644 --- a/engine/src/flutter/lib/ui/window/platform_configuration.cc +++ b/engine/src/flutter/lib/ui/window/platform_configuration.cc @@ -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 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) { diff --git a/engine/src/flutter/lib/ui/window/platform_configuration.h b/engine/src/flutter/lib/ui/window/platform_configuration.h index a335da659d..e1f3ba474e 100644 --- a/engine/src/flutter/lib/ui/window/platform_configuration.h +++ b/engine/src/flutter/lib/ui/window/platform_configuration.h @@ -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_; diff --git a/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart index 896f9d5ee1..ca59012d30 100644 --- a/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart @@ -37,6 +37,8 @@ abstract class PlatformDispatcher { FlutterView? get implicitView; + int? get engineId; + VoidCallback? get onMetricsChanged; set onMetricsChanged(VoidCallback? callback); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart index dceb68132e..528b57bf8d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -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 diff --git a/engine/src/flutter/runtime/runtime_controller.cc b/engine/src/flutter/runtime/runtime_controller.cc index f037274d0a..950a3623dc 100644 --- a/engine/src/flutter/runtime/runtime_controller.cc +++ b/engine/src/flutter/runtime/runtime_controller.cc @@ -537,7 +537,8 @@ bool RuntimeController::LaunchRootIsolate( std::optional dart_entrypoint_library, const std::vector& dart_entrypoint_args, std::unique_ptr isolate_configuration, - std::shared_ptr native_assets_manager) { + std::shared_ptr native_assets_manager, + std::optional 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."; } diff --git a/engine/src/flutter/runtime/runtime_controller.h b/engine/src/flutter/runtime/runtime_controller.h index 0f7c355b17..243891f41b 100644 --- a/engine/src/flutter/runtime/runtime_controller.h +++ b/engine/src/flutter/runtime/runtime_controller.h @@ -156,6 +156,8 @@ class RuntimeController : public PlatformConfigurationClient, /// @param[in] dart_entrypoint_args Arguments passed as a List /// 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 dart_entrypoint_library, const std::vector& dart_entrypoint_args, std::unique_ptr isolate_configuration, - std::shared_ptr native_assets_manager); + std::shared_ptr native_assets_manager, + std::optional engine_id); //---------------------------------------------------------------------------- /// @brief Clone the runtime controller. Launching an isolate with a diff --git a/engine/src/flutter/shell/common/engine.cc b/engine/src/flutter/shell/common/engine.cc index c05f9d041f..911b1685df 100644 --- a/engine/src/flutter/shell/common/engine.cc +++ b/engine/src/flutter/shell/common/engine.cc @@ -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; } diff --git a/engine/src/flutter/shell/common/fixtures/shell_test.dart b/engine/src/flutter/shell/common/fixtures/shell_test.dart index 147948d887..830d905e42 100644 --- a/engine/src/flutter/shell/common/fixtures/shell_test.dart +++ b/engine/src/flutter/shell/common/fixtures/shell_test.dart @@ -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); +} diff --git a/engine/src/flutter/shell/common/run_configuration.cc b/engine/src/flutter/shell/common/run_configuration.cc index 6c835ec5e0..6f22efbdc8 100644 --- a/engine/src/flutter/shell/common/run_configuration.cc +++ b/engine/src/flutter/shell/common/run_configuration.cc @@ -105,6 +105,15 @@ const std::vector& 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 RunConfiguration::GetEngineId() const { + return engine_id_; +} + std::unique_ptr RunConfiguration::TakeIsolateConfiguration() { return std::move(isolate_configuration_); diff --git a/engine/src/flutter/shell/common/run_configuration.h b/engine/src/flutter/shell/common/run_configuration.h index 2c327b7af0..f2f3a2b406 100644 --- a/engine/src/flutter/shell/common/run_configuration.h +++ b/engine/src/flutter/shell/common/run_configuration.h @@ -194,12 +194,22 @@ class RunConfiguration { /// std::unique_ptr 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 GetEngineId() const; + private: std::unique_ptr isolate_configuration_; std::shared_ptr asset_manager_; std::string entrypoint_ = "main"; std::string entrypoint_library_ = ""; std::vector entrypoint_args_; + std::optional engine_id_; FML_DISALLOW_COPY_AND_ASSIGN(RunConfiguration); }; diff --git a/engine/src/flutter/shell/common/shell_test.h b/engine/src/flutter/shell/common/shell_test.h index e980474364..90a312c676 100644 --- a/engine/src/flutter/shell/common/shell_test.h +++ b/engine/src/flutter/shell/common/shell_test.h @@ -71,6 +71,7 @@ class ShellTest : public FixtureTest { // Defaults to calling ShellTestPlatformView::Create with the provided // arguments. Shell::CreateCallback platform_view_create_callback; + std::optional engine_id; }; ShellTest(); diff --git a/engine/src/flutter/shell/common/shell_unittests.cc b/engine/src/flutter/shell/common/shell_unittests.cc index 9dd8085426..0d04cb6da6 100644 --- a/engine/src/flutter/shell/common/shell_unittests.cc +++ b/engine/src/flutter/shell/common/shell_unittests.cc @@ -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 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::FromDart(arg); + } + latch.Signal(); + })); + fml::AutoResetWaitableEvent check_latch; + + std::unique_ptr 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 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::FromDart(arg); + } + latch.Signal(); + })); + fml::AutoResetWaitableEvent check_latch; + + std::unique_ptr 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); } diff --git a/engine/src/flutter/shell/platform/android/android_shell_holder.cc b/engine/src/flutter/shell/platform/android/android_shell_holder.cc index 6fe15e673f..58a0eb0707 100644 --- a/engine/src/flutter/shell/platform/android/android_shell_holder.cc +++ b/engine/src/flutter/shell/platform/android/android_shell_holder.cc @@ -222,7 +222,8 @@ std::unique_ptr AndroidShellHolder::Spawn( const std::string& entrypoint, const std::string& libraryUrl, const std::string& initial_route, - const std::vector& entrypoint_args) const { + const std::vector& 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::Spawn( // Fail the whole thing. return nullptr; } + config->SetEngineId(engine_id); std::unique_ptr shell = shell_->Spawn(std::move(config.value()), initial_route, @@ -284,7 +286,8 @@ void AndroidShellHolder::Launch( std::unique_ptr apk_asset_provider, const std::string& entrypoint, const std::string& libraryUrl, - const std::vector& entrypoint_args) { + const std::vector& 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())); } diff --git a/engine/src/flutter/shell/platform/android/android_shell_holder.h b/engine/src/flutter/shell/platform/android/android_shell_holder.h index 5894c0f966..d3a88006f8 100644 --- a/engine/src/flutter/shell/platform/android/android_shell_holder.h +++ b/engine/src/flutter/shell/platform/android/android_shell_holder.h @@ -79,12 +79,14 @@ class AndroidShellHolder { const std::string& entrypoint, const std::string& libraryUrl, const std::string& initial_route, - const std::vector& entrypoint_args) const; + const std::vector& entrypoint_args, + int64_t engine_id) const; void Launch(std::unique_ptr apk_asset_provider, const std::string& entrypoint, const std::string& libraryUrl, - const std::vector& entrypoint_args); + const std::vector& entrypoint_args, + int64_t engine_id); const flutter::Settings& GetSettings() const; diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 7845b1e3e5..f1bae000df 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -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 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 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 + * + *
PlatformDispatcher.instance.engineId
+ * + *

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. */ diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index be6ac39e68..656fefddf4 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -460,7 +460,8 @@ public class FlutterJNI { @Nullable String entrypointFunctionName, @Nullable String pathToEntrypointFunction, @Nullable String initialRoute, - @Nullable List entrypointArgs) { + @Nullable List 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 entrypointArgs); + @Nullable List entrypointArgs, + long engineId); /** * Detaches this {@code FlutterJNI} instance from Flutter's native engine, which precludes any @@ -491,7 +494,7 @@ public class FlutterJNI { *

This method must not be invoked if {@code FlutterJNI} is not already attached to native. * *

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

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 entrypointArgs) { + @Nullable List 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 entrypointArgs); + @Nullable List entrypointArgs, + long engineId); // ------ End Dart Execution Support ------- // --------- Start Platform Message Support ------ diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java index 8f6b862c5a..6e2bdba986 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java @@ -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; } diff --git a/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc b/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc index a74ceb6cfc..9926fb00d7 100644 --- a/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc +++ b/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc @@ -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( 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(&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(&RunBundleAndSnapshotFromLibrary), }, { diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java index 4e91e7788c..72693896e9 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java @@ -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 secondDartEntrypointArgs = new ArrayList(); 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; diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java index 82e39f771e..dc339860b1 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java @@ -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 diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/DeferredComponentChannelTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/DeferredComponentChannelTest.java index 5768f7411d..c24b12697f 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/DeferredComponentChannelTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/DeferredComponentChannelTest.java @@ -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); diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index df4b48f8df..505a453487 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -160,6 +160,10 @@ static constexpr int kNumProfilerSamplesPerSec = 5; std::unique_ptr _connections; } +- (int64_t)engineIdentifier { + return reinterpret_cast((__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(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*)entrypointArgs { // Launch the Dart application with the inferred run configuration. - self.shell.RunEngine([self.dartProject runConfigurationForEntrypoint:entrypoint - libraryOrNil:libraryOrNil - entrypointArgs:entrypointArgs]); + flutter::RunConfiguration configuration = + [self.dartProject runConfigurationForEntrypoint:entrypoint + libraryOrNil:libraryOrNil + entrypointArgs:entrypointArgs]; + + configuration.SetEngineId(self.engineIdentifier); + self.shell.RunEngine(std::move(configuration)); } - (void)setUpShell:(std::unique_ptr)shell @@ -1451,6 +1464,8 @@ static void SetEntryPoint(flutter::Settings* settings, NSString* entrypoint, NSS libraryOrNil:libraryURI entrypointArgs:entrypointArgs]; + configuration.SetEngineId(result.engineIdentifier); + fml::WeakPtr platform_view = _shell->GetPlatformView(); FML_DCHECK(platform_view); // Static-cast safe since this class always creates PlatformViewIOS instances. diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 27e5d3728b..785b084ec7 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -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"]; diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h index 5cfb1acade..de79d011c5 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h @@ -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 diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 2c0f4c9bec..081b85bc32 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -642,6 +642,8 @@ static void SetThreadPriority(FlutterThreadPriority priority) { std::cout << message << std::endl; }; + flutterArguments.engine_id = reinterpret_cast((__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(identifier); +} + - (void)setUpPlatformViewChannel { _platformViewsChannel = [FlutterMethodChannel methodChannelWithName:@"flutter/platform_views" diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index b7d8eb2311..5a7727a50b 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -863,6 +863,31 @@ TEST_F(FlutterEngineTest, ResponseFromBackgroundThread) { } } +TEST_F(FlutterEngineTest, CanGetEngineForId) { + FlutterEngine* engine = GetFlutterEngine(); + + fml::AutoResetWaitableEvent latch; + std::optional 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::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]; diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index d1c328a226..7554749cf9 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -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*)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) diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart index 408c321519..208d466cbf 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart @@ -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); +} diff --git a/engine/src/flutter/shell/platform/embedder/embedder.cc b/engine/src/flutter/shell/platform/embedder/embedder.cc index 616465807e..8d36f33a38 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder.cc +++ b/engine/src/flutter/shell/platform/embedder/embedder.cc @@ -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, diff --git a/engine/src/flutter/shell/platform/embedder/embedder.h b/engine/src/flutter/shell/platform/embedder/embedder.h index 12c28cb720..5a76854dd6 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder.h +++ b/engine/src/flutter/shell/platform/embedder/embedder.h @@ -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 diff --git a/engine/src/flutter/shell/platform/linux/fl_engine.cc b/engine/src/flutter/shell/platform/linux/fl_engine.cc index 32c7e52e58..38edff1af9 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine.cc +++ b/engine/src/flutter/shell/platform/linux/fl_engine.cc @@ -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(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(dart_entrypoint_args); + args.engine_id = reinterpret_cast(self); FlutterCompositor compositor = {}; compositor.struct_size = sizeof(FlutterCompositor); diff --git a/engine/src/flutter/shell/platform/linux/fl_engine_private.h b/engine/src/flutter/shell/platform/linux/fl_engine_private.h index cced1a8017..dfe395b7a3 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine_private.h +++ b/engine/src/flutter/shell/platform/linux/fl_engine_private.h @@ -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_ diff --git a/engine/src/flutter/shell/platform/linux/fl_engine_test.cc b/engine/src/flutter/shell/platform/linux/fl_engine_test.cc index 800f2f013c..0571bb6c32 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_engine_test.cc @@ -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); diff --git a/engine/src/flutter/shell/platform/windows/fixtures/main.dart b/engine/src/flutter/shell/platform/windows/fixtures/main.dart index f35e9bf356..ad03800622 100644 --- a/engine/src/flutter/shell/platform/windows/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/windows/fixtures/main.dart @@ -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); +} diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows.cc b/engine/src/flutter/shell/platform/windows/flutter_windows.cc index c79c6aca60..9b01ca30c8 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows.cc @@ -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{""}; diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc index c78540bcde..6f993167ad 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc @@ -233,6 +233,10 @@ FlutterWindowsEngine::~FlutterWindowsEngine() { Stop(); } +FlutterWindowsEngine* FlutterWindowsEngine::GetEngineForId(int64_t engine_id) { + return reinterpret_cast(engine_id); +} + void FlutterWindowsEngine::SetSwitches( const std::vector& 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(argv.size()); args.command_line_argv = argv.empty() ? nullptr : argv.data(); + args.engine_id = reinterpret_cast(this); // Fail if conflicting non-default entrypoints are specified in the method // argument and the project. diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h index 2d7b730580..a2a33e277b 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h @@ -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(). // diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_internal.h b/engine/src/flutter/shell/platform/windows/flutter_windows_internal.h index bb1e6f7679..4e1a1666c7 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_internal.h +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_internal.h @@ -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 diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc index 97a9d0dbe2..25a336c8c9 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc @@ -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 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::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