Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 716dbf0

Browse files
Refactor GLFW embedding to support headless mode (#18205)
This does some long-overdue refactoring of the spaghetti code that grew in the GLFW embedding to begin providing a clearer separation between the engine and the window. It is now possible to register plugins, and run the runloop, on a headless engine, which makes headless mode much more usable. This is useful in some automated testing environments. There is more refactoring that should be done in the future, but this is a good incremental point to stop as the PR is already large, and it provides useful new functionality as-is.
1 parent 23cca32 commit 716dbf0

20 files changed

+863
-335
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,15 +1160,22 @@ FILE: ../../../flutter/shell/platform/fuchsia/runtime/dart/utils/vmo.cc
11601160
FILE: ../../../flutter/shell/platform/fuchsia/runtime/dart/utils/vmo.h
11611161
FILE: ../../../flutter/shell/platform/fuchsia/runtime/dart/utils/vmservice_object.cc
11621162
FILE: ../../../flutter/shell/platform/fuchsia/runtime/dart/utils/vmservice_object.h
1163+
FILE: ../../../flutter/shell/platform/glfw/client_wrapper/flutter_engine.cc
1164+
FILE: ../../../flutter/shell/platform/glfw/client_wrapper/flutter_engine_unittests.cc
11631165
FILE: ../../../flutter/shell/platform/glfw/client_wrapper/flutter_window_controller.cc
11641166
FILE: ../../../flutter/shell/platform/glfw/client_wrapper/flutter_window_controller_unittests.cc
11651167
FILE: ../../../flutter/shell/platform/glfw/client_wrapper/flutter_window_unittests.cc
1168+
FILE: ../../../flutter/shell/platform/glfw/client_wrapper/include/flutter/flutter_engine.h
11661169
FILE: ../../../flutter/shell/platform/glfw/client_wrapper/include/flutter/flutter_window.h
11671170
FILE: ../../../flutter/shell/platform/glfw/client_wrapper/include/flutter/flutter_window_controller.h
11681171
FILE: ../../../flutter/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h
1172+
FILE: ../../../flutter/shell/platform/glfw/event_loop.cc
1173+
FILE: ../../../flutter/shell/platform/glfw/event_loop.h
11691174
FILE: ../../../flutter/shell/platform/glfw/flutter_glfw.cc
11701175
FILE: ../../../flutter/shell/platform/glfw/glfw_event_loop.cc
11711176
FILE: ../../../flutter/shell/platform/glfw/glfw_event_loop.h
1177+
FILE: ../../../flutter/shell/platform/glfw/headless_event_loop.cc
1178+
FILE: ../../../flutter/shell/platform/glfw/headless_event_loop.h
11721179
FILE: ../../../flutter/shell/platform/glfw/key_event_handler.cc
11731180
FILE: ../../../flutter/shell/platform/glfw/key_event_handler.h
11741181
FILE: ../../../flutter/shell/platform/glfw/keyboard_hook_handler.h

shell/platform/glfw/BUILD.gn

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ source_set("flutter_glfw_headers") {
3030

3131
source_set("flutter_glfw") {
3232
sources = [
33+
"event_loop.cc",
34+
"event_loop.h",
3335
"flutter_glfw.cc",
3436
"glfw_event_loop.cc",
3537
"glfw_event_loop.h",
38+
"headless_event_loop.cc",
39+
"headless_event_loop.h",
3640
"key_event_handler.cc",
3741
"key_event_handler.h",
3842
"keyboard_hook_handler.h",

shell/platform/glfw/client_wrapper/BUILD.gn

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ import("//flutter/shell/platform/common/cpp/client_wrapper/publish.gni")
66
import("//flutter/testing/testing.gni")
77

88
_wrapper_includes = [
9+
"include/flutter/flutter_engine.h",
910
"include/flutter/flutter_window.h",
1011
"include/flutter/flutter_window_controller.h",
1112
"include/flutter/plugin_registrar_glfw.h",
1213
]
1314

14-
_wrapper_sources = [ "flutter_window_controller.cc" ]
15+
_wrapper_sources = [
16+
"flutter_engine.cc",
17+
"flutter_window_controller.cc",
18+
]
1519

1620
# This code will be merged into .../common/cpp/client_wrapper for client use,
1721
# so uses header paths that assume the merged state. Include the header
@@ -70,8 +74,8 @@ test_fixtures("client_wrapper_glfw_fixtures") {
7074
executable("client_wrapper_glfw_unittests") {
7175
testonly = true
7276

73-
# TODO: Add more unit tests.
7477
sources = [
78+
"flutter_engine_unittests.cc",
7579
"flutter_window_controller_unittests.cc",
7680
"flutter_window_unittests.cc",
7781
]
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#include "include/flutter/flutter_engine.h"
6+
7+
#include <algorithm>
8+
#include <iostream>
9+
10+
namespace flutter {
11+
12+
FlutterEngine::FlutterEngine() {}
13+
14+
FlutterEngine::~FlutterEngine() {}
15+
16+
bool FlutterEngine::Start(const std::string& icu_data_path,
17+
const std::string& assets_path,
18+
const std::vector<std::string>& arguments) {
19+
if (engine_) {
20+
std::cerr << "Cannot run an already running engine. Create a new instance "
21+
"or call ShutDown first."
22+
<< std::endl;
23+
return false;
24+
}
25+
26+
FlutterDesktopEngineProperties c_engine_properties = {};
27+
c_engine_properties.assets_path = assets_path.c_str();
28+
c_engine_properties.icu_data_path = icu_data_path.c_str();
29+
std::vector<const char*> engine_switches;
30+
std::transform(
31+
arguments.begin(), arguments.end(), std::back_inserter(engine_switches),
32+
[](const std::string& arg) -> const char* { return arg.c_str(); });
33+
if (engine_switches.size() > 0) {
34+
c_engine_properties.switches = &engine_switches[0];
35+
c_engine_properties.switches_count = engine_switches.size();
36+
}
37+
38+
engine_ = UniqueEnginePtr(FlutterDesktopRunEngine(c_engine_properties),
39+
FlutterDesktopShutDownEngine);
40+
if (!engine_) {
41+
std::cerr << "Failed to start engine." << std::endl;
42+
return false;
43+
}
44+
return true;
45+
}
46+
47+
void FlutterEngine::ShutDown() {
48+
engine_ = nullptr;
49+
}
50+
51+
FlutterDesktopPluginRegistrarRef FlutterEngine::GetRegistrarForPlugin(
52+
const std::string& plugin_name) {
53+
if (!engine_) {
54+
std::cerr << "Cannot get plugin registrar on an engine that isn't running; "
55+
"call Run first."
56+
<< std::endl;
57+
return nullptr;
58+
}
59+
return FlutterDesktopGetPluginRegistrar(engine_.get(), plugin_name.c_str());
60+
}
61+
62+
void FlutterEngine::RunEventLoopWithTimeout(std::chrono::milliseconds timeout) {
63+
if (!engine_) {
64+
std::cerr << "Cannot run event loop without a running engine; call "
65+
"Run first."
66+
<< std::endl;
67+
return;
68+
}
69+
uint32_t timeout_milliseconds;
70+
if (timeout == std::chrono::milliseconds::max()) {
71+
// The C API uses 0 to represent no timeout, so convert |max| to 0.
72+
timeout_milliseconds = 0;
73+
} else if (timeout.count() > UINT32_MAX) {
74+
timeout_milliseconds = UINT32_MAX;
75+
} else {
76+
timeout_milliseconds = static_cast<uint32_t>(timeout.count());
77+
}
78+
FlutterDesktopRunEngineEventLoopWithTimeout(engine_.get(),
79+
timeout_milliseconds);
80+
}
81+
82+
} // namespace flutter
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#include <memory>
6+
#include <string>
7+
8+
#include "flutter/shell/platform/glfw/client_wrapper/include/flutter/flutter_engine.h"
9+
#include "flutter/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h"
10+
#include "gtest/gtest.h"
11+
12+
namespace flutter {
13+
14+
namespace {
15+
16+
// Stub implementation to validate calls to the API.
17+
class TestGlfwApi : public testing::StubFlutterGlfwApi {
18+
public:
19+
// |flutter::testing::StubFlutterGlfwApi|
20+
FlutterDesktopEngineRef RunEngine(
21+
const FlutterDesktopEngineProperties& properties) override {
22+
run_called_ = true;
23+
return reinterpret_cast<FlutterDesktopEngineRef>(1);
24+
}
25+
26+
// |flutter::testing::StubFlutterGlfwApi|
27+
void RunEngineEventLoopWithTimeout(uint32_t millisecond_timeout) override {
28+
last_run_loop_timeout_ = millisecond_timeout;
29+
}
30+
31+
// |flutter::testing::StubFlutterGlfwApi|
32+
bool ShutDownEngine() override {
33+
shut_down_called_ = true;
34+
return true;
35+
}
36+
37+
bool run_called() { return run_called_; }
38+
39+
bool shut_down_called() { return shut_down_called_; }
40+
41+
uint32_t last_run_loop_timeout() { return last_run_loop_timeout_; }
42+
43+
private:
44+
bool run_called_ = false;
45+
bool shut_down_called_ = false;
46+
uint32_t last_run_loop_timeout_ = 0;
47+
};
48+
49+
} // namespace
50+
51+
TEST(FlutterEngineTest, CreateDestroy) {
52+
const std::string icu_data_path = "fake/path/to/icu";
53+
const std::string assets_path = "fake/path/to/assets";
54+
testing::ScopedStubFlutterGlfwApi scoped_api_stub(
55+
std::make_unique<TestGlfwApi>());
56+
auto test_api = static_cast<TestGlfwApi*>(scoped_api_stub.stub());
57+
{
58+
FlutterEngine engine;
59+
engine.Start(icu_data_path, assets_path, {});
60+
EXPECT_EQ(test_api->run_called(), true);
61+
EXPECT_EQ(test_api->shut_down_called(), false);
62+
}
63+
// Destroying should implicitly shut down if it hasn't been done manually.
64+
EXPECT_EQ(test_api->shut_down_called(), true);
65+
}
66+
67+
TEST(FlutterEngineTest, ExplicitShutDown) {
68+
const std::string icu_data_path = "fake/path/to/icu";
69+
const std::string assets_path = "fake/path/to/assets";
70+
testing::ScopedStubFlutterGlfwApi scoped_api_stub(
71+
std::make_unique<TestGlfwApi>());
72+
auto test_api = static_cast<TestGlfwApi*>(scoped_api_stub.stub());
73+
74+
FlutterEngine engine;
75+
engine.Start(icu_data_path, assets_path, {});
76+
EXPECT_EQ(test_api->run_called(), true);
77+
EXPECT_EQ(test_api->shut_down_called(), false);
78+
engine.ShutDown();
79+
EXPECT_EQ(test_api->shut_down_called(), true);
80+
}
81+
82+
TEST(FlutterEngineTest, RunloopTimeoutTranslation) {
83+
const std::string icu_data_path = "fake/path/to/icu";
84+
const std::string assets_path = "fake/path/to/assets";
85+
testing::ScopedStubFlutterGlfwApi scoped_api_stub(
86+
std::make_unique<TestGlfwApi>());
87+
auto test_api = static_cast<TestGlfwApi*>(scoped_api_stub.stub());
88+
89+
FlutterEngine engine;
90+
engine.Start(icu_data_path, assets_path, {});
91+
92+
engine.RunEventLoopWithTimeout(std::chrono::milliseconds(100));
93+
EXPECT_EQ(test_api->last_run_loop_timeout(), 100U);
94+
95+
engine.RunEventLoopWithTimeout(std::chrono::milliseconds::max() -
96+
std::chrono::milliseconds(1));
97+
EXPECT_EQ(test_api->last_run_loop_timeout(), UINT32_MAX);
98+
99+
engine.RunEventLoopWithTimeout(std::chrono::milliseconds::max());
100+
EXPECT_EQ(test_api->last_run_loop_timeout(), 0U);
101+
}
102+
103+
} // namespace flutter

shell/platform/glfw/client_wrapper/flutter_window_controller.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ FlutterDesktopPluginRegistrarRef FlutterWindowController::GetRegistrarForPlugin(
8484
<< std::endl;
8585
return nullptr;
8686
}
87-
return FlutterDesktopGetPluginRegistrar(controller_, plugin_name.c_str());
87+
return FlutterDesktopGetPluginRegistrar(FlutterDesktopGetEngine(controller_),
88+
plugin_name.c_str());
8889
}
8990

9091
bool FlutterWindowController::RunEventLoopWithTimeout(
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#ifndef FLUTTER_SHELL_PLATFORM_GLFW_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_ENGINE_H_
6+
#define FLUTTER_SHELL_PLATFORM_GLFW_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_ENGINE_H_
7+
8+
#include <flutter_glfw.h>
9+
10+
#include <chrono>
11+
#include <memory>
12+
#include <string>
13+
#include <vector>
14+
15+
#include "plugin_registrar.h"
16+
#include "plugin_registry.h"
17+
18+
namespace flutter {
19+
20+
// An engine for running a headless Flutter application.
21+
class FlutterEngine : public PluginRegistry {
22+
public:
23+
explicit FlutterEngine();
24+
25+
virtual ~FlutterEngine();
26+
27+
// Prevent copying.
28+
FlutterEngine(FlutterEngine const&) = delete;
29+
FlutterEngine& operator=(FlutterEngine const&) = delete;
30+
31+
// Starts running the engine with the given parameters, returning true if
32+
// successful.
33+
bool Start(const std::string& icu_data_path,
34+
const std::string& assets_path,
35+
const std::vector<std::string>& arguments);
36+
37+
// Terminates the running engine.
38+
void ShutDown();
39+
40+
// Processes the next event for the engine, or returns early if |timeout| is
41+
// reached before the next event.
42+
void RunEventLoopWithTimeout(
43+
std::chrono::milliseconds timeout = std::chrono::milliseconds::max());
44+
45+
// flutter::PluginRegistry:
46+
FlutterDesktopPluginRegistrarRef GetRegistrarForPlugin(
47+
const std::string& plugin_name) override;
48+
49+
private:
50+
using UniqueEnginePtr = std::unique_ptr<FlutterDesktopEngineState,
51+
bool (*)(FlutterDesktopEngineState*)>;
52+
53+
// Handle for interacting with the C API's engine reference.
54+
UniqueEnginePtr engine_ =
55+
UniqueEnginePtr(nullptr, FlutterDesktopShutDownEngine);
56+
};
57+
58+
} // namespace flutter
59+
60+
#endif // FLUTTER_SHELL_PLATFORM_GLFW_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_ENGINE_H_

shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ FlutterDesktopEngineRef FlutterDesktopRunEngine(
149149
return nullptr;
150150
}
151151

152+
void FlutterDesktopRunEngineEventLoopWithTimeout(
153+
FlutterDesktopEngineRef engine,
154+
uint32_t timeout_milliseconds) {
155+
if (s_stub_implementation) {
156+
s_stub_implementation->RunEngineEventLoopWithTimeout(timeout_milliseconds);
157+
}
158+
}
159+
152160
bool FlutterDesktopShutDownEngine(FlutterDesktopEngineRef engine_ref) {
153161
if (s_stub_implementation) {
154162
return s_stub_implementation->ShutDownEngine();
@@ -162,8 +170,14 @@ FlutterDesktopWindowRef FlutterDesktopGetWindow(
162170
return reinterpret_cast<FlutterDesktopWindowRef>(1);
163171
}
164172

173+
FlutterDesktopEngineRef FlutterDesktopGetEngine(
174+
FlutterDesktopWindowControllerRef controller) {
175+
// The stub ignores this, so just return an arbitrary non-zero value.
176+
return reinterpret_cast<FlutterDesktopEngineRef>(3);
177+
}
178+
165179
FlutterDesktopPluginRegistrarRef FlutterDesktopGetPluginRegistrar(
166-
FlutterDesktopWindowControllerRef controller,
180+
FlutterDesktopEngineRef engine,
167181
const char* plugin_name) {
168182
// The stub ignores this, so just return an arbitrary non-zero value.
169183
return reinterpret_cast<FlutterDesktopPluginRegistrarRef>(2);

shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ class StubFlutterGlfwApi {
8383
return nullptr;
8484
}
8585

86+
// Called for FlutterDesktopRunEngineEventLoopWithTimeout.
87+
virtual void RunEngineEventLoopWithTimeout(uint32_t millisecond_timeout) {}
88+
8689
// Called for FlutterDesktopShutDownEngine.
8790
virtual bool ShutDownEngine() { return true; }
8891
};

0 commit comments

Comments
 (0)