RAD Studio 10.3におけるABIの変更

この記事は David Millington による ABI Changes in RAD Studio 10.3 の抄訳です

RAD Studio 10.3では、DelphiとC++の両方に影響を与える、非常に低レベルながらも興味深い改善を行いました。それは、メソッドが呼び出されるときのメソッドパラメータの受け渡し方法の改善です。

大半のお客様にはこの変更による影響はありません。もし影響に気づくとしたら、それは 今回の変更によって不具合が発生しなくなるということです。

しかしながら、もし低レベルなアセンブリコードを書いているならば、10.3でどのような変更が行われているのか、なぜそれが必要だったのか、技術的な詳細はどうなっているのか、という点について興味があると思います。

C++ヘッダーの生成

C++BuilderがDelphiユニットを利用する場合は、Delphiコンパイラが生成した C++ヘッダーファイル(.hpp)をインクルードします。このヘッダーファイルには、Delphiユニットに含まれる型やメソッド、クラスの情報を「C++化」したものが含まれています。

たとえば、次のDelphiのメソッドは、

procedure Foo(APoint : TPoint);

C++では

void Foo(const TPoint& APoint);

となります。これにより、C++からDelphiコードを簡単に使用できるようになります。C++コンパイラから見れば、C++の型とメソッドを使用しているに過ぎません。

ABI vs API

するどい方は、すぐにこの変換の問題に気づいたかもしれません。DelphiコードではTPointパラメータを値渡ししているだけですが、C++側では参照渡しにしています。なぜでしょうか?

これは、API(Application Programming Interface:値渡しのような規約を含む開発者がコードの読み書きを行うレベルでのメソッド宣言)とABI(Application Binary Interface:レジスタやスタックにおけるパラメータの受け渡し方など内部的な実装方法)の違いです。

たとえば、Delphiは変数を値渡しします。 つまり、ユーザー(プログラミングインターフェース)としては値渡しパラメータです。メソッドが渡した変数は参照できますが、変更しても呼び出し元には影響しません。これは、実装レベルの話ではなく意味論的な視点です。APIを見れば、コンパイラがどのようにアセンブリコードを生成するかにかかわらず、特定の動作(インプットに対するアウトプット)を期待するでしょう。つまり、意味論的には「値渡し」なのです。コンパイラがAPIの実装を隠蔽しているので、メモリの場所へのポインタへのポインタへのポインタへのポインタといったような、どのような(不条理な)実装をしたとしても、それは開発者にとって値渡しとして振る舞うのです。

C++ では、constの参照渡しとして記述されます。これは明らかに「値渡し」ではありませんが、API的意味論からすれば値を変更できないことには変わりがないため、同じことが実現されています。

今回の例で用いたTPointは8バイト値です。Win32ではパラメータの受け渡しに使用できるレジスタが4バイト値なので、参照として(つまりポインタとして)渡されます。しかし、Win64ではレジスタが8バイト値なので、値全体を1つのレジスタで扱えます。値を参照するために、間接的な参照渡しではなく、単純にレジスタそのもので値を渡すことができます。より効率的な素晴らしいコード生成です。

C++ヘッダーの生成は、もともと32ビット Windows プラットフォームだけが対象だった90年代後半に設計されたものでした。そのため、APIとABIの違いは考慮されていなかったのです。このTPoint値は参照渡しであったため、コードでは値としてアクセスしているものの、生成されたC++ヘッダーは以下のようなコードとなっていました。。

void Foo(const TPoint& APoint); // & は参照、constは変更できないことを意味します。

ところが、このAPIに対する実装方法は、急に機能しなくなったのです。 これまで、TPointは参照渡しとされてきましたが、Win64ではそうではなくなったのです。

Win64 C++との互換性

このことは、まずい状況を作り出します。Win32ではC++は変数への参照によって、TPointの値を正確に見つけることができます(つまり、レジスタには値へのポインタが格納されています)。Win64でも、レジスタを値へのポインタとして扱います。APIがそのように示しているからです。しかし、実際にはレジスタにTPointの値そのものが格納されているため、ポインタだとすると正しくありません。この8バイトのメモリデータをポインタとして扱うと、メモリ上のンダムな場所を指す結果となります。つまり、コード内でアクセスするTPoint変数には、不適切なデータが含まれているのです。

RSP-16209やQC-115283など、この問題に関する多くの不具合報告がありました。以前のバージョンのリリースノートに記載も致しました。この問題は、サイズが5~8バイトであるすべての型または構造体に影響します。 4バイト以下の場合と8バイト以上の場合は、Win32とWin64の双方でレジスタやその他の動作が合致しています。

解決策

RAD Studio 10.3では、この問題を解決したいと考えていました。解決には、主に2つの選択肢がありました。

1.生成されたヘッダーファイルから不適切に変換されたABI情報を削除する

この選択肢では、C++メソッドへの変換が特定の実装を強制することを回避します。これにより、ABIが混在することを回避し、APIのみの視点が用いられるようになります。この実装はコンパイラ側で行われ、DelphiとC++の双方のコンパイラで、特定のサイズのパラメータに対して、ABIレベルで「値渡し」が行われるという共通認識が得られます。

この結果、ヘッダーのメソッド宣言はこれまでとは変わります。たとえば、現在の変換でconst TPoint&となる例では、「値渡し」であることをを反映してTPointに変わります。コンパイラレベルでは問題ありませんが、すべてのC++ユーザーは、フォームデザイナーが生成するイベントハンドラや、Delphiコードを継承したC++クラスで、既存のコードが、従来のヘッダーともはや一致しなくなっていることにすぐ気が付くでしょう。この選択肢では多くの書き換えやリファクタリングが必要となり、最新リリースにアップグレードする際の大きな障壁となるでしょう。それは、小さなメリットために多くの作業を強いることであり、私たちが採るべき選択肢でないことは明白です。

長期的な観点では、既存のコードを手作業で修正する必要がなくなれば、選択肢にあがるかもしれません。しかし、現時点ではその計画はありません。

2.ヘッダーファイルが有効になるようにWin64向けのコード生成を調整する

生成されたヘッダーファイルが常に有効になるように、システムの一貫性を保つために5から8バイトの間のパラメータの処理を調整する。これが10.3で実装した選択肢です。

10.3では、サイズが5~8バイトの間(つまり 5、6、7、8バイトのいずれか)のパラメータに着目して、いくつかの呼び出し規約についてパラメータの受け渡し方法に変更を加えました。また、iOS、Android、Linux、macOS、およびWindowsを含むすべてのプラットフォーム、およびそれらのプラットフォームでサポートされるすべてのビット幅、ならびにcdecl、pascal、fastcall、register、およびstdcall / winapiを含むすべての呼び出し規約も確認しました。(これらの多くは、それぞれのプラットフォームで仕様が定められているのと同様、Win64 ABIにも特定の仕様があります。これらは、コンパイラやプラットフォームによってしばしば相違があります。DLLを作成または使用する場合に、通常cdeclまたはstdcall呼び出しを選択するのはそのためです。より詳細に興味がある方は、Raymond Chenによる"The history of calling conventions , part 1"やAgner Fogによる"Calling conventions"をご参照ください。)

大部分の型に変更はありません。 これには、Single、Double、Extendedのような浮動小数点型や、クラス参照、ポインタ、動的配列、文字列などが含まれます。

私たちが行った微調整は、複数のプラットフォーム要件やコンパイラ間でも、とりわけ私たちが提供している複数のコンパイラ間で互換性を提供するものであると信じています。

私たちはdocwikiにこのような内部構造を公開することを考えていません。それは今後のリリースで変更される可能性があるからです。ただし、情報が必要な場合(たとえば、プロファイラやデバッガ向けの低レベルのコードに取り組んでいる場合や、スタック操作、インラインアセンブリ、naked関数などのコードがある場合)は、お気軽にお問い合わせください。

どのような影響があるのか?

  • Delphiでは、目に見える影響はありません。これは通常の開発者が掘り下げるものではありません。 ただしWin64用のアセンブリコードがある場合は、5~8バイトのパラメータの処理を確認してください。
  • C++では、いくつかの不具合が修正されています。特に、Win64でTPointパラメータを持つイベントハンドラは正しく機能します。それ以外の場合、この改善はDelphiと同様に影響はありません。
  • いずれの言語でもWin32アセンブリコード処理パラメータがあったとしても、更新の必要はありません。

要するに、今回の変更はベストな修正と言えます。多くの開発者にとってこの効果を目にする機会はないでしょうけれど、不具合は解消しており、もう発生することはありません。

Anonymous