LoginSignup
7
8

文字列授受(C#⇄C++)のための文字(列)知識

Last updated at Posted at 2024-03-04

C#とC++との間で文字列をやり取りするのための、C++&C#における文字列知識と方法について

TL;DR

 日本語などで非ASCII文字(?)を中心に用いる場合、C++17以前でwchar_tもしくはchar16_tを用いるのがよいと思われます(C++23未公開時点)。実装に関しては 文字列の授受(C# → C++)節と文字列の授受(C++ → C#)節を参照してください。

std::stringstd::wstringは長さが変化するため、文字列授受を行う際のC++側の出口には使えません。したがって、それらを使う場合であっても出口となる関数ではwchar_tなどに変換する必要があります。

 C#の文字列型stringはデフォルトでUTF-16によりエンコードされているため、メモリ消費量も考えると両者をUTF-16で扱うと便利かもしれません。

マジで沼

【追記 : C++23について】

2023年8月ごろにこのメモを書いてから数か月がたち、投稿時点ではC++23が策定されました。UTF-8のソースコードをサポートするようになったり、UTF-8周りの変化もあったようです。

最近UTF-8を必要としてなかったのであんまり調べてはないのですが、多分今もUTF-8を使用する必要はないんじゃないかと思います。あと現時点(2024年3月現在)ではMSVCがC++23をほとんど採用してなかったりしますし。ClangとGCC使ってる方はC++23でもいいのかも…?

目次

文字列を扱うための前提知識

文字セット&文字コード[1]

 文字種の分類として、「文字セット(character set)」と「文字コード」があります。文字セットが同じ体系を持つ文字コードの集まりであるのに対して、文字コードは何らかの規格化された文字の表し方を意味します。すなわち、(文字コード)∈(文字セット)といえます。
 …だと思いますが、調べてもこの境界がわかりません。(字面からするならば文字の集合はどのレベルであっても文字セットといえるように思う。)

 文字を表現する際必要なサイズに着目したとき、文字セットとして次の分類が可能です。

分類 バイト数 説明
シングルバイト 1 ASCII, SJISの一部
ダブルバイト 2 SJIS
マルチバイト 1or2 (MSDN)
2~ (UTF-8)
定義が異なることがあるが、MSに従えばASCIIもSJISもこれ
ワイド 2 Unicode (UTF-16, 32)

 文字コードは上に記述したASCIIやShift-JIS、UTF-8などのことを言います(というよりも文字の符号化規則の集合のことを文字コードと呼ぶような気がしますが)。このうちいくつかについて見ることにします。

Shift-JISとCP932

 CP932はマイクロソフト(等?)独自のShift-JISのスーパーセットです[2]。すなわち、(Shift-JIS)⊂(CP932)です。例えば「①」はCP932のほうにしか含まれないため、多くの場合Shift-JISはCP932を示します。C++の場合、いずれも(?)charで記述可能です。

Unicode

 Unicodeは最大21bitsのコードポイントを用いて文字の振り分けを決めている規格です。UTF-8,16,32の違いはそれぞれそのコードポイントをどのように符号化するかという点にあります[3]。

  • UTF-8は1~4Bytesで文字を記録する
  • UTF-16は基本的に2Bytesで文字を記録する。4Byteとなることもある
  • UTF-32は4Bytesで文字を記録する

C++の場合、UTF-8はchar(またはchar8_t:C++20~)で記述を行い、UTF-16,32はwchar_t(またはchar16_t/char32_t)で記述することになると思います。

したがって、一般にはUTF-8またはUTF-16を使うことになります。前者はメモリを節約したい場合やbyte単位で扱う場合に有効で、後者はコードポイントをベースに扱う場合有効といえます。一方、UTF-32を選択することはほぼないといってよいでしょう。これは、ほとんどの文字が2Bytesで表しうるなか、4Bytesのデータとして処理するという欠点がかなり大きい点などが原因です。

 ラテンベースの処理を行う場合にはUTF-16は非効率なのですが、日本語を多用する都合上UTF-16(WindowsにおけるC++&C#のデフォルト)でも問題ないだろうと感じます。

ポインタ(*)と配列([])

 基本的にchar[]char*は同じように扱えますが、前者は配列を示し、後者はそれを指し示すポインターを示すという点に違いがあります。したがって、文字列のことを示したい場合はchar[]などを用いるべきでしょう。

マルチバイト文字列とワイド文字列[4]

 前述のように、文字の符号化方式には可変長のマルチバイト文字(or narrow strings)と固定長(ではない場合もある)のワイド文字の2種類があります。前者はcharchar8_tなどで、後者はwchar_tchar16_tなどです[5]。

 char[]文字列では1byteで表せた文字もwchar_t[]文字列においては2bytesで表されるなどの違いがあるため、基本的にASCII領域の文字が多いほど後者のサイズが大きくなります。一方でワイド文字列では1文字が基本的に固定長ですので、日本語などを扱いたい場合はこちらのほうが操作はしやすいようです。

C++/C#における文字(列)型

C++における文字型

 C++における文字の型は以下の5つです。いずれも英数字、非英数字グリフ、非印刷文字(non-printing)を表すことができます。

  • ナロー文字列:char, char8_t
  • ワイド文字列:wchar_t, char16_t, char32_t

charはマルチバイト文字の個々のバイトを格納できます。wchar_tは実装定義(implementation-based: WindowsとLinuxなどで実装が異なることを示す?)のワイド文字型です。Microsoftコンパイラではcharは8ビット型であり、wchar_tは16ビット(UTF-16LEでエンコードされたUnicodeを格納する)となります。

C++における文字列

C++における文字列には大きく分けて2種類あります[6]。

Cスタイルの文字列 char[]wchar_t[]など、固定長かつnullで終わるもの
標準C++文字列 std::stringstd::wstringなど、可変長
basic_stringクラスのオブジェクトにより操作されるシーケンス

ここで、標準C++文字列は様々な操作を行いやすくした、通常の型と同様に文字列を扱えるコンテナです。

C++とUTF-8/UTF-16

 C++20では、UTF-8を扱うのはほぼ無価値なように感じます。たしかにchar8_t型が追加はされてはいるのですが、文字列を処理する関数などがあまり存在しないためです。例えばmbrtoc8(char -> char8_t)関数はC++23以降(おそらく)に追加されることとなっているようです[7]。

 こういった基本的なシステムが存在しない以上、UTF-8を扱う場合はC++17標準を用いるのがなんだかんだよいのではないでしょうか。とはいえ、UTF-8を扱う場合も結構面倒くさそう&他への影響も大きそう[8]ですので、現状無難な選択肢はUTF-16の使用だと思います。

文字列とロケール

 文字列処理関数はロケールの影響を受けるようです。このため、それらの関数を使用する前にはsetlocale関数を用いてロケールの設定をした方がいいかもしれません。

 ちなみにwchar_tは「wchar_t型のひとつのオブジェクトは、実装がサポートするロケールの文字セットの任意の一文字を表現できる」ものとして定義されたそうですが、WindowsにおいてはUTF-16を使うものとしています[9]。よくわかりませんが、Windows環境下においては文字列自体にはlocaleは影響してこないということでしょうか…?

C#の文字列

 C#における代表的な文字列の型はstringです。一応C#においてもchar[]を用いることは可能ですが、おそらく一般的でないでしょう。

 また、C#のstringのエンコード方式はUTF-16です[10]。string型文字列を他のエンコード方式に変更したい場合(エンコード方式を変更したテキストデータを取得したい場合)はEncoding関数[11]を使うことになります。

文字列の授受(C# ⇄ C++)

文字列の授受(C# → C++)

 逆方向への文字列の授受と比較すると、こちらはわりと楽です。このうちC++では、メモリ開放の責を負わせないためconstにしたほうがよいようです。また、C#側においてCharSetの指定は必須ではないですが、エンコーディング方式の不一致が生じる可能性もあるため記述します。

// C++
#define DLLEXPORT extern "C" __declspec( dllexport )
DLLEXPORT bool TestStr1(const wchar_t* in_str);
// C#
[DllImport("EditorUtility.dll", CharSet = CharSet.Unicode)]
TestStr1(string textIn);

TestStr1("hello");

文字列の授受(C++ → C#)

 ようやく本題です。先に結論を述べると、次のように記述することで文字列の授受が可能でした。

// C++
DLLEXPORT void __stdcall TestStr2(wchar_t* out_str, int len)
    wstring source = L"日本語など含む文字列";
    wcscpy_s(out_str, len, source.c_str());
}
// C#
[DllImport("EditorUtility.dll", CharSet = CharSet.Unicode)]
static extern void TestStr2(StringBuilder outBuf, int len);

StringBuilder outBuf = new StringBuilder(255);
TestStr2(outBuf, outBuf.Capacity);
Debug.WriteLine(outBuf.ToString());

 ポイントは以下の3つです。このスレッド[1]が最も役に立ちました。

  • C++では文字列の型をwchar_t*またはwstringとする
  • 文字列の授受はポインタ(C++)とStringBuilder(C#)で行う
  • DllImportにおいてCharSetCharSet.Unicodeと明示する

文字列の型をwchar_t*またはwstringとする

 unicode(ワイド文字)の文字列を扱う場合はこれらを用います。一応オプションで1バイトとマルチバイトを切り替えられるTCHARも存在はしますが、特にこれにする利点が思い浮かばないためwchar_tなどを採用しました。

文字列の授受はポインタとStringBuilderで行う

 
 文字列の授受はポインタ(C++)とStringBuilder(C#)で行います。

 文字型(?)配列と文字列型(例えばchar*string)はおおよそ同じであり、長さが不足した場合に自動的にメモリを確保するか否かという点のみ異なる(と思われます)[5]。

CharSet.Unicodeを明示する("Buffer is too small"エラー)

 文字列のコピーにおいて "Buffer is too small" エラーが生じることがあります。wcscpy_sについて、第二引数で渡す長さ(size_t)をStringBuilderのバッファサイズとすることで解決しました。

 wcscpy_s関数を使用した際のこのエラー自体は、デバッグモードの際ランダムに生じることがある[2]とのことです。

参考文献

[1] charとUnicodeとワイド文字をごっちゃにしないために

[2] Shift_JISとCP932とWindows-31Jの違いを整理した

[3] Unicodeとは? その歴史と進化、開発者向け基礎知識

[4] Multibyte and Wide Characters

[5] Microsoft, char、wchar_t、char8_t、char16_t、char32_t

[6] Microsoft, basic_string Class

[7] mbrtoc8

[8] C++(Visual Studio)でUTF-8を扱うための試行錯誤のメモ

[9] C++標準化委員会、ついに文字とは何かを理解する: char8_t

[10] C# 11.0 new features: UTF-8 string literals

[11] Microsoft, Encoding クラス

[12] Return contents of a std::wstring from C++ into C#

[13] VC++ wcscpy_s randomly assert on "Buffer is too small

7
8
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8