跨平台C应用程序的UTF8支持

4
我正在开发一个跨平台的C语言应用程序(符合C89标准),需要处理UTF8文本。我只需要基本的字符串操作函数,如substrfirstlast等。 问题1 是否有已经实现了上述函数的UTF8库?我已经查看了ICU,但它对我的要求来说太大了。我只需要支持UTF8。
我在这里找到了一个UTF8解码器(链接)。下面是该代码中的函数原型。
void utf8_decode_init(char p[], int length);

int utf8_decode_next();

初始化函数接受字符数组,但是 utf8_decode_next() 返回 int 类型。为什么会这样?我如何使用标准函数(如 printf)打印该函数返回的字符?该函数正在处理字符数据,如何将其分配给整数?
如果上述解码器不适用于生产代码,你有更好的建议吗?
问题2:
我也被一些文章搞糊涂了,它们说对于 Unicode,你需要使用 wchar_t。从我的理解来看,这并非必需,因为普通的 C 字符串可以容纳 UTF8 值。我通过查看 SQLite 和 git 的源代码进行了验证。SQLite 有以下 typedef。
typedef unsigned char u8

我理解正确吗?此外,为什么需要使用无符号字符(unsigned char)?
6个回答

4
  1. utf_decode_next()函数返回下一个Unicode码点。由于Unicode是一个21位字符集,它不能返回比int更小的任何东西,可以说从技术上讲,它应该是一个long,因为int可能是16位数量。实际上,该函数会返回一个UTF-32字符。

    您需要查看C94宽字符扩展到C89以打印宽字符(wprintf(), <wctype.h>, <wchar.h>)。但是,仅使用宽字符不能保证是UTF-8甚至Unicode。您很可能无法便携地打印来自utf8_decode_next()的字符,但这取决于您的可移植性要求。您必须要将UTF-8字符串(而不是从utf8_decode_next()获得的UTF-32字符数组)发送给其中一个常规打印函数,只要您能够便携地编写UTF-8即可。UTF-8的优点之一是它可以被主要不了解它的代码操作。

  2. 您需要理解,4字节的wchar_t可以在单个单元中容纳任何Unicode码点,但是UTF-8可能需要1到4个8位字节(1-4个存储单元)来容纳一个Unicode码点。在某些系统上,我认为wchar_t可以是16位(short)整数。在这种情况下,您被迫使用UTF-16,它使用代理项(两个存储单元)编码超出基本多语言平面(BMP,代码点U+0000 .. U+FFFF)的Unicode码点。

    使用无符号字符unsigned char可以使生活更加轻松;普通的char通常是带符号的。负数会使生活比必要的更加困难(相信我,即使没有增加复杂性,也已经足够困难了)。


对于Unicode代码点,int32_t是首选类型。正如您所说,int理论上可能太短,而在任何64位机器上,long肯定会浪费大量空间。 - R.. GitHub STOP HELPING ICE
@R..:公正的评论;而且只使用32位中不那么重要的21位,所以不必担心uint32_tint32_t - Jonathan Leffler
我喜欢使用有符号类型,以便能够轻松地使用负值来表示错误,但在解码过程中,无符号类型通常更方便。只要它是32位类型,你使用哪种类型其实并不重要,就像你所说的一样。 - R.. GitHub STOP HELPING ICE
很遗憾,少数实现提供了 int_least22_t。他们在想什么? - Steve Jessop
感谢@Jonathan。我已经实现了substrlength,并没有使用上面的解码器。新的实现看起来更简单。它发布在这里。https://dev59.com/hVLTa4cB1Zd3GeqPd9Qm#4534339 - Navaneeth K N

4

在UTF-8中,您不需要任何特殊的库函数来进行字符或子字符串搜索。使用strstr即可完成所有操作。这也是UTF-8及其设计要求的全部意义所在。


@R.. 谢谢。所以,只是为了理解,strstr有一个UTF8解码器? - Navaneeth K N
1
不需要,因为它不需要一个。这正是UTF-8的全部意义所在。 - R.. GitHub STOP HELPING ICE
非常感谢。很抱歉,我还是有点困惑。据我的理解,由于UTF8不受字节顺序的影响,比较字节本身将提供正确的信息,您无需对其进行解码。这就是为什么strstr可用于UTF8的原因。这是否正确? - Navaneeth K N
如果你只是处理字符,那么在替换它们时可能会遇到麻烦,因为新字符的字节长度可能不同。但是Unicode建议始终避免使用字符,而是使用字符串。如果你按照这种方式进行操作——用一个新字符串替换子字符串——一切都会正常工作,但需要将其复制到新缓冲区或在现有缓冲区中执行一些memmove(可能还需要调整大小)。 - R.. GitHub STOP HELPING ICE
2
(有效的)UTF-8 字符始于头字节 00-7FC2-F4,并在范围 80-BF 的所有后续字节中继续。如果您想要最后一个字符,可以从最后一个字节开始向后工作,直到找到一个头字节。另一方面,如果您知道您要查找的特定字符,只需将字符串末尾的那么多个字节与该字符的字节进行比较,看它们是否匹配。如果是,则在第一个字节上写入空字节,这样就删除了它。 - R.. GitHub STOP HELPING ICE
显示剩余4条评论

2

GLib有很多相关函数,可以独立于GTK+使用。具体信息请参见这里


我能在Windows上编译这个吗? - Navaneeth K N
当然可以,但它可能已经编译好了。你只需要获取DLL、头文件和导入库。http://www.gtk.org/download-windows.html - Ignacio Vazquez-Abrams

1

Unicode中有超过100,000个字符。在大多数C实现中,char有256个可能的值。

因此,UTF-8使用多个char来编码每个字符,并且解码器需要一个比char更大的返回类型。

wchar_t是比char更大的类型(虽然它不一定要更大,但通常是这样)。它表示实现定义的宽字符集的字符。在某些实现中(最重要的是Windows,它对“基本多语言平面”之外的字符使用代理对),它仍然不足以表示任何Unicode字符,这可能是为什么您引用的解码器使用int的原因。

您无法使用printf打印宽字符,因为它处理的是char。如果宽字符集是Unicode,并且如果wchar_t在您的系统上是int(如在Linux上),那么wprintf和相关函数将打印解码器输出而无需进一步处理。否则,它将无法打印。

无论如何,您都不能可移植地打印任意Unicode字符,因为不能保证终端可以显示它们,甚至宽字符集与Unicode有任何关系。
SQLite可能使用了unsigned char,以便:
- 他们知道符号 - char是否带符号是实现定义的。 - 他们可以进行右移和分配超出范围的值,并在所有C实现中获得一致且定义良好的结果。实现对signed char的行为比unsigned char更自由。

@Appu:这取决于您想要支持的平台以及是否需要支持全部Unicode或仅支持BMP,但很可能不需要。 - Steve Jessop

0
我已经实现了支持UTF8字符的substrlength函数。这段代码是SQLite使用的修改版本。
以下宏循环遍历输入文本并跳过所有多字节序列字符。if条件检查这是否是一个多字节序列,而其内部的循环则通过递增input来寻找下一个头字节。
#define SKIP_MULTI_BYTE_SEQUENCE(input) {              \
    if( (*(input++)) >= 0xc0 ) {                       \ 
    while( (*input & 0xc0) == 0x80 ){ input++; }       \
  }                                                    \
}

substrlength是使用此宏实现的。

typedef unsigned char utf8;

substr

void *substr(const utf8 *string, 
             int start, 
             int len, 
             utf8 **substring)
{
    int bytes, i;
    const utf8 *str2;
    utf8 *output;

    --start;
    while( *string && start ) {
        SKIP_MULTI_BYTE_SEQUENCE(string);
        --start;
    }

    for(str2 = string; *str2 && len; len--) {
        SKIP_MULTI_BYTE_SEQUENCE(str2);
    }

    bytes = (int) (str2 - string);
    output = *substring;
    for(i = 0; i < bytes; i++) {
        *output++ = *string++;
    }
    *output = '\0';
}

长度

int length(const utf8 *string)
{
    int len;
    len = 0;
    while( *string ) {
        ++len;
        SKIP_MULTI_BYTE_SEQUENCE(string);
    }
    return len;
}

0

普通的C字符串可以用于存储utf8数据,但是在utf8字符串中很难搜索子字符串。这是因为使用utf8编码的字符作为字节序列编码时,根据字符不同,一个字符可能由1到4个字节组成。也就是说,在utf8中,“字符”与“字节”不像ASCII一样等价。

为了进行子字符串搜索等操作,您需要将其解码为用于表示Unicode字符的内部格式,然后在其中进行子字符串搜索。由于Unicode有超过256个字符,一个字节(或char)是不够的。这就是为什么您找到的库使用int的原因。

至于您的第二个问题,可能只是因为谈论负字符没有意义,所以它们可以被指定为“无符号”。


5
只要UTF-8数据使用每个字符的规范表示(如果不是规范表示,可能会得到错误的结果),你可以通过查找匹配的字节序列来进行子字符串搜索。UTF-8设计得非常巧妙,以防止出现错误的结果:通过位数高低可以确定一个字节是否是一个字符的第一个字节,因此,既不能将一个字符的子序列,也不能将一个字符的结尾和另一个字符的开头混淆为另一个字符。 - Steve Jessop
也许这已经不是问题了 - 如果最后一个这样的未验证解码器已经被淘汰的话... - Steve Jessop
1
@Steve:任何解码器如果解码你所谓的非规范表示都是不符合规范极其危险的。如果你的项目中有这样的代码,你应该立即清除它。特别地,字节0xC0和0xC1(以及0xF5...0xFF)在UTF-8中完全无效,必须始终被拒绝。还有一些其他的序列也必须被拒绝。UTF-8解码最好由DFA处理,而不是传统的循环,因为传统的循环容易出错,并且倾向于将无效输入解码为有效字符串的别名。 - R.. GitHub STOP HELPING ICE
@R..:当然,它从2003年开始就不符合标准了,所以我想我的担忧可能已经不再相关了。 - Steve Jessop
我认为你正在查看IETF RFC,这已经远远落后了。据我所知,Unicode对UTF-8的定义始终对每个字符都有唯一的编码。 - R.. GitHub STOP HELPING ICE
显示剩余6条评论

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