Swanman's Horizon

性欲をもてあましつつなんらかの話をするよ。

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にあるようにインターフェースというのは関数アドレステーブルなので、関数アドレスが絶対に必要になります。ループの条件式で展開されないのは最適化が難しいからでしょうか。
こういったインライン化の条件はヘルプにまとまっているので、そちらを読んでいただけると解説する手間が省けて助かります(結構あるので…)。

inline 指令の利用
http://docwiki.embarcadero.com/RADStudio/Berlin/ja/%E6%89%8B%E7%B6%9A%E3%81%8D%E3%81%A8%E9%96%A2%E6%95%B0%E3%81%AE%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97#inline_.E6.8C.87.E4.BB.A4.E3.81.AE.E5.88.A9.E7.94.A8

なお、ループの条件式ではなくループ内でも展開されないのではないかという回答がありましたが、これは展開されます。例えば、

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時間かかった…。

*1:この辺りがvirtualはメモリを食いdynamicは遅いと言われる理由

*2:さらに言えばobject型もありますが扱いが酷く、拡張RTTI上ではなかったことにされてる感が…(エラー出たりとか)

Delphiプログラマを見分ける10の質問。

16/07/20追記

真っ当な設計をしてたら出会わない挙動が多く、ネタとしてわざと難しい質問にしてるので、間違ったら恥ずかしいなんて躊躇せず、ぜひとも勢いよく突っ込んで爆発四散してください!

はじめに

ものすごーく前に「○○プログラマを見分ける10の質問」みたいなのが流行った時に質問を数個考えてそのまま放置してたメモを発掘したんですが、せっかくなので残りを適当に考えて10個用意してみたので我こそはという方は挑戦してみてください><
なお、質問のほとんどがネタ要素であり、知らなくても問題ない、僕の勘違いが多分に含まれる、答えがひとつじゃない、等々あると思いますが、あくまでもネタであるということを念頭にお付き合いいただき、ついでに勘違い部分に関しては正しいツッコミを入れてもらえると嬉しいです。

10の質問

  1. 「const」と「var、out」の違いを参照という観点でひとつ挙げよ。またその違いを無くすためにはどうすれば良いか説明せよ。
  2. 文字列や動的配列などの型は自動で初期化されるため自分でnil等を代入する必要がないが、初めての使用時にnilや空文字列で初期化されていない場合があるのはどんな時か?またその理由を説明せよ。
  3. 複数の文字列変数を連結するとき、「sA := s1 + s2; sB := s3 + s4; s := sA + sB;」と「s := s1 + s2 + s3 + s4;」は足し算の数だけ見れば等価だが、後者の方が良いのは何故か。
  4. nilが代入されているインスタンスの(クラスメソッドではない)メソッドを呼び出そうとした場合でも読み取り違反などのエラーが発生しないのは主にどんな状況か?またその理由を説明せよ。
  5. 関数内関数をコールバックを必要とする関数の引数として渡そうとすると「ローカル手続き/関数を手続き変数に代入しました」というエラーが発生してコンパイルできないが、これを回避するにはどうすれば良いか。またその際気を付けることは何か。
  6. TComponent.FOwnerをはじめ、10 SeattleではWeak属性が指定されていたフィールドが10.1 BerlinではUnsafe属性に置き換えられているが、これはどういった理由が考えられるか。
  7. recordでインターフェース(例えばIInterface)を実装する方法を簡単に説明せよ。
  8. 通常inlineが指定された関数・メソッドは処理内容がインライン展開可能な条件であれば呼び出しはインライン展開されるが、呼び出し方によってインライン展開される場合とされない場合が発生するのはどのような状況か述べよ。複数あればなお良い。
  9. デフォルトのコンパイラ指令下において、RTTIでメソッド情報がほとんど取得できないのはどんな型か。またほとんどと書いたが、メソッドに関するどんな情報なら取得可能か。
  10. 無名メソッド型の実態は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形式ですが、BMPJPEGなど他の形式も指定可能です。
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上では動いてます。

余談

Direct2Dの初期化も、Direct2D 1.0のID2D1RenderTargetを使ったものはVcl.Direct2D内に実装されているんですが、今はID2D1DeviceContextを使ったものが主流で、例えばWIC(Windows Imaging Component)なんかでID2D1DeviceContextを必要とするメソッドがあるんですよね。起点となるインターフェースなので本当に重要なんです。
まあそのWICのメソッドってのもWindows 8で追加された新しいインターフェースで追加されたもの(かつ未移植)なので、そもそも使えないんですけどね…。

*1:まぁSDKのバージョンアップで更新された部分は全然反映されてないんだけど

Oculus SDK 1.4.0 wrapper for Delphi

今頃CV1が届いてるはず…そんなふうに考えていた時期が俺にもありました

出荷予定日:5/16/2016 - 5/26/2016

なのに未だ出荷されず。Oculusェ…。

Oculus SDK 1.3.2 wrapper for Delphi (と、いくつかデモ)


久々に

0.0.1のリリースからかなり間が空きましたが、もうすぐCV1が届くはずなのでリハビリも兼ねてヘッダを移植してみました。
今回はそれに加えて、元々入っているOculusRoomTiny系のデモもいくつか移植しています。コンパイル済みのEXE(64bit用)も入れてあるので、一応Riftさえあればすぐに試せます。

ちなみに

デモには簡易3Dフレームワークみたいなのが付いてるので、3Dモデルの追加表示程度なら割と簡単にできます*1

*1:動かすのは別として

自作プラグイン更新のお知らせ。

10.1 Berlinで動かなくなったもの

これについては後日対応したいけど、調べるのが面倒なのでいつになるかは未定。