ユーティリティ¶
この章では、一般的な作業に役立つツールと手法を紹介します。libevのマニュアルページには、APIを少し変更するだけでlibuvにも適用できるパターンが説明されています。この章では、章を丸ごと割り当てるほどではないlibuvのAPIも扱います。
タイマー¶
タイマーは、開始から一定の時間が経過するとコールバックを呼び出します。libuvのタイマーは、1回だけでなく、一定の間隔で呼び出すようにも設定できます。
基本的な使い方は、ウォッチャーを初期化し、timeoutと、必要に応じて、repeatを指定して開始することです。タイマーはいつでも停止できます。
uv_timer_t timer_req;
uv_timer_init(loop, &timer_req);
uv_timer_start(&timer_req, callback, 5000, 2000);
この例では、繰り返しタイマーを開始します。最初の実行は、timeoutとして指定した5秒が、uv_timer_startの実行から経過したときで、その後は、repeatとして指定した2秒ごとに繰り返します。停止には、次を使います。
uv_timer_stop(&timer_req);
これでタイマーを停止できます。コールバック内からも安全に使えます。
繰り返し間隔は、次を使っていつでも変更できます。
uv_timer_set_repeat(uv_timer_t *timer, int64_t repeat);
変更が反映されるのは、可能になった時点です。タイマーのコールバックからこの関数を呼び出した場合は、次のようになります。
繰り返さないタイマーなら、すでに停止しています。
uv_timer_startをもう一度使ってください。繰り返すタイマーなら、次のタイムアウトはすでに予定されています。そのため、以前の繰り返し間隔がもう1回使われてから、新しい間隔に切り替わります。
次のユーティリティ関数は、
int uv_timer_again(uv_timer_t *)
適用対象が、繰り返すタイマーに限られます。タイマーを停止し、最初の、timeoutと、repeatの両方に、以前の、repeatの値を設定して開始するのと同じです。まだ開始していないタイマーでは失敗し、エラーコードは、UV_EINVALで、戻り値は-1になります。
実際のタイマーの例は、参照カウントの節にあります。
イベントループの参照カウント¶
イベントループは、アクティブなハンドルがある間だけ実行されます。この仕組みでは、各ハンドルが開始時にイベントループの参照カウントを増やし、停止時に減らします。次を使って、ハンドルの参照カウントを手動で変更することもできます。
void uv_ref(uv_handle_t*);
void uv_unref(uv_handle_t*);
これらの関数を使えば、ウォッチャーがアクティブでもループを終了でき、独自のオブジェクトでループの実行を維持することもできます。
この使い方は、定期的なタイマーにも役立ちます。X秒ごとに動くガベージコレクターや、他のサービスへ定期的にハートビートを送るネットワークサービスがあっても、正常終了のすべての経路やエラー時にそれらを停止したくない場合があります。また、他のウォッチャーがすべて終了したときにプログラムを終了させたい場合もあります。その場合は、タイマーを作成した直後に参照を外してください。そうすると、実行中のウォッチャーがそのタイマーだけになったときにも、uv_runは終了します。
これはnode.jsでも使われており、libuvの一部のメソッドがJSのAPIとして公開されています。JSの各オブジェクトに対して、uv_handle_tという、すべてのウォッチャーのスーパークラスが作成され、参照の追加と解除ができます。
ref-timer/main.c
1uv_loop_t *loop;
2uv_timer_t gc_req;
3uv_timer_t fake_job_req;
4
5int main() {
6 loop = uv_default_loop();
7
8 uv_timer_init(loop, &gc_req);
9 uv_unref((uv_handle_t*) &gc_req);
10
11 uv_timer_start(&gc_req, gc, 0, 2000);
12
13 // could actually be a TCP download or something
14 uv_timer_init(loop, &fake_job_req);
15 uv_timer_start(&fake_job_req, fake_job, 9000, 0);
16 return uv_run(loop, UV_RUN_DEFAULT);
17}
ガベージコレクターのタイマーを初期化し、すぐに、unrefで参照を外します。9秒後に模擬のジョブが終了すると、ガベージコレクターは動作中でも、プログラムが自動的に終了することに注目してください。
アイドル処理のパターン¶
idleハンドルのコールバックは、イベントループの反復ごとに1回呼び出されます。idleコールバックは、優先度の非常に低い処理にも使えます。たとえば、処理のない時間帯に、アプリケーションの1日の性能の要約を開発者へ送って分析してもらったり、アプリケーションのCPU時間でSETIの計算を行ったりできます :) idleウォッチャーはGUIアプリケーションにも役立ちます。イベントループでファイルをダウンロードしているとしましょう。TCPソケットをまだ確立している途中で、他にイベントがなければ、イベントループは一時停止、つまり、ブロックします。その結果、進捗バーが止まり、アプリケーションが応答しなくなります。そのような場合は、idleウォッチャーを登録してUIの動作を維持します。
idle-compute/main.c
1uv_loop_t *loop;
2uv_fs_t stdin_watcher;
3uv_idle_t idler;
4char buffer[1024];
5
6int main() {
7 loop = uv_default_loop();
8
9 uv_idle_init(loop, &idler);
10
11 uv_buf_t buf = uv_buf_init(buffer, 1024);
12 uv_fs_read(loop, &stdin_watcher, 0, &buf, 1, -1, on_type);
13 uv_idle_start(&idler, crunch_away);
14 return uv_run(loop, UV_RUN_DEFAULT);
15}
ここではidleウォッチャーを初期化し、処理したい実際のイベントとともに登録します。crunch_awayは、ユーザーが何か入力してReturnキーを押すまで繰り返し呼び出されます。その後、ループが入力データを扱う間は短時間中断し、それから再びidleコールバックを呼び続けます。
idle-compute/main.c
1void crunch_away(uv_idle_t* handle) {
2 // Compute extra-terrestrial life
3 // fold proteins
4 // computer another digit of PI
5 // or similar
6 fprintf(stderr, "Computing PI...\n");
7 // just to avoid overwhelming your terminal emulator
8 uv_idle_stop(handle);
9}
10
ワーカースレッドへのデータの受け渡し¶
通常、uv_queue_workを使うときには、複雑なデータをワーカースレッドへ渡す必要があります。解決策は、structを使い、uv_work_t.dataがそれを指すようにすることです。少し異なる方法として、その構造体の最初のメンバーに、uv_work_t自体を置く方法があります。この構造体をバトンと呼びます、[1]。この方法では、1回のfree呼び出しで、作業リクエストとすべてのデータを解放できます。
1struct ftp_baton {
2 uv_work_t req;
3 char *host;
4 int port;
5 char *username;
6 char *password;
7}
1ftp_baton *baton = (ftp_baton*) malloc(sizeof(ftp_baton));
2baton->req.data = (void*) baton;
3baton->host = strdup("my.webhost.com");
4baton->port = 21;
5// ...
6
7uv_queue_work(loop, &baton->req, ftp_session, ftp_cleanup);
ここではバトンを作成し、タスクをキューに入れます。
タスク関数は、必要なデータを取り出せます。
1void ftp_session(uv_work_t *req) {
2 ftp_baton *baton = (ftp_baton*) req->data;
3
4 fprintf(stderr, "Connecting to %s\n", baton->host);
5}
6
7void ftp_cleanup(uv_work_t *req) {
8 ftp_baton *baton = (ftp_baton*) req->data;
9
10 free(baton->host);
11 // ...
12 free(baton);
13}
最後にバトンを解放すると、ウォッチャーも解放されます。
ポーリングによる外部I/O¶
通常、サードパーティーのライブラリは、自身のI/Oを扱い、ソケットや他のファイルを内部で管理します。その場合は標準のストリームI/O操作を使えませんが、libuvのイベントループにライブラリを組み込むことはできます。必要なのは、基になるファイルディスクリプターへアクセスできることと、アプリケーションの判断に従ってタスクを少しずつ進める関数があることだけです。ただし、このようなアクセスを許さず、I/Oの処理全体を行ってから戻る標準的なブロッキング関数だけを提供するライブラリもあります。それをイベントループのスレッドで使うのは適切ではないので、代わりに、スレッドプールでの作業のスケジューリングを使ってください。ただし、その場合は、ライブラリを細かく制御できなくなります。
libuvの、uv_pollの部分は、OSの通知機構を使ってファイルディスクリプターを監視します。ある意味では、libuv自身が実装するI/O操作もすべて、uv_pollと同様のコードを基にしています。ポーリング中のファイルディスクリプターの状態変化をOSが検知すると、libuvが対応するコールバックを呼び出します。
ここでは、libcurlを使ってファイルをダウンロードする単純なダウンロードマネージャーを見ていきます。libcurlにすべての制御を任せる代わりに、libuvのイベントループと、非ブロッキングで非同期の、multiインターフェイスを使い、libuvがI/Oの準備完了を通知するたびにダウンロードを進めます。
uvwget/main.c - 初期設定
1#include <assert.h>
2#include <stdio.h>
3#include <stdlib.h>
4#include <uv.h>
5#include <curl/curl.h>
6
7uv_loop_t *loop;
8CURLM *curl_handle;
9uv_timer_t timeout;
10int main(int argc, char **argv) {
11 loop = uv_default_loop();
12
13 if (argc <= 1)
14 return 0;
15
16 if (curl_global_init(CURL_GLOBAL_ALL)) {
17 fprintf(stderr, "Could not init cURL\n");
18 return 1;
19 }
20
21 uv_timer_init(loop, &timeout);
22
23 curl_handle = curl_multi_init();
24 curl_multi_setopt(curl_handle, CURLMOPT_SOCKETFUNCTION, handle_socket);
25 curl_multi_setopt(curl_handle, CURLMOPT_TIMERFUNCTION, start_timeout);
26
27 while (argc-- > 1) {
28 add_download(argv[argc], argc);
29 }
30
31 uv_run(loop, UV_RUN_DEFAULT);
32 curl_multi_cleanup(curl_handle);
33 return 0;
34}
libuvへ組み込む方法はライブラリによって異なります。libcurlでは、2つのコールバックを登録できます。ソケットのコールバック、handle_socketは、ソケットの状態が変わり、ポーリングを開始する必要が生じるたびに呼び出されます。start_timeoutはlibcurlから呼び出され、次のタイムアウトまでの間隔を通知します。その時間が経過したら、I/Oの状態にかかわらずlibcurlを進めます。これにより、libcurlはエラーを処理したり、ダウンロードを進めるために必要な処理を行ったりできます。
このダウンローダーは、次のように起動します。
$ ./uvwget [url1] [url2] ...
各引数をURLとして追加します。
uvwget/main.c - URLの追加
1
2void add_download(const char *url, int num) {
3 char filename[50];
4 sprintf(filename, "%d.download", num);
5 FILE *file;
6
7 file = fopen(filename, "w");
8 if (file == NULL) {
9 fprintf(stderr, "Error opening %s\n", filename);
10 return;
11 }
12
13 CURL *handle = curl_easy_init();
14 curl_easy_setopt(handle, CURLOPT_WRITEDATA, file);
15 curl_easy_setopt(handle, CURLOPT_URL, url);
16 curl_multi_add_handle(curl_handle, handle);
17 fprintf(stderr, "Added download %s -> %s\n", url, filename);
18}
データはlibcurlからファイルへ直接書き込むようにしていますが、必要であれば、さらに多様な処理もできます。
start_timeoutは、最初はlibcurlから即座に呼び出され、処理が動き始めます。この関数は、libuvの、タイマーを開始するだけです。タイマーが期限に達するたびに、curl_multi_socket_actionに、CURL_SOCKET_TIMEOUTを指定して実行します。curl_multi_socket_actionはlibcurlの処理を進める関数であり、ソケットの状態が変わるたびに呼び出します。ただし、その説明の前に、handle_socketが呼び出されるたびに、ソケットのポーリングを行う必要があります。
uvwget/main.c - ポーリングの設定
1
2void start_timeout(CURLM *multi, long timeout_ms, void *userp) {
3 if (timeout_ms <= 0)
4 timeout_ms = 1; /* 0 means directly call socket_action, but we'll do it in a bit */
5 uv_timer_start(&timeout, on_timeout, timeout_ms, 0);
6}
7
8int handle_socket(CURL *easy, curl_socket_t s, int action, void *userp, void *socketp) {
9 curl_context_t *curl_context;
10 if (action == CURL_POLL_IN || action == CURL_POLL_OUT) {
11 if (socketp) {
12 curl_context = (curl_context_t*) socketp;
13 }
14 else {
15 curl_context = create_curl_context(s);
16 curl_multi_assign(curl_handle, s, (void *) curl_context);
17 }
18 }
19
20 switch (action) {
21 case CURL_POLL_IN:
22 uv_poll_start(&curl_context->poll_handle, UV_READABLE, curl_perform);
23 break;
24 case CURL_POLL_OUT:
25 uv_poll_start(&curl_context->poll_handle, UV_WRITABLE, curl_perform);
26 break;
27 case CURL_POLL_REMOVE:
28 if (socketp) {
29 uv_poll_stop(&((curl_context_t*)socketp)->poll_handle);
30 destroy_curl_context((curl_context_t*) socketp);
31 curl_multi_assign(curl_handle, s, NULL);
32 }
33 break;
34 default:
35 abort();
36 }
37
38 return 0;
39}
対象は、ソケットのファイルディスクリプター、sと、actionです。ソケットごとに、まだ存在していなければ、uv_poll_tハンドルを作成し、curl_multi_assignを使ってソケットに関連付けます。これにより、コールバックが呼び出されるたびに、socketpがそのハンドルを指します。
ダウンロードが完了または失敗すると、libcurlがポーリングの解除を要求します。そこで、ポーリングのハンドルを停止し、解放します。
libcurlが監視したいイベントに応じて、UV_READABLEまたは、UV_WRITABLEでポーリングを開始します。ソケットが読み書き可能になるたびに、libuvがポーリングのコールバックを呼び出します。同じハンドルで、uv_poll_startを複数回呼び出してもかまいません。イベントマスクが新しい値に更新されるだけです。curl_performがこのプログラムの中核です。
uvwget/main.c - libcurlの処理を進める
1void curl_perform(uv_poll_t *req, int status, int events) {
2 uv_timer_stop(&timeout);
3 int running_handles;
4 int flags = 0;
5 if (status < 0) flags = CURL_CSELECT_ERR;
6 if (!status && events & UV_READABLE) flags |= CURL_CSELECT_IN;
7 if (!status && events & UV_WRITABLE) flags |= CURL_CSELECT_OUT;
8
9 curl_context_t *context;
10
11 context = (curl_context_t*)req;
12
13 curl_multi_socket_action(curl_handle, context->sockfd, flags, &running_handles);
14 check_multi_info();
15}
最初にタイマーを停止します。その間に処理が進んだからです。次に、コールバックを引き起こしたイベントに応じて正しいフラグを設定します。それから、処理が進んだソケットと、発生したイベントを示すフラグを指定して、curl_multi_socket_actionを呼び出します。この時点で、libcurlは内部のすべてのタスクを少しずつ処理し、できるだけ早く戻ろうとします。これはイベント駆動のプログラムがメインスレッドに求める動作そのものです。libcurlは、転送の進捗についてのメッセージを自身のキューへ入れ続けます。今回は、完了した転送だけが対象です。そのため、それらのメッセージを取り出し、転送が終了したハンドルを片付けます。
uvwget/main.c - 転送状態の読み取り
1void check_multi_info(void) {
2 char *done_url;
3 CURLMsg *message;
4 int pending;
5
6 while ((message = curl_multi_info_read(curl_handle, &pending))) {
7 switch (message->msg) {
8 case CURLMSG_DONE:
9 curl_easy_getinfo(message->easy_handle, CURLINFO_EFFECTIVE_URL,
10 &done_url);
11 printf("%s DONE\n", done_url);
12
13 curl_multi_remove_handle(curl_handle, message->easy_handle);
14 curl_easy_cleanup(message->easy_handle);
15 break;
16
17 default:
18 fprintf(stderr, "CURLMSG default\n");
19 abort();
20 }
21 }
22}
check・prepareウォッチャー¶
TODO
ライブラリの読み込み¶
libuvは、動的に、共有ライブラリを読み込むためのクロスプラットフォームAPIを提供します。これを使って、独自のプラグイン・拡張・モジュールの仕組みを実装できます。node.jsでも、バインディングでの、require()のサポートに使われています。ライブラリが正しいシンボルを公開していれば、使い方は簡単です。サードパーティーのコードを読み込むときは、整合性と安全性のチェックに注意してください。そうしないと、プログラムの動作が予測できなくなります。この例では、プラグインの名前を表示するだけの非常に単純なプラグインの仕組みを実装します。
まず、プラグインの作成者に提供するインターフェイスを見てみましょう。
plugin/plugin.h
1#ifndef UVBOOK_PLUGIN_SYSTEM
2#define UVBOOK_PLUGIN_SYSTEM
3
4// Plugin authors should use this to register their plugins with mfp.
5void mfp_register(const char *name);
6
7#endif
同様に、プラグインの作成者がアプリケーションで役立つ処理を行えるよう、関数をさらに追加できます、[2]。このAPIを使うプラグインの例は次のとおりです。
plugin/hello.c
1#include "plugin.h"
2
3void initialize() {
4 mfp_register("Hello World!");
5}
このインターフェイスでは、すべてのプラグインに、アプリケーションから呼び出される、initialize関数が必要と定めています。このプラグインは共有ライブラリとしてコンパイルされ、次のようにアプリケーションを起動すると読み込めます。
$ ./plugin libhello.dylib
Loading libhello.dylib
Registered plugin "Hello World!"
注記
共有ライブラリのファイル名はプラットフォームによって異なります。Linuxでは、libhello.soです。
まず、uv_dlopenを使って共有ライブラリ、libhello.dylibを読み込みます。その後、initialize関数を、uv_dlsymを使って取得し、呼び出します。
plugin/main.c
1#include "plugin.h"
2
3typedef void (*init_plugin_function)();
4
5void mfp_register(const char *name) {
6 fprintf(stderr, "Registered plugin \"%s\"\n", name);
7}
8
9int main(int argc, char **argv) {
10 if (argc == 1) {
11 fprintf(stderr, "Usage: %s [plugin1] [plugin2] ...\n", argv[0]);
12 return 0;
13 }
14
15 uv_lib_t lib;
16 while (--argc) {
17 fprintf(stderr, "Loading %s\n", argv[argc]);
18 if (uv_dlopen(argv[argc], &lib)) {
19 fprintf(stderr, "Error: %s\n", uv_dlerror(&lib));
20 continue;
21 }
22
23 init_plugin_function init_plugin;
24 if (uv_dlsym(&lib, "initialize", (void **) &init_plugin)) {
25 fprintf(stderr, "dlsym error: %s\n", uv_dlerror(&lib));
26 continue;
27 }
28
29 init_plugin();
30 }
31 return 0;
32}
uv_dlopenは共有ライブラリのパスを受け取り、不透明な、uv_lib_tポインターを設定します。成功時は0、エラー時は-1を返します。エラーメッセージの取得には、uv_dlerrorを使います。
uv_dlsymは、第2引数で指定したシンボルへのポインターを、第3引数へ格納します。init_plugin_functionは、アプリケーションのプラグインで必要とする種類の関数を指す関数ポインターです。
TTY¶
テキスト端末は古くから、おおむね標準化されたコマンド群による基本的な書式設定をサポートしています。この書式設定は、端末出力の読みやすさを改善するために、よく使われます。たとえば、grep --colourです。libuvは、uv_tty_tというストリームの抽象化と関連関数を提供し、すべてのプラットフォームでANSIエスケープコードを実装します。つまり、libuvはANSIコードをWindowsでの対応する処理に変換し、端末情報を取得する関数も提供します。
最初に、読み書きするファイルディスクリプターを使って、uv_tty_tを初期化します。次の関数を使います。
int uv_tty_init(uv_loop_t*, uv_tty_t*, uv_file fd, int unused)
現在、unusedパラメーターは自動検出され、無視されます。以前は、そのストリームで、uv_read_start()を使うために設定する必要がありました。
その後、uv_tty_set_modeを使い、モードを、通常に設定するとよいでしょう。これにより、TTYの多くの書式設定、フロー制御、その他の設定が有効になります。他のモードもあります。
プログラムを終了するときは、uv_tty_reset_modeを呼び出して端末の状態を復元してください。これは基本的な配慮です。リダイレクトへの配慮も必要です。ユーザーがコマンドの出力をファイルにリダイレクトした場合、制御シーケンスを書き込むと読みやすさや、grepの処理を妨げるので、書き込まないようにします。ファイルディスクリプターが実際にTTYかどうかを確認するには、それを指定して、uv_guess_handleを呼び出し、戻り値を、UV_TTYと比較します。
赤い背景に白い文字を表示する単純な例を示します。
tty/main.c
1#include <stdio.h>
2#include <string.h>
3#include <unistd.h>
4#include <uv.h>
5
6uv_loop_t *loop;
7uv_tty_t tty;
8
9int main() {
10 uv_write_t req;
11 uv_buf_t buf;
12 uv_write_t req1;
13 uv_buf_t buf1;
14
15 loop = uv_default_loop();
16 uv_tty_init(loop, &tty, STDOUT_FILENO, 0);
17 uv_tty_set_mode(&tty, UV_TTY_MODE_NORMAL);
18
19 if (uv_guess_handle(1) == UV_TTY) {
20 buf1.base = "\033[41;37m";
21 buf1.len = strlen(buf1.base);
22 uv_write(&req1, (uv_stream_t*) &tty, &buf1, 1, NULL);
23 }
24
25 buf.base = "Hello TTY\n";
26 buf.len = strlen(buf.base);
27 uv_write(&req, (uv_stream_t*) &tty, &buf, 1, NULL);
28
29 uv_tty_reset_mode();
30 return uv_run(loop, UV_RUN_DEFAULT);
31}
最後のTTY補助関数は、uv_tty_get_winsize()です。端末の幅と高さを取得し、成功時は、0を返します。この関数と、文字位置のエスケープコードを使ってアニメーションを行う、小さなプログラムを示します。
tty-gravity/main.c
1#include <stdio.h>
2#include <string.h>
3#include <unistd.h>
4#include <uv.h>
5
6uv_loop_t *loop;
7uv_tty_t tty;
8uv_timer_t tick;
9uv_write_t write_req;
10int width, height;
11int pos = 0;
12char *message = " Hello TTY ";
13
14void update(uv_timer_t *req) {
15 char data[500];
16
17 uv_buf_t buf;
18 buf.base = data;
19 buf.len = sprintf(data, "\033[2J\033[H\033[%dB\033[%luC\033[42;37m%s",
20 pos,
21 (unsigned long) (width-strlen(message))/2,
22 message);
23 uv_write(&write_req, (uv_stream_t*) &tty, &buf, 1, NULL);
24
25 pos++;
26 if (pos > height) {
27 uv_tty_reset_mode();
28 uv_timer_stop(&tick);
29 }
30}
31
32int main() {
33 loop = uv_default_loop();
34
35 uv_tty_init(loop, &tty, STDOUT_FILENO, 0);
36 uv_tty_set_mode(&tty, 0);
37
38 if (uv_tty_get_winsize(&tty, &width, &height)) {
39 fprintf(stderr, "Could not get TTY information\n");
40 uv_tty_reset_mode();
41 return 1;
42 }
43
44 fprintf(stderr, "Width %d, height %d\n", width, height);
45 uv_timer_init(loop, &tick);
46 uv_timer_start(&tick, update, 200, 200);
47 return uv_run(loop, UV_RUN_DEFAULT);
48}
エスケープコードは次のとおりです。
コード |
意味 |
|---|---|
2 J |
画面の一部を消去します。2では画面全体を消去します。 |
H |
カーソルを指定位置へ移動します。既定は左上です。 |
n B |
カーソルをn行下へ移動します。 |
n C |
カーソルをn列右へ移動します。 |
m |
表示設定の文字列に従います。この例では、背景を緑(40+2)、文字を白(30+7)にします。 |
このように、見やすく整えた出力を作るのに便利で、好みであれば、コンソールのアーケードゲームにも使えます。より高度な制御には、ncursesも試せます。
バージョン1.23.1での変更: 現在、readableパラメーターは未使用で、無視されます。適切な値がカーネルから自動検出されます。
この文脈でバトンという用語に初めて触れたのは、Konstantin Käferによるnode.jsのバインディングの書き方についての優れたスライドです。https://kkaefer.github.io/node-cpp-modules/#baton(動画)
mfpはMy Fancy Pluginの略です。