🌀

UnityにおけるShift_JISのややこしすぎる事情たち

2022/10/02に公開

UnityでShift_JISを扱おうとして直面した様々な問題について書いていきます。

基本

C#でShift_JISを扱うためには、基本的にSystem.Text.Encoding.GetEncoding("Shift_JIS")を使用します。

①ビルド後にエラーを吐く問題

エディタでは問題なく動作しますが、ビルドするとGetEncoding("Shift_JIS")で実行時エラーを吐きます。

NotSupportedException: Encoding 932 data could not be found. Make sure you have correct international codeset assembly installed and enabled.

どうやらUnityではビルド後にShift_JISエンコードに必要なアセンブリが含まれないらしく、これらを手動で追加してあげる必要があります。
必要なアセンブリはUnityのインストールディレクトリの中にあるI18N.dllI18N.CJK.dllになります。
https://helpdesk.unity3d.co.jp/hc/ja/articles/204694010-System-Text-Encoding-で-Shift-JIS-を使いたい

<Unityのインストールディレクトリ>/Editor/Data/MonoBleedingEdge/lib/monoフォルダを見てみます。Unityのバージョンによってフォルダ構成が異なりますが、こちらは2021.3.0f1の場合です。

それぞれのフォルダの中にいくつものDLLが入っており、I18N.dllI18N.CJK.dllを含むフォルダと含まないフォルダがあります。しかもそれぞれ内容が微妙に異なり、中にはうまく動かないものも含まれています。

http://fantom1x.blog130.fc2.com/blog-entry-364.html?sp
こちらの記事では2019.2.21f1unityフォルダの中のDLLを使用しています。

https://blog.techlab-xe.net/unity-getencoding/
こちらの記事では2019.4.12ですが、unityフォルダのDLLではうまく動かず、unityjitフォルダのDLLを使用しています。

これらのフォルダが一体で、どういう規則で命名されているのか特に情報がないので、詳しく調べてみました。

  • x.x-api実装が含まれず、APIのメタデータのみが含まれる。動作しない
  • 4.5:実装が含まれている
  • gac:実装が含まれている
  • net_4_x-(プラットフォーム名):実装が含まれている
  • unityILレベルで破壊されているため動かない(謎)
  • unityaot_(プラットフォーム名):実装が含まれている
  • unityjit_(プラットフォーム名):実装が含まれている

実装が含まれるアセンブリのうち、もしそれぞれの実装が全く同一であればどのDLLを選んでもいいことになりそうなので、各フォルダのI18N.dllに対しildasmを使ってアセンブリの内容をテキスト表現に変換し、diffをとってみました。

結果として、ILの部分に関してはどのアセンブリも同一で、ヘッダの一部にのみ値の差異がありました。また、.NETアセンブリはビルド時にタイムスタンプとランダムなGUIDが与えられるためそこにも差異がありますが、ここでは省略します。

フォルダ MD5ハッシュ FxFileVersion EnvironmentVersion
4.5 642a73a3354b662c084452abaaa0c6c5 4.6.57.0 4.0.30319.42000
gac 642a73a3354b662c084452abaaa0c6c5 4.6.57.0 4.0.30319.42000
net_4_x-linux 152a1fc51a88cc204880d32ca12c5bbb 4.6.57.0 4.0.30319.42000
net_4_x-macos 9b783197e80691076eaa41f72a53ce54 4.6.57.0 4.0.30319.42000
net_4_x-win32 72e53ebbbc4a753763c39d9e4250e47b 4.6.57.0 4.0.30319.42000
unityaot-linux 9735b11acd1e7ee3d4ce9d45176eb4b4 4.0.30319.17020 4.0.30319.17020
unityaot-macos 7fa832b1e8679714c40331a744e97f3b 4.0.30319.17020 4.0.30319.17020
unityaot-win32 78c24e1407eb7cd3abdbc3afe6ac7a08 4.0.30319.17020 4.0.30319.17020
unityjit-linux 95c89714499b09d9cc5507b51a560a54 4.6.57.0 4.0.30319.42000
unityjit-macos 642a73a3354b662c084452abaaa0c6c5 4.6.57.0 4.0.30319.42000
unityjit-win32 9b4c416bb3c0513aa783e435c459c9ec 4.6.57.0 4.0.30319.42000

いずれもランタイムのバージョン情報に関する違いのようです。これが何か動作に影響するとは考えにくいので、動くならどれを使ってもよさそうです(少なくともI18N.dllに関しては)。

私は名前になんとなく安心感のある4.5を使っていますが、普通に動いています。

②Shift_JISの実装がわりと謎

まず、Shift_JISにはいろんなバージョンがあります。

  • Shift_JIS (JIS X 0208)
    • オリジナルのShift_JIS
  • CP932 (旧)
    • MS-DOSに採用されたShift_JISに与えられた管理名
  • CP932 (IBM独自拡張)
    • CP932 (旧)にIBMが文字を追加したもの
  • CP932 (NEC独自拡張)
    • CP932 (旧)にNECが文字を追加したもの
  • CP932 = Windows-31J
    • MicrosoftがIBMとNECの独自拡張を互換性を維持しつつ取り込んで統一したもの
      • 独自拡張のうちすべてはカバーされていない
    • たぶん現在のWindowsでShift_JISと呼ばれているものはコレ
    • JavaではCP932 (旧)と区別してMS932と呼んでいる
  • Shift_JIS-2004 (JIS X 0213)
    • JIS X 0208に様々な字形が追加されたもの
    • Windows-31Jとは互換性がないが、字形自体はUnicodeでカバーされている

Unityに含まれるShift_JIS実装はMono由来のものですが、これが上記のうちどれにあたるのか調べるため、GetEncoding("Shift_JIS")で得られるEncodingを使って文字コード表を出力するプログラムを書いて検証しました。

CP932Debug.cs
CP932Debug.cs
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using UnityEditor;

public static class CP932Debug
{
    private static readonly Encoding ShiftJis = Encoding.GetEncoding("Shift_JIS");
    
    [MenuItem("PixelType/Utility/PrintCP932")]
    private static void PrintCP932()
    {
        using (var stream = new FileStream("CP932.html", FileMode.Create))
        using (var writer = new StreamWriter(stream, Encoding.UTF8))
        {
            writer.Write("<!DOCTYPE html><html><body><table>");
            writer.Write("<tr><th></th>");

            for (ushort code = 0; code <= 0xF; code++)
            {
                writer.Write("<th>");
                writer.Write($"{code:X}");
                writer.Write("</th>");
            }
            writer.Write("</tr>");

            for (ushort row = 0x0; row < 0x8; row++)
            {
                WriteRow(writer, row);
            }

            WriteEmptyRow(writer);
            
            for (ushort row = 0xA; row < 0xE; row++)
            {
                WriteRow(writer, row);
            }

            for (ushort first = 0x81; first <= 0x9F; first++)
            {

                WriteEmptyRow(writer);
                for (ushort row = 0x4; row <= 0xF; row++)
                {
                    ushort hi24bit = (ushort)((first << 4) + row);
                    WriteRow(writer, hi24bit);
                }
            }

            for (ushort first = 0xE0; first <= 0xFC; first++)
            {

                WriteEmptyRow(writer);
                for (ushort row = 0x4; row <= 0xF; row++)
                {
                    ushort hi24bit = (ushort)((first << 4) + row);
                    WriteRow(writer, hi24bit);
                }
            }
            
            writer.Write("</table></body></html>");
        }
    }

    private static void WriteEmptyRow(StreamWriter writer)
    {
        writer.Write("<tr><td>-</td></tr>");
    }

    private static void WriteRow(StreamWriter writer, ushort hi24bit)
    {
        writer.Write("<tr><td>");
        writer.Write($"{hi24bit:X3}0");
        writer.Write("</td>");
                
        for (ushort col = 0; col <= 0xF; col++)
        {
            ushort code = (ushort)((hi24bit << 4) + col);
            if (FromCode(code, ShiftJis, false, out char c))
            {
                uint cNum = c;

                writer.Write("<td>&#x");
                writer.Write($"{cNum:X}");
                writer.Write(";</td>");
            }
            else
            {
                writer.Write("<td></td>");
            }
        }
        writer.Write("</tr>");
    }
    
    
    private static bool FromCode<T>(T code, Encoding encoding, bool isLittleEndian, out char c) where T : unmanaged
    {
        Span<T> codeSpan = stackalloc T[1];
        var codeBytes = MemoryMarshal.Cast<T, byte>(codeSpan);
        codeSpan[0] = code;

        if (!isLittleEndian)
        {
            //reverse
            for (int i = 0; i < codeBytes.Length / 2; i++)
            {
                (codeBytes[i], codeBytes[^(i + 1)]) = (codeBytes[^(i + 1)], codeBytes[i]);
            }
        }

        int charCount = encoding.GetCharCount(codeBytes);

        Span<char> dest = stackalloc char[charCount];

        encoding.GetChars(codeBytes, dest);

        int nonZeroChars = 0;
        for (int i = 0; i < dest.Length; i++)
        {
            if (dest[i] != '\0')
            {
                nonZeroChars++;
            }
        }


        c = isLittleEndian ? dest[0] : dest[^1];
        if (nonZeroChars > 1) return false;
        else if (nonZeroChars > 0 && c == '\0') return false;
        return true;
    }
}

結果はこちら、同じプログラムを.NET 6で実行した結果はこちらで見られます。
.NET 6の実装はCP932に準拠していると思われ、.NET Framework 4.8でも全く同一の結果が得られました。

さて、2つのテーブルを比較してみるといろいろと面白いです。Unity (Mono) で出力したテーブルの内容が割と謎なことになっており、CP932と比較するとかなり不足があります。CP932との差異は次の通りです。

  • CP932におけるNEC選定IBM拡張文字(0xED40~0xEEFF)が無い
    • ただし0xEEEF~0xEEFCの14文字ⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹ¬¦'"はなぜか含まれている
  • CP932における外字(0xF040~0xF9FC)が無い
    • .NET 6ではUnicodeのU+E000~U+E0BBにマッピングされる
  • CP932におけるIBM拡張文字(0xFA40~0xFC4B)が無い
  • 0xA0の扱い
    • Shift_JISでは未定義で、Unityでも未定義として扱われるようだが、.NETではU+F8F0(私用領域)にマッピングされる
  • 未定義文字の扱い
    • Unityでは?(U+003F)が返るが、.NETでは(U+2523)が返る
    • 文字コードというより実装の差異

このように、Unity (Mono)のShift_JISはCP932に含まれるIBMとNECの拡張文字が結構抜けています。ただし全く無いわけではなく、NEC特殊文字(0x8740~0x879F)についてはカバーされていたりと、いろいろよくわからないことになっています。

結論としては、Unity (Mono) はShift_JISとCP932の中間くらいの実装ということになりそうです。普通に使う分には問題になることは少ないかもしれませんが、実際わりと抜けがあるという事実については今回調べてみて初めて分かったので収穫でした。

追記:Unityで正しいCP932を使う

結局Unityで.NETと同等のShift_JISを使うにはどうしたらいいの?ってところなんですが、System.Text.Encoding.CodePages.dllを使えばいけるようです。

https://www.nuget.org/packages/System.Text.Encoding.CodePages/
https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe/

以上の2つのパッケージをDownload Packageして中身のDLLをプロジェクトにぶちこみます。

そしてGetEncoding("Shift_JIS")の前に次のコードを追加します。

System.Text.Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

こうするとUnityが持っているShift_JISの実装(I18N.dllおよびI18N.CJK.dll)に優先してこちらのパッケージのShift_JIS実装を使用してくれるようになります。こちらのパッケージは.NET Core系で使われる標準のShift_JIS実装で、おそらくCP932に準拠していると思われます。

さらに追記: これでいいなら記事前半のI18N.dllまわりの苦労はまったく必要なかったのでは……ということに今気づきました。

Discussion