# CharacterEncoding

此文为Character Encoding的原创翻译,本文内容版权归原文所有,仅供学习,如需转载望注本文地址,翻译不易,谢谢理解。

这篇文章提供了UE4所使用字符编码的概览。

# Text格式

有很多格式可以用来展示文本和字符串,理解这些格式和它们相应的优缺点可以帮你决定在你的项目中使用什么样的格式。

这里不是这些格式的技术层面上定义,但是这些简化的版本更有利于阐述本文的主题。

  • ASCII,字符码在32和126之间,包括0,9,10,13。
  • ANSI,为使计算机支持更多的语言,通常使用0x80~0xFF范围地2个字节来表示一个字符,不同的国家和地区制定了不同的标准,由此产生了GB2312,BIG5,JIS等各自编码标准,使用2个字节来表示一个字符的编码方式叫ANSI编码。在简体中文系统下,ANSI编码代表GB2312编码,在日文操作系统下,ANSI编码代表JIS编码。不同ANSI编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段ANSI编码的文本中。
  • UTF-8,由单字节组成的字符串,可以使用特殊的序列来获取非ANSI字符。
  • UTF-16,把Unicode字符集的抽象码位映射为16位长的整数,Unicode字符的码位,需要1个或2个16位长的码元表示。

# 二进制文件

优点 缺点
没有定义内部格式;不论文件格式是什么都可以被加载 不能合并,这种类型的文件需要单独的被checkout
内部格式没有被定义,每个文件都可能是一个不同的格式
P4存储每个版本的完整内容,让仓库大小迅速膨胀,这可能是不必要地,因为一般我们都使用二进制的当前版本

# 文本

优点 缺点
可以合并,不必每个版本单独checkout 非常受限制;只支持ASCII字符

# UTF-8

优点 缺点
能简单地获取到我们需要的所有字符 亚洲语言可能会占用较大的内存
使用较少的内存 P4类型的Unicode在Perforce服务器上是不可用的
是ASCII码的超集,一个普通的ASCII字符能有效兼容UTF-8的字符串 字符串操作更复杂;在解析时不得不做一些事,比如计算长度
当检测到字符串是ASCII码和输出它的时候仍能工作 微软的开发环境并不能很好地处理亚洲区域中除了ASCII的文字。这是我们为什么在check-in的时候确定文本是ASCII码
如果我们有一个支持Unicode的服务器,这些文件可以被合并而不需要单独被checkout
可以检测一个字符串是否是UTF-8

# UTF-16

优点 缺点
能简单地获取到我们需要的所有字符 使用更多的内存
简单,内存使用是字符数的两倍 如果并不使用BOM那么就比较难检测出这个格式
简单,字符串操作可以不用解析字符就能分割/合并 当检测出字符串和输出是ASCII码时不能正常工作(现在可以用UTF-16验证程序检测出来)
同游戏中使用的格式一样,不需要转换,解析内存操作 微软的开发环境并不能很好地处理亚洲区域中除了ASCII的文字。这是我们为什么在check-in的时候确定文本是ASCII码
能够合并,不需要被单独地checkout
C#内部使用UTF-16

# UE4内部字符串表示

在UE4中所有的字符串在内存中都是以UTF-16格式存储的,就像FStrings或TCHAR数组。大部分代码都以2字节作为码点,所以只有Basic Multilingual Plane(BMP)是被支持的,所以Unreal的内部编码更准确来讲应该被称为UCS-2,Strings在当前的各个平台以相应的大端或小端方式被存储。

在磁盘或网络数据交换将对象序列化成包时,所有小于0xff的TCHAR字符被存储在8字节序列中,否则就是2字节的UTF-16.序列化代码可以在必要时处理任何大小端的转变。

# 被UE4加载的Text文件

在UE4加载一个外部text文件时,比如在运行时读一个.int文件,我们常常是通过UnMisc.cpp文件中的appLoadFileToString()函数来实现,主要的工作是在appBufferToString()函数中完成。

这个函数识别UTF-16文件中的Unicode字节顺序标记(byte-order-mark,BOM),如果存在这个标记,就会按相应的大小端格式加载UTF-16文件。

当没有BOM这个标记时,就会按平台的惯例来加载UTF-16文件。

在Windows上,会尝试用默认的Windows MBCS编码(比如对于US English和Western Europe来说是Windows-1252,对于Korean来说是CP949,对于Japanese来说是CP932)和MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS...)来将文本转换成UTF-16。这个特性是在2009年7月QA版本构建的时候添加的。

如果这个转换在除Windows平台外其他平台上失败,它将只读取每个字节然后填充成16位来构建一个TCHAR数组。

注意使用appLoadFileToString()来加载UTF-8编码的文件时,没有相应用来检测或解码的代码。

# 被UE4保存的Text文件

引擎通过appSaveStringToFile()来保存大部分生成的text文件。

由所有能被单字节表示的TCHAR字符组成的字符串都会被存储在8字节序列中,否则都会作为UTF-16存储,除非bAlwaysSaveAsAnsi标志位被设置为true,在这种情况里,它将被首先转换成默认的windows编码,目前只有shader文件可能需要做这个,来解决shader编译器编译UTF-16文件时的问题。

# UE4所用Text文件的建议编码

# INT和INI文件

建议使用或者是大端或者是小端的UTF-16。尽管亚洲语言的默认MBCS编码(比如CP932)能在Windows上工作,这些文件需要在PS3和Xbox360平台上被加载时,转换代码只运行在windows上。

# 源代码

一般来说,我们不建议使用C++的内置字符串字面量,建议将这些数据放在INT文件中。

# C++源码

使用UTF-8或者默认的windows编码。MSVC,Xbox360编译器和gcc应该都乐于接受UTF-8编码的源文件,应该尽可能的避免使用有高位设置字符进行的Latin-1编码,比如版权,商标或度量符号,因为这样的编码会因为不同的区域语言而破坏系统。在第三方软件中的一些实例是无法避免的(比如版权声明),所以对于MSVC我们禁用4819声明,否则该警告会在亚洲Windows上编译时出现。

# 在Perforce中存储UTF-16的Text文件

  • 不要使用"Text",如果一个UTF-x文件以text类型被存入,它将在同步后被破坏。
  • 如果你使用"Binary",标记这些文件将它们单独地checkout。
    • 人们可以存入ASCII,UTF-8,UTF-16,这在引擎中能正常工作。
    • 但是,二进制文件不能被合并,所以如果文件没有标记为单独checkout,将会导致双重改变。
  • 如果你使用"UTF-16",确保每个人存入的文件都是UTF-16。
  • "Unicode"类型是UTF-8,在这对于我们来说是没用的。

# 转换

我们有很多宏来转换字符串的编码,这些宏使用本地作用域中声明的一个类实例然后分配空间,所以千万不要让指针一直指向它们!只是用它们传字符串到函数调用里。

  • TCHAR_TO_ANSI(str)

  • TCHAR_TO_OEM(str)

  • ANSI_TO_TCHAR(str)

  • TCHAR_TO_UTF8(str)

  • UTF8_TO_TCHAR(str)

下面是UnStringConv.h文件中的帮助类的使用:

  • typedef TStringConversion<TCHAR,ANSICHAR,FANSIToTCHAR_Convert> FANSIToTCHAR;

  • typedef TStringConversion<ANSICHAR,TCHAR,FTCHARToANSI_Convert> FTCHARToANSI;

  • typedef TStringConversion<ANSICHAR,TCHAR,FTCHARToOEM_Convert> FTCHARToOEM;

  • typedef TStringConversion<ANSICHAR,TCHAR,FTCHARToUTF8_Convert> FTCHARToUTF8;

  • typedef TStringConversion<TCHAR,ANSICHAR,FUTF8ToTCHAR_Convert> FUTF8ToTCHAR;

在使用TCHAR_TO_ANSI时请注意,你不能假定字节数和TCHAR字符串的长度是一致的。多字节字符集要求每个TCHAR字符是多字节的,如果你需要知道转换后的字符串的字节长度,你可以使用帮助类而不是宏,比如:

FString String;
...
FTCHARToANSI Convert(*String);
Ar->Serialize((ANSICHAR*)Convert, Convert.Length());  // FTCHARToANSI::Length() returns the number of bytes for the encoded string, excluding the null terminator.
1
2
3
4

# ToUpper和ToLower

在使用ToUpper()和ToLower()时,在Unicode中需要注意的点

UE4目前只能处理ANSI(ASCII | code page 1252 | Western European)。

为适应所有语言的最差做法在这提到了en.wikipedia.org/wiki/ISO/IEC_8859

  • ISO/IEC 8859-1 for English, French, German, Italian, Portuguese, and both Spanishes

  • ISO/IEC 8859-2 for Polish, Czech, and Hungarian

  • ISO/IEC 8859-5 for Russian

# 东亚地区CPP编码的注意点

UTF-8和默认windows编码在C++编译器中可能会引起问题。

# 默认Windows编码

在运行单字节字符代码页(比如CP437 美国英语)的Windows上编译C++源码时,如果源码中包含东亚双字节字符编码比如CP936(简体中文),CP950(繁体中文),CP932(日语),请务必小心。

这些东亚字符编码系统使用0x81-0xFE作为第一个字节,0x40-0xFE作为第二个字节。0x5C的值在第二个字节中会在ASCII/latin-1中被解释为反斜杠,这在c++中有特殊含义(在字符串字面值中是转义序列,如果在行末尾用回延长行)。

在单字节代码页的windows上编译源码时,编译器将不关心东亚双字节字符编码,这可能会导致编译器错误,或者更糟糕地,在EXE中创建一个bug。

单行注释:

如果在东亚的注释中有0x5c,这可能很难找到因为缺失一行而导致的bug或错误。

    // EastAsianCharacterCommentThatContains0x5cInTheEndOfComment0x5c'\'
    mportant_function(); /* this line would be connected to above line as part of comment */
1
2

在一个string字面量中,这可能会导致一个被损坏的字符串,或者因为识别0x5c转义序列导致的错误。

    printf("EastAsianCharacterThatContains0x5c'\'AndIfContains0x5cInTheEndOfString0x5c'\'");
    function();
    printf("Compiler recognizes left double quotation mark in this line as the end of string literal that continued from first line, and expected this message is C++ code.");
1
2
3

如果0x5c后面的字符指定一个转义序列,编译器将转换转义序列字符集转换成单独指定的字符。如果没有指定,结果是提前定义好的结果,但是MSVC移除了0x5c,然后警告"未识别的字符转义序列"。

在上面的例子中,字符串末尾有(0x5c)反斜杠后带一个双引号,这个转义序列"会被转换成一个字符串中的双引号,编译器将会构建字符串直到文件中的下一个双引号或文件的末尾,这将导致错误。

下面是危险字符的例子:

  • CP932 (Japanese Shift-JIS) "?" 是 0x955C, 还有很多CP932字符有0x5C。
  • CP936 (Simplified Chinese GBK) "?" is 0x815C,还有很多CP936字符有0x5C。
  • CP950 (Traditional Chinese Big5) "?" is 0xA55C,还有很多CP950字符有0x5C。
  • CP949 (Korean, EUC-KR)是没问题的,因为EUC-KR并不使用0x5C作为第二个字节。

# 没有BOM的UTF-8

在有东亚代码页(CP936 简体中文,CP950 繁体中文,CP949 朝鲜,CP932 日本)上的Windows编译C++源码时,如果源代码中有东亚字符存储为UTF-8,请一定小心。

UTF-8字符编码对于东亚字符使用3个字节编码:0xE0-0xEF用于第一个字节,0x80-0xBF用于第二个字节,0x80-0xBF用于第三个字节。没有BOM的话,东亚Windows的默认编码会将3个UTF-8编码字节及随后的一个字节视为2个东亚双字节字符,第一个字节和第二个字节为一对,第三个字节和第四个字节为一对。当UTF-8编码的3个字节在字符串字面量或注释中有特殊含义时就会出现问题。

比如,在行注释中: 在东亚地区注释的末尾使用反斜杠''会导致没有缺行却难以发现的错误或bugs。

    // OddNumberOfEastAsianCharacterComment\
    description(); /* coder intended this line as comment, by using backslash at the end of above line */
1
2

这是很不常见的情况,因为程序员不会有意在注释的结尾写反斜杠''。

在字符串字面量中: 当奇数个UTF-8编码的东亚字符在字符串字面量里面时,它后面的字符有特殊含义,这会导致损坏的字符串,错误或警告。

    printf("OddNumberOfEastAsiaCharacterString");
    printf("OddNumberOfEastAsiaCharacterString%d",0);
    printf("OddNumberOfEastAsiaCharacterString\n");
1
2
3

在东亚代码页的Windows上的C++编译器将按UTF-8解码的东亚字符串的最后一个字节和下一个字节解释为一个单个的东亚字符。如果你够幸运,编译器警告"C4819"(如果没有被禁止)或一个错误将警告你这个问题,如果不够幸运,这个字符串将会被破坏。

# 结论

你可以为C++源码使用UTF-8或默认的windows编码,但一定要意识到这些问题。再一次声明,我们不建议在C++源码中使用字符串字面量。如果你不得不在C++源码中使用东亚字符编码,请确保使用东亚编码作为你的默认代码页。