Support custom entrypoints in public Windows API (#35285)

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 https://github.com/flutter/engine/pull/35273
which added support for running Dart fixture tests with a live Windows
embedder engine.

Fixes: https://github.com/flutter/flutter/issues/93537
Related: https://github.com/flutter/flutter/issues/87299
diff --git a/shell/platform/windows/accessibility_bridge_delegate_windows_unittests.cc b/shell/platform/windows/accessibility_bridge_delegate_windows_unittests.cc
index 27d2fed..5db7fc4 100644
--- a/shell/platform/windows/accessibility_bridge_delegate_windows_unittests.cc
+++ b/shell/platform/windows/accessibility_bridge_delegate_windows_unittests.cc
@@ -86,7 +86,7 @@
   MockEmbedderApiForKeyboard(modifier,
                              std::make_shared<MockKeyResponseController>());
 
-  engine->RunWithEntrypoint(nullptr);
+  engine->Run();
   return engine;
 }
 
diff --git a/shell/platform/windows/client_wrapper/flutter_engine.cc b/shell/platform/windows/client_wrapper/flutter_engine.cc
index e913080..f35c20c 100644
--- a/shell/platform/windows/client_wrapper/flutter_engine.cc
+++ b/shell/platform/windows/client_wrapper/flutter_engine.cc
@@ -16,6 +16,7 @@
   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();
@@ -40,6 +41,10 @@
   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;
diff --git a/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc b/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc
index e52fb33..6f09bb9 100644
--- a/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc
+++ b/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc
@@ -86,6 +86,23 @@
   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>());
diff --git a/shell/platform/windows/client_wrapper/include/flutter/dart_project.h b/shell/platform/windows/client_wrapper/include/flutter/dart_project.h
index cbc07ff..903cfc4 100644
--- a/shell/platform/windows/client_wrapper/include/flutter/dart_project.h
+++ b/shell/platform/windows/client_wrapper/include/flutter/dart_project.h
@@ -45,6 +45,20 @@
 
   ~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) {
@@ -77,6 +91,8 @@
   // 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_;
 };
diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h
index 6883617..897fccf 100644
--- a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h
+++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h
@@ -35,13 +35,17 @@
   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();
diff --git a/shell/platform/windows/fixtures/main.dart b/shell/platform/windows/fixtures/main.dart
index 6d20727..219c83e 100644
--- a/shell/platform/windows/fixtures/main.dart
+++ b/shell/platform/windows/fixtures/main.dart
@@ -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!');
 }
diff --git a/shell/platform/windows/flutter_project_bundle.cc b/shell/platform/windows/flutter_project_bundle.cc
index d01ef65..90b2d78 100644
--- a/shell/platform/windows/flutter_project_bundle.cc
+++ b/shell/platform/windows/flutter_project_bundle.cc
@@ -20,6 +20,10 @@
     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]));
diff --git a/shell/platform/windows/flutter_project_bundle.h b/shell/platform/windows/flutter_project_bundle.h
index 3cb5d3c..09770b5 100644
--- a/shell/platform/windows/flutter_project_bundle.h
+++ b/shell/platform/windows/flutter_project_bundle.h
@@ -50,6 +50,9 @@
   // 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 {
@@ -63,6 +66,9 @@
   // 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_;
 
diff --git a/shell/platform/windows/flutter_windows.cc b/shell/platform/windows/flutter_windows.cc
index 9e07ed0..87c6c8b 100644
--- a/shell/platform/windows/flutter_windows.cc
+++ b/shell/platform/windows/flutter_windows.cc
@@ -78,7 +78,7 @@
       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;
     }
   }
@@ -144,7 +144,7 @@
 
 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) {
diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc
index a1edfee..a0358bb 100644
--- a/shell/platform/windows/flutter_windows_engine.cc
+++ b/shell/platform/windows/flutter_windows_engine.cc
@@ -200,7 +200,11 @@
   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;
@@ -259,6 +263,26 @@
   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();
@@ -301,9 +325,6 @@
   if (aot_data_) {
     args.aot_data = aot_data_.get();
   }
-  if (entrypoint) {
-    args.custom_dart_entrypoint = entrypoint;
-  }
 
   FlutterRendererConfig renderer_config = surface_manager_
                                               ? GetOpenGLRendererConfig()
diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h
index 71233c3..1512870 100644
--- a/shell/platform/windows/flutter_windows_engine.h
+++ b/shell/platform/windows/flutter_windows_engine.h
@@ -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"
@@ -72,11 +74,22 @@
   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; }
diff --git a/shell/platform/windows/flutter_windows_engine_unittests.cc b/shell/platform/windows/flutter_windows_engine_unittests.cc
index fad87e7..191740f 100644
--- a/shell/platform/windows/flutter_windows_engine_unittests.cc
+++ b/shell/platform/windows/flutter_windows_engine_unittests.cc
@@ -129,7 +129,7 @@
   // 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);
@@ -206,7 +206,7 @@
   // Set the AngleSurfaceManager to nullptr to test software fallback path.
   modifier.SetSurfaceManager(nullptr);
 
-  engine->RunWithEntrypoint(nullptr);
+  engine->Run();
 
   EXPECT_TRUE(run_called);
 
@@ -351,7 +351,7 @@
   MockEmbedderApiForKeyboard(modifier,
                              std::make_shared<MockKeyResponseController>());
 
-  engine->RunWithEntrypoint(nullptr);
+  engine->Run();
 
   // Verify that destruction handlers don't overwrite each other.
   int result1 = 0;
diff --git a/shell/platform/windows/flutter_windows_unittests.cc b/shell/platform/windows/flutter_windows_unittests.cc
index 7b21046..0052042 100644
--- a/shell/platform/windows/flutter_windows_unittests.cc
+++ b/shell/platform/windows/flutter_windows_unittests.cc
@@ -30,7 +30,7 @@
 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.
@@ -41,5 +41,56 @@
   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
diff --git a/shell/platform/windows/flutter_windows_view_unittests.cc b/shell/platform/windows/flutter_windows_view_unittests.cc
index cf823c6..9513322 100644
--- a/shell/platform/windows/flutter_windows_view_unittests.cc
+++ b/shell/platform/windows/flutter_windows_view_unittests.cc
@@ -90,7 +90,7 @@
 
   MockEmbedderApiForKeyboard(modifier, key_response_controller);
 
-  engine->RunWithEntrypoint(nullptr);
+  engine->Run();
   return engine;
 }
 
diff --git a/shell/platform/windows/keyboard_unittests.cc b/shell/platform/windows/keyboard_unittests.cc
index f532c1a..4ce5ba4 100644
--- a/shell/platform/windows/keyboard_unittests.cc
+++ b/shell/platform/windows/keyboard_unittests.cc
@@ -524,7 +524,7 @@
 
     MockEmbedderApiForKeyboard(modifier, key_response_controller);
 
-    engine->RunWithEntrypoint(nullptr);
+    engine->Run();
     return engine;
   }
 
diff --git a/shell/platform/windows/public/flutter_windows.h b/shell/platform/windows/public/flutter_windows.h
index b5d2738..ca1655f 100644
--- a/shell/platform/windows/public/flutter_windows.h
+++ b/shell/platform/windows/public/flutter_windows.h
@@ -47,6 +47,14 @@
   // 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;
 
@@ -129,13 +137,19 @@
 // |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.
 //
-// If provided, entry_point must be the name of a top-level function from the
+// 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 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,
diff --git a/shell/platform/windows/testing/windows_test_config_builder.cc b/shell/platform/windows/testing/windows_test_config_builder.cc
index 69843ea..c3c2d0e 100644
--- a/shell/platform/windows/testing/windows_test_config_builder.cc
+++ b/shell/platform/windows/testing/windows_test_config_builder.cc
@@ -22,6 +22,13 @@
 
 WindowsConfigBuilder::~WindowsConfigBuilder() = default;
 
+void WindowsConfigBuilder::SetDartEntrypoint(std::string_view entrypoint) {
+  if (entrypoint.empty()) {
+    return;
+  }
+  dart_entrypoint_ = entrypoint;
+}
+
 void WindowsConfigBuilder::AddDartEntrypointArgument(std::string_view arg) {
   if (arg.empty()) {
     return;
@@ -36,6 +43,9 @@
   engine_properties.assets_path = context_.GetAssetsPath().c_str();
   engine_properties.icu_data_path = context_.GetIcuDataPath().c_str();
 
+  // Set Dart entrypoint.
+  engine_properties.dart_entrypoint = dart_entrypoint_.c_str();
+
   // Set Dart entrypoint argc, argv.
   std::vector<const char*> dart_args;
   dart_args.reserve(dart_entrypoint_arguments_.size());
@@ -55,7 +65,12 @@
   return engine_properties;
 }
 
-ViewControllerPtr WindowsConfigBuilder::LaunchEngine() const {
+EnginePtr WindowsConfigBuilder::InitializeEngine() const {
+  FlutterDesktopEngineProperties engine_properties = GetEngineProperties();
+  return EnginePtr(FlutterDesktopEngineCreate(&engine_properties));
+}
+
+ViewControllerPtr WindowsConfigBuilder::Run() const {
   InitializeCOM();
 
   EnginePtr engine = InitializeEngine();
@@ -78,10 +93,5 @@
   FML_CHECK(SUCCEEDED(::CoInitializeEx(nullptr, COINIT_MULTITHREADED)));
 }
 
-EnginePtr WindowsConfigBuilder::InitializeEngine() const {
-  FlutterDesktopEngineProperties engine_properties = GetEngineProperties();
-  return EnginePtr(FlutterDesktopEngineCreate(&engine_properties));
-}
-
 }  // namespace testing
 }  // namespace flutter
diff --git a/shell/platform/windows/testing/windows_test_config_builder.h b/shell/platform/windows/testing/windows_test_config_builder.h
index 0a1bfb4..33b2660 100644
--- a/shell/platform/windows/testing/windows_test_config_builder.h
+++ b/shell/platform/windows/testing/windows_test_config_builder.h
@@ -52,22 +52,29 @@
   // Returns the desktop engine properties configured for this test.
   FlutterDesktopEngineProperties GetEngineProperties() const;
 
+  // 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 SetDartEntrypoint(std::string_view entrypoint);
+
   // Adds an argument to the Dart entrypoint arguments List<String>.
   void AddDartEntrypointArgument(std::string_view arg);
 
+  // Returns a configured and initialized engine.
+  EnginePtr InitializeEngine() const;
+
   // Returns a configured and initialized view controller running the default
   // Dart entrypoint.
-  ViewControllerPtr LaunchEngine() const;
+  ViewControllerPtr Run() const;
 
  private:
   // Initialize COM, so that it is available for use in the library and/or
   // plugins.
   void InitializeCOM() const;
 
-  // Returns a configured and initialized engine.
-  EnginePtr InitializeEngine() const;
-
   WindowsTestContext& context_;
+  std::string dart_entrypoint_;
   std::vector<std::string> dart_entrypoint_arguments_;
 
   FML_DISALLOW_COPY_AND_ASSIGN(WindowsConfigBuilder);