UNICODE、UTF-8 和 Windows 混乱问题

11

我试图在Windows上实现文本支持,并打算以后也移到Linux平台。理想情况下,希望以统一的方式支持国际化语言,但考虑到这两个平台时,似乎很难实现。我花了相当多的时间阅读有关UNICODE、UTF-8(和其他编码)、widechar等方面的资料,至今所了解的如下:

UNICODE作为标准,描述了可映射字符集及其出现顺序。我把这称为"what": UNICODE指定了可用的内容。

UTF-8(和其他编码)则指定了"how": 每个字符将以何种二进制格式表示。

现在,在Windows上,他们最初选择了UCS-2编码,但这未能满足要求,因此他们使用了UTF-16,这也是多字符的。

因此,这里存在一个困境:

  1. Windows内部仅支持UTF-16,因此如果要支持国际字符,必须将其转换为宽字符版本,以便相应地使用操作系统调用。似乎没有支持使用类似CreateFileA()的函数来处理多字节UTF-8字符串并使其显示正确的方法。这是正确的吗?
  2. C语言中有一些支持多字节的函数(_mbscat、_mbscpy等),但在Windows上,这些函数的字符类型被定义为unsigned char*。鉴于_mbs系列函数不是一个完整的集合(例如,没有将多字节字符串转换为长整型的_mbstol函数),必须使用某些运行时函数的char*版本,这会导致编译器出错,因为这些函数之间存在有符号/无符号类型差异。有人甚至使用它们吗?你只是进行大量的转换以解决错误问题吗?
  3. 在C++中,std::string有迭代器,但是这些迭代器基于char_type,而不是基于码点。因此,如果我在std::string::iterator上执行++操作,我会得到下一个char_type,而不是下一个码点。同样地,如果调用std::string::operator[],你会得到一个指向char_type的引用,它很可能不是一个完整的码点。那么如何通过码点迭代std::string?(C语言中有_mbsinc()函数)。

3
请注意,几乎没有正当理由以代码点迭代Unicode字符串,因为一个字形可能由多个代码点表示(每个代码点在UTF-8或UTF-16中可以是多个代码单元,但对于许多实际目的而言,这是两个相同的问题)。归一化是一个合法的理由,编码为UTF-8是另一个合法的理由,但这些都是你可以使用库来解决的问题。 - Steve Jessop
2
@JerryCoffin:实际上,你不想按字符进行插入。你需要按照字形簇进行插入。例如,在señor中,您不想在n~之间插入一个字符。(这种特殊情况可以通过组合来解决,但并非所有此类字符都可以组合。) - Dietrich Epp
1
@DietrichEpp 那么如何确定字形簇边界呢? - Murrgon
1
@Murrgon:阅读[UAX#29](http://www.unicode.org/reports/tr29/)。这并不简单。 - Dietrich Epp
1
推荐阅读:http://utf8everywhere.org - Pavel Radzivilovsky
显示剩余6条评论
4个回答

10

只需使用UTF-8

每个平台都有许多支持UTF-8的支持库,其中一些还是跨平台的。正如您已经注意到的那样,在Win32中的UTF-16 APIs很有限且不一致,因此最好将所有内容保留在UTF-8中,并在最后时刻转换为UTF-16。此外,Windows API也提供了一些便利的UTF-8封装。

此外,在应用程序级别文档中,UTF-8作为标准被越来越广泛地接受。每个文字处理应用程序都接受UTF-8,或者最坏的情况下将其显示为“带有一些特殊符号的ASCII”,而仅有少数应用程序支持UTF-16文档,那些不支持的应用程序则将其显示为“大量的空格!”


1
我想添加一个非常好的参考资料,解释为什么应该在任何地方使用UTF-8 http://utf8everywhere.org/ - Anton Kochkov
还有一些方便的UTF-8封装可用于Windows API。比如哪些? - jamesdlin
微软正在使Windows API越来越能够支持UTF-8。请注意,您的应用程序清单必须配置正确! - Dúthomhas

8
  1. 正确。你需要将UTF-8转换为UTF-16,以便进行Windows API调用。

  2. 大多数情况下,你将使用普通的字符串函数来处理UTF-8,如strlenstrcpy(不推荐)、snprintfstrtol。它们可以很好地处理UTF-8字符。要么使用char *来处理UTF-8,要么就需要强制类型转换。

    需要注意的是,带下划线的版本(如_mbstowcs)并不是标准的,它们通常没有下划线,比如mbstowcs

  3. 实际上很难举出你想要在Unicode字符串上使用operator[]的示例,我的建议是尽量避免使用它。同样地,遍历字符串的应用场景很少:

    • 如果你正在解析一个字符串(例如,该字符串是C或JavaScript代码,也许你想要语法高亮),则可以逐字节地完成大部分工作,并忽略多字节方面的问题。

    • 如果你正在进行搜索,则也会逐字节进行(但记得先进行规范化)。

    • 如果你正在寻找单词分隔符或字形群集边界,则需要使用类似于ICU的库。这个算法并不简单。

    • 最后,你总是可以将一块文本转换为UTF-32,并以这种方式处理它。如果你正在实现任何Unicode算法(如排序或拆分),我认为这是最明智的选择。

    参见:C++ iterate or split UTF-8 string into array of symbols?


2
Windows内部只支持UTF-16,因此如果您想支持国际字符,就必须将其转换为宽字符版本,以便相应地使用操作系统调用。似乎没有支持使用多字节UTF-8字符串调用类似CreateFileA()的函数并使其正确显示的方法。这是正确的吗?
是的,这是正确的。*A函数变体根据当前活动代码页(在美国和西欧大多数计算机上为Windows-1252,但通常可以是其他代码页)解释字符串参数并将它们转换为UTF-16。虽然有一个UTF-8代码页,但据我所知,没有一种编程方式可以设置活动代码页(有GetACP获取活动代码页,但没有对应的SetACP)。
在 C 语言中,有一些支持多字节的函数(_mbscat、_mbscpy 等),但是在 Windows 上,这些函数的字符类型被定义为 unsigned char*。由于 _mbs 系列函数不是一个完整的集合(例如没有将多字节字符串转换为长整型的 _mbstol 函数),因此您被迫使用某些 char* 版本的运行时函数,这会导致编译器问题,因为这些函数之间存在有符号/无符号类型差异。在我的经验中,几乎没有人使用 mbs* 函数族,除了 mbstowcs、mbsrtowcs 和 mbsinit 函数。
在C++中,std::string具有迭代器,但这些基于char_type,而不是基于码点。因此,如果我在std::string::iterator上执行++操作,我会得到下一个char_type,而不是下一个代码点。同样,如果调用std::string::operator[],您会得到对char_type的引用,它有可能不是完整的代码点。那么,如何通过代码点迭代std::string?(C使用_mbsinc()函数)。
我认为mbrtowc(3)是解码多字节字符串的单个代码点的最佳选择。
总体而言,我认为跨平台Unicode兼容性的最佳策略是在内部使用单字节字符使用UTF-8进行所有操作。当您需要调用Windows API函数时,请将其转换为UTF-16并始终调用*W变体。大多数非Windows平台已经使用UTF-8,因此使用它们非常方便。

1
很遗憾,mbrtowc 在 Windows 上无法解码代码点。 - Dietrich Epp

0
在Windows中,您可以调用WideCharToMultiByteMultiByteToWideChar来在UTF-8字符串和UTF-16字符串(在Windows中称为wstring)之间进行转换。因为Windows API不使用UTF-8,所以每当您调用任何支持Unicode的Windows API函数时,都必须将字符串转换为wstring(UTF-16中的Windows版本Unicode)。当您从Windows获取输出时,您必须将UTF-16转换回UTF-8。Linux内部使用UTF-8,因此您不需要进行此类转换。为了使您的代码可移植到Linux,请坚持使用UTF-8并提供以下内容进行转换:
#if (UNDERLYING_OS==OS_WINDOWS)
 
using os_string = std::wstring;

std::string utf8_string_from_os_string(const os_string &os_str)
{
    size_t length = os_str.size();
    int size_needed = WideCharToMultiByte(CP_UTF8, 0, os_str, length, NULL, 0, NULL, NULL);
    std::string strTo(size_needed, 0);
    WideCharToMultiByte(CP_UTF8, 0, os_str, length, &strTo[0], size_needed, NULL, NULL);
    return strTo;
}

os_string utf8_string_to_os_string(const std::string &str)
{
    size_t length = os_str.size();
    int size_needed = MultiByteToWideChar(CP_UTF8, 0, str, length, NULL, 0);
    os_string wstrTo(size_needed, 0);
    MultiByteToWideChar(CP_UTF8, 0, str, length, &wstrTo[0], size_needed);
    return wstrTo;
}

#else

// Other operating system uses UTF-8 directly and such conversion is
// not required
using os_string = std::string;
#define utf8_string_from_os_string(str)    str
#define utf8_string_to_os_string(str)    str

#endif

要迭代 utf8 字符串,你需要两个基本函数:一个用于计算 utf8 字符的字节数,另一个用于确定字节是否是 utf8 字符序列的首字节。以下代码提供了一种非常有效的测试方法:

inline size_t utf8CharBytes(char leading_ch)
{
    return (leading_ch & 0x80)==0 ? 1 : clz(~(uint32_t(uint8_t(leading_ch))<<24));
}

inline bool isUtf8LeadingByte(char ch)
{
    return  (ch & 0xC0) != 0x80;
}

使用这些函数,实现自己的UTF8字符串迭代器不应该很难,其中一个是正向迭代器,另一个是反向迭代器。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接