从 ASCII 到 Unicode
ASCII 时代:一切都很简单
最早的计算机只需要处理英文。ASCII 用 7 位编码了 128 个字符------26 个大小写字母、数字、标点和控制字符,一个 char 就是一个字符,一个字节就是一个编码单元。那时候写文件路径,fopen("data/output.txt", "r") 毫无问题,全世界的计算机都能正确理解。
ANSI 代码页:各国各自为政
当计算机走出英语世界,问题来了------一个字节最多 256 个码位(8 位),装不下中文的几万个汉字。于是各国自行扩展:
- 中国大陆制定了 GB2312 ,后来扩展为 GBK(代码页 936)
- 日本制定了 Shift-JIS(代码页 932)
- 韩国制定了 EUC-KR(代码页 949)
- 西欧使用 Latin-1(代码页 1252)
这些编码统称为 ANSI 编码 。它们在 ASCII 范围内(0x00--0x7F)完全一致,但在 0x80 以上各不相同。同一串字节 B0 A1,在 GBK 里是"啊",在 Shift-JIS 里是完全不同的字符。
这意味着:一个文件在中文电脑上显示正常,复制到日文电脑上就成乱码。
在这个时代,同一个系统上只要编码统一,文件路径不会有问题。中文 Windows 上的应用程序传 GBK 路径给 fopen,系统按 GBK 解码,一切正常。
Unicode 的诞生:统一字符集
为了终结编码混战,Unicode 联盟定义了一个 全球统一的字符集------每个字符分配一个唯一的码点(Code Point),比如"中"是 U+4E2D,"A"是 U+0041。
但 Unicode 只是字符集,不是编码方案。怎么把这些码点存成字节?这就产生了两条路线:
UTF-16 :用 2 个字节(或 4 个字节用于代理对)表示一个字符。优点是大多数常用字符只需要固定 2 字节,处理效率高。缺点是不兼容 ASCII------英文字母 A 在 UTF-16 里是 0x00 0x41,多了一个 \0,会破坏所有基于 char* 和 \0 结尾的 C 字符串函数。
UTF-8 :用 1--4 个变长字节表示一个字符。ASCII 范围的字符只占 1 个字节且编码完全相同,中文通常占 3 个字节。它最大的优势是 向后兼容 ASCII------任何合法的 ASCII 字符串同时也是合法的 UTF-8 字符串。
时间线上的错位:问题的真正根源
1993 年 ,Windows NT 3.1 发布。微软需要让 Windows 支持全球语言,而此时 UTF-8 刚刚被 Ken Thompson 和 Rob Pike 发明(1993 年 1 月),还没有被广泛采用。微软选择了当时看起来更合理的方案------UTF-16 作为内核的原生编码。Windows NT 的所有内核 API、文件系统、注册表都统一使用 UTF-16(当时叫 UCS-2)。
这个决策在当时是正确的:UTF-16 对大多数字符固定 2 字节,处理方便;而 UTF-8 的变长编码在那个年代被认为不够高效。
但历史的发展出乎意料。
类 Unix 系统(Linux、macOS、FreeBSD)晚了几年才认真处理国际化。到它们需要做决策时,UTF-8 已经被证明是一个绝佳方案------完全兼容现有的 char* 字符串、\0 结尾约定、所有 POSIX API。于是类 Unix 系统直接拥抱了 UTF-8:
- Linux 从 2000 年代开始,主流发行版默认 locale 切换为
en_US.UTF-8 - macOS 从 OS X 开始就默认 UTF-8
- Android 基于 Linux,天然 UTF-8
在这些系统上,char* 就是 UTF-8,fopen("中文路径/文件.txt", "r") 直接送到内核,内核也理解 UTF-8,一切顺畅。
而 Windows 被自己的历史选择锁住了。它的内核是 UTF-16,char* API 是为 ANSI 代码页设计的兼容层。当全世界都转向 UTF-8 时,Windows 的 char* 还停留在 ANSI 时代。
C/C++ 中的 wchar_t 与 std::wstring
为了在 C/C++ 语言层面支持宽字符,标准引入了 wchar_t 类型和 std::wstring。但这个设计也有先天不足:
wchar_t的大小没有被标准固定------Windows 上是 2 字节(UTF-16),Linux/macOS 上是 4 字节(UTF-32)- 这意味着涉及
wchar_t的代码天然不可移植 - 标准库的宽字符函数(
wfopen、wstat等)在 POSIX 系统上几乎没人用,因为char*+ UTF-8 已经够用
结果就是:wchar_t 和 std::wstring 在实践中变成了 Windows 专用的解决方案,跨平台项目无法依赖它们作为统一接口。
Windows 的编码架构
Windows API 的 A/W 后缀体系
Windows 为几乎所有涉及字符串的 API 都提供了两个版本:
c
// A 后缀:接受 char*,按系统 ANSI 代码页解释
HANDLE CreateFileA(LPCSTR lpFileName, ...);
// W 后缀:接受 wchar_t*,UTF-16 编码
HANDLE CreateFileW(LPCWSTR lpFileName, ...);
而我们平时写的"无后缀"版本 CreateFile,其实是一个宏:
c
#ifdef UNICODE
#define CreateFile CreateFileW
#else
#define CreateFile CreateFileA
#endif
定义 UNICODE 宏后,CreateFile 展开为 CreateFileW,函数签名变成了接受 wchar_t*------但这只是让编译器要求你传宽字符串 ,它不会自动帮你把 UTF-8 的 char* 变成 wchar_t*。
C/C++ 标准库:不走 A/W 体系
很多人以为 fopen 内部调用的是 CreateFileA。事实并非如此。
fopen、std::ifstream、stat 等 C/C++ 标准库函数有自己独立的实现路径。以 MSVC 的 CRT(C Runtime)为例,fopen 的内部流程是:
fopen(const char* path, ...)
│
├─ MultiByteToWideChar(CP_ACP, ..., path, ...) ← 用 ANSI 代码页转换
│ │
│ CP_ACP = 系统代码页
│ 中文 Windows = GBK (936)
│ 日文 Windows = Shift-JIS (932)
│
└─ CreateFileW(wchar_t* wide_path, ...) ← 最终调用的是 W 版本
注意到关键点了吗?CRT 内部已经统一调用 W 版本了 ,但转换那一步用的是 CP_ACP(当前系统 ANSI 代码页),而不是 CP_UTF8。所以:
- 传入 GBK 编码的
char*(中文 Windows)→ 按 GBK 解码 → 正确 - 传入 UTF-8 编码的
char*→ 按 GBK 解码 → 乱码 → 文件找不到
UNICODE 宏对这个流程毫无影响。 C/C++ 标准库函数的签名是语言标准规定的,永远接受 char*,不会因为任何宏定义而变成接受 wchar_t*。
Windows 10 1903+ 的 UTF-8 代码页方案
微软也意识到了这个历史包袱的严重性。从 Windows 10 1903 开始,可以通过应用程序清单(manifest)将进程的 ACP 设置为 UTF-8:
xml
<application>
<windowsSettings>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>
设置后,CRT 内部的 MultiByteToWideChar(CP_ACP, ...) 就等价于 MultiByteToWideChar(CP_UTF8, ...),fopen 可以正确处理 UTF-8 路径。
但这个方案有限制:
- 要求 Windows 10 1903 及以上------如果你的软件需要支持更早的 Windows 版本,就不能依赖它
- 它改变了整个进程的 ACP,可能影响第三方库中假设 ACP = GBK 的代码
- 一些边缘情况下行为可能与预期不同
对于需要支持 Windows 10 早期版本或 Windows 7 的项目,仍然需要自行处理 UTF-8 到 UTF-16 的转换。
影响范围
在跨平台 C/C++ 项目中,所有接受 char* 路径参数的 API 在 Windows 上都存在 UTF-8 路径问题。主要涉及以下几类:
- C 标准库文件 I/O :
fopen、freopen、remove、rename等 - C++ 文件流 :
std::ifstream、std::ofstream、std::fstream的构造函数和.open()方法 - POSIX / CRT 文件操作 :
stat、access、mkdir、getcwd、unlink等 - std::filesystem :
std::filesystem::path(std::string)构造函数内部同样按 ACP 转换,和fopen是一样的坑 - 环境变量 :
std::getenv/getenv返回值按 ACP 编码,不是 UTF-8 - 进程操作 :
system()、_popen()等命令字符串含非 ASCII 路径会失败 - Windows API 的 A 后缀版本 :
GetFileAttributesA、CreateFileA、LoadLibraryA等
完整的受影响接口清单见本文末尾[附录 A](#附录 A)。
解决方案
设计原则
- 对调用者透明 :调用者统一传入 UTF-8 的
char*/std::string,不需要关心平台差异 - 非 Windows 平台零开销:通过条件编译,在非 Windows 平台直接调用原始函数
- ANSI 编码兼容降级:如果传入的恰好不是 UTF-8(比如误传了 GBK 路径),不会崩溃,会降级到 ANSI 方式尝试
- C 和 C++ 双覆盖 :C 代码用纯 C 接口封装函数,C++ 代码用
std::filesystem::path+ 类型推导
C 接口封装:utf8_fopen / utf8_remove / utf8_stat
对于 C 代码中大量使用的 fopen、remove、stat,提供一套接口签名完全对等的替换函数。
核心实现(以 utf8_fopen 为例):
c
#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
#include <stdlib.h>
#endif
FILE* utf8_fopen(const char *filename_utf8, const char *mode_utf8)
{
if (!filename_utf8 || !mode_utf8) return NULL;
#ifdef _WIN32
// 1. 尝试将 UTF-8 路径转换为 UTF-16
// MB_ERR_INVALID_CHARS:如果输入不是合法 UTF-8,转换失败返回 0
int n = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
filename_utf8, -1, NULL, 0);
if (n > 0) {
wchar_t *filename_w = (wchar_t *)calloc(n, sizeof(wchar_t));
if (filename_w) {
int m = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
mode_utf8, -1, NULL, 0);
if (m > 0) {
wchar_t *mode_w = (wchar_t *)calloc(m, sizeof(wchar_t));
if (mode_w) {
MultiByteToWideChar(CP_UTF8, 0, filename_utf8, -1, filename_w, n);
MultiByteToWideChar(CP_UTF8, 0, mode_utf8, -1, mode_w, m);
// 2. 用宽字符版本打开文件
FILE *fh = _wfopen(filename_w, mode_w);
free(mode_w);
free(filename_w);
if (fh) return fh;
return fopen(filename_utf8, mode_utf8);
}
}
free(filename_w);
}
}
#endif
// 非 Windows 平台 / Windows 上 UTF-8 转换失败
// 直接调用标准 fopen(按系统默认编码处理)
return fopen(filename_utf8, mode_utf8);
}
降级策略解析:
utf8_fopen("D:/素材/视频.mp4", "rb")
│
├─ MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, ...)
│ ├─ 输入是合法 UTF-8 → 转换成功 → _wfopen(L"D:/素材/视频.mp4")
│ │ ├─ 成功 → 返回 FILE* ✓
│ │ └─ 失败(文件不存在等)→ 降级到 fopen(按 ACP 尝试)
│ │
│ └─ 输入不是合法 UTF-8(如 GBK)→ 返回 0 → 跳过宽字符路径
│
└─ fopen(filename_utf8, mode_utf8) ← 按 ACP(GBK)处理,兼容旧代码
MB_ERR_INVALID_CHARS 标志是降级策略的关键------它让 MultiByteToWideChar 在遇到非法 UTF-8 序列时直接失败,而不是静默产生乱码。
utf8_remove 和 utf8_stat 的实现思路完全一致,完整代码见本文末尾[附录 B](#附录 B)。
C++ 接口:to_path()
对于 C++ 的文件流和 std::filesystem 操作,提供一个跨平台的路径转换函数:
cpp
#include <string>
#if defined(_WIN32)
#include <filesystem>
#endif
inline decltype(auto) to_path(const std::string& utf8_path) {
#if defined(_WIN32)
return std::filesystem::u8path(utf8_path); // → std::filesystem::path
#else
return utf8_path; // → const std::string&
#endif
}
inline auto to_path(std::string&& utf8_path) {
#if defined(_WIN32)
return std::filesystem::u8path(utf8_path);
#else
return std::move(utf8_path);
#endif
}
inline auto to_path(const char* const utf8_path) {
if (utf8_path == nullptr) {
return decltype(to_path(std::string{})){};
}
return to_path(std::string(utf8_path));
}
这个函数涉及 decltype(auto) 返回类型推导、右值引用重载避免悬垂引用等 Modern C++ 细节,详细的设计分析可以参考我的另一篇博客:《C++不要返回const左值引用捕获的右值引用》。
使用示例:
cpp
// 替换前(Windows 上中文路径会失败)
std::ifstream ifs(model_path);
std::ofstream ofs(output_path, std::ios::binary);
// 替换后(全平台正确)
std::ifstream ifs(to_path(model_path));
std::ofstream ofs(to_path(output_path), std::ios::binary);
对于 std::filesystem 操作:
cpp
// 替换前
std::filesystem::remove(file_path); // string → path 按 ACP 解码
// 替换后
std::filesystem::remove(to_path(file_path)); // u8path 正确处理 UTF-8
Windows API 层:utf8_to_wstring + W 后缀
对于直接调用 Windows API 的场景,使用 utf8_to_wstring 转换后调用 W 后缀版本:
cpp
#ifdef _WIN32
std::wstring utf8_to_wstring(const std::string& utf8_str) {
const int len = static_cast<int>(utf8_str.size());
int wstr_size = MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), len, nullptr, 0);
if (wstr_size == 0) return L"";
std::wstring wstr(wstr_size, 0);
MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), len, &wstr[0], wstr_size);
return wstr;
}
#endif
使用示例:
cpp
// 替换前
DWORD dwAttrib = GetFileAttributes(szPath); // 依赖 UNICODE 宏
// 替换后
DWORD dwAttrib = GetFileAttributesW(utf8_to_wstring(szPath).c_str());
关于 std::filesystem::u8path
std::filesystem::u8path 是 C++17 引入的函数,专门用于从 UTF-8 字符串构造 std::filesystem::path。在 Windows 上,它会正确地将 UTF-8 转为 UTF-16 存储在 path 内部。
C++20 已将 u8path 标记为 deprecated ,推荐的替代方式是使用 char8_t 类型:
cpp
// C++17
auto p = std::filesystem::u8path(utf8_string);
// C++20
auto p = std::filesystem::path(reinterpret_cast<const char8_t*>(utf8_string.c_str()));
如果项目使用 C++17 标准,u8path 是正确的选择。
实践中踩过的坑
MultiByteToWideChar 的 -1 参数与 std::wstring 的隐蔽 Bug
MultiByteToWideChar 的第 4 个参数(cbMultiByte)可以传 -1,表示输入是 \0 结尾的字符串,让函数自动计算长度。但它的返回值会包含末尾的 \0:
cpp
// "abc" 的 UTF-8 是 3 字节,但传 -1 时返回 4(包含 \0)
int wstr_size = MultiByteToWideChar(CP_UTF8, 0, "abc", -1, nullptr, 0);
// wstr_size == 4
如果直接用这个值构造 std::wstring:
cpp
std::wstring wstr(wstr_size, 0); // wstr.size() == 4,内部: ['a','b','c','\0']
MultiByteToWideChar(CP_UTF8, 0, "abc", -1, &wstr[0], wstr_size);
wstr 的 size() 是 4 而不是 3。std::wstring 在 size() 之后还会自动维护一个 \0,所以内存中实际是 ['a', 'b', 'c', '\0', '\0']------尾部有两个 \0。
wstr.c_str() 传给 Windows API 不会有问题(API 遇到第一个 \0 就停了),但以下场景会出 bug:
cpp
wstr == L"abc" // false!长度不同(4 vs 3)
wstr + L".txt" // 拼接结果中间嵌入了 \0
std::wcout << wstr; // 可能只输出到第一个 \0
正确做法是传入实际长度而非 -1:
cpp
const int len = static_cast<int>(utf8_str.size());
int wstr_size = MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), len, nullptr, 0);
// 此时 wstr_size 不包含 \0,是正确的字符数
struct stat 与 struct _stat 的强转
在封装 utf8_stat 时,内部需要调用 _wstat,而 _wstat 的第二个参数类型是 struct _stat*。函数签名接受的却是 struct stat*。直接强转安全吗?
c
int ret = _wstat(path_w, (struct _stat *)buf); // 安全吗?
答案是安全的 。MSVC 的 UCRT 头文件 <sys/stat.h> 中,标准 stat() 函数的内联实现就是这么做的:
c
// UCRT 源码(ucrt/sys/stat.h)
static __inline int __CRTDECL stat(const char* _FileName, struct stat* _Stat)
{
_STATIC_ASSERT(sizeof(struct stat) == sizeof(struct _stat64i32));
return _stat64i32(_FileName, (struct _stat64i32*)_Stat); // ← 微软自己也在强转
}
微软用 _STATIC_ASSERT 在编译期验证了两个结构体大小一致,然后直接强转指针。这个模式是 CRT 官方的做法。
总结
Windows 的 UTF-8 路径问题,本质上是一个历史时序错位导致的工程问题:Windows 在 UTF-8 诞生之前就选择了 UTF-16 作为内核编码,而后来 UTF-8 成为了事实标准。
核心经验:
- Windows 上的
char*不是 UTF-8 ------这是与所有类 Unix 平台最大的区别。CRT 内部虽然最终调用 W 版本,但char*→wchar_t*的转换用的是 ACP(ANSI 代码页),不是 UTF-8 UNICODE宏只影响 Windows API 的宏展开,不影响 C/C++ 标准库 ------fopen、ifstream不走 A/W 宏体系- 设计降级路径很重要 ------不能假设输入一定是 UTF-8,通过
MB_ERR_INVALID_CHARS检测编码合法性,失败时降级到 ANSI 方式 MultiByteToWideChar的-1参数有坑 ------返回值包含\0,与std::wstring配合时要用实际长度而非-1- 如果目标平台是 Windows 10 1903+,可以考虑通过 manifest 设置 UTF-8 代码页,从根本上解决问题
附录 A:受影响的完整 API 清单
C 标准库文件 I/O
| 函数 | Windows 宽字符版本 | 说明 |
|---|---|---|
fopen() |
_wfopen() |
打开文件,最常见的问题源 |
fopen_s() |
_wfopen_s() |
安全版本 |
freopen() |
_wfreopen() |
重新打开文件到已有 FILE* |
remove() |
_wremove() |
删除文件 |
rename() |
_wrename() |
重命名文件 |
tmpnam() |
_wtmpnam() |
生成临时文件名 |
注意:
remove()和rename()需要人工区分------STL 容器的std::remove()不是文件操作。
C++ 文件流
| 函数 | 说明 |
|---|---|
std::ifstream(path) |
构造函数传入 char* 或 std::string |
std::ofstream(path) |
同上 |
std::fstream(path) |
同上 |
.open(path) |
上述三者的 .open() 方法 |
MSVC 有非标准扩展,支持 wchar_t* / std::wstring 作为构造参数,这不是标准 C++ 行为,不可移植。
POSIX / CRT 文件与目录操作
| 函数 | Windows 宽字符版本 | 说明 |
|---|---|---|
stat() / _stat() |
_wstat() |
获取文件元信息 |
access() / _access() |
_waccess() |
检查文件可访问性 |
mkdir() / _mkdir() |
_wmkdir() |
创建目录 |
rmdir() / _rmdir() |
_wrmdir() |
删除目录 |
open() / _open() |
_wopen() |
低级文件打开 |
_sopen() / _sopen_s() |
_wsopen() / _wsopen_s() |
共享模式打开 |
opendir() / readdir() |
_wopendir() / _wreaddir() |
遍历目录 |
realpath() / _fullpath() |
_wfullpath() |
解析完整路径 |
getcwd() / _getcwd() |
_wgetcwd() |
获取当前工作目录 |
chdir() / _chdir() |
_wchdir() |
切换工作目录 |
unlink() / _unlink() |
_wunlink() |
删除文件 |
C++ std::filesystem
std::filesystem::path(std::string) 构造函数在 Windows 上按 ACP 解码,不是 UTF-8 。所有接受 std::string 路径参数的 std::filesystem 函数(exists、create_directories、remove、file_size、copy_file 等)都会隐式构造 path,因此全部受影响。
安全的构造方式:
| 用法 | C++ 标准 |
|---|---|
std::filesystem::u8path(str) |
C++17(C++20 已 deprecated) |
std::filesystem::path(std::u8string) |
C++20 |
std::filesystem::path(std::wstring) |
所有版本(Windows 原生 UTF-16) |
环境变量
| 函数 | Windows 宽字符版本 | 说明 |
|---|---|---|
std::getenv() / getenv() |
_wgetenv() |
返回值按系统 ANSI 代码页编码,不是 UTF-8 |
进程 / 命令执行
| 函数 | 说明 |
|---|---|
system() |
命令字符串含非 ASCII 路径会失败 |
_popen() / popen() |
同上,_wpopen() 为宽字符版本 |
execv() / execvp() 系列 |
POSIX 进程执行(Windows 上少用) |
Windows API(A/W 版本问题)
建议始终显式使用 W 后缀版本,避免依赖 UNICODE 宏展开。
| ANSI 版本 | 应使用的宽字符版本 | 说明 |
|---|---|---|
LoadLibraryA / LoadLibrary |
LoadLibraryW |
加载动态库 |
LoadLibraryExA / LoadLibraryEx |
LoadLibraryExW |
加载动态库(带标志) |
GetModuleHandleA / GetModuleHandle |
GetModuleHandleW |
获取已加载模块句柄 |
GetModuleFileNameA / GetModuleFileName |
GetModuleFileNameW |
获取模块路径 |
CreateFileA / CreateFile |
CreateFileW |
创建/打开文件句柄 |
DeleteFileA / DeleteFile |
DeleteFileW |
删除文件 |
MoveFileA / MoveFileExA |
MoveFileW / MoveFileExW |
移动/重命名 |
CopyFileA / CopyFile |
CopyFileW |
复制文件 |
ReplaceFileA / ReplaceFile |
ReplaceFileW |
原子替换文件 |
CreateDirectoryA / CreateDirectory |
CreateDirectoryW |
创建目录 |
RemoveDirectoryA / RemoveDirectory |
RemoveDirectoryW |
删除目录 |
GetFileAttributesA / GetFileAttributes |
GetFileAttributesW |
获取文件属性 |
SetFileAttributesA / SetFileAttributes |
SetFileAttributesW |
设置文件属性 |
FindFirstFileA / FindFirstFile |
FindFirstFileW |
文件查找 |
FindNextFileA / FindNextFile |
FindNextFileW |
文件查找(续) |
GetFullPathNameA / GetFullPathName |
GetFullPathNameW |
获取完整路径 |
GetCurrentDirectoryA / GetCurrentDirectory |
GetCurrentDirectoryW |
获取当前目录 |
SetCurrentDirectoryA / SetCurrentDirectory |
SetCurrentDirectoryW |
设置当前目录 |
GetTempPathA / GetTempPath |
GetTempPathW |
获取临时目录 |
GetTempFileNameA / GetTempFileName |
GetTempFileNameW |
生成临时文件名 |
SearchPathA / SearchPath |
SearchPathW |
搜索路径 |
GetEnvironmentVariableA / GetEnvironmentVariable |
GetEnvironmentVariableW |
获取环境变量 |
SetEnvironmentVariableA / SetEnvironmentVariable |
SetEnvironmentVariableW |
设置环境变量 |
ExpandEnvironmentStringsA / ExpandEnvironmentStrings |
ExpandEnvironmentStringsW |
展开环境变量字符串 |
CreateProcessA / CreateProcess |
CreateProcessW |
创建进程 |
ShellExecuteA / ShellExecute |
ShellExecuteW |
执行外部程序 |
SHGetFolderPathA / SHGetFolderPath |
SHGetFolderPathW |
获取特殊文件夹路径 |
SHGetSpecialFolderPathA / SHGetSpecialFolderPath |
SHGetSpecialFolderPathW |
获取特殊文件夹路径(简化版) |
SHFileOperationA / SHFileOperation |
SHFileOperationW |
Shell 文件操作 |
附录 B:utf8_remove 与 utf8_stat 实现
c
int utf8_remove(const char *path_utf8)
{
if (!path_utf8) return -1;
#ifdef _WIN32
int n = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path_utf8, -1, NULL, 0);
if (n > 0) {
wchar_t *path_w = (wchar_t *)calloc(n, sizeof(wchar_t));
if (path_w) {
MultiByteToWideChar(CP_UTF8, 0, path_utf8, -1, path_w, n);
int ret = _wremove(path_w);
free(path_w);
if (ret == 0) return 0;
return remove(path_utf8);
}
}
#endif
return remove(path_utf8);
}
int utf8_stat(const char *path_utf8, struct stat *buf)
{
if (!path_utf8 || !buf) return -1;
#ifdef _WIN32
int n = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path_utf8, -1, NULL, 0);
if (n > 0) {
wchar_t *path_w = (wchar_t *)calloc(n, sizeof(wchar_t));
if (path_w) {
MultiByteToWideChar(CP_UTF8, 0, path_utf8, -1, path_w, n);
int ret = _wstat(path_w, (struct _stat *)buf);
free(path_w);
return ret;
}
}
#endif
return stat(path_utf8, buf);
}