使用iconv进行简单的UTF8到UTF16字符串转换

4
我想编写一个函数将UTF8字符串转换为UTF16(小端)。问题是,iconv函数似乎无法提前告知您需要存储输出字符串的字节数。
我的解决方案是先分配2*strlen(utf8)的空间,然后在循环中运行iconv函数,如果必要,使用realloc增加缓冲区的大小:
static int utf8_to_utf16le(char *utf8, char **utf16, int *utf16_len)
{
    iconv_t cd;
    char *inbuf, *outbuf;
    size_t inbytesleft, outbytesleft, nchars, utf16_buf_len;

    cd = iconv_open("UTF16LE", "UTF8");
    if (cd == (iconv_t)-1) {
        printf("!%s: iconv_open failed: %d\n", __func__, errno);
        return -1;
    }

    inbytesleft = strlen(utf8);
    if (inbytesleft == 0) {
        printf("!%s: empty string\n", __func__);
        iconv_close(cd);
        return -1;
    }
    inbuf = utf8;
    utf16_buf_len = 2 * inbytesleft;            // sufficient in many cases, i.e. if the input string is ASCII
    *utf16 = malloc(utf16_buf_len);
    if (!*utf16) {
        printf("!%s: malloc failed\n", __func__);
        iconv_close(cd);
        return -1;
    }
    outbytesleft = utf16_buf_len;
    outbuf = *utf16;

    nchars = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft);
    while (nchars == (size_t)-1 && errno == E2BIG) {
        char *ptr;
        size_t increase = 10;                   // increase length a bit
        size_t len;
        utf16_buf_len += increase;
        outbytesleft += increase;
        ptr = realloc(*utf16, utf16_buf_len);
        if (!ptr) {
            printf("!%s: realloc failed\n", __func__);
            free(*utf16);
            iconv_close(cd);
            return -1;
        }
        len = outbuf - *utf16;
        *utf16 = ptr;
        outbuf = *utf16 + len;
        nchars = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft);
    }
    if (nchars == (size_t)-1) {
        printf("!%s: iconv failed: %d\n", __func__, errno);
        free(*utf16);
        iconv_close(cd);
        return -1;
    }

    iconv_close(cd);
    *utf16_len = utf16_buf_len - outbytesleft;

    return 0;
}

这真的是最好的方法吗?重复使用realloc似乎很浪费,但是如果不知道utf8中可能包含哪些字符序列以及它们在utf16中的结果,我不知道如何为初始缓冲区大小做出比2 * strlen(utf8)更好的猜测。

2个回答

6
那是使用iconv的正确方式。 请记住,iconv旨在能够从任意字符编码重新编码为另一种任意字符编码,支持任何组合。基于此,基本上只有两种方法可以知道你需要多少输出空间: 1. 猜测。进行转换,并在必要时逐步增加猜测。 2. 进行两次转换。第一次只计算,丢弃输出。分配您计算的总空间量,然后再次转换。 第一种方法是您需要采取的方式。第二个显然的缺点是您必须做两次工作。 (顺便提一下,您可以通过使用局部变量中的刮板缓冲区作为第一遍的输出缓冲区来使用iconv的第二种方法。) 实际上没有其他办法。要么您事先知道输入中有多少字符(而不是字节),其中有多少字符位于BMP之外; 要么您不知道并且必须对其进行计数。 在这种情况下,您恰好知道输入和输出编码将事先是什么。如果在开始之前自己对输入字符串进行一些UTF-8技巧,您可以更好地猜测所需的输出缓冲区空间量。这有点像上述的第二个选项,但更加优化,因为所需的UTF-8技巧不像完整的iconv那样昂贵。 不过我建议您不要这样做。因为您仍然需要对输入字符串进行两次传递,所以您没法节省多少,编写更多的代码,并且如果技巧不够正确,会导致缓冲区的大小不足而引入漏洞。 我甚至不会描述这些技巧,因为它实际上或多或少相当于实现UTF-8解码器,虽然其核心只是一些简单的位掩码和移位情况,但与拒绝无效序列有关的细节很容易在安全方面出现错误。所以别这么做。

6
将UTF-8转换为UTF-16,数据大小最多只会增加一倍。最坏情况是ASCII(1->2字节)。UTF-8中的所有其他BMP代码点都需要2或3个字节(因此在转换为UTF-16时保持相同的大小或变小)。非BMP代码点在UTF-8或UTF-16中均为确切的4个字节。
因此,您可以消除浪费、复杂和容易出错的realloc逻辑来扩大缓冲区。
顺便说一句,请确保留有空间用于null终止符,这不会被strlen计算。

关于 strlen 的观点很好,但在我的情况下,我想要一个以空字符结尾的输入字符串,并且对于输出字符串,需要一个非终止缓冲区和长度。我没有表达清楚。 - craig65535

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