探索 Windows 平台下 argv 和文件名编码

探索 Windows 平台下 argv 和文件名编码

RayAlto OP

1. 状况

就像自带的记事本里有“ ANSI 编码”一样, Windows 非常喜欢创造一些对于三次元的我们来说很超前的东西,这几天写了一个 C++ 小工具,读取 argv[1] 里的文件名来完成一些工作,然后我发现这样的代码:

1
2
3
4
5
6
int main(int argc, const char* argv[]) {
std::fstream f(argv[1], std::ios::binary);
f << "🖕Windows Fuck You\n";
f.close();
return 0;
}

我的 Windows 语言设置了简体中文

对于一些文件不起作用,比如包含希腊字符 Ελληνική γλώσσα

2. Windows C++ argv 的编码

我就比较好奇这个 argv 不是 UTF-8 编码的吗?

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, const char* argv[]) {
uchardet_t chardet_ctx = uchardet_new();
if (uchardet_handle_data(chardet_ctx, argv[1], std::strlen(argv[1])) != 0) {
std::cerr << "uchardet failed!\n";
std::exit(1);
}
uchardet_data_end(chardet_ctx);
std::cout << "Charset: " << uchardet_get_charset(chardet_ctx) << '\n';
uchardet_delete(chardet_ctx);
return 0;
}

输出:

1
Charset: GB18030

??? Windows 你在干什么?我又去万能的 stackoverflow 翻了翻,发现 Windows 为了使

1
int main(int argc, char* argv[]);

支持 Unicode 创建了一个自己的扩展

1
int wmain(int argc, wchar_t* argv[])

??????? wchar_t 是啥?我又去问咕咕噜, Windows 称其为 Unicode ,又起了个名叫“宽字符 (wide character) ”,今天也是跟 Windows 学专有名词的一天,看了看这个 wchar_t 发现它的 size 还不是一定的,我的 Windows 下是 2 字节,而 Linux 下是 4 字节(感觉不如 intxx_t ),这些先不说, UTF-8 的话 char* 就够了呀,我一查才发现 wmainargv 是他妈的 UTF-16 编码的(而且 Windows 称 UTF-16 为“宽字符”,“ Unicode ”),我们 Windows 真是太超前 🌶!

3. 喷 Windows 和它钟爱的 UTF-16

Windows 又提出了以下等式:

1
UTF-16 = Unicode = 宽字符 = wchar_t

一想 Windows 是一个商业产品,又觉得这种误导大众的做法有一丝合理,但这么一查才发现 Windows API 到处都是 UTF-16 ,为啥要选这么一个不上不下的编码呢?哦,好像跟 Windows 不上不下一个道理。

我不明白 UTF-16 有什么好的,你说它比 UTF-8 优秀,可以根据字节数直接求出字符个数?它和定长编码 UTF-32 不一样,是变长编码,有可能 2 字节也有可能 4 字节,一个 emoji “😂” 就是 4 个字节,一个汉字“绷”就是两个字节;你说它比 UTF-32 优秀,可以节省空间?当今计算机体系下至少 50% 的内容都是英文的,使用 UTF-8 编码一个字节就可以解决(因为 UTF-8 兼容 ASCII ),而 UTF-16 要两个字节(也就不兼容 ASCII ),每 16 个 bit 有 6 个 bit 都用来标记字节位置( 1101 10xx xxxx xxxx1101 11xx xxxx xxxx ),综合起来我觉得比 UTF-8 浪费了更多空间,又没有 UTF-32 的固定长度的优势,属于是综合了两边的缺点,而优点不值一提(一些 CJK 字符可以比 UTF-8 少用 1 个字节)。

你说的对,但是《 UTF-16 》是由 Unicode 联盟自主研发的一种字符编码标准。游戏发生在一个被称作「 wchar_t 」的幻想世界,在这里,被 Windows 选中的人将被授予「 DecodeError 」,导引解码之力。你将扮演一位名为「 wmain 」的神秘角色,在编码解码的过程中邂逅性格各异、能力独特的同伴们,和他们一起击败 Illegal byte sequence ,找回失散的亲人——同时,逐步发掘「 std::filesystem::filesystem_error 」的真相。

3. Windows Exclusive

我寻思再怎么为了程序能跑在 Windows 上也不能直接把 main 改成 wmain 吧,然后我又找到了这个:

1
2
3
4
int argc = 0;
wchar_t** argv_u16 = CommandLineToArgvW(GetCommandLineW(), &argc);
// argv_u16 是 UTF-16LE 编码的 argv
LocalFree(argv_u16);

包装一下应该可以用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "windows.h"

namespace windows {

std::string u16_to_u8(wchar_t* u16) {
int u16len = wcslen(u16);
int u8len = WideCharToMultiByte(
CP_UTF8, 0, u16, u16len, nullptr, 0, nullptr, nullptr);
char buf[u8len + 1] = {0};
buf[u8len] = '\0';
WideCharToMultiByte(CP_UTF8, 0, u16, u16len, buf, u8len, nullptr, nullptr);
return buf;
}

std::vector<std::string> get_argv_u8() {
int argc = 0;
wchar_t** argv_u16 = CommandLineToArgvW(GetCommandLineW(), &argc);
std::vector<std::string> argv_u8;
argv_u8.reserve(argc);
for (int i = 0; i < argc; i++) {
argv_u8.emplace_back(u16_to_u8(argv_u16[i]));
}
LocalFree(argv_u16);
return argv_u8;
}

} // namespace windows

4. 嗑 Windows x UTF-16

修改成了这样

1
2
3
4
5
6
7
int main(int argc, const char* argv[]) {
std::vector<std::string> argv_u8 = windows::get_argv_u8();
std::fstream f(argv_u8[1], std::ios::binary);
f << "🖕Windows Fuck You\n";
f.close();
return 0;
}

还是不行,程序没有异常,但文件还是没有被修改,我又去看了看 Win API ,发现 Win 打开文件的 API 有两个,但都不支持 UTF-8 :

  • CreateFileA :使用 Windows 所谓的“ ANSI 编码”实际在我的电脑上是 GB18030 ,回到最初的起点了,闭环了属于是。
  • CreateFileW :使用 Windows 所谓的“ Unicode ”实际是 UTF-16 ,就非逼着你用 wchar_t* , Windows 和 UTF-16 太甜了,太好磕了。

也就是说 std::fstream 无论用哪个 constructor 最后到 Win API 都是不支持 UTF-8 的。

5. C++17 std::filesystem

好在 C++17 之后有了 std::filesystem ,可以这样:

1
2
3
4
5
6
7
int main(int argc, const char* argv[]) {
std::vector<std::string> argv_u8 = windows::get_argv_u8();
std::fstream f(std::filesystem::path(argv_u8[1]), std::ios::binary);
f << "🖕Windows Fuck You\n";
f.close();
return 0;
}

std::filesystem 会帮你把 UTF-8 的文件名转换成平台特定的编码, Cppreference 说它会在 Windows 上把文件名转成 UTF-16 ,使用 wchar_t* , POSIX 环境下直接使用 UTF-8 (char*) ,问题解决。