Skip to content

Commit 933596c

Browse files
stuartmorgan-ggoderbauer
authored andcommitted
Fix Windows clipboard handling (flutter#17706)
Fixes several bugs in the clipboard code, and makes some structural improvements: - Adds scoped wrappers for clipboard open/close and global lock/unlock, to prevent missing cleanup, fixing at least one case where the lock was not released. - Adds the relevant window handle to the clipboard calls, since the docs suggest that some operations won't work without one. - Adds a missing clear step to setting the clipboard data. - Switches from TEXT to UNICODETEXT to handle non-ASCII text correctly. - To enable that, adds UTF-16/-8 conversion utilities built on the Win32 APIs (rather than the deprecated std::codecvt functions, as have been previously used in the engine). - Fixes handling of getting data when the clipboard is empty, correctly returning null. - Passes more errors back through the method channel, with details, for easier debugging of future issues. Fixes flutter/flutter#54226
1 parent 7e5f202 commit 933596c

File tree

7 files changed

+336
-56
lines changed

7 files changed

+336
-56
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,9 @@ FILE: ../../../flutter/shell/platform/windows/keyboard_hook_handler.h
11821182
FILE: ../../../flutter/shell/platform/windows/platform_handler.cc
11831183
FILE: ../../../flutter/shell/platform/windows/platform_handler.h
11841184
FILE: ../../../flutter/shell/platform/windows/public/flutter_windows.h
1185+
FILE: ../../../flutter/shell/platform/windows/string_conversion.cc
1186+
FILE: ../../../flutter/shell/platform/windows/string_conversion.h
1187+
FILE: ../../../flutter/shell/platform/windows/string_conversion_unittests.cc
11851188
FILE: ../../../flutter/shell/platform/windows/text_input_plugin.cc
11861189
FILE: ../../../flutter/shell/platform/windows/text_input_plugin.h
11871190
FILE: ../../../flutter/shell/platform/windows/win32_flutter_window.cc

shell/platform/windows/BUILD.gn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ source_set("flutter_windows_source") {
4949
"keyboard_hook_handler.h",
5050
"platform_handler.cc",
5151
"platform_handler.h",
52+
"string_conversion.cc",
53+
"string_conversion.h",
5254
"text_input_plugin.cc",
5355
"text_input_plugin.h",
5456
"win32_flutter_window.cc",
@@ -108,6 +110,7 @@ executable("flutter_windows_unittests") {
108110

109111
sources = [
110112
"dpi_utils_unittests.cc",
113+
"string_conversion_unittests.cc",
111114
"testing/win32_flutter_window_test.cc",
112115
"testing/win32_flutter_window_test.h",
113116
"testing/win32_window_test.cc",

shell/platform/windows/platform_handler.cc

Lines changed: 216 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
#include <windows.h>
88

99
#include <iostream>
10+
#include <optional>
1011

1112
#include "flutter/shell/platform/common/cpp/json_method_codec.h"
13+
#include "flutter/shell/platform/windows/string_conversion.h"
14+
#include "flutter/shell/platform/windows/win32_flutter_window.h"
1215

1316
static constexpr char kChannelName[] = "flutter/platform";
1417

@@ -18,11 +21,182 @@ static constexpr char kSetClipboardDataMethod[] = "Clipboard.setData";
1821
static constexpr char kTextPlainFormat[] = "text/plain";
1922
static constexpr char kTextKey[] = "text";
2023

21-
static constexpr char kUnknownClipboardFormatError[] =
22-
"Unknown clipboard format error";
24+
static constexpr char kClipboardError[] = "Clipboard error";
25+
static constexpr char kUnknownClipboardFormatMessage[] =
26+
"Unknown clipboard format";
2327

2428
namespace flutter {
2529

30+
namespace {
31+
32+
// A scoped wrapper for GlobalAlloc/GlobalFree.
33+
class ScopedGlobalMemory {
34+
public:
35+
// Allocates |bytes| bytes of global memory with the given flags.
36+
ScopedGlobalMemory(unsigned int flags, size_t bytes) {
37+
memory_ = ::GlobalAlloc(flags, bytes);
38+
if (!memory_) {
39+
std::cerr << "Unable to allocate global memory: " << ::GetLastError();
40+
}
41+
}
42+
43+
~ScopedGlobalMemory() {
44+
if (memory_) {
45+
if (::GlobalFree(memory_) != nullptr) {
46+
std::cerr << "Failed to free global allocation: " << ::GetLastError();
47+
}
48+
}
49+
}
50+
51+
// Prevent copying.
52+
ScopedGlobalMemory(ScopedGlobalMemory const&) = delete;
53+
ScopedGlobalMemory& operator=(ScopedGlobalMemory const&) = delete;
54+
55+
// Returns the memory pointer, which will be nullptr if allocation failed.
56+
void* get() { return memory_; }
57+
58+
void* release() {
59+
void* memory = memory_;
60+
memory_ = nullptr;
61+
return memory;
62+
}
63+
64+
private:
65+
HGLOBAL memory_;
66+
};
67+
68+
// A scoped wrapper for GlobalLock/GlobalUnlock.
69+
class ScopedGlobalLock {
70+
public:
71+
// Attempts to acquire a global lock on |memory| for the life of this object.
72+
ScopedGlobalLock(HGLOBAL memory) {
73+
source_ = memory;
74+
if (memory) {
75+
locked_memory_ = ::GlobalLock(memory);
76+
if (!locked_memory_) {
77+
std::cerr << "Unable to acquire global lock: " << ::GetLastError();
78+
}
79+
}
80+
}
81+
82+
~ScopedGlobalLock() {
83+
if (locked_memory_) {
84+
if (!::GlobalUnlock(source_)) {
85+
DWORD error = ::GetLastError();
86+
if (error != NO_ERROR) {
87+
std::cerr << "Unable to release global lock: " << ::GetLastError();
88+
}
89+
}
90+
}
91+
}
92+
93+
// Prevent copying.
94+
ScopedGlobalLock(ScopedGlobalLock const&) = delete;
95+
ScopedGlobalLock& operator=(ScopedGlobalLock const&) = delete;
96+
97+
// Returns the locked memory pointer, which will be nullptr if acquiring the
98+
// lock failed.
99+
void* get() { return locked_memory_; }
100+
101+
private:
102+
HGLOBAL source_;
103+
void* locked_memory_;
104+
};
105+
106+
// A Clipboard wrapper that automatically closes the clipboard when it goes out
107+
// of scope.
108+
class ScopedClipboard {
109+
public:
110+
ScopedClipboard();
111+
~ScopedClipboard();
112+
113+
// Prevent copying.
114+
ScopedClipboard(ScopedClipboard const&) = delete;
115+
ScopedClipboard& operator=(ScopedClipboard const&) = delete;
116+
117+
// Attempts to open the clipboard for the given window, returning true if
118+
// successful.
119+
bool Open(HWND window);
120+
121+
// Returns true if there is string data available to get.
122+
bool HasString();
123+
124+
// Returns string data from the clipboard.
125+
//
126+
// If getting a string fails, returns no value. Get error information with
127+
// ::GetLastError().
128+
//
129+
// Open(...) must have succeeded to call this method.
130+
std::optional<std::wstring> GetString();
131+
132+
// Sets the string content of the clipboard, returning true on success.
133+
//
134+
// On failure, get error information with ::GetLastError().
135+
//
136+
// Open(...) must have succeeded to call this method.
137+
bool SetString(const std::wstring string);
138+
139+
private:
140+
bool opened_ = false;
141+
};
142+
143+
ScopedClipboard::ScopedClipboard() {}
144+
145+
ScopedClipboard::~ScopedClipboard() {
146+
if (opened_) {
147+
::CloseClipboard();
148+
}
149+
}
150+
151+
bool ScopedClipboard::Open(HWND window) {
152+
opened_ = ::OpenClipboard(window);
153+
return opened_;
154+
}
155+
156+
bool ScopedClipboard::HasString() {
157+
// Allow either plain text format, since getting data will auto-interpolate.
158+
return ::IsClipboardFormatAvailable(CF_UNICODETEXT) ||
159+
::IsClipboardFormatAvailable(CF_TEXT);
160+
}
161+
162+
std::optional<std::wstring> ScopedClipboard::GetString() {
163+
assert(opened_);
164+
165+
HANDLE data = ::GetClipboardData(CF_UNICODETEXT);
166+
if (data == nullptr) {
167+
return std::nullopt;
168+
}
169+
ScopedGlobalLock locked_data(data);
170+
if (!locked_data.get()) {
171+
return std::nullopt;
172+
}
173+
return std::optional<std::wstring>(static_cast<wchar_t*>(locked_data.get()));
174+
}
175+
176+
bool ScopedClipboard::SetString(const std::wstring string) {
177+
assert(opened_);
178+
if (!::EmptyClipboard()) {
179+
return false;
180+
}
181+
size_t null_terminated_byte_count =
182+
sizeof(decltype(string)::traits_type::char_type) * (string.size() + 1);
183+
ScopedGlobalMemory destination_memory(GMEM_MOVEABLE,
184+
null_terminated_byte_count);
185+
ScopedGlobalLock locked_memory(destination_memory.get());
186+
if (!locked_memory.get()) {
187+
return false;
188+
}
189+
memcpy(locked_memory.get(), string.c_str(), null_terminated_byte_count);
190+
if (!::SetClipboardData(CF_UNICODETEXT, locked_memory.get())) {
191+
return false;
192+
}
193+
// The clipboard now owns the global memory.
194+
destination_memory.release();
195+
return true;
196+
}
197+
198+
} // namespace
199+
26200
PlatformHandler::PlatformHandler(flutter::BinaryMessenger* messenger,
27201
Win32FlutterWindow* window)
28202
: channel_(std::make_unique<flutter::MethodChannel<rapidjson::Document>>(
@@ -47,76 +221,65 @@ void PlatformHandler::HandleMethodCall(
47221
const rapidjson::Value& format = method_call.arguments()[0];
48222

49223
if (strcmp(format.GetString(), kTextPlainFormat) != 0) {
50-
result->Error(kUnknownClipboardFormatError,
51-
"Windows clipboard API only supports text.");
224+
result->Error(kClipboardError, kUnknownClipboardFormatMessage);
52225
return;
53226
}
54227

55-
auto clipboardData = GetClipboardString();
56-
57-
if (clipboardData.empty()) {
58-
result->Error(kUnknownClipboardFormatError,
59-
"Failed to retrieve clipboard data from win32 api.");
228+
ScopedClipboard clipboard;
229+
if (!clipboard.Open(window_->GetWindowHandle())) {
230+
rapidjson::Document error_code;
231+
error_code.SetInt(::GetLastError());
232+
result->Error(kClipboardError, "Unable to open clipboard", &error_code);
60233
return;
61234
}
235+
if (!clipboard.HasString()) {
236+
rapidjson::Document null;
237+
result->Success(&null);
238+
return;
239+
}
240+
std::optional<std::wstring> clipboard_string = clipboard.GetString();
241+
if (!clipboard_string) {
242+
rapidjson::Document error_code;
243+
error_code.SetInt(::GetLastError());
244+
result->Error(kClipboardError, "Unable to get clipboard data",
245+
&error_code);
246+
return;
247+
}
248+
62249
rapidjson::Document document;
63250
document.SetObject();
64251
rapidjson::Document::AllocatorType& allocator = document.GetAllocator();
65-
document.AddMember(rapidjson::Value(kTextKey, allocator),
66-
rapidjson::Value(clipboardData, allocator), allocator);
252+
document.AddMember(
253+
rapidjson::Value(kTextKey, allocator),
254+
rapidjson::Value(Utf8FromUtf16(*clipboard_string), allocator),
255+
allocator);
67256
result->Success(&document);
68-
69257
} else if (method.compare(kSetClipboardDataMethod) == 0) {
70258
const rapidjson::Value& document = *method_call.arguments();
71259
rapidjson::Value::ConstMemberIterator itr = document.FindMember(kTextKey);
72260
if (itr == document.MemberEnd()) {
73-
result->Error(kUnknownClipboardFormatError,
74-
"Missing text to store on clipboard.");
261+
result->Error(kClipboardError, kUnknownClipboardFormatMessage);
262+
return;
263+
}
264+
265+
ScopedClipboard clipboard;
266+
if (!clipboard.Open(window_->GetWindowHandle())) {
267+
rapidjson::Document error_code;
268+
error_code.SetInt(::GetLastError());
269+
result->Error(kClipboardError, "Unable to open clipboard", &error_code);
270+
return;
271+
}
272+
if (!clipboard.SetString(Utf16FromUtf8(itr->value.GetString()))) {
273+
rapidjson::Document error_code;
274+
error_code.SetInt(::GetLastError());
275+
result->Error(kClipboardError, "Unable to set clipboard data",
276+
&error_code);
75277
return;
76278
}
77-
SetClipboardString(std::string(itr->value.GetString()));
78279
result->Success();
79280
} else {
80281
result->NotImplemented();
81282
}
82283
}
83284

84-
std::string PlatformHandler::GetClipboardString() {
85-
if (!OpenClipboard(nullptr)) {
86-
return nullptr;
87-
}
88-
89-
HANDLE data = GetClipboardData(CF_TEXT);
90-
if (data == nullptr) {
91-
CloseClipboard();
92-
return nullptr;
93-
}
94-
95-
const char* clipboardData = static_cast<char*>(GlobalLock(data));
96-
97-
if (clipboardData == nullptr) {
98-
CloseClipboard();
99-
return nullptr;
100-
}
101-
102-
auto result = std::string(clipboardData);
103-
GlobalUnlock(data);
104-
CloseClipboard();
105-
return result;
106-
}
107-
108-
void PlatformHandler::SetClipboardString(std::string data) {
109-
if (!OpenClipboard(nullptr)) {
110-
return;
111-
}
112-
113-
auto htext = GlobalAlloc(GMEM_MOVEABLE, data.size());
114-
115-
memcpy(GlobalLock(htext), data.c_str(), data.size());
116-
117-
SetClipboardData(CF_TEXT, htext);
118-
119-
CloseClipboard();
120-
}
121-
122285
} // namespace flutter

shell/platform/windows/platform_handler.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ class PlatformHandler {
2929
// The MethodChannel used for communication with the Flutter engine.
3030
std::unique_ptr<flutter::MethodChannel<rapidjson::Document>> channel_;
3131

32-
static std::string GetClipboardString();
33-
static void SetClipboardString(std::string data);
34-
3532
// A reference to the win32 window.
3633
Win32FlutterWindow* window_;
3734
};

0 commit comments

Comments
 (0)