Skip to content

Commit

Permalink
Support custom entrypoints in public Windows API (flutter#35285)
Browse files Browse the repository at this point in the history
This adds a dart_entrypoint field to FlutterDesktopEngineProperties in
the public C Windows API, which mirrors that in the embedder API.

When a null or empty entrypoint is specified, a default entrypoint of
'main' is assumed. Otherwise, the app is launched at the top-level
function specified, which must be annotated with
@pragma('vm:entry-point') in the Dart source.

This change is backward-compatible for existing users of the Windows C API
and the C++ client wrapper API. To avoid breaking backward compatibility,
this patch preserves the entry_point parameter to FlutterDesktopEngineRun
in the public Windows C API as well as in the FlutterEngine::Run method
in the C++ client wrapper API. The entrypoint can be specified in either
the engine properties struct or via the parameter, but if conflicting
non-empty values are specified, the engine launch will intentionally fail
with an error message.

This change has no effect on existing Flutter Windows desktop apps and no
migration is required, because our app templates never specify a custom
entrypoint, nor was the option to specify one via the old method particularly
feasible, because the FlutterViewController class constructor immediately
invokes FlutterViewControllerCreate which immediately launches the engine
passed to it with a null entrypoint argument, so long as the engine is not
already running. However, running the engine without a view controller
previously resulted in errors due to failure to create a rendering surface.

This is a followup patch to flutter#35273
which added support for running Dart fixture tests with a live Windows
embedder engine.

Fixes: flutter/flutter#93537
Related: flutter/flutter#87299
  • Loading branch information
cbracken authored and emilyabest committed Aug 11, 2022
1 parent 6ac76b4 commit 56ac5e2
Show file tree
Hide file tree
Showing 18 changed files with 205 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ std::unique_ptr<FlutterWindowsEngine> GetTestEngine() {
MockEmbedderApiForKeyboard(modifier,
std::make_shared<MockKeyResponseController>());

engine->RunWithEntrypoint(nullptr);
engine->Run();
return engine;
}

Expand Down
5 changes: 5 additions & 0 deletions shell/platform/windows/client_wrapper/flutter_engine.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ FlutterEngine::FlutterEngine(const DartProject& project) {
c_engine_properties.assets_path = project.assets_path().c_str();
c_engine_properties.icu_data_path = project.icu_data_path().c_str();
c_engine_properties.aot_library_path = project.aot_library_path().c_str();
c_engine_properties.dart_entrypoint = project.dart_entrypoint().c_str();

const std::vector<std::string>& entrypoint_args =
project.dart_entrypoint_arguments();
Expand All @@ -40,6 +41,10 @@ FlutterEngine::~FlutterEngine() {
ShutDown();
}

bool FlutterEngine::Run() {
return Run(nullptr);
}

bool FlutterEngine::Run(const char* entry_point) {
if (!engine_) {
std::cerr << "Cannot run an engine that failed creation." << std::endl;
Expand Down
17 changes: 17 additions & 0 deletions shell/platform/windows/client_wrapper/flutter_engine_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,23 @@ TEST(FlutterEngineTest, CreateDestroy) {
EXPECT_EQ(test_api->destroy_called(), true);
}

TEST(FlutterEngineTest, CreateDestroyWithCustomEntrypoint) {
testing::ScopedStubFlutterWindowsApi scoped_api_stub(
std::make_unique<TestFlutterWindowsApi>());
auto test_api = static_cast<TestFlutterWindowsApi*>(scoped_api_stub.stub());
{
DartProject project(L"fake/project/path");
project.set_dart_entrypoint("customEntrypoint");
FlutterEngine engine(project);
engine.Run();
EXPECT_EQ(test_api->create_called(), true);
EXPECT_EQ(test_api->run_called(), true);
EXPECT_EQ(test_api->destroy_called(), false);
}
// Destroying should implicitly shut down if it hasn't been done manually.
EXPECT_EQ(test_api->destroy_called(), true);
}

TEST(FlutterEngineTest, ExplicitShutDown) {
testing::ScopedStubFlutterWindowsApi scoped_api_stub(
std::make_unique<TestFlutterWindowsApi>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ class DartProject {

~DartProject() = default;

// Sets the Dart entrypoint to the specified value.
//
// If not set, the default entrypoint (main) is used. Custom Dart entrypoints
// must be decorated with `@pragma('vm:entry-point')`.
void set_dart_entrypoint(const std::string& entrypoint) {
if (entrypoint.empty()) {
return;
}
dart_entrypoint_ = entrypoint;
}

// Returns the Dart entrypoint.
const std::string& dart_entrypoint() const { return dart_entrypoint_; }

// Sets the command line arguments that should be passed to the Dart
// entrypoint.
void set_dart_entrypoint_arguments(std::vector<std::string> arguments) {
Expand Down Expand Up @@ -77,6 +91,8 @@ class DartProject {
// The path to the AOT library. This will always return a path, but non-AOT
// builds will not be expected to actually have a library at that path.
std::wstring aot_library_path_;
// The Dart entrypoint to launch.
std::string dart_entrypoint_;
// The list of arguments to pass through to the Dart entrypoint.
std::vector<std::string> dart_entrypoint_arguments_;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@ class FlutterEngine : public PluginRegistry {
FlutterEngine(FlutterEngine const&) = delete;
FlutterEngine& operator=(FlutterEngine const&) = delete;

// Starts running the engine at the entrypoint function specified in the
// DartProject used to configure the engine, or main() by default.
bool Run();

// Starts running the engine, with an optional entry point.
//
// If provided, entry_point must be the name of a top-level function from the
// same Dart library that contains the app's main() function, and must be
// decorated with `@pragma(vm:entry-point)` to ensure the method is not
// tree-shaken by the Dart compiler. If not provided, defaults to main().
bool Run(const char* entry_point = nullptr);
bool Run(const char* entry_point);

// Terminates the running engine.
void ShutDown();
Expand Down
7 changes: 6 additions & 1 deletion shell/platform/windows/fixtures/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@
// found in the LICENSE file.

void main() {
print('Hello windows engine test!');
print('Hello windows engine test main!');
}

@pragma('vm:entry-point')
void customEntrypoint() {
print('Hello windows engine test customEntrypoint!');
}
4 changes: 4 additions & 0 deletions shell/platform/windows/flutter_project_bundle.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ FlutterProjectBundle::FlutterProjectBundle(
aot_library_path_ = std::filesystem::path(properties.aot_library_path);
}

if (properties.dart_entrypoint && properties.dart_entrypoint[0] != '\0') {
dart_entrypoint_ = properties.dart_entrypoint;
}

for (int i = 0; i < properties.dart_entrypoint_argc; i++) {
dart_entrypoint_arguments_.push_back(
std::string(properties.dart_entrypoint_argv[i]));
Expand Down
6 changes: 6 additions & 0 deletions shell/platform/windows/flutter_project_bundle.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class FlutterProjectBundle {
// Logs and returns nullptr on failure.
UniqueAotDataPtr LoadAotData(const FlutterEngineProcTable& engine_procs);

// Returns the Dart entrypoint.
const std::string& dart_entrypoint() const { return dart_entrypoint_; }

// Returns the command line arguments to be passed through to the Dart
// entrypoint.
const std::vector<std::string>& dart_entrypoint_arguments() const {
Expand All @@ -63,6 +66,9 @@ class FlutterProjectBundle {
// Path to the AOT library file, if any.
std::filesystem::path aot_library_path_;

// The Dart entrypoint to launch.
std::string dart_entrypoint_;

// Dart entrypoint arguments.
std::vector<std::string> dart_entrypoint_arguments_;

Expand Down
4 changes: 2 additions & 2 deletions shell/platform/windows/flutter_windows.cc
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ FlutterDesktopViewControllerRef FlutterDesktopViewControllerCreate(
std::unique_ptr<flutter::FlutterWindowsEngine>(EngineFromHandle(engine)));
state->view->CreateRenderSurface();
if (!state->view->GetEngine()->running()) {
if (!state->view->GetEngine()->RunWithEntrypoint(nullptr)) {
if (!state->view->GetEngine()->Run()) {
return nullptr;
}
}
Expand Down Expand Up @@ -144,7 +144,7 @@ bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine_ref) {

bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine,
const char* entry_point) {
return EngineFromHandle(engine)->RunWithEntrypoint(entry_point);
return EngineFromHandle(engine)->Run(entry_point);
}

uint64_t FlutterDesktopEngineProcessMessages(FlutterDesktopEngineRef engine) {
Expand Down
29 changes: 25 additions & 4 deletions shell/platform/windows/flutter_windows_engine.cc
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,11 @@ void FlutterWindowsEngine::SetSwitches(
project_->SetSwitches(switches);
}

bool FlutterWindowsEngine::RunWithEntrypoint(const char* entrypoint) {
bool FlutterWindowsEngine::Run() {
return Run("");
}

bool FlutterWindowsEngine::Run(std::string_view entrypoint) {
if (!project_->HasValidPaths()) {
std::cerr << "Missing or unresolvable paths to assets." << std::endl;
return false;
Expand Down Expand Up @@ -259,6 +263,26 @@ bool FlutterWindowsEngine::RunWithEntrypoint(const char* entrypoint) {
args.icu_data_path = icu_path_string.c_str();
args.command_line_argc = static_cast<int>(argv.size());
args.command_line_argv = argv.empty() ? nullptr : argv.data();

// Fail if conflicting non-default entrypoints are specified in the method
// argument and the project.
//
// TODO(cbracken): https://github.com/flutter/flutter/issues/109285
// The entrypoint method parameter should eventually be removed from this
// method and only the entrypoint specified in project_ should be used.
if (!project_->dart_entrypoint().empty() && !entrypoint.empty() &&
project_->dart_entrypoint() != entrypoint) {
std::cerr << "Conflicting entrypoints were specified in "
"FlutterDesktopEngineProperties.dart_entrypoint and "
"FlutterDesktopEngineRun(engine, entry_point). "
<< std::endl;
return false;
}
if (!entrypoint.empty()) {
args.custom_dart_entrypoint = entrypoint.data();
} else if (!project_->dart_entrypoint().empty()) {
args.custom_dart_entrypoint = project_->dart_entrypoint().c_str();
}
args.dart_entrypoint_argc = static_cast<int>(entrypoint_argv.size());
args.dart_entrypoint_argv =
entrypoint_argv.empty() ? nullptr : entrypoint_argv.data();
Expand Down Expand Up @@ -301,9 +325,6 @@ bool FlutterWindowsEngine::RunWithEntrypoint(const char* entrypoint) {
if (aot_data_) {
args.aot_data = aot_data_.get();
}
if (entrypoint) {
args.custom_dart_entrypoint = entrypoint;
}

FlutterRendererConfig renderer_config = surface_manager_
? GetOpenGLRendererConfig()
Expand Down
19 changes: 16 additions & 3 deletions shell/platform/windows/flutter_windows_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

#include "flutter/shell/platform/common/accessibility_bridge.h"
Expand Down Expand Up @@ -72,11 +74,22 @@ class FlutterWindowsEngine {
FlutterWindowsEngine(FlutterWindowsEngine const&) = delete;
FlutterWindowsEngine& operator=(FlutterWindowsEngine const&) = delete;

// Starts running the engine with the given entrypoint. If null, defaults to
// main().
// Starts running the entrypoint function specifed in the project bundle. If
// unspecified, defaults to main().
//
// Returns false if the engine couldn't be started.
bool RunWithEntrypoint(const char* entrypoint);
bool Run();

// Starts running the engine with the given entrypoint. If the empty string
// is specified, defaults to the entrypoint function specified in the project
// bundle, or main() if both are unspecified.
//
// Returns false if the engine couldn't be started or if conflicting,
// non-default values are passed here and in the project bundle..
//
// DEPRECATED: Prefer setting the entrypoint in the FlutterProjectBundle
// passed to the constructor and calling the no-parameter overload.
bool Run(std::string_view entrypoint);

// Returns true if the engine is currently running.
bool running() { return engine_ != nullptr; }
Expand Down
6 changes: 3 additions & 3 deletions shell/platform/windows/flutter_windows_engine_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ TEST(FlutterWindowsEngine, RunDoesExpectedInitialization) {
// Set the AngleSurfaceManager to !nullptr to test ANGLE rendering.
modifier.SetSurfaceManager(reinterpret_cast<AngleSurfaceManager*>(1));

engine->RunWithEntrypoint(nullptr);
engine->Run();

EXPECT_TRUE(run_called);
EXPECT_TRUE(update_locales_called);
Expand Down Expand Up @@ -206,7 +206,7 @@ TEST(FlutterWindowsEngine, RunWithoutANGLEUsesSoftware) {
// Set the AngleSurfaceManager to nullptr to test software fallback path.
modifier.SetSurfaceManager(nullptr);

engine->RunWithEntrypoint(nullptr);
engine->Run();

EXPECT_TRUE(run_called);

Expand Down Expand Up @@ -351,7 +351,7 @@ TEST(FlutterWindowsEngine, AddPluginRegistrarDestructionCallback) {
MockEmbedderApiForKeyboard(modifier,
std::make_shared<MockKeyResponseController>());

engine->RunWithEntrypoint(nullptr);
engine->Run();

// Verify that destruction handlers don't overwrite each other.
int result1 = 0;
Expand Down
53 changes: 52 additions & 1 deletion shell/platform/windows/flutter_windows_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ TEST(WindowsNoFixtureTest, GetTextureRegistrar) {
TEST_F(WindowsTest, LaunchMain) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
ViewControllerPtr controller{builder.LaunchEngine()};
ViewControllerPtr controller{builder.Run()};
ASSERT_NE(controller, nullptr);

// Run for 1 second, then shut down.
Expand All @@ -41,5 +41,56 @@ TEST_F(WindowsTest, LaunchMain) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}

TEST_F(WindowsTest, LaunchCustomEntrypoint) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
builder.SetDartEntrypoint("customEntrypoint");
ViewControllerPtr controller{builder.Run()};
ASSERT_NE(controller, nullptr);

// Run for 1 second, then shut down.
//
// TODO(cbracken): Support registring a native function we can use to
// determine that execution has made it to a specific point in the Dart
// code. https://github.com/flutter/flutter/issues/109242
std::this_thread::sleep_for(std::chrono::seconds(1));
}

// Verify that engine launches with the custom entrypoint specified in the
// FlutterDesktopEngineRun parameter when no entrypoint is specified in
// FlutterDesktopEngineProperties.dart_entrypoint.
//
// TODO(cbracken): https://github.com/flutter/flutter/issues/109285
TEST_F(WindowsTest, LaunchCustomEntrypointInEngineRunInvocation) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
EnginePtr engine{builder.InitializeEngine()};
ASSERT_NE(engine, nullptr);

ASSERT_TRUE(FlutterDesktopEngineRun(engine.get(), "customEntrypoint"));

// Run for 1 second, then shut down.
//
// TODO(cbracken): Support registring a native function we can use to
// determine that execution has made it to a specific point in the Dart
// code. https://github.com/flutter/flutter/issues/109242
std::this_thread::sleep_for(std::chrono::seconds(1));
}

// Verify that engine fails to launch when a conflicting entrypoint in
// FlutterDesktopEngineProperties.dart_entrypoint and the
// FlutterDesktopEngineRun parameter.
//
// TODO(cbracken): https://github.com/flutter/flutter/issues/109285
TEST_F(WindowsTest, LaunchConflictingCustomEntrypoints) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
builder.SetDartEntrypoint("customEntrypoint");
EnginePtr engine{builder.InitializeEngine()};
ASSERT_NE(engine, nullptr);

ASSERT_FALSE(FlutterDesktopEngineRun(engine.get(), "conflictingEntrypoint"));
}

} // namespace testing
} // namespace flutter
2 changes: 1 addition & 1 deletion shell/platform/windows/flutter_windows_view_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ std::unique_ptr<FlutterWindowsEngine> GetTestEngine() {

MockEmbedderApiForKeyboard(modifier, key_response_controller);

engine->RunWithEntrypoint(nullptr);
engine->Run();
return engine;
}

Expand Down
2 changes: 1 addition & 1 deletion shell/platform/windows/keyboard_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ class KeyboardTester {

MockEmbedderApiForKeyboard(modifier, key_response_controller);

engine->RunWithEntrypoint(nullptr);
engine->Run();
return engine;
}

Expand Down
22 changes: 18 additions & 4 deletions shell/platform/windows/public/flutter_windows.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ typedef struct {
// it will be ignored in that case.
const wchar_t* aot_library_path;

// The name of the top-level Dart entrypoint function. If null or the empty
// string, 'main' is assumed. If a custom entrypoint is used, this parameter
// must specifiy the name of a top-level function in the same Dart library as
// the app's main() function. Custom entrypoint functions must be decorated
// with `@pragma('vm:entry-point')` to ensure the method is not tree-shaken
// by the Dart compiler.
const char* dart_entrypoint;

// Number of elements in the array passed in as dart_entrypoint_argv.
int dart_entrypoint_argc;

Expand Down Expand Up @@ -129,13 +137,19 @@ FLUTTER_EXPORT FlutterDesktopEngineRef FlutterDesktopEngineCreate(
// |engine| is no longer valid after this call.
FLUTTER_EXPORT bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine);

// Starts running the given engine instance and optional entry point in the Dart
// project. If the entry point is null, defaults to main().
// Starts running the given engine instance.
//
// The entry_point parameter is deprecated but preserved for
// backward-compatibility. If desired, a custom Dart entrypoint function can be
// set in the dart_entrypoint field of the FlutterDesktopEngineProperties
// struct passed to FlutterDesktopEngineCreate.
//
// If provided, entry_point must be the name of a top-level function from the
// If sprecified, entry_point must be the name of a top-level function from the
// same Dart library that contains the app's main() function, and must be
// decorated with `@pragma(vm:entry-point)` to ensure the method is not
// tree-shaken by the Dart compiler.
// tree-shaken by the Dart compiler. If conflicting non-null values are passed
// to this function and via the FlutterDesktopEngineProperties struct, the run
// will fail.
//
// Returns false if running the engine failed.
FLUTTER_EXPORT bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine,
Expand Down
Loading

0 comments on commit 56ac5e2

Please sign in to comment.