FreePascalとDelphiでリフレクションとxUnitフレームワーク
DelphiとFreePascalで動作する自作ライブラリのテストコードを書くために、自作のxUnitフレームワークを使っています。
自作のテストフレームワークでは、 Test
で始まるメソッド名をテストメソッドとして自動で実行するxUnitフレームワークでよくある仕様としています。
このような仕様を実装する場合に、フレームワーク側でテストコード側のメソッド名を取得するコードが必要となります。 私のテストフレームワークでは、リフレクションでこれを実装することにしました。
元々Delphiのみ対応で最初に作成したのですが、FreePascalに対応するPullRequestをもらってマージしたので、そこからはDelphi, FPC両対応としてメンテナンスしています。
DelphiとFreePascalでリフレクションの実装状況に違いがあるので、コンパイラ指令で環境ごとに処理を分けることになります。
どちらも実行時型情報(RTTI)が必要になるので、実装クラス側はpublished宣言でテストメソッドを定義して利用します。
テストコードの例(抜粋):
// 中略 TArgumentParserAddArgumentTest = class(TTestCase) private FArgumentParser: TArgumentParser; public procedure SetUp; override; procedure TearDown; override; published procedure TestTArgument; // 中略 procedure TArgumentParserAddArgumentTest.TestTArgument; begin FArgumentParser.AddArgument(TArgument.Create('--foo', 'bar', saStore)); AssertEquals(FArgumentParser.ArgumentsCount, 1); AssertEquals(FArgumentParser.Arguments[0].Option, '--foo'); AssertEquals(FArgumentParser.Arguments[0].Dest, 'bar'); end;
Delphi向けのTRttiContextを使った実装
Delphi向けの実装についてはTRttiContextを使って実装しています。
TRttiContextは、実行時型情報を扱うためのクラスで、メソッドオブジェクトを取り出して実行したりできます。
10年以上前ですが記事にまとめています。
FreePascal向けの実装
FreePascal向けの実装については、DelphiのTRttiContext相当のクラスが無いので、仮想メソッドテーブルからメソッドのエントリを取り出して、メソッド名を取得しています。
先月記事にまとめています。
自作のテストフレームワークを作ってみて良かったこと
自作のテストフレームワークを作った元々のモチベーションとしては、Delphiの標準のテストフレームワークであった DUnit だとXML形式でテスト結果をレポートする機能がなくて、JenkinsなどのCIツールから結果を集計するのが手軽にできなかったり、APIのスタイルで気になる部分もあり、「1ファイルで動くような手軽なテストフレームワークがあるとよいな」と思ったところからでした。
ちょうどDelphiにTRttiContextが実装されたところでもあったので、思ったより簡単にできるかも、と考えて作成してました。
FreePascal向けのPullRequestをマージしてからは、FreePascalの仕様を読みつつメンテするのも勉強になりました。
クラスシステムをメモリ上にどのように展開して処理を呼び出す、みたいな部分もイメージを持てるようになってよかったです。
terapyon channel podcast #99 ゲスト参加
FreePascalでリフレクションを実装する
リフレクションは便利なのでFreePascalでも使いたい。
Delphiの場合はRttiユニットがあるのでリフレクションは簡単に実装できるのですが、FreePascalには同等のユニットがありません。
昔作成してGitHubで公開していたテストフレームワークのほうにPullRequestをもらって、FPC用のリフレクション実装をマージしたことがあったのですが、古いバージョンのFPCでしか動かないものでした。
x86_64のFreePascalCompilerで動くように試行錯誤してうまくいったので、まとめておきます。
メモリレイアウトの情報を見つけきれなくて、結局メモリダンプで推測しつつ調整することになりました。
試したFPCのバージョンは、 Free Pascal Compiler version 3.2.2+dfsg-9ubuntu1 [2022/04/11] for x86_64
コード
my_class.pp:
unit my_class; {$M+} // publishedを使うのでM+オプション指定 {$MODE Delphi} interface type TMyClass = class(TObject) published // RTTI生成のためにpublishedを使う procedure SayHello; end; implementation procedure TMyClass.SayHello; begin WriteLn('Hello'); end; end.
invokes.pp:
unit invokes; {$MODE Delphi} interface uses SysUtils; type TMethodtableEntry = packed record Name: PShortString; // メソッド名 Address: Pointer; // メソッドの関数ポインタ end; TPlainMethod = procedure of object; // 今回は引数無しのメソッド procedure InvokeMethod(Obj: TObject; Name: string; AClass: TClass); implementation procedure InvokeMethod(Obj: TObject; Name: string; AClass: TClass); var pp: ^Pointer; pMethodTable: Pointer; pMethodEntry: ^TMethodTableEntry; I, numEntries: Word; VMethod: TMethod; VPlainMethod: TPlainMethod absolute VMethod; begin if AClass = nil then Exit; pp := Pointer(NativeUInt(AClass) + vmtMethodtable); // 仮想メソッドテーブルのオフセット分アドレスずらし pMethodTable := pp^; if pMethodtable <> nil then begin numEntries := PDWord(pMethodTable)^; // メソッドテーブルのエントリ数をポインタ経由で取得 pMethodEntry := Pointer(NativeUInt(pMethodTable) + SizeOf(DWord)); // ポインタ分ずらしてエントリにアクセス for I := 1 to numEntries do begin if LowerCase(pMethodEntry^.Name^) = LowerCase(Name) then // 指定されたメソッド名と同じ場合 begin VMethod.Code := pMethodEntry^.address; // メソッドの関数ポインタ VMethod.Data := Obj; // SelfをAssign VPlainMethod; // メソッド呼び出し end; pMethodEntry := Pointer(NativeUInt(pMethodEntry) + SizeOf(TMethodtableEntry)); end; end; InvokeMethod(Obj, Name, AClass.ClassParent); end; end.
method_invoke.lpr:
program method_invoke; {$MODE Delphi} uses MyClass in './my_class.pp', Invokes in './invokes.pp'; var obj: TMyClass; begin obj := TMyClass.Create; // TMyClassのSayHelloを呼び出し、Selfにはobjを指定 InvokeMethod(obj, 'SayHello', TMyClass); end.
ビルド
$ fpc method_invoke.lpr
実行結果
$ ./method_invoke Hello
参考
FreePascalのコンソールアプリでメモリダンプ
FreePascalのコンソールアプリで、デバッグするときにメモリダンプするユーティリティがほしかったので、ChatGPTにざっくり書いてもらって少し手直しして使っている。
コード
memory_dump.pp:
program memory_dump; uses SysUtils; procedure DumpMemory(addr: Pointer; size: Integer); var i, j: Integer; p: PByte; asciiLine: string; begin p := PByte(addr); for i := 0 to (size div 16) - 1 do begin // 16バイトずつ表示 Write(Format('%p ', [Pointer(p)])); // メモリアドレスを表示 asciiLine := ''; // ASCII表現の行 // 16バイト分のデータを表示 for j := 0 to 15 do begin Write(Format('%02x ', [p^])); // 16進数で表示 if (p^ >= 32) and (p^ <= 126) then asciiLine := asciiLine + Chr(p^) // 表示可能なASCII文字を保存 else asciiLine := asciiLine + '.'; // 表示できない文字は '.' に置換 Inc(p); end; // ASCII表現を表示 WriteLn(' ', asciiLine); end; // 残りのバイトがある場合 if (size mod 16) <> 0 then begin Write(Format('%p ', [Pointer(p)])); // メモリアドレスを表示 asciiLine := ''; // ASCII表現の行 // 残りのバイトを表示 for j := 0 to (size mod 16) - 1 do begin Write(Format('%02x ', [p^])); if (p^ >= 32) and (p^ <= 126) then asciiLine := asciiLine + Chr(p^) else asciiLine := asciiLine + '.'; Inc(p); end; // 残り部分の整列 for j := (size mod 16) to 15 do Write(' '); // ASCII表現を表示 WriteLn(' ', asciiLine); end; end; var testData: array[0..31] of Byte; i: Integer; begin // ダミーデータの初期化 for i := 0 to High(testData) do testData[i] := i + 32; // メモリダンプの実行 DumpMemory(@testData, SizeOf(testData)); end.
コンパイル
$ fpc memory_dump.pp
実行結果
$ ./memory_dump 000000000047D530 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F !"#$%&'()*+,-./ 000000000047D540 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 0123456789:;<=>?
メモリアドレスの表示とascii出力があるので、ポインタのアドレス探しつつ... みたいなデバッグで役に立ちました。 このprocedure自体は、そのままDelphiでも使えそう。
Djangoで作成したアプリを本番環境で動かすときにWSGIとASGIのどちらを使うのか
先日、 django-jaのDiscord で出た話題ですが、Djangoで作成したアプリを本番環境で動かす際のアプリケーションサーバーについてです。
具体的にはWSGIとASGIどちらを使うのか、そしてアプリケーションサーバーはどれがよいのか。
WSGIかASGIか
DjangoをWSGIかASGIのどちらで動かすかですが、迷うぐらいなら現状はWSGIでいいです。
「ASGIは必要になったら使う」ぐらいの気持ちでいるのをおすすめします。
ASGIは、PythonのAsync(非同期)に対応するための、アプリケーションインターフェースです。
WSGIをおすすめする理由は次の通り。
- 同期処理なのでデバッグしやすい
ASGIをおすすめしない理由は次の通り。
- 非同期処理になるのでデバッグしづらい
- とにかくこれです。IO待ちの状況では並行で実行されてコンピュータリソース的には効率良いわけですが、エラーのときのスタックトレースが読みづらいです。実行順序もアプリによってはバラバラになることもあり、エラーの再現もしづらいです。
- まだPython標準仕様ではない(が、デファクトスタンダードにはなっている)
- 積極的に採用するほどでもない、という感じ
ASGIをおすすめする状況は、現状だと限定的かなあと思ってます。
- Django Channelsを使っている場合
- アプリがASGI対応、Asyncのコードになっている場合
- そもそもasync/awaitを使ったアプリの場合は、ASGIサーバーで動かさないと恩恵があまりないため、この場合もASGIサーバーを利用することになるでしょう。
Djangoでasync、awaitを使った場合、I/O待ちの部分では非同期処理によって高速化を期待できます。しかし、CPU依存の処理については特に速くなるわけではありません。
もしもWSGIアプリケーションのパフォーマンスを上げたい場合、次のような方法を試して、どうにもならない場合にようやく非同期IOやASGIサーバーを検討する、ぐらいでいいかなと思います。
- アプリケーションサーバーの設定見直し
- プロセス数を増やす
- スレッド数を増やす
- WSGIサーバーの変更を検討
- gunicornの場合はuWSGIに変えてみる、など。
- DBクエリの最適化
- n+1クエリをなくす、速度の遅いクエリを改善する
- DB自体の最適化
- サーバー性能を上げて高速にする
- インデックスを作ってクエリが速くなるようにする
- 外部通信回数の削減
- DBや外部ストレージなどとの通信レイテンシの影響を減らして高速化する
- 外部呼び出し部分の非同期タスク化
ここに記載した方法以外にも、ウェブアプリ自体のパフォーマンス改善方法が色々あるので、それらを一通りやったあとに、非同期IOについて考えてみるぐらいでいいかも。
おすすめのWSGIサーバー
WSGIサーバーは好みもあるので、人によって答えは異なります。
個人的にはGunicornを長く使っていて、2024年現在でもこれをおすすめしています。
個人的に気に入ってる点は次の通りです。
- Gunicorn自体のコードはPure Pythonで書かれている
- 何か不具合のときに調査しやすい
- セットアップでハマりが少ない
- マルチプロセス+マルチスレッドでの動作に対応している
- Workerスタイルでの動作のため、アプリケーションが不安定でプロセスが落ちても自動再起動がかかる
一方、気をつける点は以下です。
- Linuxでしか動かない(そもそもコンテナで動かすなら最近はあまり問題にならないですが..)
- Pure Pythonなので、限界までCPUリソースを使い切りたいアプリには向かない
- uWSGIのように機能が豊富ではない(サイドカー的に色々動かすとかはuWSGIのほうが簡単)
おすすめのASGIサーバー
AWGIサーバーについては、おすすめできるほどの運用実績やノウハウがまだ個人的には少ないです。運用実績はあまり急激に増えることもなさそうです。
自社内としてはUvicornを使ってる部分が多いです。ただし、DaphneやHypercornを評価、比較したわけでもないです。
Uvicornを使っている理由としては、GunicornのWorkerとして動作させることもできるからです。 これはつまり、Gunicornのプロセス管理機能をそのまま使えるということです。
アプリケーション起因でWorkerプロセスが落ちても自動再起動できます。
Daphneで同様のことをしたい場合は、別途Supervisorなどを併用すればよいのですが、Gunicorn+Uvicornで同じPythonプロセスの中で完結するならそのほうが楽かなという印象を持ってます。
TechRAMEN 2024 Conferenceに参加しました
7/26(金)~7/27(土)に北海道旭川市で開催された TechRAMEN 2024 Conference に参加してきました。
Djangoハンズオン
私は2日目にDjangoのハンズオンセッションの講師をしました。休憩入れて4時間ほど、ボリュームのある内容になりました。
ハンズオン資料は公開しています。
家に帰ってきてから資料を少し調整しました。
ページ分割してSphinxの設定やCSSを変更したので、スマートフォンで見やすくなったかと思います。
パネルディスカッション
2日目、パネルディスカッションにも登壇しました。
旭川の地元企業でがんばる みょう さんのお悩みについてワイワイ話す内容でした。
後日祭 OSS Gate(ゆるい勉強会@旭川)
イベント翌日の7/28(日)は後日祭として同じ場所でOSS Gateのワークショップをやるとのことでしたので、参加してきました。
Pythonをテーマにしていた人が何人かいたので、サポートできて良かったと思います。
ラーメン
帰りに旭川空港で梅光軒のラーメンを食べました。うまいうまい
TechRAMEN懇親会のラーメンも美味しかったよ!
おわりに
仕事とかプライベートでも色々あり、この一週間とても忙しかったのですが、なんとか乗り切れてよかったです。
楽しいイベントだったので、また参加したい。旭川にもまたそのうち行きます。