質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.47%
C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

MinGW

MinGW(ミン・ジー・ダブリュー)は GNUツールチェーンのWindows移植版です。 ランタイムライブラリと開発ツールで構成されています。

Q&A

解決済

2回答

584閲覧

MSYS2/MINGW64環境を使いfputwcで日本語を出力したい

dameo

総合スコア943

C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

MinGW

MinGW(ミン・ジー・ダブリュー)は GNUツールチェーンのWindows移植版です。 ランタイムライブラリと開発ツールで構成されています。

0グッド

0クリップ

投稿2023/12/16 06:24

編集2023/12/16 16:09

実現したいこと

  • MSYS2/MINGW64環境でfputwcで日本語を出力したい

前提

  • MSYS2環境のmintty上で、下記スクリプトを実行する
  • 実行して出力されるCプログラムを同じく実行してビルド・実行し、端末上にという文字を表示させたい
  • chcpは932を出力する
  • minttyの設定はデフォルト
  • C部分だけを修正する
  • MSVCRT.dllのfputwcを使用する

発生している問題・エラーメッセージ

以下のようにfputwcは何も表示出来ず、意図通りにロケールに932が設定されているが、なぜか何も表示されず、意味不明な戻り値とエラーが設定されている。

bash

1$ bash test_fputwc.sh 2Japanese_Japan.932 3test_fputwc: No space left on device 4failed! errno=28 r=65535 5stdout test finished 6 7$

該当のソースコード

bash

1cat >test_fputwc.c <<EOF 2#include <stdio.h> 3#include <wchar.h> 4#include <locale.h> 5#include <errno.h> 6void test(FILE* fp) { 7 wint_t r = fputwc(L'本', fp); 8 if (r==WEOF) { 9 perror("test_fputwc"); 10 printf("failed! errno=%d r=%d\\n", errno, r); 11 } else { 12 printf("success...0x%04x\\n", r); 13 } 14} 15int main(int argc, char* argv[]) { 16 printf("%s\\n", setlocale(LC_CTYPE, "")); 17 test(stdout); 18 printf("stdout test finished\\n"); 19 // FILE* fp = fopen("hoge.txt", "wt"); 20 // test(fp); 21 // fclose(fp); 22 // printf("file test finished\\n"); 23 return 0; 24} 25EOF 26gcc -g -Wall test_fputwc.c -o test_fputwc 27./test_fputwc 28# iconv -f cp932 hoge.txt

試したこと

コメントにした部分を解除し、標準出力ではなくファイル出力を試みた。
結果、以下の出力を得た。

bash

1$ bash test_fputwc.sh 2Japanese_Japan.932 3test_fputwc: No space left on device 4failed! errno=28 r=65535 5stdout test finished 6success...0x672c 7file test finished 89$

標準出力ではなくファイルに出力した場合は正しく出力されている。
なぜこういう動作になるのか理由が分からない。

補足情報(FW/ツールのバージョンなど)

関連パッケージバージョン
base 2022.06-1
base-devel 2022.12-2
mingw-w64-x86_64-gcc 12.2.0-10

依存ライブラリなど

bash

1$ ldd test_fputwc 2 ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ffe8fcb0000) 3 KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ffe8e940000) 4 KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ffe8d8b0000) 5 msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7ffe8fbb0000)

最後に

以下の自分の質問の調査中に見つけた疑問点です

「MINGW64で作ったC++プログラムで正しく日本語を扱いたい」
https://teratail.com/questions/la96e9z2k99cw5


後出し前提条件

  • リダイレクトした場合はロケールに従った出力(システムロケールであるCP932)になること

追加希望

  • MSVCRTを使ったVCであるVS2015をお持ちの方、同様の現象が起きるかご確認して頂けますと嬉しいです

注意事項

MSのバグを疑ってMSVCRTの中を追ってたからなのか、Windows Defenderにウィルスとして検出されました。詳細情報を見ても何も分からないので、本当にウィルスである可能性を考慮して一応ご連絡。
イメージ説明

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

Windows のストリームはバイナリモードとテキストモードの他にユニコードモードが存在するらしく、その設定がされていないストリームに対してワイド文字版の出力関数を使うとエラーになるようです。

fcntl.h をインクルードした上で

c

1_setmode(_fileno(stdout), _O_U16TEXT);

の一文を追加すればおそらく期待する動作になります。

追記

標準出力から出力する文字コードはシステムロケールにしたいという条件に対処する方法を調査・検証してみましたが私なりの結論としては MSVCRT はこのような使い方を想定していないか、単純に対応が充分ではない (バグ) ように見えます。 モードの設定などで期待する動作を出来そうな気がしません。

色々と資料を見た感じをおおざっぱに箇条書きするとこんな感じです。

  • ストリームには三種類のモードがある
    • バイナリモード
    • テキストモード
    • ユニコードモード、これはさらに三種類に分けられる
      • Unicode (UTF-16, BOM が付く)
      • UTF-16LE
      • UTF-8
  • それぞれのモードのストリームにワイド文字出力系関数で出力を試みたときの結果は
    • バイナリモード → UTF-16LE
    • テキストモード → ロケール設定に従う
      • だたし標準出力をこのモードにしたときは何故か出力時にエラーが返る
    • ユニコードモード → ユニコードモードの種類に従う
      • ただしストリームがコンソールに接続されているときは化けないような調整があるっぽい?

需要を考えれば「入出力はユニコードであって欲しいがコンソールでは化けて欲しくない」という場合が多いでしょうからユニコードモードの挙動は望ましいものです。 ただ、コンソールで化けないというのは不自然な場合分けをしているような雰囲気があるので互換性と辻褄を合わせながら需要も満たす無理のある仕組みがあるように見えます。

システムロケールの設定に従って出力して欲しいというのが目的であればストリームはテキストモードにしつつロケール設定するのが筋です。 しかしそれが何故か出力時にエラーになるというのが現時点での問題点です。 しかも返ってくるエラーも状況にそぐわない意味不明なものなので対処法を見出せません。

投稿2023/12/16 08:04

編集2023/12/16 15:05
SaitoAtsushi

総合スコア5461

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

dameo

2023/12/16 08:18 編集

以下のスクリプトで確認しました。 cat >test_fputwc.c <<EOF #include <stdio.h> #include <wchar.h> #include <locale.h> #include <errno.h> #include <fcntl.h> void test(FILE* fp) { int mode = _setmode(_fileno(stdout), _O_U16TEXT); wint_t r = fputwc(L'本', fp); _setmode(_fileno(stdout), mode); if (r==WEOF) { perror("test_fputwc"); printf("failed! errno=%d r=%d\\n", errno, r); } else { printf("success...0x%04x\\n", r); } } int main(int argc, char* argv[]) { printf("%s\\n", setlocale(LC_CTYPE, "")); test(stdout); printf("stdout test finished\\n"); //FILE* fp = fopen("hoge.txt", "wt"); //test(fp); //fclose(fp); //printf("file test finished\\n"); return 0; } EOF gcc -g -Wall test_fputwc.c -o test_fputwc ./test_fputwc #iconv -f cp932 hoge.txt [出力] $ bash test_fputwc_setmode.sh Japanese_Japan.932 本success...0x672c stdout test finished $ fopenの説明を読んだときに FILE *fp = fopen("newfile.txt", "rt+, ccs=UTF-8"); みたいな説明をしており、UTF-8をUNICODEかUTF-16LEにしたのが該当すると思っていたので、またUnicodeモードは排他的である旨記載があったため、ファイル書き込み時に怒られなかったこともあり、それではなかろうと思っておりました。ちなみにファイルのオープン時には_O_TEXTしかついていないのを確認までしてたのです。 イマイチ良く分かりませんが、現象が全てなので、これでベストアンサーとしますね。
SaitoAtsushi

2023/12/16 09:21

ファイルが通常のテキストモード出力のときにワイド文字出力を使うと設定されたロケールに従って文字コード「変換」して出力することになっているようで、ユニコードモードでなくても問題なく動作はします。 標準入出力のときに限ってはユニコードモードであることが必要ならしく、ユニコードモードに設定された標準出力は少し特殊な挙動が見て取れます。 ユニコードモードに設定された標準出力をリダイレクトでファイルに出力してみてください。 設定されているロケールに関係なくユニコード (_setmode で設定した符号化) で出力されていることがわかります。 その一方ではコンソールに表示されたときにも化けません。 ロケールに合わせて変換をかけるというだけの挙動ではないんだと思います。 私も細かいことはよくわかりませんが。
dameo

2023/12/16 10:56 編集

通知なくて気付きませんでした。 こちらでも調べてみて、まるで違う出力になることに気付きました。 欲しい挙動はロケールに従って動作することです。 ユニコードモードではロケールによらずストリームそのものがUTF-16LEに強制されるようで、リダイレクトした場合も同様に動作します。 前提に書いたことは満たしているのですが、欲しい物ではないのでベストアンサーは外しますね。 一言で言えば原因はそこではないということです。 例えば元の質問にあるコードで「本」ではなく「A」を出力するとワイド文字でも出力されるからです。 これをリダイレクトすると、1バイトになります。
dameo

2023/12/16 15:28

回答追記ありがとうございます。概ね認識は一致しているようですね。 仕様については参照して引用くらいに抑えるべきというスタンスなので、普段必要以上に記述していませんが、認識の一致を確認する目的には良いですね。Unicodeモードは多分例によって誰も使ってないMS仕様の1つだと思います(何となくPowerShell辺りを意識したワイド系への流れを強制したかった過去の仕様なのかなと思ってる)。 個人的な推測では最初から「単純に対応が充分ではない (バグ) ように見えます」なのです。 追うのがしんどかったので、並行して情報がないか募っていたわけです。 現時点ではgdbで追っかけてますが、wctomb_sを使った「本」のUnicode値をCP932の値に変換してるところまでは確認でき、その後の非公開関数の_flsbufというどうやらfputcのような挙動をする関数にCP932のバイト値ではなくUnicode値を放り込んで失敗してるように見えます(asciiが通るのも説明できる)。非公開な上に名前も意味不明なので分かりませんが、そういうレベルでバグっぽいです。
dameo

2023/12/16 17:06

Unicode値を放り込んで失敗は私の見間違いだったようです。
guest

0

自己解決

回答

現状では個人的に不可能と言わざるを得ません。

原因

setlocale()で設定されたロケールに従いって端末出力する場合、_write()が2バイト文字の1バイト目のみを出力しようとすると、errno=ENOSPCなエラーとなるから。fputwc()は2バイト文字の1バイト目を出力するために_write()を使用する。

https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/reference/write?view=msvc-140

を見る限り、_write()にそのような仕様はない。

対策

VS2015がなく、この挙動がMINGW64によるビルドの影響なのか、MSVCRT.dllの実装のせいなのか、切り分けられないので、個人的には対処のしようがない。
→(個人的には)不可能

調査詳細

調査用スクリプト

bash

1cat >test_write.c <<EOF 2#include <stdio.h> 3#include <locale.h> 4#include <io.h> 5#include <errno.h> 6void test(char* s, size_t len) { 7 int r = _write(_fileno(stdout), s, len); 8 if (r != -1) { 9 printf("r=%d\\n", r); 10 } else { 11 perror("test_write:"); 12 printf("r=%d errno=%d\\n", r, errno); 13 } 14} 15int main() { 16 setlocale(LC_CTYPE, ""); 17 printf("[_write(1, \\"\\\\x96\\", 1)]\\n"); 18 test("\\x96", 1); // 本のCP932の1バイト目だけ 19 printf("[_write(1, \\"\\\\x96\\\\x7b\\", 2)]\\n"); 20 test("\\x96\\x7b", 2); // 本のCP932の1文字分 21 return 0; 22} 23EOF 24gcc -g -Wall test_write.c -o test_write 25./test_write 26cat >test_fputwc.c <<EOF 27#include <stdio.h> 28#include <wchar.h> 29#include <locale.h> 30#include <errno.h> 31void test(FILE* fp) { 32 wint_t r = fputwc(L'本', fp); 33 if (r==WEOF) { 34 perror("test_fputwc"); 35 printf("failed! errno=%d r=%d\\n", errno, r); 36 } else { 37 printf("success...0x%04x\\n", r); 38 } 39} 40int main(int argc, char* argv[]) { 41 setlocale(LC_CTYPE, ""); 42 test(stdout); 43} 44EOF 45gcc -g -Wall test_fputwc.c -o test_fputwc 46gdb ./test_fputwc <<EOF 47r 48b _write 49r 50where 51info register 52x/16bx \$rdx 53delete 1 54c 55q 56EOF

バージョンアップなどで将来挙動が変わる可能性もあるので、ログも控えておきます。

bash

1$ bash test_write.sh 2[_write(1, "\x96", 1)] 3test_write:: No space left on device 4r=-1 errno=28 5[_write(1, "\x96\x7b", 2)] 6本r=2 7GNU gdb (GDB) 13.1 8Copyright (C) 2023 Free Software Foundation, Inc. 9License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> 10This is free software: you are free to change and redistribute it. 11There is NO WARRANTY, to the extent permitted by law. 12Type "show copying" and "show warranty" for details. 13This GDB was configured as "x86_64-w64-mingw32". 14Type "show configuration" for configuration details. 15For bug reporting instructions, please see: 16<https://www.gnu.org/software/gdb/bugs/>. 17Find the GDB manual and other documentation resources online at: 18 <http://www.gnu.org/software/gdb/documentation/>. 19 20For help, type "help". 21Type "apropos word" to search for commands related to "word"... 22Reading symbols from ./test_fputwc... 23(gdb) Starting program: C:\msys64\home\user\mingw\test_fputwc.exe 24[New Thread 16188.0x4b88] 25test_fputwc: No space left on device 26failed! errno=28 r=65535 27[Thread 16188.0x4b88 exited with code 0] 28[Inferior 1 (process 16188) exited normally] 29(gdb) Breakpoint 1 at 0x7ffe8fbcfaeb 30(gdb) Starting program: C:\msys64\home\user\mingw\test_fputwc.exe 31[New Thread 17372.0x62bc] 32 33Thread 1 hit Breakpoint 1, 0x00007ffe8fbcfaeb in msvcrt!_write () 34 from C:\WINDOWS\System32\msvcrt.dll 35(gdb) #0 0x00007ffe8fbcfaeb in msvcrt!_write () from C:\WINDOWS\System32\msvcrt.dll 36#1 0x00007ffe8fbf719e in msvcrt!_flsbuf () from C:\WINDOWS\System32\msvcrt.dll 37#2 0x00007ffe8fbfd521 in msvcrt!fputs () from C:\WINDOWS\System32\msvcrt.dll 38#3 0x00007ffe8fbfd5ed in msvcrt!fputwc () from C:\WINDOWS\System32\msvcrt.dll 39#4 0x00007ff74ccd14c4 in test (fp=0x7ffe8fc3fa30 <msvcrt!_iob+48>) at test_fputwc.c:6 40#5 0x00007ff74ccd155e in main (argc=1, argv=0xd4870) at test_fputwc.c:16 41(gdb) rax 0x40 64 42rbx 0x7ffe8fc3fa30 140731310406192 43rcx 0x1 1 44rdx 0x5ffd90 6290832 45rsi 0x1 1 46rdi 0x0 0 47rbp 0x1 0x1 48rsp 0x5ffd00 0x5ffd00 49r8 0x1 1 50r9 0x5ffdb6 6290870 51r10 0x0 0 52r11 0x5ffd40 6290752 53r12 0xd48b0 870576 54r13 0x0 0 55r14 0x5ffdb4 6290868 56r15 0x0 0 57rip 0x7ffe8fbcfaeb 0x7ffe8fbcfaeb <msvcrt!_write+27> 58eflags 0x206 [ PF IF ] 59cs 0x33 51 60ss 0x2b 43 61ds 0x2b 43 62es 0x2b 43 63fs 0x53 83 64gs 0x2b 43 65(gdb) 0x5ffd90: 0x96 0xff 0xff 0xff 0x00 0x00 0x00 0x00 660x5ffd98: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 67(gdb) (gdb) Continuing. 68test_fputwc: No space left on device 69failed! errno=28 r=65535 70[Thread 17372.0x62bc exited with code 0] 71[Inferior 1 (process 17372) exited normally] 72(gdb)

簡単な解説

まずは_write()の挙動を確認するためのCファイルtest_write.cをビルドして実行しています。実行結果の

bash

1[_write(1, "\x96", 1)] 2test_write:: No space left on device 3r=-1 errno=28

という部分から、_write()でCP932の2バイト文字を1バイト分しか書き込もうとしなかったときに、fputwc()と同じ現象が発生してることが確認できます。

あとはfputwc()から_write()が呼ばれることを実証できれば因果関係が確認できます。そのためにtest_fputwc.cを生成、ビルドして、gdbというデバッガで実行しています。このデバッガはコマンドで動くタイプなので、コマンドをhere documentで直に記述しています。

やっていることは、一度実行してDLLを読み込ませ、_write()にブレークポイントをしかけて実行し、止まったらコールスタックを表示し、引数を見るためにレジスタを表示し(第1引数がrcx, 第2引数がrdx, 第3引数がr8)、第2引数はポインタなのでメモリのダンプをしているだけです。あとはブレークポイントを解除して残りの処理を実行させて終わりです。


追記

若干調査してわかったことがあるので追記しておきます。

bash

1locales=("C" ".932" ".UTF-8" ".65001" ".20932") 2first=("\\x96" "\\x96" "\\xe6" "\\xe6" "\\xcb") 3second=("\\x7b" "\\x7b" "\\x9c" "\\x9c" "\\xdc") 4third=("" "" "\\xac" "\\xac" "") 5for i in `seq 0 4`;do 6 loc=${locales[$i]} 7 fbase="test_write_locale_${loc}" 8 cat >${fbase}.c <<EOF 9#include <stdio.h> 10#include <locale.h> 11#include <io.h> 12#include <errno.h> 13void test(char* s, size_t len) { 14 int r = _write(_fileno(stdout), s, len); 15 if (r != -1) { 16 printf("r=%d\\n", r); 17 } else { 18 perror("test_write:"); 19 printf("r=%d errno=%d\\n", r, errno); 20 } 21} 22int main() { 23 if (setlocale(LC_CTYPE, "${loc}") == NULL) { 24 fprintf(stderr, "setlocale fail!\\n"); 25 return 1; 26 } 27 printf("[_write(1, \\"\\${first[$i]}\\", 1)]\\n"); 28 test("${first[$i]}", 1); // 本の1バイト目だけ 29EOF 30 if [ "${second[$i]}" != "" ]; then 31 cat >>${fbase}.c <<EOF 32 printf("[_write(1, \\"\\${first[$i]}\\${second[$i]}\\", 2)]\\n"); 33 test("${first[$i]}${second[$i]}", 2); // 本の2バイト目まで 34EOF 35 fi 36 if [ "${third[$i]}" != "" ]; then 37 cat >>${fbase}.c <<EOF 38 printf("[_write(1, \\"\\${first[$i]}\\${second[$i]}\\${third[$i]}\\", 3)]\\n"); 39 test("${first[$i]}${second[$i]}${third[$i]}", 3); // 本の3バイト目まで 40EOF 41 fi 42 cat >>${fbase}.c <<EOF 43 return 0; 44} 45EOF 46 gcc -g -Wall ${fbase}.c -o ${fbase} 47 echo "=== ${fbase} ===" 48 ./${fbase} 49done

スクリプトを修正して他のロケールでどうなるか?UCRT64でどうなるか?を見てみました。

MINGW64での結果

bash

1=== test_write_locale_C === 2[_write(1, "\x96", 1)] 3=1 4[_write(1, "\x96\x7b", 2)] 5本r=2 6=== test_write_locale_.932 === 7[_write(1, "\x96", 1)] 8test_write:: No space left on device 9r=-1 errno=28 10[_write(1, "\x96\x7b", 2)] 11本r=2 12=== test_write_locale_.UTF-8 === 13setlocale fail! 14=== test_write_locale_.65001 === 15setlocale fail! 16=== test_write_locale_.20932 === 17[_write(1, "\xcb", 1)] 18test_write:: No space left on device 19r=-1 errno=28 20[_write(1, "\xcb\xdc", 2)] 21本r=2

UCRT64の結果

bash

1=== test_write_locale_C === 2[_write(1, "\x96", 1)] 3=1 4[_write(1, "\x96\x7b", 2)] 5本r=2 6=== test_write_locale_.932 === 7[_write(1, "\x96", 1)] 8=1 9[_write(1, "\x96\x7b", 2)] 10本r=2 11=== test_write_locale_.UTF-8 === 12[_write(1, "\xe6", 1)] 13[_write(1, "\xe6\x9c", 2)] 14[_write(1, "\xe6\x9c\xac", 3)] 15本r=3 16=== test_write_locale_.65001 === 17[_write(1, "\xe6", 1)] 18[_write(1, "\xe6\x9c", 2)] 19[_write(1, "\xe6\x9c\xac", 3)] 20本r=3 21=== test_write_locale_.20932 === 22[_write(1, "\xcb", 1)] 23?=1 24[_write(1, "\xcb\xdc", 2)] 25本r=2

わかったこと

  • "C"ロケールでは問題が発生しないこと
  • UCRT64では問題が発生しないこと
  • MINGW64(MSVCRT)ではコードページ20932(EUC-JP)でも同様の問題が発生していること
  • EUC-JPで正しく表示されていることからbreakpointで仕掛けてみたところ、_write()呼出中MultiByteToWideChar()WideCharToMultiByte()が呼ばれていた。つまりMSVCRTでも端末のコードページに合わせてエンコーディングの変換をかけている

投稿2023/12/17 02:28

編集2023/12/19 23:37
dameo

総合スコア943

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.47%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問