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

时间:2012-11-08 20:41:23

标签: c string utf-8 posix iconv

我想写一个函数将UTF8字符串转换为UTF16(little-endian)。问题是,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中会产生什么,我不知道我是否可以更好地猜测初始缓冲区大小{ {1}}。

2 个答案:

答案 0 :(得分:5)

将UTF-8转换为UTF-16绝不会超过数据大小的两倍。最坏情况是ASCII(1-> 2字节)。 UTF-8中的所有其他BMP代码点占用2或3个字节(因此在转换为UTF-16时保持相同的大小或变小。非BMP代码点在UTF-8或UTF-16中恰好是4个字节。

因此,您可以消除浪费,复杂且容易出错的realloc逻辑,以扩大缓冲区。

顺便说一句,请确保为空终止留出空间,strlen不会计算空格。

答案 1 :(得分:4)

这是使用iconv的正确方法。

请记住,iconv旨在能够从任意字符编码重新编码为另一个任意字符编码。它支持任何组合。鉴于此,基本上只有两种方法可以知道输出需要多少空间:

  1. 猜猜看。进行转换,并在必要时增加猜测。
  2. 进行两次转换。第一次,只计数,丢弃输出。分配您计算的总空间量,然后再次进行转换。
  3. 首先是你做的。第二个显然有缺点,你必须做两次工作。 (顺便说一下,你可以通过在局部变量中使用暂存器缓冲区作为第一遍的输出缓冲区,以iconv的第二种方式完成它。)

    没有别的办法。要么你事先知道输入中有多少个字符(而不是字节),那么它们中有多少个/不在BMP中;或者你没有,你必须计算它们。

    在这种情况下,您碰巧知道输入和输出编码将提前是什么。如果你在开始之前在输入字符串上做一些UTF-8体操,你可以更好地猜测你需要的输出缓冲空间量。这有点像上面的第二个选项,但更优化,因为必要的UTF-8体操并不像成熟iconv那样昂贵。

    但我建议你不要这样做。你仍然要对输入字符串进行两次传递,这样你就不会节省那么多,这将是你编写更多的代码,并且它引入了一个错误的可能性,如果缓冲区可能会过小体操不太对劲。

    我甚至不打算描述体操,因为它实际上或多或少是实现了一个UTF-8解码器,虽然它的核心只是几个简单的位屏蔽和移位的情况,那里与拒绝无效序列有关的细节,这些序列容易出错,具有安全隐患。所以不要这样做。