UnityにおけるShift_JISのややこしすぎる事情たち
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.dll
とI18N.CJK.dll
になります。
<Unityのインストールディレクトリ>/Editor/Data/MonoBleedingEdge/lib/mono
フォルダを見てみます。Unityのバージョンによってフォルダ構成が異なりますが、こちらは2021.3.0f1の場合です。
それぞれのフォルダの中にいくつものDLLが入っており、I18N.dll
とI18N.CJK.dll
を含むフォルダと含まないフォルダがあります。しかもそれぞれ内容が微妙に異なり、中にはうまく動かないものも含まれています。
unity
フォルダの中のDLLを使用しています。
unity
フォルダのDLLではうまく動かず、unityjit
フォルダのDLLを使用しています。
これらのフォルダが一体何で、どういう規則で命名されているのか特に情報がないので、詳しく調べてみました。
-
x.x-api
:実装が含まれず、APIのメタデータのみが含まれる。動作しない -
4.5
:実装が含まれている -
gac
:実装が含まれている -
net_4_x-(プラットフォーム名)
:実装が含まれている -
unity
:ILレベルで破壊されているため動かない(謎) -
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と呼んでいる
- MicrosoftがIBMとNECの独自拡張を互換性を維持しつつ取り込んで統一したもの
-
Shift_JIS-2004 (JIS X 0213)
- JIS X 0208に様々な字形が追加されたもの
- Windows-31Jとは互換性がないが、字形自体はUnicodeでカバーされている
Unityに含まれるShift_JIS実装はMono由来のものですが、これが上記のうちどれにあたるのか調べるため、GetEncoding("Shift_JIS")
で得られるEncodingを使って文字コード表を出力するプログラムを書いて検証しました。
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文字
ⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹ¬¦'"
はなぜか含まれている
- ただし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では
このように、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を使えばいけるようです。
以上の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