C#とC++との間で文字列をやり取りするのための、C++&C#における文字列知識と方法について
TL;DR
日本語などで非ASCII文字(?)を中心に用いる場合、C++17以前でwchar_t
もしくはchar16_t
を用いるのがよいと思われます(C++23未公開時点)。実装に関しては 文字列の授受(C# → C++)節と文字列の授受(C++ → C#)節を参照してください。
std::string
やstd::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種類があります。前者はchar
やchar8_t
などで、後者はwchar_t
やchar16_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::string やstd::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
においてCharSet
をCharSet.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