在Windows上使用fgets()从标准输入读取UTF-8

4

我正在尝试使用fgets()函数从stdin中读取一个UTF-8字符串。在此之前,已将控制台输入模式设置为CP_UTF8。 我还将PowerShell的控制台字体设置为Lucida Console。 最后,我通过使用printf()将德语字母Ä(UTF-8编码:0xC3,0x84)打印到控制台来验证UTF-8输出是否正常工作。这个操作是正确的,但是fgets()似乎无法从控制台读取UTF-8。下面是一个小型测试程序:

#include <stdio.h>  
#include <windows.h>

int main(int argc, char *argv[])
{
    unsigned char s[64];

    memset(s, 0, 64);

    SetConsoleOutputCP(CP_UTF8);    
    SetConsoleCP(CP_UTF8);

    printf("UTF-8 Test: %c%c\n", 0xc3, 0x84);  // print Ä

    fgets(s, 64, stdin);

    printf("Result: %d %d\n", s[0], s[1]);

    return 0;
}

运行此程序并输入“Ä”,然后按下ENTER键,它只会打印以下内容:
Result: 0 0

即,s 中没有写入任何内容。然而,当我输入"A"时,我得到了以下正确的结果:
Result: 65 10

请问如何在Windows上使fgets()函数支持UTF-8字符?

编辑

根据Barmak的解释,我已经更新了我的代码,使用了wchar_t函数代替ANSI函数。然而,它仍然不起作用。以下是我的代码:

#include <stdio.h>
#include <io.h>
#include <fcntl.h>

#include <windows.h>

int main(int argc, char *argv[])
{
    wchar_t s[64];

    memset(s, 0, 64 * sizeof(wchar_t));

    _setmode(_fileno(stdin), _O_U16TEXT);       
    fgetws(s, 64, stdin);

    wprintf(L"Result: %d\n", s[0]);

    return 0;
}   

当输入 A 时,程序打印出的结果是 Result: 3393,但我预期的结果应该是 65。当输入 Ä 时,程序打印出的结果是 Result: 0,但我预期的结果应该是 196。这是怎么回事?为什么现在甚至对 ASCII 字符也不起作用了?我的旧程序只使用 fgets(),对于像 A 这样的 ASCII 字符工作正常,只有对于非 ASCII 字符(如 Ä)失败了。但新版本甚至对 ASCII 字符也不起作用,或者说 3393A 的正确结果吗?我很困惑,请帮忙!

从我的角度来看,所有的工作都已完成。 - RbMm
0x41代表字符'A',0xc4代表字符'Ä'。 - RbMm
1
我没有完全编译您的示例,但是使用自己的代码进行了测试。我建议您首先使用ReadConsoleW进行测试-这是低级函数-必须为'A'(0x41)和'Ä'(0xc4)返回正确的值。如果在此处正常,请尝试fgetws,它在_setmode(_fileno(stdin),_O_WTEXT)的情况下内部调用ReadConsoleW(使用anode模式-使用ReadFile时会出现错误,因为Windows bug不支持非英语字符)。 - RbMm
1
你使用了静态libc库 - 而且在实现中还存在错误。他们甚至没有导入ReadConsoleW - 但是只有这个API在非英语字符集时才能得到正确的结果。一直使用ReadFile。并且你会得到ANSI字符串的结果 - "我仍然得到A的3393" - 3393 = 0x0D41 - 这是0x41 + 0x0d = 'A' + '\r',0表示Ä - 我描述了这个Windows bug。所以尝试使用动态链接而不是静态链接 - 使用msvcrt.lib中的链接 - 我使用msvcrt.dll中的导入 - 这给了我正确的结果。 - RbMm
1
双重错误 - 第一个错误在Windows本身中 - 在conhost.exe或conhostV2.dll(win10)中,我在答案中描述了它(在调用WideCharToMultiByte时缓冲区大小不正确),第二个错误在具体的libc.lib中 - 没有使用ReadConsoleW。 - RbMm
显示剩余11条评论
2个回答

4

Windows使用UTF16编码。很可能Windows的控制台不支持UTF8编码。

使用UTF16编码以及宽字符串函数(wcsxxx而不是strxxx)。然后可以使用WideCharToMultiByte将UTF16编码转换为UTF8编码。示例:

#include <stdio.h>
#include <string.h>
#include <io.h> //for _setmode
#include <fcntl.h> //for _O_U16TEXT

int main()
{
    _setmode(_fileno(stdout), _O_U16TEXT);
    _setmode(_fileno(stdin), _O_U16TEXT);
    wchar_t s[64];
    fgetws(s, 64, stdin);
    _putws(s);
    return 0;
}

请注意,在调用_setmode(_fileno(stdout), _O_U16TEXT)之后,您不能使用ANSI打印函数,必须进行重置。您可以尝试以下函数来重置文本模式。
char* mygets(int wlen)
{
    //may require fflush here, see _setmode documentation
    int save = _setmode(_fileno(stdin), _O_U16TEXT);
    wchar_t *wstr = malloc(wlen * sizeof(wchar_t));
    fgetws(wstr, wlen, stdin);

    //make UTF-8:
    int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, 0, 0, 0, 0);
    if (!len) return NULL;
    char* str = malloc(len);
    WideCharToMultiByte(CP_UTF8, 0, wstr, -1, str, len, 0, 0);
    free(wstr);

    _setmode(_fileno(stdin), save);
    return str;
}

void myputs(const char* str)
{
    //may require fflush here, see _setmode documentation
    int save = _setmode(_fileno(stdout), _O_U16TEXT);

    //make UTF-16
    int wlen = MultiByteToWideChar(CP_UTF8, 0, str, -1, 0, 0);
    if (!wlen) return;
    wchar_t* wstr = malloc(wlen * sizeof(wchar_t));
    memset(wstr, 0, wlen * 2);
    MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, wlen);

    _putws(wstr);
    _setmode(_fileno(stdout), save);
}

int main()
{
    char* utf8 = mygets(100);
    if (utf8)
    {
        myputs(utf8);
        free(utf8);
    }
    return 0;
}

我也怀疑是这样,但奇怪的是printf()与UTF-8字符串实际上可以工作(请参见我的上面的示例)。只是fgets()似乎不起作用... - Andreas
我不知道为什么 SetConsoleCP(CP_UTF8) 会表现出那种方式。无论如何,制作自己的 UTF-8 函数很容易,参见编辑。这可能对于 Ä 字符来说太麻烦了。setlocale 是一个选项吗? - Barmak Shemirani
1
不幸的是,使用 fgetws() 也不起作用。我根据您的评论更新了原始帖子,包括新的示例代码。但它仍然无法正常工作。 - Andreas
我只在英文键盘上尝试过那个。你使用的是什么语言? - Barmak Shemirani
是的,但也许问题与Windows 7和PowerShell 2.0有关?我现在已经在Windows 10(在VMware中)上测试过它,并且使用PowerShell 5.0,它在那里不会返回0。在PowerShell 5.0上输入Ä在Windows 10上的结果为3470。但我也不理解这些字符代码。我仍然得到A的3393和现在Ä的3470。在UTF-16中,A不应该是65,Ä不应该是193吗? - Andreas
显示剩余2条评论

3

所有的Windows本地字符串操作(极少例外)都是使用UNICODE(UTF-16)-因此我们必须在任何地方使用Unicode函数。使用ANSI变体-非常糟糕的做法。如果您在示例中使用Unicode函数,则所有内容都将正确工作。使用ANSI则无法正常工作,这是由于Windows错误!

我可以详细解释这一点(在Win 8.1上进行了研究):

1)在控制台服务器进程中存在2个全局变量:

UINT gInputCodePage, gOutputCodePage;

GetConsoleCP/SetConsoleCP和GetConsoleOutputCP/SetConsoleOutputCP可以用于读写控制台,需要进行转换时作为WideCharToMultiByte/MultiByteToWideChar的第一个参数使用。如果您只使用unicode函数-它们永远不会被使用。

2.a)当您在控制台中写入UNICODE文本时-它将按原样写入而不进行任何转换。在服务器端,这是由SB_DoSrvWriteConsole函数完成的。请看图片: enter image description here 2.b) 当您在控制台中写入ANSI文本时-SB_DoSrvWriteConsole也将被调用,但还有一个额外的步骤-MultiByteToWideChar(gOutputCodePage,...) -您的文本将首先被转换为UNICODE。 enter image description here 但是这里有一点。看: enter image description here 在MultiByteToWideChar调用中,cchWideChar == cbMultiByte。如果我们仅使用“英语”字符集(字符<0x80),则UNICODE和多字节字符串的长度始终相等,但是对于其他语言-通常的多字节版本使用的字符比UNICODE多,但这不是问题,只是输出缓冲区的大小超过了需要,而且一切都很好。所以您的printf通常会正常工作。只有一个笔记-如果您在源代码中硬编码多字节字符串-最快的形式将是CP_ACP形式,而使用CP_UTF8转换为UNICODE将给出不正确的结果。因此,这取决于您的源文件在磁盘上保存的格式 :)

3.a)当您使用UNICODE函数从控制台读取时-您将得到完全相同的UNICODE文本。这里没有任何问题。如果需要-您可以自己进行转换为多字节

3.b)当您使用ANSI函数从控制台读取时-服务器首先将UNICODE字符串转换为ANSI,然后将其以ANSI形式返回给您。这由函数完成。

int ConvertToOem(UINT CodePage /*=gInputCodePage*/, PCWSTR lpWideCharStr, int cchWideChar, PSTR lpMultiByteStr, int cbMultiByte)
{
    if (CodePage == g_OEMCP)
    {
        ULONG BytesInOemString;
        return 0 > RtlUnicodeToOemN(lpMultiByteStr, cbMultiByte, &BytesInOemString, lpWideCharStr, cchWideChar * sizeof(WCHAR)) ? 0 : BytesInOemString;
    }
    return WideCharToMultiByte(CodePage, 0, lpWideCharStr, cchWideChar, lpMultiByteStr, cbMultiByte, 0, 0);
}

但是让我们更仔细地看一下ConvertToOem的调用方式:enter image description here 这里再次出现了cbMultiByte == cchWideChar,但这绝对是一个错误!多字节字符串可以比UNICODE字符串(当然是字符数)更长。例如 "Ä" - 这是1个UNICODE字符和2个UTF8字符。结果WideCharToMultiByte返回0(ERROR_INSUFFICIENT_BUFFER)。


你在这里使用哪个反汇编器?我没见过任何像那样插入注释的东西。它是从Microsoft符号中获取这些内容,还是手动输入的? - Cody Gray
我使用自己的调试器。这些是来自它的截图。当然,我也使用PDB符号。绿色注释“// cbMultibytes*” - 我稍后在MSPaint中添加到截图中 :) - RbMm
你写了自己的调试器?太牛了!它不是开源的,对吧? - Cody Gray
那么你的意思是我的 fgets() 方法 应该 能够工作,但是由于 Windows 中存在一个 bug 所以不能工作? - Andreas
1
@Andreas - 是的,Windows 的 bug。在调用 WideCharToMultiByte 时,多字节字符串的缓冲区长度不够。 - RbMm
显示剩余3条评论

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