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の質問。
16/07/20追記
真っ当な設計をしてたら出会わない挙動が多く、ネタとしてわざと難しい質問にしてるので、間違ったら恥ずかしいなんて躊躇せず、ぜひとも勢いよく突っ込んで爆発四散してください!
はじめに
ものすごーく前に「○○プログラマを見分ける10の質問」みたいなのが流行った時に質問を数個考えてそのまま放置してたメモを発掘したんですが、せっかくなので残りを適当に考えて10個用意してみたので我こそはという方は挑戦してみてください><
なお、質問のほとんどがネタ要素であり、知らなくても問題ない、僕の勘違いが多分に含まれる、答えがひとつじゃない、等々あると思いますが、あくまでもネタであるということを念頭にお付き合いいただき、ついでに勘違い部分に関しては正しいツッコミを入れてもらえると嬉しいです。
10の質問
- 「const」と「var、out」の違いを参照という観点でひとつ挙げよ。またその違いを無くすためにはどうすれば良いか説明せよ。
- 文字列や動的配列などの型は自動で初期化されるため自分でnil等を代入する必要がないが、初めての使用時にnilや空文字列で初期化されていない場合があるのはどんな時か?またその理由を説明せよ。
- 複数の文字列変数を連結するとき、「sA := s1 + s2; sB := s3 + s4; s := sA + sB;」と「s := s1 + s2 + s3 + s4;」は足し算の数だけ見れば等価だが、後者の方が良いのは何故か。
- nilが代入されているインスタンスの(クラスメソッドではない)メソッドを呼び出そうとした場合でも読み取り違反などのエラーが発生しないのは主にどんな状況か?またその理由を説明せよ。
- 関数内関数をコールバックを必要とする関数の引数として渡そうとすると「ローカル手続き/関数を手続き変数に代入しました」というエラーが発生してコンパイルできないが、これを回避するにはどうすれば良いか。またその際気を付けることは何か。
- TComponent.FOwnerをはじめ、10 SeattleではWeak属性が指定されていたフィールドが10.1 BerlinではUnsafe属性に置き換えられているが、これはどういった理由が考えられるか。
- recordでインターフェース(例えばIInterface)を実装する方法を簡単に説明せよ。
- 通常inlineが指定された関数・メソッドは処理内容がインライン展開可能な条件であれば呼び出しはインライン展開されるが、呼び出し方によってインライン展開される場合とされない場合が発生するのはどのような状況か述べよ。複数あればなお良い。
- デフォルトのコンパイラ指令下において、RTTIでメソッド情報がほとんど取得できないのはどんな型か。またほとんどと書いたが、メソッドに関するどんな情報なら取得可能か。
- 無名メソッド型の実態はInvokeメソッドを持つインターフェースであり、通常はAnonMethod()のようにそのまま実行できるが、Invokeメソッドを明示的に呼び出さないと実行できない場合がある。どんな時か。
答えは近いうちに載せる予定。
TPdfDocument クラス。
これは何?
前回の成果を簡単なクラスにまとめて、Delphi的にCreateしてLoadFromFileで読み込み的なやつです。読み込んでExportAsImageで画像化するシンプルなクラスになってます。
使い方
デフォルト設定で使う場合はめちゃくちゃシンプルです。
uses ..., PdfDoc; // <-今回作成したユニット var pdf: TPdfDocument; begin pdf := TPdfDocument.Create; try pdf.LoadFromFile('C:\Sample\Sample.pdf', procedure begin pdf[0].ExportAsImage('C:\Sample\Sample_page1.png'); end); fianlly pdf.Free; end; end;
以上のコードでPDFを読み込んで1ページ目をPNGファイルとして保存します。
LoadFromFileで無名メソッドを指定してますが、WinRTのファイル読み込みは非同期なのでLoadFromFile直後ではまだロードが終わっていない可能性があり、読み込み完了通知用のコールバックを指定する必要があります。また、ExportAsImageでのファイル保存も非同期なので、上記コードでは省略していますが同じようにコールバックを書いて保存終了通知を受け取ることができます。
ExportAsImageメソッドは省略されたオプションやオーバーロードがいくつかあり、正式には以下のようになっています。
procedure ExportAsImage(Adapter: IStream; FileType: TExportFileType = eftPng; DestWidth: UInt32 = 0; DestHeight: UInt32 = 0; CompleteProc: TProc = nil); overload; procedure ExportAsImage(Stream: TStream; FileType: TExportFileType = eftPng; DestWidth: UInt32 = 0; DestHeight: UInt32 = 0; CompleteProc: TProc = nil); overload; procedure ExportAsImage(const FileName: string; FileType: TExportFileType = eftPng; DestWidth: UInt32 = 0; DestHeight: UInt32 = 0; CompleteProc: TProc = nil); overload;
1つ目の引数は保存先の指定です。3つのオーバーロード中、StreamやFileNameは特に解説せずともよくあるストリームとファイルへの書き出しなので分かると思いますが、IStreamへの書き出しは単にStreamとFileNameのメソッドで内部的に使用するものです。privateにしても良かったんですが、IStreamを直接指定できた方が楽な場合もあるので一応。
2つ目のFileTypeですが、指定するとエクスポートする画像の形式を指定できます。デフォルトではPNG形式ですが、BMPやJPEGなど他の形式も指定可能です。
3つ目と4つ目のDestWidthとDestHeightは出力画像サイズになります。0を指定するとページの縦横サイズがそのまま使用されます。両方指定するとそのサイズに拡大縮小され、どちらか一方だけ指定するともう片方のサイズはアスペクト比を考慮して自動的に計算されます。例えば横:縦=3:2のPDFの場合、DestWidthに600を指定してDestHeightはデフォルトのまま(=0)にすると、DestHeightは自動的に400が指定されます。
CompleteProcは完了通知を受け取ります。TPdfDocument.OnExportCompleteイベントでも受け取れますが、両方指定した場合はこのCompleteProcが優先されます(LoadFromFileのCompleteProcとOnLoadCompleteイベントも同じ仕様です)。
なお、上記のコードはパスさえ通せばユニットをインストールせずとも使えますが、TPdfDocumentはTComponentを継承する非ビジュアルコンポーネントにもなっているので、インストールしてフォーム上にTPdfDocumentを貼り付けることで、IDE上でOnLoadCompleteやOnExportCompleteイベントをセットするような使い方も可能です。
ダウンロード
この中に入っているPdfDoc.pasが今回追加したユニットです。他のラッパーユニットと同じようにパスの通ったところに置くか、そのフォルダ自体をパスに追加して使ってください。サンプルの方はProject1.dprがDirect2Dを使った前回のサンプルで、Project2.dprがPdfDoc.pasを使った簡易PDFビューア的なサンプルになります。動作環境はOS側はWindows 10、Delphi側はWinRTが入手できるバージョン(後期のXEシリーズや10.x系)であれば動くと思います。サンプルコード内ではVCLを使ってますが、PdfDocユニット自体は特にVCL依存は無いので、FMXでも吐き出したTStreamを(FMXの)TBitmapクラスに食べさせれば画像を取得できるはず。
あとライセンスを書くのを忘れてましたが、Boost Software License 1.0です。
外部ライブラリ無しでPDFを描画する。
WinRTって知ってる?
WinRTとは、Windowsストアアプリ専用のAPIセット…ではないです。すでにトーストAPIなどを使っていてご存じの方もいると思いますが、デスクトップからも利用可能なCOMベースのAPIです。
Windows 8の時点だとWinRTは未来があるのか怪しいAPIでしたが、Windows 10がWindowsの最終バージョンであるとMSが宣言し、そのWindows 10に載っているAPIなのでWin32APIと同等に扱っても良さそうです。
WinRTでできること
ストアアプリは基本的にこのAPIを使って構築することになるので、上述のトーストAPIで画面に通知を出すこと以外にもとにかく色んなことができます。
で、その中にはPDFを扱うAPIもあります。こちらのブログ経由で知ったんですが、これを利用するとどうやら外部ライブラリを使わずにPDFを扱えるようです。
WinRTは腰の重いエンバカデロには珍しいことに、すでにラッパーユニットが用意されています。10 Seattle以降では標準搭載、それ以前ではGetIt経由で入手可能です。
しかし…
じゃあこれでPDF APIも使用可能かというと、残念ながらPDF関連のヘッダはほぼ全く移植されてませんでした(関連する列挙型が1つあるだけ)。
無いなら自分でやればいいということで、さくっと移植。これでようやくAPIが使える…かというとそうは問屋が卸さず。
PDF描画の心臓部であるIPdfRendererNativeインターフェースのRenderPageToDeviceContextメソッドで引数として使われているID2D1DeviceContextが存在しないとコンパイラ様が仰るわけです。
怠慢ですよねぇ
DelphiにはWinapi.D2D1ユニットがはじめから入ってます。Direct2D用のユニットです。これはd2d1.h(とそれに必要な他のいくつかのヘッダ)を移植したもので、これ自体には問題ありません*1。実はDirect2Dにはバージョンがあり、Vista/7で導入された1.0、8で導入された1.1、8.1で導入された1.2、10で導入された1.3があります。d2d1.hはこのうち1.0のみを扱っており、1.1はd2d1_1.hが、1.2はd2d1_2.hが、1.3はd2d1_3.hが必要になります。なりますが、Delphiにはそれらを移植したユニットは存在しません。
大事なことなのでもう一度言いますが、Delphiにはそれらを移植したユニットは存在しません。新バージョンが発売されて「WINDOWS 10の最新機能をサポート」なんてエンバカデロは言ってますが、やはりDelphiにはそれらを移植したユニットは存在しません。エンバカデロの「Windows XXに対応!」はセールス的に見栄えのするUI系やセンサーのサポートみたいなのはやってくれるんですけど、そのOSで追加された機能の大半、特に裏方的な機能はほんとに対応してくれません。PDFはまだいいとしても、Direct2DはGDI/GDI+の後継APIでかなり重要なので、こんな状態でサポートとか言うのほんと詐欺なんでやめて欲しい…。
もっと頑張って移植
なんて愚痴を言ってるときりがないので、諦めて自分で移植します。幸いなことにID2D1DeviceContextはDirect2D 1.1で導入されたので、d2d1_1.hだけを移植すれば良さそうです。「だけ」と言いつつ、d2d1_1.hは5000行を超すサイズなのでPDFヘッダを移植する時の手軽さでは全然ないんですが、これも何とか移植しました。
ようやく本編
これでPDF関連のユニット(WinAPI.Data.Pdf.pas, WinAPI.Data.Pdf.Interop.pas)とそれに必要なDirect2D用ユニット(Winapi.D2D1_1.pas)が揃いました。あとは前述のブログを参考に実装するだけです。本質部分とは関係ないDirect2Dの初期化だけで結構量があり、ここに載せるにはちょっと長いので必須ユニットとサンプルをまとめてアップしますが、こんな感じでフォームにPDFを表示できます。
ちなみに表示だけじゃなく右クリックで画像として保存可能ですが、フォーム上に表示するのと画像化では処理が異なり、前者はIPdfRendererNativeとIPdfDocumentインターフェースを、後者はIPdfDocumentインターフェースのみを使っています。前者はID2D1DeviceContextやIDXGISurfaceのように画面上に書き出すためのもので、後者はIRandomAccessStreamなどストリームに書き出すことができます。特に後者はDirect2D等を必要としないので、PdfDocumentのインスタンスさえ作れば簡単に扱うことが可能です。IStreamにも書き出せるので、IStreamを実装してTMemoryStream辺りに書き出すようにして、それをDelphi側の画像クラスに食わせて画面表示した方が全体としては楽そうです。
ダウンロード
ユニットとサンプルをダウンロード
サンプルを試してみる時は、libフォルダをパスに追加するか、中身をパスの通った場所に置いてください。
動作環境ですが、MSDNを見るとWindows 8.1でも使えるのかWindows 10じゃないと使えないのかAPIやページによってまちまちでよく分からないんですが、少なくとも手元のWindows 10+10.1 Berlin上では動いてます。