Skip to content

Commit 49c2690

Browse files
fix(windows): use wide Win32 APIs for runtime library paths (non-ASCII path support) (#68)
* fix(windows): use wide Win32 APIs for runtime library paths The launchers convert paths to UTF-8 (GetModuleFileNameW / CommandLineToArgvW -> WideCharToMultiByte(CP_UTF8)) but then passed them to ANSI APIs (GetFileAttributesA / LoadLibraryA / GetEnvironmentVariableA), which reinterpret the bytes in the active codepage. Any non-ASCII character in the bundle path (e.g. a Japanese user profile) broke colocated runtime detection, --runtime, and LAUFEY_RUNTIME_PATH. Convert back to UTF-16 and use the wide APIs instead. Fixes the launch failure reported in denoland/deno#36591. * fix(windows): finish the wide-API sweep and share one UTF-8/UTF-16 helper Review follow-ups for the runtime-path Unicode fix: - Export Utf8ToWide/WideToUtf8 from backend-common (new strings_win.cc) and delete the seven local copies in cef/webview runtime loaders, webview_windows.cc, and the backend-common *_win.cc files. - cef: build the CEF cache path with GetTempPathW (checked) instead of unchecked GetTempPathA, whose ANSI bytes garble non-ASCII profile names once CefString decodes them as UTF-8. - cef: read LAUFEY_REMOTE_DEBUGGING_PORT wide with an upper bound on the returned length; the old '> 0' check passed on the required-size return and atoi() then parsed uninitialized stack. - cef/webview: re-query LAUFEY_RUNTIME_PATH with a grown buffer when the value is MAX_PATH or longer instead of silently dropping it. - cef: accept --runtime=<path> on Windows too (Linux/macOS already do, and the Rust launcher emits that form). - cef: show a MessageBoxW with the attempted path when the runtime fails to load; the WIN32-subsystem binary's stderr is detached. - webview: probe the fallback DLL candidates with GetFileAttributesW and use MessageBoxW consistently for the error dialogs. * ci: clang-format --------- Co-authored-by: mogwai-dev <mogwai-dev@users.noreply.github.com> Co-authored-by: Leo Kettmeir <crowlkats@toaxl.com>
1 parent 99fee05 commit 49c2690

12 files changed

Lines changed: 148 additions & 125 deletions

backend-common/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ elseif(UNIX)
6262
)
6363
elseif(WIN32)
6464
list(APPEND LAUFEY_COMMON_SRCS
65+
src/strings_win.cc
6566
src/notifications_win.cc
6667
src/dialog_win.cc
6768
src/clipboard_win.cc

backend-common/include/laufey_backend_common.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@
2121

2222
namespace laufey_common {
2323

24+
#ifdef _WIN32
25+
// ---------------------------------------------------------------------------
26+
// Win32 string conversion
27+
// ---------------------------------------------------------------------------
28+
//
29+
// Strings cross the C API and flow through both backends as UTF-8, while the
30+
// wide (*W) Win32 APIs want UTF-16; the ANSI (*A) APIs would garble non-ASCII
31+
// text in the active codepage. Every conversion goes through this one pair of
32+
// helpers so a future fix (e.g. long-path prefixing) lands in one place.
33+
34+
std::wstring Utf8ToWide(const std::string& s);
35+
std::string WideToUtf8(const std::wstring& w);
36+
#endif
37+
2438
// ---------------------------------------------------------------------------
2539
// Notifications
2640
// ---------------------------------------------------------------------------

backend-common/src/clipboard_win.cc

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,6 @@ namespace laufey_common {
1515

1616
namespace {
1717

18-
std::wstring Utf8ToWide(const std::string& s) {
19-
if (s.empty())
20-
return std::wstring();
21-
int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, nullptr, 0);
22-
std::wstring out;
23-
if (n > 0) {
24-
out.resize(n - 1);
25-
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, out.data(), n);
26-
}
27-
return out;
28-
}
29-
3018
char* WideToUtf8Strdup(const wchar_t* w) {
3119
if (!w)
3220
return nullptr;

backend-common/src/dialog_win.cc

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,6 @@
1414

1515
namespace laufey_common {
1616

17-
namespace {
18-
19-
std::wstring Utf8ToWide(const std::string& s) {
20-
if (s.empty())
21-
return std::wstring();
22-
int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, nullptr, 0);
23-
std::wstring out;
24-
if (n > 0) {
25-
out.resize(n - 1);
26-
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, out.data(), n);
27-
}
28-
return out;
29-
}
30-
31-
} // namespace
32-
3317
int ShowDialogWin(int dialog_type, const std::string& title,
3418
const std::string& message,
3519
const std::string& default_value, char** out_input_value) {

backend-common/src/notifications_win.cc

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -230,18 +230,6 @@ HWND EnsureNotifMessageWindow() {
230230
return g_notif_msg_hwnd;
231231
}
232232

233-
std::wstring Utf8ToWide(const std::string& s) {
234-
if (s.empty())
235-
return std::wstring();
236-
int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, nullptr, 0);
237-
std::wstring out;
238-
if (n > 0) {
239-
out.resize(n - 1);
240-
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, out.data(), n);
241-
}
242-
return out;
243-
}
244-
245233
} // namespace
246234

247235
uint32_t ShowNotificationWin(const NotificationOptions& opts,

backend-common/src/strings_win.cc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2025 Divy Srivastava. All rights reserved. MIT license.
2+
3+
#include "laufey_backend_common.h"
4+
5+
#define WIN32_LEAN_AND_MEAN
6+
#include <windows.h>
7+
8+
namespace laufey_common {
9+
10+
std::wstring Utf8ToWide(const std::string& s) {
11+
if (s.empty())
12+
return std::wstring();
13+
int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, nullptr, 0);
14+
if (n <= 0)
15+
return std::wstring();
16+
std::wstring w(static_cast<size_t>(n - 1), L'\0');
17+
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, &w[0], n);
18+
return w;
19+
}
20+
21+
std::string WideToUtf8(const std::wstring& w) {
22+
if (w.empty())
23+
return std::string();
24+
int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, nullptr, 0, nullptr,
25+
nullptr);
26+
if (n <= 0)
27+
return std::string();
28+
std::string s(static_cast<size_t>(n - 1), '\0');
29+
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &s[0], n, nullptr, nullptr);
30+
return s;
31+
}
32+
33+
} // namespace laufey_common

backend-common/src/tray_win.cc

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -252,17 +252,6 @@ HICON DecodePngToHicon(const void* bytes, size_t len, int desired) {
252252
return hicon;
253253
}
254254

255-
std::wstring Utf8ToWide(const char* s) {
256-
if (!s || !*s) return std::wstring();
257-
int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, nullptr, 0);
258-
std::wstring out;
259-
if (n > 0) {
260-
out.resize(n - 1);
261-
MultiByteToWideChar(CP_UTF8, 0, s, -1, out.data(), n);
262-
}
263-
return out;
264-
}
265-
266255
HMENU BuildWinMenuFromValue(laufey_value_t* val, const laufey_backend_api_t* api,
267256
std::map<UINT, std::string>& cmd_to_id) {
268257
if (!val || !api->value_is_list(val)) return nullptr;
@@ -420,7 +409,7 @@ void SetTrayIconDarkWin(uint32_t tray_id, const void* png_bytes, size_t len) {
420409
void SetTrayTooltipWin(uint32_t tray_id, const char* tooltip_or_null) {
421410
HWND hwnd = g_tray_msg_hwnd;
422411
if (!hwnd) return;
423-
std::wstring wtip = Utf8ToWide(tooltip_or_null);
412+
std::wstring wtip = Utf8ToWide(tooltip_or_null ? tooltip_or_null : "");
424413
NOTIFYICONDATAW nid = {};
425414
nid.cbSize = sizeof(nid);
426415
nid.hWnd = hwnd;

cef/src/main_windows.cc

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <iostream>
77
#include <string>
8+
#include <cstdlib>
89
#include <cstring>
910

1011
#include "include/base/cef_callback.h"
@@ -17,15 +18,14 @@
1718
#include "include/wrapper/cef_helpers.h"
1819

1920
#include "app.h"
21+
#include "laufey_backend_common.h"
2022
#include "renderer_app.h"
2123
#include "runtime_loader.h"
2224

2325
void LaufeyOpenExternalURL(const std::string& url) {
24-
int wlen = MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0);
25-
if (wlen <= 0)
26+
std::wstring wurl = laufey_common::Utf8ToWide(url);
27+
if (wurl.empty())
2628
return;
27-
std::wstring wurl(static_cast<size_t>(wlen - 1), L'\0');
28-
MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, &wurl[0], wlen);
2929
ShellExecuteW(nullptr, L"open", wurl.c_str(), nullptr, nullptr,
3030
SW_SHOWNORMAL);
3131
}
@@ -208,7 +208,15 @@ class LaufeyCombinedApp : public CefApp, public CefBrowserProcessHandler {
208208

209209
if (!g_runtime_path.empty()) {
210210
if (!RuntimeLoader::GetInstance()->Load(g_runtime_path)) {
211-
std::cerr << "Failed to load runtime, exiting" << std::endl;
211+
// WIN32 subsystem: stderr is usually detached, so also show a
212+
// dialog (matches the webview binary's load-failure behavior).
213+
std::cerr << "Failed to load runtime from: " << g_runtime_path
214+
<< std::endl;
215+
MessageBoxW(nullptr,
216+
(L"Failed to load runtime from: " +
217+
laufey_common::Utf8ToWide(g_runtime_path))
218+
.c_str(),
219+
L"LAUFEY Error", MB_OK | MB_ICONERROR);
212220
CefQuitMessageLoop();
213221
return;
214222
}
@@ -253,20 +261,28 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
253261
if (argv) {
254262
for (int i = 1; i < argc; ++i) {
255263
if (wcscmp(argv[i], L"--runtime") == 0 && i + 1 < argc) {
256-
++i;
257-
int size = WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, nullptr, 0,
258-
nullptr, nullptr);
259-
g_runtime_path.resize(size - 1);
260-
WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, &g_runtime_path[0], size,
261-
nullptr, nullptr);
264+
g_runtime_path = laufey_common::WideToUtf8(argv[++i]);
265+
} else if (wcsncmp(argv[i], L"--runtime=", 10) == 0) {
266+
g_runtime_path = laufey_common::WideToUtf8(argv[i] + 10);
262267
}
263268
}
264269
}
265270

266271
if (g_runtime_path.empty()) {
267-
char envPath[MAX_PATH];
268-
if (GetEnvironmentVariableA("LAUFEY_RUNTIME_PATH", envPath, MAX_PATH) > 0) {
269-
g_runtime_path = envPath;
272+
// Read as UTF-16 and convert to UTF-8; the ANSI variant would garble
273+
// non-ASCII paths in the active codepage.
274+
std::wstring envPath(MAX_PATH, L'\0');
275+
DWORD envLen = GetEnvironmentVariableW(L"LAUFEY_RUNTIME_PATH", &envPath[0],
276+
static_cast<DWORD>(envPath.size()));
277+
if (envLen >= envPath.size()) {
278+
// Buffer too small; envLen is the required size including the NUL.
279+
envPath.resize(envLen);
280+
envLen = GetEnvironmentVariableW(L"LAUFEY_RUNTIME_PATH", &envPath[0],
281+
static_cast<DWORD>(envPath.size()));
282+
}
283+
if (envLen > 0 && envLen < envPath.size()) {
284+
envPath.resize(envLen);
285+
g_runtime_path = laufey_common::WideToUtf8(envPath);
270286
}
271287
}
272288

@@ -287,17 +303,27 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
287303
settings.no_sandbox = true;
288304
settings.log_severity = LaufeyCefLogSeverity();
289305

290-
// Set cache path
291-
char tempPath[MAX_PATH];
292-
GetTempPathA(MAX_PATH, tempPath);
293-
std::string cache_path = std::string(tempPath) + "laufey_cef_" +
294-
std::to_string(GetCurrentProcessId());
295-
CefString(&settings.root_cache_path) = cache_path;
296-
297-
char port_buf[16];
298-
if (GetEnvironmentVariableA("LAUFEY_REMOTE_DEBUGGING_PORT", port_buf,
299-
sizeof(port_buf)) > 0) {
300-
int port = atoi(port_buf);
306+
// Set cache path. CefString decodes std::string as UTF-8, so read the
307+
// temp dir wide and convert; GetTempPathA would hand over active-codepage
308+
// bytes that garble non-ASCII profile names. On failure leave the cache
309+
// path unset (CEF then runs with its in-memory default) rather than
310+
// pointing it at garbage.
311+
wchar_t tempPath[MAX_PATH + 2];
312+
DWORD tempLen = GetTempPathW(MAX_PATH + 2, tempPath);
313+
if (tempLen > 0 && tempLen < MAX_PATH + 2) {
314+
std::string cache_path = laufey_common::WideToUtf8(tempPath) +
315+
"laufey_cef_" +
316+
std::to_string(GetCurrentProcessId());
317+
CefString(&settings.root_cache_path) = cache_path;
318+
}
319+
320+
wchar_t port_buf[16];
321+
DWORD port_len =
322+
GetEnvironmentVariableW(L"LAUFEY_REMOTE_DEBUGGING_PORT", port_buf, 16);
323+
// On a value of 16+ chars the API returns the required size and leaves the
324+
// buffer untouched, so the upper bound is load-bearing.
325+
if (port_len > 0 && port_len < 16) {
326+
int port = _wtoi(port_buf);
301327
if (port > 0 && port < 65536) {
302328
settings.remote_debugging_port = port;
303329
}

cef/src/runtime_loader.cc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ std::string GetExecutablePath() {
7474

7575
bool PathExists(const std::string& path) {
7676
#if defined(_WIN32)
77-
return GetFileAttributesA(path.c_str()) != INVALID_FILE_ATTRIBUTES;
77+
// Paths flow through this file as UTF-8 (see GetExecutablePath), so the
78+
// ANSI (*A) APIs would misread any non-ASCII characters in the active
79+
// codepage. Convert back to UTF-16 for the wide (*W) APIs.
80+
return GetFileAttributesW(laufey_common::Utf8ToWide(path).c_str()) !=
81+
INVALID_FILE_ATTRIBUTES;
7882
#else
7983
return access(path.c_str(), F_OK) == 0;
8084
#endif
@@ -1698,7 +1702,7 @@ bool RuntimeLoader::Load(const std::string& path) {
16981702
return false;
16991703
}
17001704
#else
1701-
library_handle_ = LoadLibraryA(path.c_str());
1705+
library_handle_ = LoadLibraryW(laufey_common::Utf8ToWide(path).c_str());
17021706
if (!library_handle_) {
17031707
std::cerr << "Failed to load runtime: error " << GetLastError()
17041708
<< std::endl;

webview/src/main_windows.cc

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright 2025 Divy Srivastava. All rights reserved. MIT license.
22

3+
#include "laufey_backend_common.h"
34
#include "runtime_loader.h"
45

56
#define WIN32_LEAN_AND_MEAN
@@ -24,21 +25,27 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
2425
if (argv) {
2526
for (int i = 1; i < argc; ++i) {
2627
if (wcscmp(argv[i], L"--runtime") == 0 && i + 1 < argc) {
27-
++i;
28-
int size = WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, nullptr, 0,
29-
nullptr, nullptr);
30-
runtimePath.resize(size - 1);
31-
WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, &runtimePath[0], size,
32-
nullptr, nullptr);
28+
runtimePath = laufey_common::WideToUtf8(argv[++i]);
3329
}
3430
}
3531
LocalFree(argv);
3632
}
3733

3834
if (runtimePath.empty()) {
39-
char envPath[MAX_PATH];
40-
if (GetEnvironmentVariableA("LAUFEY_RUNTIME_PATH", envPath, MAX_PATH) > 0) {
41-
runtimePath = envPath;
35+
// Read as UTF-16 and convert to UTF-8; the ANSI variant would garble
36+
// non-ASCII paths in the active codepage.
37+
std::wstring envPath(MAX_PATH, L'\0');
38+
DWORD envLen = GetEnvironmentVariableW(L"LAUFEY_RUNTIME_PATH", &envPath[0],
39+
static_cast<DWORD>(envPath.size()));
40+
if (envLen >= envPath.size()) {
41+
// Buffer too small; envLen is the required size including the NUL.
42+
envPath.resize(envLen);
43+
envLen = GetEnvironmentVariableW(L"LAUFEY_RUNTIME_PATH", &envPath[0],
44+
static_cast<DWORD>(envPath.size()));
45+
}
46+
if (envLen > 0 && envLen < envPath.size()) {
47+
envPath.resize(envLen);
48+
runtimePath = laufey_common::WideToUtf8(envPath);
4249
}
4350
}
4451

@@ -47,22 +54,22 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
4754
}
4855

4956
if (runtimePath.empty()) {
50-
const char* searchPaths[] = {".\\runtime.dll",
51-
".\\target\\debug\\hello.dll",
52-
".\\target\\release\\hello.dll"};
53-
for (const char* path : searchPaths) {
54-
if (GetFileAttributesA(path) != INVALID_FILE_ATTRIBUTES) {
55-
runtimePath = path;
57+
const wchar_t* searchPaths[] = {L".\\runtime.dll",
58+
L".\\target\\debug\\hello.dll",
59+
L".\\target\\release\\hello.dll"};
60+
for (const wchar_t* path : searchPaths) {
61+
if (GetFileAttributesW(path) != INVALID_FILE_ATTRIBUTES) {
62+
runtimePath = laufey_common::WideToUtf8(path);
5663
break;
5764
}
5865
}
5966
}
6067

6168
if (runtimePath.empty()) {
62-
MessageBoxA(nullptr,
63-
"No runtime library found.\nSet LAUFEY_RUNTIME_PATH or use "
64-
"--runtime <path>",
65-
"LAUFEY Webview Error", MB_OK | MB_ICONERROR);
69+
MessageBoxW(nullptr,
70+
L"No runtime library found.\nSet LAUFEY_RUNTIME_PATH or use "
71+
L"--runtime <path>",
72+
L"LAUFEY Webview Error", MB_OK | MB_ICONERROR);
6673
CoUninitialize();
6774
return 1;
6875
}
@@ -73,16 +80,20 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
7380
loader->SetBackend(backend);
7481

7582
if (!loader->Load(runtimePath)) {
76-
MessageBoxA(nullptr,
77-
("Failed to load runtime from: " + runtimePath).c_str(),
78-
"LAUFEY Webview Error", MB_OK | MB_ICONERROR);
83+
// The path is UTF-8; show it through the wide API so non-ASCII
84+
// characters render correctly in the dialog.
85+
MessageBoxW(nullptr,
86+
(L"Failed to load runtime from: " +
87+
laufey_common::Utf8ToWide(runtimePath))
88+
.c_str(),
89+
L"LAUFEY Webview Error", MB_OK | MB_ICONERROR);
7990
delete backend;
8091
CoUninitialize();
8192
return 1;
8293
}
8394

8495
if (!loader->Start()) {
85-
MessageBoxA(nullptr, "Failed to start runtime", "LAUFEY Webview Error",
96+
MessageBoxW(nullptr, L"Failed to start runtime", L"LAUFEY Webview Error",
8697
MB_OK | MB_ICONERROR);
8798
delete backend;
8899
CoUninitialize();

0 commit comments

Comments
 (0)