C# Advent Calendar 2017 で 「C# から C++ のインスタンスメソッドを呼び出す」という記事を書いたのですが、記事を書く際に "呼び出し規約" について確認も兼ねて調べてみたら意外といろいろある話だったのでまとめてみました。 (記事を上げた後も気になって継続で調べてました)
x86 呼び出し規約の謎
x86 の呼び出し規約はバリエーションが多く、使い分けが混乱しがちです。
Windows と Linux で調査しました。 Linux はおそらく他の Unix 系 OS でも同じ。
Windows | Linux | |
---|---|---|
C 関数 / C++ static default | cdecl | cdecl |
C++ non static default | thiscall | cdecl |
標準のAPIコール (dll の export 関数) | stdcall (可変引数で cdecl な事もある) | cdecl |
.NET CallingConvertion.Winapi の対応 | CallingConvertion.StdCall | CallingConvertion.Cdecl |
調べ直してみると、複雑なのは Windows だけで、 Linux では常時 cdecl だけのようです。 cdecl 以外の呼び出し規約は WINE など Windows と ABI 互換性が必要なケース以外では使う必要はなく、あまり深く考える必要はなさそうです。
thiscall とはなにか
thiscall は C++ の non static method (インスタンスメソッド) の呼び出し用に規定されている規約のようですが
- Linux GCC においては下記のような動作になっていた。
- non static method は static method のデフォルト (cdecl) と同じだった。
- 明示的に thiscall を指定すると thiscall らしい定義になる。 (ecx に第一引数 (non statice method では this ポインタ) を格納)
- thiscall の明示指定は C 関数 / C++ static method でも有効だった。
- Windows VC++ では下記のような動作になっていた。
- クラスメソッドのデフォルトの呼び出し規約が non static / static で変わります。
- static method は C 関数のデフォルトと同じ (cdecl) 。
- non static method は thiscall になる。
- non static method でも明示指定をすると thiscall 以外のものにできる。
- thiscall は C 関数 / C++ static method には指定できない。
- クラスメソッドのデフォルトの呼び出し規約が non static / static で変わります。
Wikipedia の thiscall の記述 では下記のようになっています。 (2017/11/26 現在)
For the GCC compiler, thiscall is almost identical to cdecl: The caller cleans the stack, and the parameters are passed in right-to-left order. The difference is the addition of the this pointer, which is pushed onto the stack last, as if it were the first parameter in the function prototype.
C++ non static method において、第一引数の前に暗黙の this ポインターが入るのは呼び出し規約と直接関係があるわけではない (どの呼び出し規約でもそうなる) のと、 thiscall を明示指定すると明らかにコンパイル結果が ecx を使ったものに変化するので、この記述は適切ではなく、 Linux GCC の C++ non static method も cdecl であると思います。
ということで thiscall の特徴。
- thiscall は原則として第一引数が ecx レジスタに設定される呼び出し規約である。
- non static method はメソッドの第一引数の前に this ポインターが入る、はどの呼び出し規約でもそうなるので関係がない。
thiscall の場合は結果として ecx に this ポインターが入るというだけ。 - 第一引数のサイズがポインター幅をこえる時は未確認。
- 可変引数にすると cdecl 相当になる (ecx は使わない) 。
- VC++ の場合、可変引数 non static method に __thiscall を付けるとエラー。未指定で cdecl 相当に。
- Linux GCC においてスタック管理の責任が Caller (cdecl 相当) か Callee (stdcall 相当) かは結局よくわかりませんでした (すいません) 。
- 基本的には stdcall 相当と思います。 VC++ は stdcall 相当。
- テストコードだと C++ non static method の thiscall では stdcall 相当になってた。 C 言語関数は・・・なんだこれ?
- non static method はメソッドの第一引数の前に this ポインターが入る、はどの呼び出し規約でもそうなるので関係がない。
- VC++ の場合は non static method でのみ使用できる。
やはり thiscall も互換性用途で Linux GCC においては特に使う必要もない規約のように思います。 thiscall が C 関数 / static method でも使えるのは面白いと思いましたが (ただこれ正しく動くの?) 。
x86 以外での呼び出し規約
複雑なのは Windows x86 だけで、多くは主要な一つの呼び出し規約になっているので面倒な事を考える必要はありません。 1 ソースで複数の CPU 、プラットフォーム向けビルドをする際はちょっと注意。
.NET の P/Invoke での指針
通常は下記がお勧め。
- DllImportAttribute, UnmanagedFunctionPointerAttribute で CallingConvertion の設定はしない。 (デフォルトは CallingConvertion.Winapi)
- ネイティブ側の実装は Windows では stdcall を指定する。他は cdecl にする。
調査環境
- Windows 10 (FCU) + Visual Studio 2017
- Windows 10 (FCU) + WSL + Ubuntu 16.04.3 LTS + gcc 5.4.0
Linux の方は x86 での動作確認はしていません。 -S でアセンブラコードを出力してそちらで確認。
参考情報
Wikipedia:
コンパイラ毎の呼び出し規約の説明 (公式) :
- VC++ : Calling Conventions
- GCC: x86 Function Attributes
- Clang: Attributes in Clang
こちらも詳しいですが詳細は未確認。