Rubyで実装されたプロジェクト管理システムであるRetrospectivaのバージョン2.0がまもなくリリースされます。cozmixng.orgで最新バージョンが運用されているので、それを触ってみることで最新の機能を確認することができます。見てもらえばわかる通り、日本語表示にも対応しています。
Retrospectivaは一時期開発が停滞していて、その間にRedmineの方が普及しました。しかし、その後、再び開発が活発になり、現在は2.0 RC1がリリースされています。1.xから2.0では多くの改良が行われています。そのいくつかを紹介します。
Single Step Installerが用意されていて、コマンド一発でインストールできるようになっています。以前より導入の敷居が下がっています。
AgilePMというアジャイル開発を支援するプラグインが公開され、プロジェクト管理機能がさらに充実しています。tDiaryプロジェクト用のAgilePMがあるので、そこで触ってみることができます。ただし、現時点ではまだ利用されていないのであまり雰囲気がわからないかもしれません。これからのtDiaryプロジェクトの利用に期待しましょう。
Subversionだけではなく、gitにも対応しました。また、Retrospectiva自体のバージョン管理システムもSubversionからgitに移行しています。
最近はgitを採用するプロジェクトも増えているため、これは嬉しい機能ではないでしょうか。
まもなくリリースされるRetrospectiva 2.0を簡単に紹介しました。以前は「ブログがついたTrac」みたいな書かれ方をされていたRetrospectivaですが、実際に使ってみるとその表現が間違っていたことに気付いた人も多かったのではないでしょうか。以前からコミットログで連携する機能などがあり、使っていた人は「便利なプロジェクト管理ツール」という方がしっくりくることに気付いていたはずです。2.0ではより便利で有用な機能がスマートなインターフェイスで追加されています。2.0の紹介のために「ブログがついた〜」と書かれることは減ることでしょう。
Redmineもよいですが、プロジェクト管理ツールとしてRetrospectivaも検討してみてはいかがでしょうか。
もし、使用してみてRetrospectivaの開発に参加したくなった場合はRetrospectivaを使って開発に参加するとよいでしょう。まずは、未翻訳メッセージの翻訳から参加するのが敷居が低いでしょう。kou@clear-code.comまで連絡してもらえれば相談にのります。
ELFから公開されている関数名を抜き出す、Mach-Oから公開されている関数名を抜き出すのPE(Portable Executable)版です。PEはWindowsの.exeや.dllなどで利用されているファイルフォーマットです。
artonさんがCodeZineでDbgHelpを利用してDLLがエクスポートしている関数を列挙するという同様の内容の記事を書いています。PEのフォーマットについても説明しているので、まず、この記事を読んでおくとよいでしょう。
ここでは、DbgHelpなどライブラリを一切使わずに自力でPEをパースし、関数名を抜き出します。そのため、MinGWでクロスコンパイルすることも簡単です。実際、CutterはMinGWを用いたクロスコンパイルに対応しています。
簡略化のためファイルの内容をすべてメモリに読み込んでから処理します。コツコツ資源を利用したい場合は少しづつ読み込みながら処理することになります。
ファイルの内容を読み込むために、便利なGLibのg_file_get_contents()を使いたいところですが、Windows環境ではGLibがインストールされていないことが多いので、ここでは自力で読み込むことにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
char *content = NULL; FILE *file; char buffer[4096]; size_t bytes, total_bytes = 0; file = fopen(argv[1], "rb"); if (!file) { perror("failed to fopen()"); return -1; } while (!feof(file)) { char *original_content; bytes = fread(buffer, 1, sizeof(buffer), file); total_bytes += bytes; original_content = content; content = realloc(content, total_bytes); if (!content) { free(original_content); fclose(file); perror("failed to realloc()"); return -1; } memcpy(content + total_bytes - bytes, buffer, bytes); } fclose(file); |
これで、contentの中にファイルの内容が格納されました。これを使って公開されている関数名を抜き出します。
PEのフォーマットに関する情報はwinnt.h
で定義されています。winnt.h
はwindows.h
をincludeすると暗黙のうちにincludeされるので、windows.h
だけincludeします。
1 |
#include <windows.h> |
まず、ファイルがPEかどうかを判断します。
PEであればNTヘッダに"PE\0\0"という署名が入っているので、これを確認します。"PE\0\0"という署名はIMAGE_NT_SIGNATURE
というマクロとして定義されているので、これを利用します。
1 2 3 4 5 6 7 8 9 10 11 |
IMAGE_DOS_HEADER *dos_header; IMAGE_NT_HEADERS *nt_headers; /* ファイルの先頭はDOSヘッダ */ dos_header = (IMAGE_DOS_HEADER *)content; /* NTヘッダを見つける */ nt_headers = (IMAGE_NT_HEADERS *)(content + dos_header->e_lfanew); /* 署名が"PE\0\0"かどうか確認 */ if (nt_headers->Signature == IMAGE_NT_SIGNATURE) { /* PEファイル */ } |
PEであることが確認できたら、DLLかどうかを確認します。
1 2 3 |
if (nt_headers->FileHeader.Characteristics & IMAGE_FILE_DLL) { /* DLL */ } |
公開されているシンボルはエクスポートデータセクションを見るとわかります。また、シンボルが関数かどうかは、実体がテキストセクションにあるかどうかで判断します。この方法が関数かどうかを判断する標準的な方法かはわかりませんが、実用上はこれで問題なさそうです。
よって、まず、エクスポートデータセクションヘッダとテキストセクションヘッダを見つけます。それぞれ、ヘッダの名前は以下のようになっているので、それを目印に見つけます。
以下がソースコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
WORD i; IMAGE_SECTION_HEADER *first_section; IMAGE_SECTION_HEADER *edata_section; IMAGE_SECTION_HEADER *text_section; /* 最初のセクションヘッダ */ first_section = IMAGE_FIRST_SECTION(nt_headers); for (i = 0; i < nt_headers->FileHeader.NumberOfSections; i++) { const char *section_name; section_name = (const char *)((first_section + i)->Name); /* 各セクションの名前を確認 */ if (strcmp(".edata", section_name) == 0) { /* エクスポートデータセクションを発見 */ edata_section = first_section + i; } else if (strcmp(".text", section_name) == 0) { /* テキストセクションを発見 */ text_section = first_section + i; } } |
ヘッダが見つかったら、セクションの内容を見て、関数であるシンボル名を出力します。*1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
IMAGE_EXPORT_DIRECTORY *export_directory; const char *base_address; ULONG *name_addresses; ULONG *function_addresses; DWORD min_text_section_address, max_text_section_address; /* エクスポートデータセクションの内容 */ export_directory = (IMAGE_EXPORT_DIRECTORY *)(content + edata_section->PointerToRawData); /* エクスポートデータセクション内のデータがあるアドレスを解決するための 基準になるアドレス */ base_address = content + edata_section->PointerToRawData - edata_section->VirtualAddress; /* シンボル名があるアドレス */ name_addresses = (ULONG *)(base_address + export_directory->AddressOfNames); /* シンボルの実体への相対的なアドレス(RVA)があるアドレス */ function_addresses = (ULONG *)(base_address + export_directory->AddressOfFunctions); /* テキストセクションのデータの相対的なアドレスの下限 */ min_text_section_address = text_section->VirtualAddress; /* テキストセクションのデータの相対的なアドレスの上限 */ max_text_section_address = min_text_section_address + text_section->SizeOfRawData; /* シンボル名毎に関数かどうか判断 */ for (i = 0; i < export_directory->NumberOfNames; i++) { const char *name; DWORD function_address; /* シンボル名 */ name = base_address + name_addresses[i]; /* シンボルの実体の相対的なアドレス */ function_address = function_addresses[i]; if (min_text_section_address < function_address && function_address < max_text_section_address) { /* シンボルの実体がテキストセクションにあるなら関数 */ printf("found: %s\n", name); } } |
winnt.h
を使って、DbgHelpなどに依存せずに、PEから公開されている関数名を抜き出す方法を紹介しました。
サンプルプログラムはDebian GNU/Linux上でMinGWを使ってクロスコンパイルし、Wineで動作を確認しました。
今のところ、Cutterがサポートしている共有ライブラリのフォーマットはELF/Mach-O/PEです。このPE編で、公開されている関数名を抜き出す方法を紹介するシリーズは最後です。もし、今後Cutterが対応するフォーマットが増えれば、そのフォーマットから関数名を抜き出す方法を紹介するかもしれません。
*1 コメント中のRVAの説明はざっくりなので、詳しくは冒頭のartonさんの記事を参照してください。
8/29(土)に行われる第09回 まっちゃ445勉強会のテーマは「迷惑メール対策」で、迷惑メール対策の方法を提案している方や迷惑メール対策を実現するためのツールの開発に関わっている方が発表者として参加する予定です。milter managerも迷惑メール対策関連ツールの1つとして参加します。
今回はmilter managerそのものよりも、milter managerがベースとしている「milter」という技術・仕組みについて話す予定です。
勉強会には迷惑メール対策に深く関わっている方が多く参加する予定なので、興味のある方は参加してみてはいかがでしょうか。
現在クリアコードでインターン中のはやみずです。クリアコードのインターンシップ制度は今年度から始まり、最初のインターン生として2週間クリアコードで働かせていただくことになりました。今回と次回の2つの記事で、現在インターンシップで取り組んでいる内容について紹介したいと思います。今回の記事では、C言語用単体テストフレームワークCutterへのHTTPテスト機能追加について紹介します。
ククログでも度々紹介されているCutterですが、このテストフレームワークを利用することでC言語での単体テストを非常に効率良く開発することができます。
Cutterはできるだけ簡潔にテストを記述できるように、基本的な assert 系関数以外にも様々なユーティリティ関数を提供しています。また、GLibを利用したプログラムのテストを開発するためのGCutterや、gdk-pixbuf(C言語で画像を扱うためのライブラリ)用の GdkCutter Pixbufなどのモジュールが含まれています。これらを利用することで、GLib や Gdk Pixbuf を使ったプログラムはよりこれらのライブラリに特化したテストを簡単に書くことが可能となっています。これからも対応ライブラリは増えてゆくのかも?
Cutterの強みの1つに、C言語でありながらメモリ管理の手間が非常に少ないということが挙げられます。ほとんどのテストフレームワークは set up(準備)→test実行→tear down(後片付け) という処理の流れを基本としているので、テスト中に利用するオブジェクトは tear down のときに解放してやればよいことがわかっています。Cutter は「このオブジェクトは tear down 時に解放しといてね」ということを Cutter に教えるための API を提供しているため、この API を利用することで解放忘れによるメモリリークを防ぐことができます。例えば文字列であれば、cut_take_string(const gchar *string) を利用することで文字列を動的にアロケートした領域にコピーしてそのポインタを得ることができ、tear down時にはこの文字列が自動的に解放されます。
今回開発しているCutterのHTTPテスト機能も、このパターンを使ってオブジェクトを簡単に生成して、しかも勝手に解放してくれるようになっています。
さて、本題のHTTPテスト機能を実装した SoupCutter に話を移しましょう。今回の開発では、HTTPの機能を簡単に実装するために libsoup というライブラリを利用しました。というよりも、GLibをサポートするのが GCutter、Gdk Pixbuf をサポートする GdkCutter Pixbuf などのように、libsoup をサポートする SoupCutter という位置付けのほうが正確です。しかし、簡単なテストであれば libsoup 自体には一切触れることなく作成することができるので、Cutter で HTTP サーバーや HTTP クライアントのテストを簡単にできるようにするためのモジュールだと思っていただいても大丈夫です。
SoupCutter を使って HTTP サーバープログラムが正しくレスポンスを返しているかをテストするには、次のように書くことができます。
1 2 3 4 5 6 7 8 9 |
SoupCutClient *client = soupcut_client_new(); /* http://localhost:8080/?key=value に HTTP Request を送信 */ soupcut_client_get(client, "http://localhost:8080/", "key", "value", NULL); soupcut_client_assert_response(client); soupcut_client_assert_equal_content_type("text/plain", client); soupcut_client_assert_equal_body("Hello, world", client); |
SoupCutClient というのは、サーバーとやりとりしたHTTPリクエスト/レスポンスを内包しているオブジェクトです。..._assert_response では、最後に受け取ったレスポンスが 2XX (200 OK など) であるかをチェックしています。同様に、..._assert_content_type では Content-Type が text/plain であることを、..._assert_equal_body ではレスポンスの本文が Hello, world であることをチェックしています。このようにして、SoupCutter を使うと非常に簡潔な記述で HTTP サーバーが思った通りに動いているかを調べることができるようになっています。
また、SoupCutter の最初の機能としては HTTP サーバーのテスト、つまりHTTPクライアントとしての機能を実装しているのですが、このHTTPクライアント機能が正しく動作しているかをテストしなければなりません。HTTPクライアント機能をテストするためには、HTTPサーバが必要です。HTTPサーバーをテストするためにHTTPクライアントを実装し、そのHTTPクライアントをテストするためにHTTPサーバーを実装する。ややこしいですね。
というわけで、SoupCutter は簡単にHTTPサーバーを作ることもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static void server_callback (SoupServer *server, SoupMessage *msg, const gchar *path, GHashTable *query, SoupClientContext *client, gpointer user_data) { .... /* リクエストを処理して結果を返す */ } SoupServer *server; server = soupcut_server_take_new(NULL); soup_server_add_handler(server, "/", server_callback, NULL, NULL); soup_server_run(server); |
HTTPサーバーの作成自体はたった3行でできてしまいました。server_callback は実際にリクエストを処理してレスポンスを生成するコールバック関数です。これを SoupServer のリクエストハンドラに追加して、soup_server_run でメインループに入り、サーバーが動き始めます。
ここで注目してほしいのは、サーバーを生成するときの soupcut_server_take_new です。Cutterでは take と名前のつく関数で生成したオブジェクトは、tear down時に自動で解放されます。HTTPサーバーの場合は、ちゃんとソケットの後処理まで行い、オブジェクトを解放してくれます。つまり、HTTPサーバーを簡単に作れるだけではなく、勝手に後片付けまでしてくれます。
今回は、現在開発中である Cutter の HTTPテストモジュール SoupCutter について簡単に紹介しました。SoupCutterを使うと簡単便利にHTTPサーバー/クライアントのテストを作成することができるようになります。SoupCutter は現在クリアコードのインターンシップで開発していて、来週末に SoupCutter を含めた Cutter をリリースすることを目標に頑張っています。もし HTTP のテストを作る必要に迫られた場合には、SoupCutter を検討してみてください。
前回に引き続き、クリアコードインターン記事の2回目です。前回の記事で紹介したCutterのHTTPテストモジュールであるSoupCutterを使って、全文検索エンジンgroongaのHTTPインターフェースのテストを作成したので、今回はその紹介をしたいと思います。SoupCutterが実際どのように使えるかという実例として、よい題材なのではないかと思います。
全文検索エンジンgroongaはHTTPサーバー機能を備えており、Webブラウザからアクセスすることでテーブルを作成したり、データベースの中身を調べたりすることができます。このようにブラウザからデータベースを管理するために、groongaではデータベースを操作するための基本的なAPIをHTTPリクエストによって呼び出せるようにしています。例えば、localhost:10041 で groonga のサーバーを実行しているときに、http://localhost:10041/table_list を GET すると、テーブルの一覧を取得することができます。
それでは、Cutter でどのようにしてテストを開発していくかを見ていきましょう。
今回は groonga のHTTPサーバー機能のテストを行うため、まずは groonga でHTTPサーバーを走らせなければなりません。Cutterには外部コマンドを簡単に扱うことができる GCutEgg というオブジェクトがあります。groonga でポート 4545 を listen する HTTPサーバーを起動するには、以下のようなコマンドを実行します。
groonga -s -p 4545 -n /path/to/dbfile
このコマンドをテストのプログラムから実行しなければなりません。GCutEgg を使うと、以下のように簡単にコマンドを実行することができます。
1 2 3 |
GCutEgg *egg = gcut_egg_new("groonga", "-s", "-p", "4545", "-n", "/path/to/dbfile", NULL); gcut_egg_hatch(egg, NULL); |
たったこれだけで、簡単に groonga のHTTPサーバーを準備することができました。このサーバーはテストの間は実行していて、テストが終わるごとに終了してほしいので、setup で実行を始めて、tear down で終了してあげればよいでしょう。
また、前回の記事で紹介した Cutter のHTTPクライアント SoupCutClient も setup で準備しておくとよいでしょう。
1 2 |
client = soupcut_client_new();
soupcut_client_set_base(client, "http://localhost:4545/");
|
soupcut_client_set_base で SoupCutClient にベースURIを設定しておくことで、実際にGETリクエストを送信するときのURI指定で楽をすることができます。SoupCutter では、soupcut_client_get(client, "http://localhost:4545/path/to/something") のようにGETリクエストを送ることができるのですが、ベースURIを設定ておけば soupcut_client_get(client, "/path/to/something") と書くだけで、 http://localhost:4545/path/to/something にGETリクエストを送ることができるようになります。
さらにもうひとつ。GCutEgg と SoupCutClient はどちらも GLib のオブジェクトとして実装されており、解放するときは g_object_unref を呼ぶだけでデストラクタが呼ばれ、適切にオブジェクトを解放してくれます。Cutter では、オブジェクトを破棄する関数と共にオブジェクトを登録しておくと、テストの tear down 時に自動でオブジェクトを解放してくれるという便利機能があります。どうやるかというと、下記のようにするだけです。
1 2 |
cut_take(client, g_object_unref); cut_take(egg, g_object_unref); |
またこれらは、GLibをサポートしたGCutterの関数を使うと、
1 2 |
gcut_take_object(G_OBJECT(client)); gcut_take_object(G_OBJECT(egg)); |
と書くこともできます。 これで client と egg は自動的に tear down 時に解放されるようになります。Cutterでは適切な下準備をしておくと、tear down 用の関数でわざわざ後片付けをしなくてもOKです。便利ですね。
ここまでをまとめると、テストのセットアップは次のように書くことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static GCutEgg *egg; static SoupCutClient *client; void cut_setup(void) { client = soupcut_client_new(); soupcut_client_set_base(client, "http://localhost:4545/"); gcut_take_object(G_OBJECT(client)); egg = gcut_egg_new("groonga", "-s", "-p", "4545", "-n", "/tmp/http.db", NULL); gcut_egg_hatch(egg, NULL); gcut_take_object(G_OBJECT(egg)); g_usleep(G_USEC_PER_SEC); /* groonga の listen が完了するまで適当な時間待つ */ } |
まずは簡単なところからテストしていきましょう。groongaのHTTPサーバーはルートにGETリクエストを送ると、本文無しで 200 STATUS OK を返してくるのでこれをテストしてみます。
1 2 3 4 5 6 7 8 9 |
void test_get_root(void) { soupcut_client_get(client, "/", NULL); /* http://localhost:4545/ を GET */ soupcut_client_assert_response(client); /* status code は 2XX かチェック */ soupcut_client_assert_equal_content_type("text/javascript", client); /* Content-Type をチェック */ soupcut_client_assert_equal_body("", client); /* 本文が空かをチェック */ } |
このように、非常に簡潔にテストを書くことができます*1。
もう1つテストを書いてみましょう。groongaのHTTPサーバーは、/status にリクエストを送ると、{"starttime":1251190614,"uptime":39} というようにサーバーが開始した時刻とuptime をJSON形式でレスポンスとして応答します。starttimeもuptimeも開始した時刻や現在時刻によって刻々と変化するため、単純に assert_equal_body で期待した文字列と一致するかどうかを調べるには無理があります。このような要求に答えるために、SoupCutterでは正規表現に本文がマッチするかをテストできる soupcut_client_assert_match_body という関数を提供しています。
1 2 3 4 5 6 7 8 9 10 |
void test_get_status(void) { soupcut_client_get(client, "/status", NULL); soupcut_client_assert_response(client); soupcut_client_assert_equal_content_type("text/javascript", client); soupcut_client_assert_match_body("{\"starttime\":\\d+,\"uptime\":\\d+}", client); } |
soupcut_client_assert_match_body を利用すると、このようにして /status をGETしたときのテストを実装することができます。
このように、柔軟なテストも簡単に作成できるのが Cutter の特徴であり、開発方針でもあります。
その他のHTTPインターフェースのテストも、groongaの側でテーブルを作っておいたりカラムを作っておいたりというコードを書かなければならないことを除けば、ほとんど上記の2つのテストと同様に開発してゆくことができます。テーブルを作成する API は、/table_create にクエリーパラメータとしてテーブル作成に必要な情報を渡すことで呼び出すことができますが、これも SoupCutter では次のように簡潔に書くことができます。
1 2 3 4 5 6 7 8 |
soupcut_client_get(client, "/table_create", "name", "newtable1", "flags", flags, "key_type", "Int8", "value_type", "Object", "default_tokenizer", "", NULL); |
今回はテストを開発する実例を通して、SoupCutter の使い方について紹介しました。SoupCutter を使って開発された groonga のテストは、実際に groonga のレポジトリにも取り込まれています。
SoupCutter を含めた Cutter は今週中にリリース予定なので、是非みなさん使ってみてください。
*1 関数名がやや長いのは御愛嬌ということで ;-)
本日、C言語用単体テストフレームワークであるCutterの新バージョン1.0.8が肉リリースされました。
先日の2つの記事(その1、その2)でも紹介しましたが、1.0.8の重要な新機能はHTTPインターフェースのテスト機能 SoupCutter です。SoupCutter では GNOMEプロジェクトで開発されている HTTPサーバー・クライアントライブラリの libsoup をバックエンドに利用しており、HTTPサーバーやクライアントプログラムのテストを簡単に記述できるようになっています。
SoupCutter の使い方は、前回の紹介記事に詳しく書いてあるので是非これを参考に使ってみてください。この記事では SoupCutterの使用例として groonga のHTTPインターフェースのテストを作成しているのですが、SoupCutter を使った HTTP インターフェースのテストは実際に groonga 本体にも取り込まれています。
また、1.0.8からFedoraのrpmパッケージや、Mac OS Xのportsパッケージ(MacPorts)、Debian、Ubuntuのdebパッケージもサポートするようになったので、これまでよりも手軽に Cutter を導入できるようになりました。
汎用的なHTTPのライブラリを使ったとしても、C言語でHTTPインターフェースのテストを開発しようと思うと一手間かかってしまうのではないかと思います。しかし、HTTPのテストに特化したSoupCutterを利用すれば簡潔にテストを記述できる上に、その気になれば libsoup の豊富な機能をフル活用することもできるようになります。
ますます便利になった Cutter を使って、皆さんが関わっているプロジェクトのテストを作成してみませんか?
第09回 まっちゃ445勉強会で使用した資料を公開しました。
今回は、Ruby関連で話すときのように自由な感じで話したのですが、楽しんでもらえた方もいたようでよかったです。
話した内容はmilter managerのことというより、milter managerがベースとしているmilterのことです。milter managerについては、最後に少し触れた程度です。
プログラムを見てもらえればわかりますが、送信側の対策、経路情報を利用する対策、メッセージの内容を利用する対策と網羅的な内容でした。そのなかで、手法そのものではなく、これらの手法を活用するためのツールであるmilter managerは異色と言えます*1。
milter managerと同じくmilterも手法そのものではありません。milterは手法を実現するための仕組みの1つです。セッションの順番が経路情報を利用する対策のセッションとメッセージの内容を利用する対策のセッションの間という切り替えのタイミング(で、さらにおやつの後)ということもあり、umqさんのイントロダクションの一部をもう少し詳しく取り上げて、勉強会の話題に上がっている手法をmilterを使って実現できるよ、という流れになっています。全体を再確認しつつ、適度な中休みになったのならmilter managerセッションの役割は果たせたかと思います。
個別ではなく全体的に見てですが、みんな楽しそうでいいなぁ、集まれてよかったなぁ、というのが率直なところです。また、同じような機会ができればいいなぁと思いますので、実現したときはまたよろしくお願いします。
勉強会で使ったプレゼンテーションツールRabbitの機能を紹介しておきます。
さとうさんの資料はOpenOffice.orgのImpressで作成してPDF化されていました。少し時間がオーバーしそうということだったので、Rabbitの「うさぎとかめタイマー」をおすすめして実際に使ってもらいました。
RabbitはPDFレンダリング機能もあるので、他のプレゼンテーションツールで資料を作成しPDFに出力すれば、Rabbitで表示することができます。Rabbitは読み込んだPDFのスライドの上に「うさぎとかめタイマー」を表示するので、PDF側には特に何もする必要はありません。
「うさぎとかめタイマー」が何かについては、ぜひ自分の目で確認してください。
SMTPのやりとりのデモをするときにスライドの真ん中を開けて、後ろにあるターミナル上でtelnet(とMutt*2)を使いました。この穴のことを「Rabbit Hole」と呼んでいます。名前の由来はもちろん不思議の国のアリスです。
ちなみに、Rabbitにはアリス画像も用意されているので、アリスとうさぎを追いかけさせることもできます。