C++/CLIでstd::stringとSystem::string^を相互に変換する

タイトルの通り C++/CLI で C++ の文字列型の std::string と C# の文字列型の System::String^ を相互に変換する方法の紹介です。

確認環境

確認環境は以下の通りです。

  • VisualStudio 2019
  • C# 8.0 / VC++2019
  • Windows10

実装方法

先ず、以下のヘッダーをincludeします。

#include <msclr/marshal_cppstd.h>

C# → C++への文字列変換(System::String^ → std::string)

includeしたヘッダーにある msclr::interop::marshal_as 関数を使って変換します。

System::String^ cs_string = gcnew System::String("ああああ");
std::string cs_string_2_cpp_string = msclr::interop::marshal_as<std::string>(cs_string);

std::cout << cs_string_2_cpp_string << std::endl;

文字列のリテラルはプレフィックスに "L" や "u8" を付けても正常に処理してくれます。 マルチバイト文字をstringで受けたためバッファの中身は以下のように読めない状態になりますがこれで想定通りです。

char buff[] = { 0xe3, 0x81, 0x82, 0xe3, 0x81, 0x82, 0xe3, 0x81, 0x82, 0xe3, 0x81, 0x82 } // utf8::e38182 = 'あ'

また、テンプレートの引数を std::wstring に変更しても問題なく処理が完了します。

// wstringに変換
std::wstring cs_string_2_cpp_string_2 = msclr::interop::marshal_as<std::wstring>(cs_string);

余談ですが、この時std::wstringのバッファの内容は以下のようになります。

wchar_t buff[] = { 0x3044, 0x3044, 0x3044, 0x3044 }

これ、処理上どっち正しいとか存在しないのでないので、その時の仕様に従ってください。

C++ → C#への変換(std::string → System::String^)

こちらも上述の関数を呼び出せばいいだけなのですが、パターンがいくつかあります。

いちばん簡単なのが以下パターンです。std::stringにconst char*(のように)設定されているパターン

std::string cpp_string = "ああああ";
System::String^ cpp_string_2_cs_string = msclr::interop::marshal_as<System::String^>(cpp_string);

文字化けする場合

C++ 側の std::string に設定されている文字のエンコードが UTF-8 の場合上記の方法では C# 側の System::String^ が文字化けして読み取れなくなってしまいます。この場合、UTF-8 (C++) ⇔ Unicode (C#) な変換を行う必要があります。

C++(UTF-8 な std::string) → C#(System::String^)

以下のように文字化けになってしまう場合の対応方法です。

std::string cpp_string = u8"ああああ";
System::String^ cpp_string_2_cs_string = msclr::interop::marshal_as<System::String^>(cpp_string);
// 文字化け発生 = "縺ゅ≠縺ゅ≠"

以下のように処理を書き直します。

// UTF-8な文字列データ
std::string cpp_string = u8"ああああ";

// 型の変換
array<unsigned char>^ c_array = gcnew array<unsigned char>(cpp_string.length());
for (int i = 0; i < cpp_string.length(); i++)
{
    c_array[i] = cpp_string[i];
}

// .NETのライブラリでバイト配列
System::Text::Encoding^ u8enc = System::Text::Encoding::UTF8;
System::String^ u8_array = u8enc->GetString(c_array);

以下処理のポイントです。

  • utf-8のバイト配列を取得する
  • 取得したバイト配列をC#のEncoding.UTF8のGetStringで処理する

C#(System::String^) → C++(UTF-8 な std::string)

C# から C++ にUTF-8な文字列を渡すときの処理方法です。

static std::string Cli2Native(System::String^ src)
{
    // C# の文字列をCLIのバイト配列に変換
    auto utf8Array = Encoding::UTF8->GetBytes(src);

    // CLIのバイト配列をC++の配列に変換
    unsigned char* nativeArray = new unsigned char[utf8Array->Length];
    for (int i = 0; i < utf8Array->Length; i++)
    {
        nativeArray[i] = utf8Array[i];
    }

    // C++ の文字列型にデータを設定
    string nativeString(reinterpret_cast<char const*>(nativeArray), utf8Array->Length);
    for (int i = 0; i < utf8Array->Length; i++)
    {
        nativeArray[i] = '\0';
    }

    // 後片付け
    delete[] nativeArray;
    return nativeString;
}

こっちは非常に使用する局面が少ないと思いますがこれで変換が可能です。最近はC++の文字エンコードが UTF-8 なケースが増えてきているので相互運用時に稀に必要になるかと思います。

最後に

というか文字列に関係ある型がいろいろあって中に設定されている文字のエンコードと両方を考慮しないといけないのは結構面倒ですね、、、エンコードごとに処理のバリエーションがたくさんあるので、普通の開発であれば開発規約で文字列の取り扱いをある程度決定できますが、どの局面でも利用できるような汎用的な操作を一括で提供はなかなか難しいかと思います。