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); PlatformDispatcher.instance._sendViewFocusEvent(viewFocusEvent);
} }
@pragma('vm:entry-point')
void _setEngineId(int engineId) {
PlatformDispatcher.instance._engineId = engineId;
}
@pragma('vm:entry-point') @pragma('vm:entry-point')
void _updateDisplays( void _updateDisplays(
List<int> ids, List<int> ids,

View File

@ -305,6 +305,13 @@ class PlatformDispatcher {
_invoke1<ViewFocusEvent>(onViewFocusChange, _onViewFocusChangeZone, event); _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. // Called from the engine, via hooks.dart.
// //
// Updates the available displays. // Updates the available displays.

View File

@ -49,6 +49,8 @@ void PlatformConfiguration::DidCreateIsolate() {
send_view_focus_event_.Set( send_view_focus_event_.Set(
tonic::DartState::Current(), tonic::DartState::Current(),
Dart_GetField(library, tonic::ToDart("_sendViewFocusEvent"))); Dart_GetField(library, tonic::ToDart("_sendViewFocusEvent")));
set_engine_id_.Set(tonic::DartState::Current(),
Dart_GetField(library, tonic::ToDart("_setEngineId")));
update_window_metrics_.Set( update_window_metrics_.Set(
tonic::DartState::Current(), tonic::DartState::Current(),
Dart_GetField(library, tonic::ToDart("_updateWindowMetrics"))); Dart_GetField(library, tonic::ToDart("_updateWindowMetrics")));
@ -168,6 +170,20 @@ bool PlatformConfiguration::SendFocusEvent(const ViewFocusEvent& event) {
return true; 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( bool PlatformConfiguration::UpdateViewMetrics(
int64_t view_id, int64_t view_id,
const ViewportMetrics& view_metrics) { const ViewportMetrics& view_metrics) {

View File

@ -356,6 +356,14 @@ class PlatformConfiguration final {
/// @return Whether the focus event was sent. /// @return Whether the focus event was sent.
bool SendFocusEvent(const ViewFocusEvent& event); 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. /// @brief Update the view metrics for the specified view.
/// ///
@ -548,6 +556,7 @@ class PlatformConfiguration final {
tonic::DartPersistentValue add_view_; tonic::DartPersistentValue add_view_;
tonic::DartPersistentValue remove_view_; tonic::DartPersistentValue remove_view_;
tonic::DartPersistentValue send_view_focus_event_; tonic::DartPersistentValue send_view_focus_event_;
tonic::DartPersistentValue set_engine_id_;
tonic::DartPersistentValue update_window_metrics_; tonic::DartPersistentValue update_window_metrics_;
tonic::DartPersistentValue update_displays_; tonic::DartPersistentValue update_displays_;
tonic::DartPersistentValue update_locales_; tonic::DartPersistentValue update_locales_;

View File

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

View File

@ -140,6 +140,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
@override @override
EngineFlutterWindow? get implicitView => viewManager[kImplicitViewId] as EngineFlutterWindow?; EngineFlutterWindow? get implicitView => viewManager[kImplicitViewId] as EngineFlutterWindow?;
@override
int? get engineId => null;
/// A callback that is invoked whenever the platform's [devicePixelRatio], /// A callback that is invoked whenever the platform's [devicePixelRatio],
/// [physicalSize], [padding], [viewInsets], or [systemGestureInsets] /// [physicalSize], [padding], [viewInsets], or [systemGestureInsets]
/// values change, for example when the device is rotated or when the /// 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, std::optional<std::string> dart_entrypoint_library,
const std::vector<std::string>& dart_entrypoint_args, const std::vector<std::string>& dart_entrypoint_args,
std::unique_ptr<IsolateConfiguration> isolate_configuration, 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()) { if (root_isolate_.lock()) {
FML_LOG(ERROR) << "Root isolate was already running."; FML_LOG(ERROR) << "Root isolate was already running.";
return false; return false;
@ -586,6 +587,11 @@ bool RuntimeController::LaunchRootIsolate(
if (!FlushRuntimeStateToIsolate()) { if (!FlushRuntimeStateToIsolate()) {
FML_DLOG(ERROR) << "Could not set up initial isolate state."; 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 { } else {
FML_DCHECK(false) << "RuntimeController created without window binding."; 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> /// @param[in] dart_entrypoint_args Arguments passed as a List<String>
/// to Dart's entrypoint function. /// to Dart's entrypoint function.
/// @param[in] isolate_configuration The isolate configuration /// @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 /// @return If the isolate could be launched and guided to the
/// `DartIsolate::Phase::Running` phase. /// `DartIsolate::Phase::Running` phase.
@ -167,7 +169,8 @@ class RuntimeController : public PlatformConfigurationClient,
std::optional<std::string> dart_entrypoint_library, std::optional<std::string> dart_entrypoint_library,
const std::vector<std::string>& dart_entrypoint_args, const std::vector<std::string>& dart_entrypoint_args,
std::unique_ptr<IsolateConfiguration> isolate_configuration, 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 /// @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.GetEntrypointLibrary(), //
configuration.GetEntrypointArgs(), // configuration.GetEntrypointArgs(), //
configuration.TakeIsolateConfiguration(), // configuration.TakeIsolateConfiguration(), //
native_assets_manager_) // native_assets_manager_, //
) { configuration.GetEngineId())) //
{
return RunStatus::Failure; return RunStatus::Failure;
} }

View File

@ -640,3 +640,11 @@ void testSendViewFocusEvent() {
}; };
notifyNative(); 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_; 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> std::unique_ptr<IsolateConfiguration>
RunConfiguration::TakeIsolateConfiguration() { RunConfiguration::TakeIsolateConfiguration() {
return std::move(isolate_configuration_); return std::move(isolate_configuration_);

View File

@ -194,12 +194,22 @@ class RunConfiguration {
/// ///
std::unique_ptr<IsolateConfiguration> TakeIsolateConfiguration(); 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: private:
std::unique_ptr<IsolateConfiguration> isolate_configuration_; std::unique_ptr<IsolateConfiguration> isolate_configuration_;
std::shared_ptr<AssetManager> asset_manager_; std::shared_ptr<AssetManager> asset_manager_;
std::string entrypoint_ = "main"; std::string entrypoint_ = "main";
std::string entrypoint_library_ = ""; std::string entrypoint_library_ = "";
std::vector<std::string> entrypoint_args_; std::vector<std::string> entrypoint_args_;
std::optional<int64_t> engine_id_;
FML_DISALLOW_COPY_AND_ASSIGN(RunConfiguration); FML_DISALLOW_COPY_AND_ASSIGN(RunConfiguration);
}; };

View File

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

View File

@ -5003,7 +5003,69 @@ TEST_F(ShellTest, SendViewFocusEvent) {
latch.Wait(); latch.Wait();
ASSERT_EQ(last_event, ASSERT_EQ(last_event,
"2 ViewFocusState.unfocused ViewFocusDirection.backward"); "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); 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& entrypoint,
const std::string& libraryUrl, const std::string& libraryUrl,
const std::string& initial_route, 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()) FML_DCHECK(shell_ && shell_->IsSetup())
<< "A new Shell can only be spawned " << "A new Shell can only be spawned "
"if the current Shell is properly constructed"; "if the current Shell is properly constructed";
@ -269,6 +270,7 @@ std::unique_ptr<AndroidShellHolder> AndroidShellHolder::Spawn(
// Fail the whole thing. // Fail the whole thing.
return nullptr; return nullptr;
} }
config->SetEngineId(engine_id);
std::unique_ptr<flutter::Shell> shell = std::unique_ptr<flutter::Shell> shell =
shell_->Spawn(std::move(config.value()), initial_route, shell_->Spawn(std::move(config.value()), initial_route,
@ -284,7 +286,8 @@ void AndroidShellHolder::Launch(
std::unique_ptr<APKAssetProvider> apk_asset_provider, std::unique_ptr<APKAssetProvider> apk_asset_provider,
const std::string& entrypoint, const std::string& entrypoint,
const std::string& libraryUrl, const std::string& libraryUrl,
const std::vector<std::string>& entrypoint_args) { const std::vector<std::string>& entrypoint_args,
int64_t engine_id) {
if (!IsValid()) { if (!IsValid()) {
return; return;
} }
@ -294,6 +297,7 @@ void AndroidShellHolder::Launch(
if (!config) { if (!config) {
return; return;
} }
config->SetEngineId(engine_id);
UpdateDisplayMetrics(); UpdateDisplayMetrics();
shell_->RunEngine(std::move(config.value())); shell_->RunEngine(std::move(config.value()));
} }

View File

@ -79,12 +79,14 @@ class AndroidShellHolder {
const std::string& entrypoint, const std::string& entrypoint,
const std::string& libraryUrl, const std::string& libraryUrl,
const std::string& initial_route, 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, void Launch(std::unique_ptr<APKAssetProvider> apk_asset_provider,
const std::string& entrypoint, const std::string& entrypoint,
const std::string& libraryUrl, 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; 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.platform.PlatformViewsController2;
import io.flutter.plugin.text.ProcessTextPlugin; import io.flutter.plugin.text.ProcessTextPlugin;
import io.flutter.util.ViewUtils; import io.flutter.util.ViewUtils;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -115,6 +117,20 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
// Engine Lifecycle. // Engine Lifecycle.
@NonNull private final Set<EngineLifecycleListener> engineLifecycleListeners = new HashSet<>(); @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 @NonNull
private final EngineLifecycleListener engineLifecycleListener = private final EngineLifecycleListener engineLifecycleListener =
new EngineLifecycleListener() { new EngineLifecycleListener() {
@ -312,6 +328,10 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
boolean automaticallyRegisterPlugins, boolean automaticallyRegisterPlugins,
boolean waitForRestorationData, boolean waitForRestorationData,
@Nullable FlutterEngineGroup group) { @Nullable FlutterEngineGroup group) {
this.engineId = nextEngineId++;
idToEngine.put(engineId, this);
AssetManager assetManager; AssetManager assetManager;
try { try {
assetManager = context.createPackageContext(context.getPackageName(), 0).getAssets(); assetManager = context.createPackageContext(context.getPackageName(), 0).getAssets();
@ -326,7 +346,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
} }
this.flutterJNI = flutterJNI; this.flutterJNI = flutterJNI;
this.dartExecutor = new DartExecutor(flutterJNI, assetManager); this.dartExecutor = new DartExecutor(flutterJNI, assetManager, engineId);
this.dartExecutor.onAttachedToJNI(); this.dartExecutor.onAttachedToJNI();
DeferredComponentManager deferredComponentManager = DeferredComponentManager deferredComponentManager =
@ -451,7 +471,8 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
dartEntrypoint.dartEntrypointFunctionName, dartEntrypoint.dartEntrypointFunctionName,
dartEntrypoint.dartEntrypointLibrary, dartEntrypoint.dartEntrypointLibrary,
initialRoute, initialRoute,
dartEntrypointArgs); dartEntrypointArgs,
nextEngineId);
return new FlutterEngine( return new FlutterEngine(
context, // Context. context, // Context.
null, // FlutterLoader. A null value passed here causes the constructor to get it from the 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(); FlutterInjector.instance().deferredComponentManager().destroy();
deferredComponentChannel.setDeferredComponentManager(null); deferredComponentChannel.setDeferredComponentManager(null);
} }
idToEngine.remove(engineId);
} }
/** /**
@ -679,6 +701,25 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
return pluginRegistry; 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. */ /** Lifecycle callbacks for Flutter engine lifecycle events. */
public interface EngineLifecycleListener { public interface EngineLifecycleListener {
/** Lifecycle callback invoked before a hot restart of the Flutter engine. */ /** 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 entrypointFunctionName,
@Nullable String pathToEntrypointFunction, @Nullable String pathToEntrypointFunction,
@Nullable String initialRoute, @Nullable String initialRoute,
@Nullable List<String> entrypointArgs) { @Nullable List<String> entrypointArgs,
long engineId) {
ensureRunningOnMainThread(); ensureRunningOnMainThread();
ensureAttachedToNative(); ensureAttachedToNative();
FlutterJNI spawnedJNI = FlutterJNI spawnedJNI =
@ -469,7 +470,8 @@ public class FlutterJNI {
entrypointFunctionName, entrypointFunctionName,
pathToEntrypointFunction, pathToEntrypointFunction,
initialRoute, initialRoute,
entrypointArgs); entrypointArgs,
engineId);
Preconditions.checkState( Preconditions.checkState(
spawnedJNI.nativeShellHolderId != null && spawnedJNI.nativeShellHolderId != 0, spawnedJNI.nativeShellHolderId != null && spawnedJNI.nativeShellHolderId != 0,
"Failed to spawn new JNI connected shell from existing shell."); "Failed to spawn new JNI connected shell from existing shell.");
@ -482,7 +484,8 @@ public class FlutterJNI {
@Nullable String entrypointFunctionName, @Nullable String entrypointFunctionName,
@Nullable String pathToEntrypointFunction, @Nullable String pathToEntrypointFunction,
@Nullable String initialRoute, @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 * 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>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 * <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. * accumulated thereafter.
* *
* <p>It is permissible to re-attach this instance to native after detaching it from native. * <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 entrypointFunctionName,
@Nullable String pathToEntrypointFunction, @Nullable String pathToEntrypointFunction,
@NonNull AssetManager assetManager, @NonNull AssetManager assetManager,
@Nullable List<String> entrypointArgs) { @Nullable List<String> entrypointArgs,
long engineId) {
ensureRunningOnMainThread(); ensureRunningOnMainThread();
ensureAttachedToNative(); ensureAttachedToNative();
nativeRunBundleAndSnapshotFromLibrary( nativeRunBundleAndSnapshotFromLibrary(
@ -1011,7 +1015,8 @@ public class FlutterJNI {
entrypointFunctionName, entrypointFunctionName,
pathToEntrypointFunction, pathToEntrypointFunction,
assetManager, assetManager,
entrypointArgs); entrypointArgs,
engineId);
} }
private native void nativeRunBundleAndSnapshotFromLibrary( private native void nativeRunBundleAndSnapshotFromLibrary(
@ -1020,7 +1025,8 @@ public class FlutterJNI {
@Nullable String entrypointFunctionName, @Nullable String entrypointFunctionName,
@Nullable String pathToEntrypointFunction, @Nullable String pathToEntrypointFunction,
@NonNull AssetManager manager, @NonNull AssetManager manager,
@Nullable List<String> entrypointArgs); @Nullable List<String> entrypointArgs,
long engineId);
// ------ End Dart Execution Support ------- // ------ End Dart Execution Support -------
// --------- Start Platform Message Support ------ // --------- Start Platform Message Support ------

View File

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

View File

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

View File

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

View File

@ -79,6 +79,7 @@ public class FlutterEngineTest {
.when(flutterJNI) .when(flutterJNI)
.attachToNative(); .attachToNative();
GeneratedPluginRegistrant.clearRegisteredEngines(); GeneratedPluginRegistrant.clearRegisteredEngines();
FlutterEngine.resetNextEngineId();
} }
@After @After
@ -130,6 +131,23 @@ public class FlutterEngineTest {
.dispatchPlatformMessage(eq("flutter/localization"), any(), anyInt(), anyInt()); .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 // Helps show the root cause of MissingPluginException type errors like
// https://github.com/flutter/flutter/issues/78625. // https://github.com/flutter/flutter/issues/78625.
@Test @Test

View File

@ -60,7 +60,7 @@ public class DeferredComponentChannelTest {
public void deferredComponentChannel_installCompletesResults() { public void deferredComponentChannel_installCompletesResults() {
MethodChannel rawChannel = mock(MethodChannel.class); MethodChannel rawChannel = mock(MethodChannel.class);
FlutterJNI mockFlutterJNI = mock(FlutterJNI.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(); TestDeferredComponentManager testDeferredComponentManager = new TestDeferredComponentManager();
DeferredComponentChannel fakeDeferredComponentChannel = DeferredComponentChannel fakeDeferredComponentChannel =
new DeferredComponentChannel(dartExecutor); new DeferredComponentChannel(dartExecutor);

View File

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

View File

@ -273,6 +273,20 @@ FLUTTER_ASSERT_ARC
XCTAssertNotNil(spawn); 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 { - (void)testSetHandlerAfterRun {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
XCTestExpectation* gotMessage = [self expectationWithDescription:@"gotMessage"]; XCTestExpectation* gotMessage = [self expectationWithDescription:@"gotMessage"];

View File

@ -87,6 +87,22 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, readonly) FlutterDartProject* project; @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 @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View File

@ -642,6 +642,8 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
std::cout << message << std::endl; std::cout << message << std::endl;
}; };
flutterArguments.engine_id = reinterpret_cast<int64_t>((__bridge void*)self);
static size_t sTaskRunnerIdentifiers = 0; static size_t sTaskRunnerIdentifiers = 0;
const FlutterTaskRunnerDescription cocoa_task_runner_description = { const FlutterTaskRunnerDescription cocoa_task_runner_description = {
.struct_size = sizeof(FlutterTaskRunnerDescription), .struct_size = sizeof(FlutterTaskRunnerDescription),
@ -1159,6 +1161,11 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
_engine = nullptr; _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 { - (void)setUpPlatformViewChannel {
_platformViewsChannel = _platformViewsChannel =
[FlutterMethodChannel methodChannelWithName:@"flutter/platform_views" [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) { TEST_F(FlutterEngineTest, ThreadSynchronizerNotBlockingRasterThreadAfterShutdown) {
FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init];
[threadSynchronizer shutdown]; [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. * Returns an array of screen objects representing all of the screens available on the system.
*/ */
- (NSArray<NSScreen*>*)screens; - (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 @end
@interface FlutterEngine (Tests) @interface FlutterEngine (Tests)

View File

@ -85,3 +85,11 @@ void backgroundTest() {
void sendFooMessage() { void sendFooMessage() {
PlatformDispatcher.instance.sendPlatformMessage('foo', null, (ByteData? result) {}); 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)); run_configuration.SetEntrypointArgs(std::move(arguments));
} }
if (SAFE_ACCESS(args, engine_id, 0) != 0) {
run_configuration.SetEngineId(args->engine_id);
}
if (!run_configuration.IsValid()) { if (!run_configuration.IsValid()) {
return LOG_EMBEDDER_ERROR( return LOG_EMBEDDER_ERROR(
kInvalidArguments, kInvalidArguments,

View File

@ -2631,6 +2631,11 @@ typedef struct {
/// the native view. The callback is invoked from a task posted to the /// the native view. The callback is invoked from a task posted to the
/// platform thread. /// platform thread.
FlutterViewFocusChangeRequestCallback view_focus_change_request_callback; 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; } FlutterProjectArgs;
#ifndef FLUTTER_ENGINE_NO_PROTOTYPES #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); g_return_val_if_fail(FL_IS_RENDERER(renderer), nullptr);
FlEngine* self = FL_ENGINE(g_object_new(fl_engine_get_type(), nullptr)); FlEngine* self = FL_ENGINE(g_object_new(fl_engine_get_type(), nullptr));
self->project = FL_DART_PROJECT(g_object_ref(project)); self->project = FL_DART_PROJECT(g_object_ref(project));
self->renderer = FL_RENDERER(g_object_ref(renderer)); self->renderer = FL_RENDERER(g_object_ref(renderer));
if (binary_messenger != nullptr) { if (binary_messenger != nullptr) {
@ -563,6 +564,12 @@ static FlEngine* fl_engine_new_full(FlDartProject* project,
return self; 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, FlEngine* fl_engine_new_with_renderer(FlDartProject* project,
FlRenderer* renderer) { FlRenderer* renderer) {
g_return_val_if_fail(FL_IS_DART_PROJECT(project), nullptr); 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; dart_entrypoint_args != nullptr ? g_strv_length(dart_entrypoint_args) : 0;
args.dart_entrypoint_argv = args.dart_entrypoint_argv =
reinterpret_cast<const char* const*>(dart_entrypoint_args); reinterpret_cast<const char* const*>(dart_entrypoint_args);
args.engine_id = reinterpret_cast<int64_t>(self);
FlutterCompositor compositor = {}; FlutterCompositor compositor = {};
compositor.struct_size = sizeof(FlutterCompositor); 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); 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 G_END_DECLS
#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_ENGINE_PRIVATE_H_ #endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_ENGINE_PRIVATE_H_

View File

@ -416,6 +416,29 @@ TEST(FlEngineTest, DartEntrypointArgs) {
EXPECT_TRUE(called); 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) { TEST(FlEngineTest, Locales) {
g_autofree gchar* initial_language = g_strdup(g_getenv("LANGUAGE")); g_autofree gchar* initial_language = g_strdup(g_getenv("LANGUAGE"));
g_setenv("LANGUAGE", "de:en_US", TRUE); g_setenv("LANGUAGE", "de:en_US", TRUE);

View File

@ -396,3 +396,11 @@ void onMetricsChangedSignalViewIds() {
void mergedUIThread() { void mergedUIThread() {
signal(); 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; return result;
} }
FLUTTER_EXPORT FlutterDesktopEngineRef FlutterDesktopEngineForId(
int64_t engine_id) {
return HandleForEngine(
flutter::FlutterWindowsEngine::GetEngineForId(engine_id));
}
bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine,
const char* entry_point) { const char* entry_point) {
std::string_view entry_point_view{""}; std::string_view entry_point_view{""};

View File

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

View File

@ -96,6 +96,12 @@ class FlutterWindowsEngine {
virtual ~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 // Starts running the entrypoint function specifed in the project bundle. If
// unspecified, defaults to main(). // unspecified, defaults to main().
// //

View File

@ -65,6 +65,15 @@ FLUTTER_EXPORT void FlutterDesktopEngineRegisterPlatformViewType(
const char* view_type_name, const char* view_type_name,
FlutterPlatformViewTypeEntry view_type); 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) #if defined(__cplusplus)
} }
#endif #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 testing
} // namespace flutter } // namespace flutter