Universal C Runtimeの互換性に影響する変更
Visual Studio 2015でCランタイムが再構成され、標準Cライブラリ、POSIX拡張、Microsoft固有関数の多くがUniversal C Runtime(UCRT)へ移されました。UCRTはVisual StudioだけでなくWindowsのコンポーネントでもあるため、同じ実行ファイルでも実行先にあるucrtbase.dllのバージョンによって結果が変わる場合があります。私が調査した範囲では、Microsoft LearnにVisual Studio 2015以降のUCRTの変更を時系列でまとめたページは見つかりませんでした。本ページでは、個々の関数リファレンスに記載された変更と、公開されているWindows SDKに付属するUCRTソースの世代間差分を組み合わせて整理します。
記載の根拠とバージョンの読み方
本ページでは掲載内容を判断した根拠を次のように区別します。
- 公式ドキュメント:Microsoft Learnに動作と導入バージョンが明記されている変更
- SDKソース:Windows SDK付属ソースの差分から、戻り値、出力、エラー、または副作用の変化を確認した変更
- サービス更新ソース:同じSDK系列で公開された、異なるサービス更新版のソースを比較した結果
本ページの「最初に確認したSDK」は、その変更を含むソースを最初に確認できたスナップショットです。Windows Updateでucrtbase.dllへ配信された正確な時期とは限りません。また、SDKソースの#ifdefで囲まれた処理が製品版DLLで有効かどうかも、ソースだけでは断定できません。
Microsoft LearnがOSバージョンを明記している場合だけ、そのOSを「導入バージョン」としています。それ以外は、対象環境のucrtbase.dllで再現テストしてください。
UCRTのリンク方法による違い
/MD:基本的に実行先Windowsのucrtbase.dllを使用します。Windows 10以降のシステムUCRTを更新するにはOSの更新が必要です。/MT:ビルド時のUCRTコードがアプリケーションへ静的リンクされます。動作は主にビルドに使用したツールセットとSDKに依存します。- ヘッダーで定義されるインライン関数:
/MDでもビルド時のヘッダーに依存する場合があります。
互換性問題を調査するときは、Platform Toolset、Windows SDK、/MDまたは/MT、実行先のWindowsビルド、C:\Windows\System32\ucrtbase.dllのファイルバージョンを記録してください。配置方法の詳細はUniversal CRT deploymentを参照してください。
Visual Studio 2015以前からの変更
Visual Studio 2015でUCRTへ移行したときの変更は、公式のMicrosoft C/C++ change history 2003 - 2015にまとまっています。
移行時のリンクエラー、FILEや_iob、stdio関数のインライン化などはUpgrade your code to the Universal CRTも参照してください。Visual Studio 2015以降のツールセット間のバイナリ互換性はC++ binary compatibility between Visual Studio versionsで説明されています。
公式ドキュメントで確認できる2015年以降の変更
UTF-8ロケール
| 関数 | 変更内容と互換性への影響 | 対処方法 | 導入バージョン |
|---|---|---|---|
setlocale、_wsetlocaleとロケール依存関数 | ".UTF8"または".UTF-8"をコードページとして指定できるようになりました。UTF-8ロケールでは、mbtowc、mbstowcs、_mkdir、_getcwdなど、従来ACPを使用していた関数がchar文字列をUTF-8として扱います。 | ACP前提の既存処理では意図せずUTF-8ロケールを設定しないようにします。UTF-8を選ぶ場合は外部入出力を含めてエンコーディングを統一し、1~4バイトのUTF-8、不正シーケンス、UTF-16サロゲートペアをテストします。 | Windows 10 Version 1803(10.0.17134) |
詳細はsetlocale, _wsetlocaleのUTF-8サポートを参照してください。比較したSDKソースでは、10.0.16299.0から10.0.17134.0の間に、変換、ファイルシステム、環境変数、コンソールなどへUTF-8ロケール対応が追加されていることも確認しました。
printfファミリーの浮動小数点丸め
| 関数 | 変更内容と互換性への影響 | 対処方法 | 導入バージョン |
|---|---|---|---|
printf、fprintf、sprintf、snprintf、wprintfなど | 正確に表現できる浮動小数点数の丸めが、常に上側へ丸める従来動作からIEEE 754の最近接偶数丸めへ変わりました。fesetroundで設定した丸めモードも反映されます。たとえばprintf("%.0f", 2.5)は3ではなく2になります。 | 期待値、ログ、CSV、ハッシュ、スナップショットテストを更新します。旧動作が必要な既存アプリケーションはlegacy_stdio_float_rounding.objをリンクします。 | Windows 10 Version 2004(build 19041)。新動作を選択するコードはVisual Studio 2019 Version 16.2以降でビルド |
この変更は実行先OSだけでは決まりません。printfリファレンスのImportantとCRTのリンクオプションにあるように、対応UCRTと新しいヘッダー/リンク設定の組み合わせで選択されます。
浮動小数点書式の規格上の保証、丸め方式の違い、ほかのCランタイムとの比較は浮動小数点数の書式化と丸めで解説しています。
SDKソース比較で確認した変更
次の表は、SDKソースを比較し、公開動作への影響を差分から具体的に説明できると判断した変更だけを掲載しています。単なるリファクタリング、性能改善、アーキテクチャ追加、注釈やコメントだけの変更は除外しました。
| 関数 | 変更内容と互換性への影響 | 対処方法 | 最初に確認したSDKまたは更新版 |
|---|---|---|---|
_stat、_wstatファミリー | ファイル時刻をtime_tで表現できない場合、呼び出し全体をEOVERFLOWで失敗させず、その時刻フィールドを-1にしてサイズや属性などを返すようになりました。ファイルの存在確認やサイズ取得が成功へ変わる場合があります。 | 成功時も必要な時刻フィールドが-1でないことを確認します。存在確認だけなら時刻を成功条件に含めません。 | 10.0.10586.0 |
fread、fseek | stdioバッファーを迂回する直接読み取りの前にバッファー状態をリセットするようになりました。古い状態を後続のfseekが有効と誤認し、位置がずれる問題への修正です。 | 読み取りサイズが内部バッファーをまたぐケースと、その直後のfseekをテストします。古い誤動作への依存は削除します。 | 10.0.10586.0 |
scanf、fscanf、sscanf、wscanfなど | 戻り値を「値を受け取った引数の数」として数えるよう修正され、%nと%*による代入抑止を数えなくなりました。最初の受け取り引数へ代入する前に入力エラーが起きた場合はEOFになります。 | 戻り値を変換指定子の数として扱わず、実際に必要な代入数と比較します。%n、%*、空入力を含むテストを追加します。 | 10.0.15063.0 |
printf、fprintf、sprintf、wprintfなど | 文字出力でEILSEQ以外の書き込みエラーが起きた場合、文字数ではなく-1を返すようになりました。また浮動小数点書式の内部精度上限512がなくなり、より大きい精度で指定どおりの桁数を生成するようになりました。 | 戻り値が負かどうかを必ず確認します。極端に大きい精度を外部入力から指定できないよう制限し、従来の512桁打ち切りへ依存しないようにします。 | 10.0.16299.0 |
wcrtomb、wctombとセキュア版 | UTF-8ロケール専用の変換経路が追加されました。これらのAPIは部分的なコードポイントを保持できないため、UTF-16の孤立サロゲートなどをEILSEQとして扱います。 | 非BMP文字を扱う場合はサロゲートペアを分割しません。状態を保持する変換にはc16rtombなど目的に合うAPIを使用します。 | 10.0.17763.0 |
tolower、toupper、towlower、towupper、_memicmp、_stricmp、_wcsicmpと長さ指定版 | 256未満のワイド文字でも、単純な1バイト表を使う前にUTF-16の文字分類を確認する処理が追加されました。Cロケールと非Cロケールで、一部の非ASCII文字の大小変換や大小無視比較結果が変わる可能性があります。 | 識別子やプロトコルの比較にはASCIIだけを明示して比較するか、必要なUnicode正規化・ケースフォールディングをアプリケーション側で定義します。 | 10.0.18362.0。10.0.17763系列では17763.7010のサービス更新ソースにも同系統の変更を確認 |
printfファミリーの浮動小数点書式 | 19041で導入された丸め処理について、丸め対象より前の桁が存在しない場合の境界処理と、未出力の非ゼロ桁の追跡が修正されました。%eと%fで必要な桁数も分離され、境界値の最下位桁が変わる場合があります。 | ちょうど中間となる値、非常に小さい値、%e、%f、%gを複数の精度と丸めモードでテストします。 | 10.0.20348.0。10.0.19041系列では19041.5609のサービス更新ソースにも確認 |
_dup2 | 複製先のファイル記述子を、DuplicateHandleの成功後に閉じるようになりました。複製に失敗した場合、従来は複製先も閉じられましたが、新しい実装では元の複製先が保持されます。 | 失敗時に複製先が閉じられることへ依存しません。戻り値を確認し、必要なら呼び出し側で明示的に閉じます。 | 10.0.22000.0 |
printfファミリーの浮動小数点書式 | FE_UPWARDとFE_DOWNWARDで、最初の捨てる桁だけでなく、それ以降の全桁に非ゼロがあるかを確認するようになりました。最初の桁が0でも後続に非ゼロがある値の方向丸めが変わります。 | 方向丸めを利用する数値処理では、最初の捨てる桁が0で後続に非ゼロがある値を回帰テストへ追加します。 | 10.0.22621.0 |
tmpnam、tmpnam_s | 内部の一時パス取得がGetTempPathWからGetTempPath2Wへ変わりました。利用できないOSでは従来APIへフォールバックします。一時ファイル名の生成先が変わる場合があります。 | 生成先を固定値として仮定しません。セキュリティ上、名前の取得と作成が分離されたtmpnamより、競合なくファイルを作成できるAPIを優先します。 | 10.0.22621.0 |
printf/wprintfファミリーの%s、%Sなど | UTF-8ロケールで異なる文字幅の文字列を出力する専用処理が追加され、4バイトUTF-8とUTF-16サロゲートペアを扱うようになりました。非BMP文字の出力と、不正シーケンスでのEILSEQが変わる可能性があります。 | UTF-8ロケールでASCII、2~4バイト文字、孤立サロゲート、不完全なUTF-8をテストします。書式指定子の意味がprintf系とwprintf系で異なる点にも注意します。 | 10.0.26100.0(26100.1742公開パッケージ) |
wprintfファミリーの狭文字列変換 | UTF-8入力の終端直前で残り1バイトしかない場合、変換上限を1に制限するようになりました。文字列終端を越えて2バイト読み取る可能性への修正で、不完全な末尾シーケンスのエラーやメモリアクセスに影響します。 | NUL終端を保証し、不完全なUTF-8を事前に拒否します。ガードページ直前や最小サイズのバッファーでもテストします。 | 26100.9169および28000.2705のサービス更新ソース |
_stat、_wstatファミリー | ENABLE_FAST_STATの高速経路で、シンボリックリンクまたはマウントポイントを検出した場合に従来実装へ戻る処理が追加されました。高速経路と従来経路の結果差をなくす修正と考えられます。 | リンク、ジャンクション、マウントポイントではst_mode、サイズ、時刻を実行対象のDLLで確認します。ENABLE_FAST_STATが製品版で有効かはソースだけでは断定できません。 | 10.0.28000.0(28000.1公開パッケージ) |
調査したWindows SDK
筆者は、Windows SDK archiveとWindows SDK downloadsで公開されているインストーラーからUCRTソースを抽出し、基底SDK間と同一系列のサービス更新版をファイル単位および行単位で比較しました。前節では、戻り値、出力、エラー、副作用の変化をコードから確認できた差分だけを掲載し、コメント、リファクタリング、性能改善などは除外しています。
基底SDK間の比較
| UCRTソースのバージョン | 調査した公開パッケージ | ソースファイル数 | 前行から差分があるファイル |
|---|---|---|---|
| 10.0.10240.0 | 10240 | 482 | 比較元 |
| 10.0.10586.0 | 10586 | 481 | 21 |
| 10.0.14393.0 | 14393 | 480 | 49 |
| 10.0.15063.0 | 15063 | 480 | 28 |
| 10.0.16299.0 | 16299 | 483 | 52 |
| 10.0.17134.0 | 17134 | 491 | 98 |
| 10.0.17763.0 | 17763 | 489 | 44 |
| 10.0.18362.0 | 18362 | 489 | 29 |
| 10.0.19041.0 | 19041 | 492 | 40 |
| 10.0.20348.0 | 20348 | 494 | 78 |
| 10.0.22000.0 | 22000 | 494 | 2 |
| 10.0.22621.0 | 22621.755 | 496 | 9 |
| 10.0.26100.0 | 26100.1742 | 511 | 63 |
| 10.0.28000.0 | 28000.1 | 511 | 12 |
「差分があるファイル」には、追加、削除、改行、コメント、ビルド対応、性能改善だけの変更も含みます。互換性変更の件数ではありません。
同じSDK系列のサービス更新版
SDKのサービス更新番号が変わっても、ソースのディレクトリ名は10.0.26100.0のように基底バージョンのままです。そのため、パッケージの更新番号も併記します。
| 比較 | 差分があるファイル | 主な確認結果 |
|---|---|---|
| 17763.0 → 17763.7010 | 18 | 文字分類、大小変換、大小無視比較などの修正 |
| 19041.0 → 19041.5609 | 6 | 浮動小数点書式の丸め修正 |
| 20348.0 → 20348.3330 | 0 | ソース差分なし |
| 26100.1742 → 26100.7705 | 6 | 主にARM64系の文字列比較最適化と検証処理の分離 |
| 26100.7705 → 26100.8249 | 0 | ソース差分なし |
| 26100.8249 → 26100.9169 | 1 | UTF-8ロケールでのstdio境界外読み取り修正 |
| 28000.1 → 28000.2114 | 0 | ソース差分なし |
| 28000.2114 → 28000.2705 | 1 | 26100.9169と同じstdio修正 |
差分がないことは、SDK全体やWindowsのバイナリに変更がないことを意味しません。ここでは配布パッケージに含まれるucrtソースだけを比較しています。