Starterでフォームデザイナオプションが表示されないバグの修正プラグイン作った。
VM上で検証してたら原因が分かったので直しました。
ダウンロード
https://github.com/lynatan/StarterFix
「Clone or Download -> Download ZIP」でダウンロードできます。
Delphinusにも対応しているので、インストールしている方はそちらから導入した方が簡単です。
Delphinusパッケージマネージャの紹介。
パッケージマネージャとはなんぞや
Delphiは数多くのパッケージの集合で成り立っています。パッケージには製品本体に元々付属しているもの以外に企業や個人が作った追加パッケージがあり、この追加パッケージを簡単に導入できるようにするのがパッケージマネージャです。
Delphinusとはなんぞや
Delphinusはパッケージマネージャのひとつで、Embarcadero公式のGetItパッケージマネージャと違い、申請不要で誰でも自作のパッケージを公開することができます。というのも、Delphinusは自前のサーバを持たず、ファイルの管理は全てGitHubに任せています。そのGitHubの検索APIを使い、特定の条件に合致したプロジェクトをパッケージとしてリストアップし、インストールできるようになっています。そのため、GitHubでソースコードを公開すれば誰でもパッケージを公開することができるというわけです。
ちなみに対応バージョンはXE以降となっています。
Delphinusのインストール方法
Gitが統合されているバージョンの場合(XE7以降)
Delphiを起動し、「ファイル→バージョン管理リポジトリから開く」を選択、バージョン管理システムとしてGitを選び、ソース欄にDelphinusのプロジェクトページのURLを、保存先には任意のフォルダを選んでください。
OKを押すとダウンロードが開始し、それが終わると開くプロジェクトを選択する画面が出るので、リストの中からDelphiXE6フォルダ内のDelphinus.dprojを選択してOKを押します。
プロジェクトを開いたらプロジェクトマネージャの「Delphinus.bpl」上で右クリックし、「インストール」を選択してください。「ツール→Delphinus」というメニューが追加されていればインストール成功です。
Gitが統合されていないバージョンの場合(XE6以前)
Delphinusのプロジェクトページにアクセスし、緑色の「Clone or download」というボタンを押して「Download ZIP」を選択し、ソースコード一式をダウンロードします。
次に、ダウンロードしたファイルを任意のフォルダに展開し、Delphiを起動してXE-XE5はDelphiXEフォルダ内の、XE6以降はDelphiXE6フォルダ内のDelphinus.dprojを開いてください。
プロジェクトを開いたらプロジェクトマネージャの「Delphinus.bpl」上で右クリックし、「インストール」を選択してください。「ツール→Delphinus」というメニューが追加されていればインストール成功です。
Delphinusでのパッケージのインストール
「ツール→Delphinus」メニューを選択すると、Delphinus Packagemanagerが起動します。左上の緑の更新ボタンを押すとパッケージ一覧が表示されるので、好きなパッケージを選択し、インストールボタン(下向き矢印の付いたアイコン)を押すことでパッケージがインストールできます。
アクセストークンの設定
これは任意の設定項目ですが、歯車アイコンを押すことでアクセストークンが設定できます。これはDelphinusがバックエンドとしてGitHub APIを利用して検索していることから、APIの利用制限を緩和するために設定するもので、無くても利用自体は可能です。
アクセストークンはGitHubにサインインし、メニューのSettingsからPersonal access tokensを選び、Generate new tokenを押すことで生成できます。付与する権限は最小限でいいとのことなので、特にチェックは付けないまま生成してOKです。トークンの文字列が入手できたら、歯車アイコンを押して表示されたエディットに貼り付け、Testボタンを押して成功すれば登録されます。
Delphinusでのパッケージの公開
Delphinusでは誰でも自作のパッケージを公開できます。パッケージはGitHub上でパブリックリポジトリとして公開されていて、かつoriginalであるもの(forkではないもの)である必要があります。
パッケージの登録に必要な手順はPublishing your Project for Delphinusにまとめられていますが、大きく言えば「Delphinus.Info.jsonをリポジトリのルートに置く」「Delphinus.Install.jsonをリポジトリのルートに置く」「readmeに『Delphinus-Support』という文字列を加える」の3点です。
詳しい説明は省きますが、Delphinusに登録されているパッケージは全てGitHub上に公開されているので、これらの設定は各パッケージのソースコードが参考になります。
DelphiとFreePascalの最適化比較。
検証用コード
Wikipediaより拝借したコードをPascalに書き直した以下のコードを使用、それぞれのコンパイラでどの程度最適化がかかるか調べる。ともにx86ターゲットで、Delphiは10.1 BerlinのO+、FreePascalは3.0.0の-O4で検証した。
function GetValue: Integer; inline; var a, b, c: Integer; begin a := 30; b := 9 - a div 5; c := b * 4; if c > 10 then c := c - 10; Result := c * (60 div a); end; var a: Integer; begin a := GetValue; Random(a); // aが無効化されないように end;
ちなみに変数を展開していけば分かりますが、最終的に4になります。
FreePascalの場合
mov eax,$0000001e mov eax,$00000003 mov eax,$0000000c mov eax,$00000002 mov eax,$00000004
最終的に4が導き出されてはいるものの、途中の不要な定数が残ってしまっているのが残念。
Delphiの場合
mov ebx,$0000001e mov eax,ebx mov ecx,$00000005 cdq idiv ecx push eax mov eax,$00000009 pop edx sub eax,edx mov ecx,eax add ecx,ecx add ecx,ecx cmp ecx,$0a jle @@1 sub ecx,$0a @@1: mov eax,$0000003c cdq idiv ebx imul ecx
ザ・ウンコ。FreePascalの足元にも及ばないまさかの最適化ゼロ。あまりにも酷いので、最適化オンにしてもデバッグ実行しちゃうとオフになるバグでもあるのでは?と思い、Releaseビルドした上で別のデバッガで逆アセンブルかけてみましたが結果は同じでした…。ちなみに「x64ターゲットだとマシ」という話もあったんで念のためx64でも試したところ、ほぼ同じコードが生成されて膝から崩れ落ちましたw
class helperで擬似的にインスタンス変数を追加する。
はじめに
class helperは任意のクラスにメソッドや定数、クラス変数を追加する機能を持ちますが、インスタンス変数を追加することはできません。しかしインスタンスの実態が単なるポインタであり、任意のフィールドへのアクセスが「インスタンスアドレス+フィールドオフセット」の逆参照をしているだけに過ぎないと考えると、インスタンスサイズさえ増やすことができれば、増やした部分にアクセスするclass helperを書くことでインスタンス変数を追加するのと同等の効果を得ることができます。
なお、この方法は「最後の手段」であり、dcuしかないから対象のコードに手が入れられないとかそういう場合を想定しています。
インスタンスサイズはどこで決まるか
TObject.Createのようにコンストラクタを実行することで、自身が記述した処理とは別に暗黙的にインスタンスの初期化が行われます。この初期化は大きく分けて2つの処理があり、ひとつがメモリの確保、もうひとつが確保したメモリの初期化です。この暗黙的な初期化がTObject.NewInstanceで、その中で実行されるメモリの確保が_GetMem関数、それに続いて行われるインスタンスの初期化がTObject.InitInstanceにあたります。
メモリの確保をする_GetMem関数はTObject.InstanceSizeを引数として実行されます。これで得られる値はクラス毎に異なり、これを書き換えれば任意のクラスのインスタンスサイズを増やすことができそうです。しかしTObject.InstanceSizeは単なるメソッドであり、好きな数値を代入することはできません。そのためクラス毎のインスタンスサイズ値が記録されているメモリアドレスを計算し、そこを書き換える必要があります。また、その場所は実行可能なメモリ領域であり、通常は書き込みが禁止されているため、メモリ保護属性を書き換えて書き込み可能にする必要があります。
コード例
説明だけグダグダ続けても分かりづらいので、試しにTButtonにフィールドを2つ追加するコードを書いてみます。
unit ExtraButtonFields; interface uses Winapi.Windows, Vcl.StdCtrls; type TButtonHelper = class helper for TButton private const ExtraFields = SizeOf(UInt8) + SizeOf(Int64); // 追加フィールドがある場合はここに追加 ExtraFieldSize = (ExtraFields + (SizeOf(Pointer) - 1)) and not (SizeOf(Pointer) - 1); FieldOffset1 = 0; FieldOffset2 = 1; function GetValue1: UInt8; inline; function GetValue2: Int64; inline; procedure SetValue1(const Value: UInt8); inline; procedure SetValue2(const Value: Int64); inline; public property Value1: UInt8 read GetValue1 write SetValue1; property Value2: Int64 read GetValue2 write SetValue2; end; implementation { TButtonHelper } function TButtonHelper.GetValue1: UInt8; begin Result := PByte(PByte(Self) + InstanceSize - (ExtraFieldSize + hfFieldSize - FieldOffset1))^; end; function TButtonHelper.GetValue2: Int64; begin Result := PInt64(PByte(Self) + InstanceSize - (ExtraFieldSize + hfFieldSize - FieldOffset2))^; end; procedure TButtonHelper.SetValue1(const Value: UInt8); begin PByte(PByte(Self) + InstanceSize - (ExtraFieldSize + hfFieldSize - FieldOffset1))^ := Value; end; procedure TButtonHelper.SetValue2(const Value: Int64); begin PInt64(PByte(Self) + InstanceSize - (ExtraFieldSize + hfFieldSize - FieldOffset2))^ := Value; end; procedure ResizeInstance(Cls: TClass; ExtraSize: Integer); var p: PInteger; oldProtect: DWORD; begin p := PInteger(PByte(Cls) + vmtInstanceSize); VirtualProtect(p, SizeOf(Integer), PAGE_READWRITE, oldProtect); p^ := p^ + ExtraSize; VirtualProtect(p, SizeOf(Integer), oldProtect, nil); end; initialization ResizeInstance(TButton, TButton.ExtraFieldSize); end.
このコードをプロジェクトソースの一番最初でusesするとTButtonにValue1とValue2が追加されます。厳密にはTButtonを使用しているユニットより先にusesしてあれば一番じゃなくてもいいんですが、まあ一番最初に追加しておけば間違いは無いということで。
コードの解説
ResizeInstance手続きでインスタンスサイズの変更を行っています。インスタンスサイズの場所はインスタンスアドレスにvmtInstanceSize定数を足せば取れるんですが、上述のようにそのままでは書き込みができないのでVirtualProtect関数で一時的に書き込みできるように変更しています。
class helper側はメソッドが複数ありますが、肝はフィールドアドレスの計算ただひとつです。まずインスタンスアドレスにTObject.InstanceSizeを足すことで、インスタンスフィールドの終端を得ます。そこからExtraFieldSizeを引けば自身が追加した領域にアクセスできそうなものですが、実は似た仕組みをDelphi自身が使っていて、各インスタンスには末尾にHidden fieldと呼ばれる領域*1が存在します。なので、ここのサイズであるhfFieldSize定数*2も引く必要があります。これでようやく自身の追加した領域の先頭アドレスが得られたので、後は個々のフィールドのオフセットを足せば完了です。
このコードを改造してフィールドを追加する場合は、ExtraFieldsと各FieldOffset、そしてプロパティとそのアクセッサメソッドを追加すればOKです。ResizeInstance手続きは一応再利用可能にしたので、任意のクラスに適用できます。
注意点としては、インスタンスの拡張は指定したクラスのみに適用され、継承クラスには反映されないということです。例えば今回の例でいえばTButtonを継承したTButtonExというクラスがあっても、TButtonEx自体にResizeInstanceを適用しない限りサイズは拡張されません*3。
ちなみにWindows APIを使っていることからも分かるように、このコードはWindows専用です。ただ、VirtualProtectの代わりにmprotectを使えばOSXとiOS*4でも同じことができると思います。mprotectはページ境界アドレス*5しか指定できないのでちょっとした計算が必要です。Androidは全く触ってないので知らない。
余談
ちなみにフィールドアドレスの計算部分、括弧を使わずに以下のように素直に書いた方が分かりやすいと思うんですが、
PInt64(PByte(Self) + InstanceSize - ExtraFieldSize - hfFieldSize + FieldOffset2)^ := Value;
この書き方だと、ExtraFieldSizeもhfFieldSizeもFieldOffset2も定数なのにもかかわらず、最適化がかからず全部律儀にそのまま機械語に落とすというアホみたいなコード生成をしているので、仕方なく変更しました。
ちなみにこうなる。
; PByte(Self) + InstanceSize - ExtraFieldSize - hfFieldSize + FieldOffset2 mov edx,[eax] ; PByte(Self) add edx,-$34 ; mov edx,[edx] ; add edx,eax ; + InstanceSize sub edx,$0c ; - ExtraFieldSize sub edx,$04 ; - hdFieldSize inc edx ; + FieldOffset2 ; PByte(Self) + InstanceSize - (ExtraFieldSize + hfFieldSize - FieldOffset2) mov edx,[eax] ; PByte(Self) add edx,-$34 ; mov edx,[edx] ; add edx,eax ; + InstanceSize sub edx,$0f ; - (ExtraFieldSize + hfFieldSize - FieldOffset2)
1回の減算と複数回の減算ではフラグレジスタの結果が変わるので(使われてないけど)、ぎりっぎり分からなくも無いかなとさっきまでは思ってましたが、いざVC++で同じようなコードを書いてみたらあっさり最適化してくれたので、単純にDelphiコンパイラの実装が糞なだけのようです。本当にありがとうございました。
ジェネリック関数を作る。
作る(作れるとは言ってない)
現在のDelphiの仕様では、ジェネリクスを使用した処理を記述しようとした場合、クラス単位、あるいはクラスに属するメソッド単位でしか使用することはできません。つまり、ジェネリック手続きやジェネリック関数は作ることができません。
先日発表されたロードマップでは、Delphi 10.3において言語仕様の拡張が明記され、今後使えるようになる可能性は少しだけ見えてきました。しかし、例えば文字列を列挙型に変換する以下のような関数は今はまだ作ることができません。
program GenericFunction; uses Unit1; type TNumber = (One, Two, Three); var num: TNumber; begin num := StrToEnum<TNumber>('Two'); end.
作る(作れないとも言ってない)
しかし上記のコードを一切変えること無くコンパイルする方法があります。ジェネリック関数はもちろん作れないんですが、ジェネリック関数っぽい記述は実はできたりします。それを可能にするUnit1の中身は以下のようになります。
unit Unit1; interface uses System.TypInfo; type StrToEnum<T: record> = record private FValue: T; public class operator Explicit(const Value: string): StrToEnum<T>; class operator Implicit(const Value: StrToEnum<T>): T; inline; end; implementation class operator StrToEnum<T>.Explicit(const Value: string): StrToEnum<T>; var ret: Integer; begin if GetTypeKind(T) = tkEnumeration then begin ret := GetEnumValue(TypeInfo(T), Value); Move(ret, Result.FValue, SizeOf(T)); end else raise Exception.Create('Type parameter ''T'' must be a enumeration type'); end; class operator StrToEnum<T>.Implicit(const Value: StrToEnum<T>): T; begin Result := Value.FValue; end; end.
解説
蓋を開けてみればなんてことの無い、単なるキャストのオーバーロードです。文字列を一旦StrToEnum
論理上は2つのメソッドコールが発生していますが、片方にinlineが指定されているのでインライン展開されてひとつの呼び出しになり、速度的なデメリットはほぼないです。また、以前書いたようにGetTypeKindはコンパイル時に静的に解決されるため、実際に出来上がったEXEにはraise Exception.Create(...)の部分は生成されません。
余談
ちなみにこのExplicitとImplitcitを連携させる手法*1はジェネリクスじゃなくても通用するんですが、振る舞いだけを見るとC++のファンクタ(関数オブジェクト)のDelphi版と言えるかもしれません。もちろんインスタンス化できないので同等とまではとても言えませんが、class varでフィールドを用意すれば一応状態を持った関数と言えなくもないです。常に2つのメソッドを必要とするので実装はめんどくさいですが。
*1:ImplicitとImplicitでも動くけど
コンパイラが自身のバージョンをEXEに埋め込むようになっていた件。
きっかけ
ふと「最新コンパイラで小さいEXEってまだ作れるのかな?」と気になり、XE2の頃に試したコードを再コンパイルしたところ、当時3,584バイトだったEXEが4,608バイトに増えてしまっていました。で、原因を調べるためにとりあえずバイナリエディタで開いたところ、こんなデータが。
いつから?
少なくとも公式情報としては出ていない感じだったので、仕方なく少しずつバージョンを下げつつコンパイルを試したところ、XE7で搭載されたことが判明。バイナリ内の位置としてはPE形式でいうところの.rdataセクション内にあります。
お前を消す方法
さらに調べたところ、--no-compiler-signatureというUndocumentedなコンパイラオプションを発見し、こいつをDCC32に投げてやったところ見事にバージョン情報が消えました。これでようやく前と同じサイズのバイナリが生成される…と思いきや、出力されたEXEサイズを見てみると4,096バイト…。
.relocも増えてた
以前のバイナリと見比べたところ、バージョン情報以外にも.relocセクションが増えていました。.relocセクションというのはリロケーションテーブルとも呼ばれるもので、EXEやDLLが指定したベースアドレスにロードできなかった時にアドレス情報を再配置するための情報です。
この情報は以前はDLLだけが持てば良かったんですが*1、ASLRに対応した影響かデフォルトでEXEもリロケーションテーブルを持つようになってしまったみたいです。
ASLRのオンオフにかかわらず生成されるこいつに関しては今のところ消し去る方法が見つからないので、最新バージョンでの最小EXEサイズは4,096バイトが限界になってしまったみたいです。まあこのサイズであればギリギリ4KBと言えるからいいか…。
ちなみに
コンパイラのバージョンが上がるにつれてEXEのサイズが大きくなるのは「RTTIのサイズが大きいからだ」と言われることが多いですけど、実際に調べた人って見たことないですよね。ということでついでなので調べてみました。
RTTIのサイズはTRttiType.RttiDataSizeでわりと簡単に取れます。あとはTRttiContext.GetTypesで列挙して合計してやれば取れそうですが、このRttiDataSizeが指すのはTTypeInfoのサイズ(必要最小限のTTypeDataも含む)なので、これを指すPTypeInfoのサイズ、さらにそれを指すPPTypeInfoのサイズも考慮してやる必要があります。
PTypeInfoは各TTypeInfoの直前にあります。なので(Win32の場合)4 + RttiDataSizeになります。ところがRTTIは全部詰めて配置されているわけではなく、4バイト境界に合わせて配置されているので、パディング分も考慮する必要があります。また、PPTypeInfoは一括でドンと確保されているんですが、型の数だけではなく、ユニット毎に分けるためのセパレータデータもユニット数-1配置されています。そしてユニット毎にユニット名データもRTTIとしてあります。ということでこれらをまとめたコードが以下になります。厳密にやるならSystem.Rtti分を省いたりパッケージ分のちょっとしたデータを足したりする必要がありますが、今回はとりあえずざっくり。
function GetRttiDataSize: Integer; var ctx: TRttiContext; typ: TRttiType; lib: PLibModule; i: Integer; p: PByte; begin Result := 0; for typ in ctx.GetTypes do Inc(Result , (SizeOf(PTypeInfo) + typ.RttiDataSize + SizeOf(Pointer) - 1) and not (SizeOf(Pointer) - 1)); lib := LibModuleList; while lib <> nil do begin if lib^.TypeInfo <> nil then begin Inc(Result, SizeOf(Pointer) * lib^.TypeInfo.TypeCount); p := PByte(lib^.TypeInfo.UnitNames); for i:= 0 to lib^.TypeInfo.UnitCount-1 do begin Inc(Result, 1 + p^); Inc(p, 1 + p^); end; end; lib := lib^.Next; end; end;
この関数を新規作成したVCLアプリケーションで実行してやると、10.1 Berlin上では98,376バイトになりました。約100KBなので結構でかいですが、そもそもこのEXEサイズが2,196,480バイトもあることを考えると、サイズに占める割合としては割と低くも感じます。ということでおまけでした。
さいごに
最小EXEなんて実用性ゼロなので誰も興味ないと思いますが、一応Win32/64両対応したものを置いておきます。動作確認は10.1 Berlin上で行ってます。64bit版は残念ながらちょっとでかい(4,608バイト)です。
ダウンロード
*1:EXEは一番最初にロードされるので基本的に再配置は起きない
Delphiプログラマを見分ける10の質問(解説編)。
はじめに
先日公開した10の質問、いかがだったでしょうか。「この質問を作ったのは誰だあっ!」と海原雄山が怒鳴り込んできそうなくらいDelphiを扱う上では全く必要ない知識ばかりでしたが、知っていれば明日のDelphiトークのひとつとして使える程度には役立つかもしれません。
なお、基本的にDelphi 10.1 Berlin上で確認していますが、他のバージョンのコンパイラが同じ挙動を示すかは不明です。
1.「const」と「var、out」の違いを参照という観点でひとつ挙げよ。またその違いを無くすためにはどうすれば良いか説明せよ。
「var、out」は必ず参照渡しになるが、「const」は必ずしも参照渡しにはならない。Ref属性を使うことで参照渡しを強制できる。
「const」と「var、out」の違いは上述の参照渡しの件以外に「書き換えが可能か否か」というものもあり、constで渡されたものが必ず参照渡しになっていれば引数のアドレスを逆参照することで書き換えが可能になります。しかしすでに書いたとおり参照渡しにならず値渡しになる時もあるため、その場合は逆参照して書き換えても引数として渡した元の変数は変わりません。
constが値渡しになる条件は明記されていませんが、基本的にはレジスタに乗るか乗らないかということがひとつの条件になります。つまり32bit環境では4バイトまでの変数が、64bit環境では8バイトまでの変数が値渡しになります。
Ref属性はXE4で新しく導入された属性で、関数パラメータに付けることで参照渡しを強制します。例えば以下のように使用します。
function Foo(const [Ref] Value: Integer): Pointer; begin Result := @Value; end;
このRef属性はdocwikiの説明を見る限りconst用に実装されたみたいですが、constが付いていなくても動作します。例えば以下のように指定すると、あたかもvarとして渡されたかのように振る舞います。
procedure Bar([Ref] Value: Integer); begin Value := Value + 1; end;
これは、
procedure Bar(var Value: Integer); begin Value := Value + 1; end;
と等価です。
また、constに関する面白い挙動として、プロパティを渡した時のものがあります。以下のようにフィールドが指定されたプロパティと、メソッドが指定されたプロパティでは動作が異なります。
type TFoo = class private FField1: Integer; function GetField2: Integer; public property Field1: Integer read FField1; property Field2: Integer read GetField2; end;
このようなプロパティをそれぞれ上述のFoo関数に渡すと、Field2はコピーの参照が渡される(実質値渡し)のに対して、Field1の時はFField1変数の参照が渡されます。これを利用すると、RTTIやclass helperを用いることなく、手軽にreadonlyなプロパティを書き換えるハックとして使用可能です。
2.文字列や動的配列などの型は自動で初期化されるため自分でnil等を代入する必要がないが、初めての使用時にnilや空文字列で初期化されていない場合があるのはどんな時か?またその理由を説明せよ。
関数の戻り値として文字列や動的配列が含まれるrecordを使用している時。実態としては代入先変数が暗黙の引数として参照渡しされるため、代入先が使用済みだとその値が渡されてしまう。
文字列、動的配列、インターフェース、およびバリアントはコンパイラによって自動管理され、使用時には自動的に初期化されます。これは明示的に初期化しないと不定値となるローカル変数の場合も同様で、変数の中にゴミが入ったままだと参照カウンタが正常に動作しないため、必ず初期化されます。
それでは何故戻り値の型がrecordだと初期化されないかというと、戻り値の型がレジスタサイズに収まる場合はレジスタで、そうではない場合は暗黙の引数を参照渡しして返すようになっているためです。
例えば以下のような関数があったとします。
function Foo: TBar; begin Result.Text := 'ABCDEF'; ... end;
これは以下のような手続きと等価で、実際にこのように変換して呼び出されます。
procedure Foo(var Result: TBar); begin Result.Text := 'ABCDEF'; ... end;
このため、Foo関数に渡す引数がすでに使用済みだと、Foo関数には何らかの値が入った状態で「Resultが渡される」という状況が発生します。特殊なケースではありますが、自動管理される型は必ず初期化されていると思っていると、以下のような処理でバグが発生する恐れがあります。
function Foo(const Values: array of Integer): TBar; var value: Integer; begin for value in Values do Result.Text := Result.Text + value.ToString; // <= Result.Textは代入済みの可能性があり、その場合文字列の先頭にゴミが付く end;
初期化していないResult.Textをいきなり参照している時点でおかしいとツッコミを受けそうですが、上述の通り文字列は自動で初期化される型のひとつであり、これらの型は変数宣言後に何も代入しないまま参照しても文法的に違法ではありません。
3.複数の文字列変数を連結するとき、「sA := s1 + s2; sB := s3 + s4; s := sA + sB;」と「s := s1 + s2 + s3 + s4;」は足し算の数だけ見れば等価だが、後者の方が良いのは何故か。
文字列の+演算子での連結は、連結する個数がいくつであろうと1つの文につき1つの関数が呼び出されるため。
これは単純な話で、それぞれを実際の関数呼び出しに置き換えると以下のようになります。
begin // sA := s1 + s2; sB := s3 + s4; s := sA + sB; UStrCat3(sA, s1, s2); UStrCat3(sB, s3, s4); UStrCat3(s, sA, sB); // s := s1 + s2 + s3 + s4; UStrCatN(s, 4, s1, s2, s3, s4);
このように連結と代入を複数の文に分けた場合、関数呼び出しもその分増えることになるため、関数内で行われるメモリの再確保やコピーもその度に行われることを考えれば、一度に行う後者の方がより良いと考えられます。
余談として、Concat関数という文字列や動的配列の連結用関数があります。この関数をdocwikiで調べると「プラス演算子の方が Concat より高速です」と以前から書いてあるんですが、今のコンパイラでは上記コードと同じように2つの連結であればUStrCat3関数に、3つ以上の連結であればUStrCatN関数にそれぞれ置き換えられます。つまり現在では両者は等価です。
4.nilが代入されているインスタンスの(クラスメソッドではない)メソッドを呼び出そうとした場合でも読み取り違反などのエラーが発生しないのは主にどんな状況か?またその理由を説明せよ。
自身が静的メソッドであり、インスタンスのフィールドに一切触らない場合。nilが入っているSelfには一切アクセスしないため。
以前書いた記事でも触れましたが、メソッドの呼び出しは実際には手続き/関数のそれと同等であり、変換して書くと以下のようになります。
procedure TFoo.Bar(Value: Integer); begin FBaz := Value; end; // ↑これと↓これは等価 procedure Bar(Self: TFoo; Value: Integer); begin Self.FBaz := Value; end;
ここで下の手続き版を基準に考えると、Selfにnilが入っていれば代入が失敗することは容易に想像できます。クラスとは大雑把に言えばレコードのポインタ参照のような物であり、実体のないレコードに代入はできないからです。逆に考えれば、Selfに触りさえしなければエラーは発生しません。
メソッド内の処理でエラーが発生しない条件は分かりましたが、メソッドを呼び出した時点でエラーが発生する場合もあります。仮想メソッド(virtual)、あるいは動的メソッド(dynamic)を使用している場合です。
仮想メソッドの呼び出しは「仮想メソッドテーブル(VMT)」と呼ばれる暗黙のインスタンス変数を通じて行われます。VMTの中身は継承元を含めvirtualが指定されたメソッド全てのメソッドアドレスを並べた配列のようなものです。VMTはコンストラクタの呼び出し時に暗黙的に初期化されるため、nilが入っているようなインスタンスでは中身を読み出すことはできず、従って仮想メソッドの呼び出しは失敗します。
動的メソッドの呼び出しは「動的メソッドテーブル(DMT)」と呼ばれるVMTのマイナス領域にあるポインタを通じて行われます。DMTはVMTと違って継承元のメソッドは含まれず、自身が実装したメソッドのみテーブル内に存在します*1。呼び出し処理も他と違い、自身が実装していないメソッドは親クラスのDMTを探しに行く必要があるため、CallDynaInstという関数にVMTと動的メソッドインデックスを渡して代わりに呼び出してもらいます。しかしnilが入っているインスタンスの場合、渡すべきVMTも初期化されていないためDMTすら特定できず、動的メソッドの場合も呼び出しは失敗します。
以上を総合すると、virtualでもdynamicでもない静的メソッドであればエラーは発生しません。
5.関数内関数をコールバックを必要とする関数の引数として渡そうとすると「ローカル手続き/関数を手続き変数に代入しました」というエラーが発生してコンパイルできないが、これを回避するにはどうすれば良いか。またその際気を付けることは何か。
「@Func」のように関数ポインタとして渡せば通る。親関数内のローカル変数にアクセスしないようにする。
@演算子は型無しのポインタ(Pointer型)を生成します。そして型無しのポインタは全てのポインタ型に対して代入可能な互換性を持ちます。一方、コールバックとして指定されている引数の型は手続き型や関数型と呼ばれたりもしますが、大きく分けると型付きポインタのひとつです。
関数内関数をコールバックとして渡そうとすると質問文のようにエラーが発生しますが、これを@演算子を用いて単なる型無しポインタとして認識させることで、コンパイラは「コールバックに関数内関数を渡した」のではなく「コールバックにポインタを渡した」と解釈し、コンパイルを通してくれるようになります。
その場限りでしか使わないコールバック関数の実装であれば、この手法はローカル内で記述が完結できるという利点を提供しますが、一方で注意が必要な点もあります。それは親関数のローカル変数に触らないということです。
関数内関数から親関数内のローカル変数を変更することは通常の使用であれば問題ありません。問題はコンパイラが親関数内ローカル変数へのアクセスを「関数内関数が親関数から呼び出された」としてコードを生成することにあります。当然ながらコールバックとして関数内関数を渡した時、その関数内関数を呼び出す元となるのは親関数ではなくコールバックを実装した関数になります。親関数から呼び出された場合は親関数内のローカル変数を触るための情報を関数内関数が得ることができますが、それ以外の場合は情報がないため間違った情報を元にアクセスすることになり、多くの場合エラーが発生します(発生しない場合でも無効な値を読み書きすることになりバグの元になります)。そのため、関数内関数をコールバック関数として渡す場合は親関数に依存しない、自身だけで完結する処理内容にする必要があります。
ちなみに、この方法は「ポインタ型のチェック」がオフの場合(デフォルトはオフ)のみ使用できます。オフの時は関数ポインタに限らずすべての@演算子によるポインタは型無しポインタ(Pointer)になるのであらゆるポインタ型への代入互換性がありますが、オンにして@演算子で型付きポインタが生成されるようになると、当然ながら型が違って代入できないため使用できません。
6.TComponent.FOwnerをはじめ、10 SeattleではWeak属性が指定されていたフィールドが10.1 BerlinではUnsafe属性に置き換えられているが、これはどういった理由が考えられるか。
Weak属性を指定したフィールドは参照先が解放されたときにnilが代入されるようになっており、それが必要無い場合は単純に処理コスト増のデメリットを受けてしまうため。
ARC未実装の処理系ではインターフェースが、ARC実装済みの処理系ではインターフェースとクラスが参照カウントを用いて自動管理されますが、参照カウントの欠点として循環参照が発生し得ることがあります。
循環参照というのは、つまりクラスAがクラスBを、クラスBがクラスAを参照している状態のことで、この場合両者がどこからも参照されていなくても、お互いがお互いを参照し合っているため参照カウントがゼロにならず、自動解放がされなくなりメモリリークが発生します。
これを解決するため、通常の「強い参照」に対して参照カウントを増減しない「弱い参照」がWeak属性です。前述の例で言えば、クラスAがクラスBを強い参照で持ち、クラスBがクラスAを弱い参照で持っている場合、クラスAはクラスBの所有権を持った状態ですが、クラスBはクラスAを見ているだけのような状態であり、クラスAは自身の参照がなくなると自身が持っているクラスBの参照もゼロにして両者を解放します。
ではUnsafe属性というのは何なのかというと、Weak属性からある処理を除いたものになります。そのある処理というのは、強い参照がどこかで解放された場合、同じインスタンスへの弱い参照を持つ変数全てにnilを代入して回るという処理です。この処理を実現するため、Weak属性の指定された変数への代入は単なる代入処理では終わらず、自身の変数アドレスを共通の弱い参照リストに登録する処理が含まれます。また、Weak変数が解放されないままその変数を持つインスタンスの方が解放された場合、弱い参照リストを見てnilを代入されては困るので、弱い参照リストから自身を削除する処理が一緒に走るようになります。説明が下手なので文章としてみると分かりにくいかもしれませんが、TComponentの子管理と似た仕組みと言えば分かる人は分かるかもしれません。
まとめると、Unsafe属性というのは参照カウントも何もかも無視して、単なるポインタと同然に扱う処理と言えそうです。代入や解放時にいちいち登録や登録解除処理が走るWeakに比べれば単なる値の代入で済むUnsafeは速度的にメリットがあるため、これがWeakからUnsafeに置き換えられた理由だと考えられます(※実装者の意図は分からないため、あくまでも推測です)。
7.recordでインターフェース(例えばIInterface)を実装する方法を簡単に説明せよ。
インターフェースで宣言されているメソッドを手続き/関数、あるいはrecordの静的メソッドとして実装し、そのアドレスをフィールドとして持ったrecordを用意し、そのポインタをIInterfaceなどにキャストする。
これはちょっと質問が悪かったと思います。「record『も』使ってインターフェースを実装せよ」の方が正確かもしれません。
インターフェースとは、分かりやすく言えば「どういう名前でどういう引数を持ったメソッドがあるか」ということを並べた定義であり、実態はそれらの関数ポインタを並べたテーブルです。実装コードは含まないため、通常はクラスを使って実装します。というか通常はクラスでしか実装できません。
ではどうやってrecordで実装するかというと、上述の「関数ポインタを並べたテーブル」を自分で作り出す、ということになります。
type TIInterface = record QueryInterface: function(Self: Pointer; const IID: TGUID; out Obj): HResult; stdcall; _AddRef: function(Self: Pointer): Integer; stdcall; _Release: function(Self: Pointer): Integer; stdcall; end; PIInterface = ^TIInterface; PPIInterface = ^PIInterface; function NopQueryInterface(Self: Pointer; const IID: TGUID; out Obj): HResult; stdcall; begin Result := E_NOINTERFACE; end; function NopAddRef(Self: Pointer): Integer; stdcall; begin Result := -1; end; function NopRelease(Self: Pointer): Integer; stdcall; begin Result := -1; end; var rec: TIInterface; prec: PIInterface; intf: IInterface; begin // テーブルを作る @rec.QueryInterface := @NopQueryInterface; @rec._AddRef := @NopAddRef; @rec._Release := @NopRelease; prec := @rec; // インターフェースにキャストすればそのまま使用可能 intf := IInterface(@recP); intf._AddRef; intf._Release; end;
これを逆に応用すると、インターフェースからレコードにキャストすることでインターフェースのメソッドアドレスを取り出すことや、メソッドアドレス自体を書き換えることも可能になります。
var intf: IInterface; pprec: PPIInterface absolute intf; oldProtect: DWORD; begin intf := TInterfacedObject.Create; intf._AddRef; // <= TInterfacedObject._AddRefが呼ばれる VirtualProtect(@pprec^^, SizeOf(TIInterface), PAGE_EXECUTE_READWRITE, oldProtect); // <= Delphiのインターフェーステーブルは読み取り専用のコード領域にあるため書き換え属性を付与 @pprec^^._AddRef := @NopAddRef; intf._AddRef; // <= NopAddRefが呼ばれる end;
なお、今回はインターフェース→レコードへのキャストを行うため各メソッドを関数ポインタとして定義していますが、レコードでインターフェースを実装するという部分だけであれば関数ポインタは必要無く、Pointer型で十分です。さらに言えばレコードすら不要で単なるポインタの静的配列で良く、実際に同様の手法でインターフェースを実装しているSystem.Generics.Defaults内では配列が使用されています。
8.通常inlineが指定された関数・メソッドは処理内容がインライン展開可能な条件であれば呼び出しはインライン展開されるが、呼び出し方によってインライン展開される場合とされない場合が発生するのはどのような状況か述べよ。複数あればなお良い。
他ユニットの外部シンボルを使っていてそのユニットをusesしていない場合、インターフェースから呼び出す場合、whileやrepeatの条件式で使用する場合、コールバック関数として渡す場合(関数アドレスが必要になる場合)など。
関数のインライン展開を軽く説明すると、あまり大きくないサイズの関数呼び出しを関数の中身で丸ごと置き換えてしまう機能です。例えば以下のような関数Fooとその呼び出しがあるとします。
function Foo(Value: Integer): Integer; inline; begin Result := Value * 2; end; var value: Integer; begin value := Foo(10); ... end;
このFoo関数がインライン展開されると、Fooの関数内にある処理がFoo(10)という呼び出しと直接置き換えられて、
value := 10 * 2;
となります。さらに10も2も定数同士なので、定数畳み込みと呼ばれる最適化が施されて20となり、最終的には以下のように20を代入するコードが生成されます。
value := 20;
これがインライン展開です。
インライン展開される条件が整っているにもかかわらずインライン展開が行われない場合というのは、つまりインライン展開される条件が整っているかどうか関係なく必ず関数呼び出しになる場合、と言い換えた方が分かりやすいかもしれません。そしてそのような場面はわりとあります。
「関数アドレスが必要になるので展開できない」のは分かりやすい例で、上述の中で言えばインターフェースから呼び出す場合と、コールバック関数として渡す場合が当てはまります。インターフェースの方は以前pikさんもハマってましたが、質問7にあるようにインターフェースというのは関数アドレステーブルなので、関数アドレスが絶対に必要になります。ループの条件式で展開されないのは最適化が難しいからでしょうか。
こういったインライン化の条件はヘルプにまとまっているので、そちらを読んでいただけると解説する手間が省けて助かります(結構あるので…)。
なお、ループの条件式ではなくループ内でも展開されないのではないかという回答がありましたが、これは展開されます。例えば、
function Foo: Integer; inline; begin Result := 5; end; var i, value: Integer; begin value := 0; for i := 1 to 100 do value := value + Foo; end;
このような処理はFoo関数の呼び出しがインライン展開で定数(この場合は5)に置き換わります。
9.デフォルトのコンパイラ指令下において、RTTIでメソッド情報がほとんど取得できないのはどんな型か。またほとんどと書いたが、メソッドに関するどんな情報なら取得可能か。
インターフェース全般。デフォルト状態ではRTTIが生成されないので取得もできない。唯一メソッドの数だけが取得可能。
実行時型情報(RTTI)が生成されない型というだけであれば複数あるんですが、メソッドを持っていてかつRTTIが生成されない型というとインターフェースになります。元々はインターフェースだけでなくクラスやレコードもデフォルトではRTTIの生成対象外だったんですが、拡張RTTIが実装されてクラスとレコードはデフォルトでRTTIが生成されるようになりました。他にメソッドを持つ型というとクラスヘルパーとレコードヘルパーがありますが*2、これらもRTTIが生成されます(クラス扱い)。
メソッドの数は以下のようなコードで取得できます。
var p: PTypeInfo; begin p := TypeInfo(IInterface); p^.TypeData^.IntfMethods.Count; // <= メソッドの数 end;
ちなみにクラスの方はTPersistentを代表として$M+指令が指定されているとRTTIが生成されていましたが、インターフェースもこのコンパイラ指令があるとRTTIが生成されるようになります。紛らわしいんですが、$RTTI指令はクラスとレコードのみを制御するためにあるので、インターフェースでは依然として$M+が有効です。
RTTI 指令(Delphi)
http://docwiki.embarcadero.com/RADStudio/Berlin/ja/RTTI_%E6%8C%87%E4%BB%A4%EF%BC%88Delphi%EF%BC%89
10.無名メソッド型の実態はInvokeメソッドを持つインターフェースであり、通常はAnonMethod()のようにそのまま実行できるが、Invokeメソッドを明示的に呼び出さないと実行できない場合がある。どんな時か。
ジェネリッククラスやメソッドで型パラメータに無名メソッド型を指定し、その型パラメータを型として指定された引数や変数などを実行しようとした場合。
具体的には以下のようなコードになります。
type TFoo = class public procedure Bar<T: TProc>(const Proc: T); end; procedure TFoo.Bar<T>(const Proc: T); begin // Proc(); // <= コンパイルエラー Proc.Invoke; // <= Invokeの明示的呼び出しが必要 end;
型制約に無名メソッド型を指定する意味はほぼ無いので、こんなコードはまず書くことはないんですが、万が一あった場合はコード補完でInvokeが出てくれないので、知ってないと詰みます。
おわりに
以上、解説でした。こちらの勘違いなどで実際とは挙動が異なることもあるかもしれませんが、そういう場合は是非ツッコミをお願いします。
それにしても、つ、つかれた…。書き上げるのに4時間かかった…。