C++ ときどき ごはん、わりとてぃーぶれいく☆

Wonder Rabbit Projectのなかのひとのブログ。主にC++。

UE4/C++: 実行時に取得したドキュメントが SJIS だった時に ICU で UTF8 にしたり UTF16 で TCHAR にするメモ

実行時にどこかのレガシー文書を HTTP で拾ってきて表示しようとしたら SJIS だった、 EUC-JP だった。あるいはどこか別の文化圏の文字コードだった。 UE4 の文字列処理系は内部表現の UTF16 または変換マクロが定義された UTF8 か ANSI じゃないと扱いが困難。そんな時に ICUUE4 のプロジェクトレベルで取り込んで UTF8 や UTF16 に変換して UE4 で扱いやすくするメモ。

手順

  1. ICU を拾ってくる
    • より具体的には ICU4C を拾ってくる
    • Windows にしかデプロイしないならバイナリーを拾ってくると楽
  2. ICU4C を UE4 プロジェクトの適当なディレクトリーにそれっぽく放り込む
    • 例えば Windows(x86_64) だけでよいのなら ThirdParty/icu63bin64, lib64, include が展開された状態等にする
    • 実際のとろこ、配置場所やディレクトリーの命名は好きにしてよい。
  3. <Project>.Build.csICU のビルド時のリンクと実行時のリンクとそのためのバイナリーのコピーを書く
    • icuuc だけリンクすれば基本的には使える
  4. 変換が必要なソースで icu::UnicodeString を使いお好みで変換する

ソース

.Build.cs

// ...
using System.IO;
// ...
  public MyProject(ReadOnlyTargetRules Target) : base(Target)
  {
    // ...
    var base_path = Path.GetDirectoryName( RulesCompiler.GetFileNameFromType( GetType() ) );
    string third_party_path = Path.Combine( base_path, "..", "..", "ThirdParty");

    string icu_path_prefix = "icu";
    string icu_version = "63";
    var icu_path = icu_path_prefix + icu_version;
    PublicIncludePaths.Add( Path.Combine( third_party_path, icu_path, "include") );
    switch ( Target.Platform )
    {
      case UnrealTargetPlatform.Win64:
        PublicLibraryPaths.Add( Path.Combine( third_party_path, icu_path, "lib64" ) );
        // ICUのライブラリー全てを使いたければ { "icudt", "icuin", "icuio", "icutu", "icuuc", "icutest" }
        var icu_filenames = new string[]{ "icuuc", "icudt" };
        string binaries_directory = Path.Combine( base_path,"..","..", "Binaries", "Win64" );
        if ( ! Directory.Exists( binaries_directory ) )
          System.IO.Directory.CreateDirectory( binaries_directory );
        foreach ( var icu_filename in icu_filenames )
        {
          string icu_lib = icu_filename + ".lib";
          string icu_dll = icu_filename + icu_version + ".dll";
          PublicAdditionalLibraries.Add( Path.Combine( third_party_path, icu_path, "lib64", icu_lib ) );
          string icu_dll_path = Path.Combine( third_party_path, icu_path, "bin64", icu_dll );
          RuntimeDependencies.Add( icu_dll_path );
          string icu_dll_destination = System.IO.Path.Combine( binaries_directory, icu_dll );
          CopyFile( icu_dll_path, icu_dll_destination );
          PublicDelayLoadDLLs.Add( icu_dll );
        }
        break;
      default:
        throw new System.Exception( System.String.Format( "Unsupported platform {0}", Target.Platform.ToString() ) );
    }
    // ...
  }
  // ...
  private void CopyFile( string source, string destination )
  {
    System.Console.WriteLine( "Copying {0} to {1}", source, destination );
    if ( System.IO.File.Exists( destination ) )
      System.IO.File.SetAttributes( destination, System.IO.File.GetAttributes( destination ) & ~System.IO.FileAttributes.ReadOnly );
    try
    { System.IO.File.Copy( source, destination, true ); }
    catch ( System.Exception e )
    { System.Console.WriteLine( "Failed to copy file: {0}", e.Message ); }
  }

↓MyActor.cpp 等の使い所で

// ...
// ↓ <Project>.Build.cs で `bEnableExceptions = true` しない場合は ICU でも例外を使わないCPP定義をする
// さしあたり unicode/unistr で SJIS -> UTF8 等を行うだけなら noexcept の定義だけ制御すればよい
#define U_NOEXCEPT 
#include <unicode/unistr.h>
// ...
  // ↓例えばHTTP で拾ってきたりした SJIS が詰まっているバッファーを受け取るなどする想定
  // ファイルオープンでもなんでも適当に必要な状況に併せて読み替える
  void Something1( const TArray< uint8 >& original_source )
  {
    // SJIS -> UTF8 とかしたい場合
    constexpr auto target_encode = "utf8";
    icu::UnicodeString us( original_source.GetData() );
    int length = us.extract( 0, us.length(), nullptr, target_encode );
    TArray< char > u8buffer;
    u8buffer.Init( 0, length + 1 );
    us.extract( 0, us.length(), (char*)u8buffer.GetData(), target_encode );
    // UTF8 になったので好きにする
    UE_LOG( LogTemp, Log, TEXT( "u8buffer=%s" ), UTF8_TO_TCHAR( u8buffer.GetData() ) )
  }
  void Something1( const TArray< uint8 >& original_source )
  {
    // SJIS -> UTF16 とかしたい場合
    constexpr auto target_encode = "utf16";
    icu::UnicodeString us( original_source.GetData() );
    int length = us.extract( 0, us.length(), nullptr, target_encode );
    TArray< TCHAR > u16buffer;
    u16buffer.Init( 0, length + 1 );
    us.extract( 0, us.length(), (char*)u16buffer.GetData(), target_encode );
    // UTF16 になったので好きにする
    UE_LOG( LogTemp, Log, TEXT( "u16buffer=%s" ), u16buffer.GetData() ) )
  }

Note: TCHARwchar_t になるので UTF16 の内部表現に一致するとは限らないとかそういう話は UE4-4.20 では問題にならないのでこのメモには特に書かない。もし UE4 ではない環境でこのメモを参考にする事があれば注意。

追記: この実装に必要最小限のリンクすべき ICU のライブラリーは何か?

実装例では icuucicudt をリンクしている。 uc の方は実行バイナリーへリンクしないと compiler は何も言わなくても linker が未定義の関数使用などでエラーを報告しビルドが完了しないのですぐにわかる。 dt の方は気付かずにリンクしないままバイナリーライブラリーも同梱しないままで実行すると icu::UnicodeString など使おうとしたソースが実行される瞬間に例えば Windows/MSVC++ 系なら delayhlp.cpp がどうちゃらなどと言ってアプリが落ちる。

アプリ -> uc -> dt と依存しているのだけど、 uc -> dt の依存解決は実行時に遅延されているので、 uc だけをリンクしバイナリーライブラリーを配置してもビルドは通っても実際には実行できないアプリになる。 ucdt へ依存している、あるいは他の何かへ依存していないかは Windows では古臭いが dependency walker を使うと把握しやすい。 GNU/Linux では ldd でわかる。

dependency walker ( Windows ) の場合

f:id:USAGI-WRP:20181024125204p:plain

ldd ( GNU/Linux; Ubuntu ) の場合

f:id:USAGI-WRP:20181024130832p:plain