Hello, DSAPI!
DSAPI(Domino WebServer API)は、DominoサーバのHTTPタスクにアドインするためのAPI。 今回は、これでHelloアプリを作る。
することは2つ。
1つは、インストールに成功したら、DominoのログにHello, DSAPI Init!
の表示をする。
もう1つは、ブラウザから/hello
としたらHellp. DSAPI Filter!
の表示をする。
#include <dsapi.h> #include <string> extern "C" __declspec(dllexport) unsigned int FilterInit(FilterInitData *pInitData) { pInitData->appFilterVersion = kInterfaceVersion; pInitData->eventFlags = kFilterParsedRequest; strcpy_s(pInitData->filterDesc, "Hello, DSAPI Init!"); return kFilterHandledEvent; } extern "C" __declspec(dllexport) unsigned int HttpFilterProc(FilterContext* ctx, unsigned int eventType, void*) { try { unsigned int errId = 0; switch (eventType) { case kFilterParsedRequest: FilterRequest req; ctx->GetRequest(ctx, &req, &errId); if (errId != 0) throw errId; if (req.method == kRequestGET && std::strncmp(req.URL, "/hello", 6) == 0) { char content[] = "HTTP/1.1 200 OK\n" "Content-Type: text/plain; charset=utf8\n" "\n" "Hello, DSAPI Filter!"; ctx->WriteClient(ctx, content, strlen(content), 0, &errId); if (errId != 0) throw errId; return kFilterHandledRequest; } break; default: return kFilterNotHandled; } } catch (...) { return kFilterError; } }
- コンパイルして、DLLを作成する。例えば、
hello
というアドインであれば、nhello.dll
という名前でDLLを作る。頭文字のn
はWindows用のプリフィックス。 nhello.dll
をDominoサーバのプログラムディレクトリにコピーする(他に必要なモジュールも同様にプログラムディレクトリに置く)。- サーバ文書かWeb設定のDSAPI欄に、
hello
と記述する(n
と.dll
は書かない)。 - Dominoサーバを再起動する。
すると、Dominoコンソールには以下のように表示される。
続いて、ブラウザから次のようなリクエストを送る。
http://localhost/hello
すると、以下のようなレスポンスが返ってくる。
自分用メモ: Notes C APIアプリをマルチプラットフォーム開発するときのマクロ定義
- Windows 32ビット版
-DNT -DW32 -DW -D_X86_ -DND32 -DDTRACE -D_CRT_SECURE_NO_WARNINGS -DPRODUCTION_VERSION -DDUMMY
- Windows 64ビット版
-DNT -DW32 -DW -DW64 -DND64 -D_AMD64_ -DDTRACE -D_CRT_SECURE_NO_WARNINGS -DND64SERVER -DPRODUCTION_VERSION -DDUMMY
- MaxOSX版
-DNO_NULL_VTABLE_ENTRY -DMAC -DMAC_OSX -DMAC_CARBON -D__CF_USE_FRAMEWORK_INCLUDES__ -DLARGE64_FILES -DHANDLE_IS_32BITS -DTARGET_API_MAC_CARBON -DPRODUCTION_VERSION -DOVERRIDEDEBUG // オフィシャルではないが、これも必要 -DLONGIS64BIT
- Linux 32ビット版
-DGCC3 -DGCC4 -DGCC_LBLB_NOT_SUPPORTED -DUNIX -DLINUX -DLINUX86 -DW -DW32 -DDTRACE -DPTHREAD_KERNEL -D_REENTRANT -DUSE_THREADSAFE_INTERFACES -D_POSIX_THREAD_SAFE_FUNCTIONS -DHANDLE_IS_32BITS -DHAS_IOCP -DHAS_BOOL -DHAS_DLOPEN -DUSE_PTHREAD_INTERFACES -DLARGE64_FILES -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -DPRODUCTION_VERSION -DOVERRIDEDEBUG
- Linux 64ビット版
-DGCC3 -DGCC4 -DGCC_LBLB_NOT_SUPPORTED -DUNIX -DLINUX -DLINUX86 -DND64 -DW32 -DLINUX64 -DW -DLINUX86_64 -DDTRACE -DPTHREAD_KERNEL -D_REENTRANT -DUSE_THREADSAFE_INTERFACES -D_POSIX_THREAD_SAFE_FUNCTIONS -DHANDLE_IS_32BITS -DHAS_IOCP -DHAS_BOOL -DHAS_DLOPEN -DUSE_PTHREAD_INTERFACES -DLARGE64_FILES -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -DNDUNIX64 -DLONGIS64BIT -DPRODUCTION_VERSION -DOVERRIDEDEBUG
Qtプロジェクトインクルードファイル(*.pri)
// ../platform.pri (for Windows) NotesCAPIPath = C:/Users/myhome/notesapi901
// ../platform.pri (for MacOS) NotesCAPIPath = /Users/myhome/notesapi901 NotesLibsPath = '/Applications/IBM Notes.app/Contents/MacOS’
// ../platform.pri (for Linux) NotesCAPIPath = /home/myhome/notesapi901 NotesLibsPath = /opt/ibm/notes
Qt親プロジェクトファイル(*.pro)
// myproject.pro TEMPLATE = subdir // サブディレクトリプロジェクト CONFIG += ordered // 順番にビルドする SUBDIRS += myshare myapp
Qt子プロジェクトファイル(*.pro)
// myshare/myshare.pro and myapp/myapp.pro include(../../platform.pri) DEFINES += PRODUCTION_VERSION !macx { DEFINES += W32 W DTRACE } !win32 { DEFINES += HANDLE_IS_32BITS LARGE64_FILES OVERRIDEDEBUG } win32 { DEFINES += NT _CRT_SECURE_NO_WARNINGS DUMMY QMAKE_CXXFLAGS += -wd4503 -wd4005 contains(QMAKE_TARGET.arch, x86_64) { DEFINES += W64 ND64 _AMD64_ ND64SERVER NotesLibsPath = $$NotesCAPIPath/lib/mswin64 } else { DEFINES += ND32 _X86_ NotesLibsPath = $$NotesCAPIPath/lib/mswin32 } } else:macx { DEFINES += MAC MAC_OSX MAC_CARBON NO_NULL_VTABLE_ENTRY __CF_USE_FRAMEWORK_INCLUDES__ TARGET_API_MAC_CARBON DEFINES += LONGIS64BIT } else:unix { DEFINES += UNIX LINUX LINUX86 GCC3 GCC4 GCC_LBLB_NOT_SUPPORTED PTHREAD_KERNEL _REENTRANT USE_THREADSAFE_INTERFACES _POSIX_THREAD_SAFE_FUNCTIONS HAS_IOCP HAS_BOOL HAS_DLOPEN USE_PTHREAD_INTERFACES _LARGEFILE_SOURCE _LARGEFILE64_SOURCE contains(QMAKE_TARGET.arch, x86_64) { DEFINES += ND64 LINUX64 LINUX86_64 NDUNIX64 LONGIS64BIT } LIBS += -Wl,-rpath,$$NotesLibsPath } INCLUDEPATH += $$NotesCAPIPath/include DEPENDPATH += $$NotesCAPIPath/include LIBS += -lnotes -L$$NotesLibsPath
www.slideshare.net
自分用メモ: Qtコマンドラインアプリのmain.cppとNotes C APIの初期化
典型的なQtコマンドラインアプリのmain.cppの書き方。
#include <QCoreApplication> int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); // 処理 return 0; }
Qtのイベントループをコマンドラインでも使いたい場合、QTimer::singleShot
が使える。
qApp->exit(0)
を使わずにapp.exec()
を呼び出すと、イベントループを抜け出せなくなるので注意する。
#include <QCoreApplication> #include <QObject> #include <QTimer> class DoSomething : public QObject { Q_OBJECT public: slots: void run() { // 処理 qApp->exit(0); } }; int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); DoSomething doSomething; QTimer::singleShot(0, &doSomething, SLOT(run())); return app.exec(); }
Qt5とC++11を使うと、ラムダ式で呼び出せるようになり、記述が楽になる。
#include <QCoreApplication> #include <QTimer> int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QTimer::singleShot(0, [&]() { // 処理 app.exit(0); }); return app.exec(); }
これに、Notes C APIの初期化を組み合わせると、こんな感じになる。
#include <QCoreApplication> #include <QTimer> #ifdef NT #pragma pack(push, 1) #endif #include <global.h> #ifdef NT #pragma pack(pop, 1) #endif int main(int argc, char *argv[]) { STATUS status = NotesInitExtended(argc, argv); if (ERR(status) != NOERROR) return 1; QCoreApplication app(argc, argv); QTimer::singleShot(0, [&]() { // 処理 app.exit(0); }); int result = app.exec(); NotesTerm(); return result; }
NCAPI & C++11+ & RX & Qt #11 ~LMBCS対応文字列クラス
前回の続きになります。
ソースコードはこちらで、git clone
後にgit checkout v0.0.3
をしてください。
QByteArray
QStringがワイド文字列(UTF-16)を扱うクラスであるならば、QByteArrayはシングルバイト、マルチバイト文字列を扱うクラスです。ただ、その性質上バイナリコンテナとしても扱えるので、とても重宝します。
なぜこのクラスをこのタイミングで紹介するかというと、LMBCS自体を何で管理しようかと考えたときに、QByteArrayが適しているんじゃないかな~というわけです。
notespp::TranslateLmbcsクラス
Notes C API関数OSTranslate
をラップするクラスです。名前にLmbcs
と付けてしまっていますが、LMBCS以外にもOSTranslate
関数は変換モードを持っているので、将来的にはリネームする可能性大ですww。
// ヘッダー部 /** * @brief LMBCS変換関数オブジェクトの基底クラス */ class TranslateLmbcs { public: /** * @brief コンストラクタ * @param bufferSize バッファサイズ */ TranslateLmbcs(WORD bufferSize = MAXWORD - 1); /** * @brief 文字列を指定したモードで変換する。 * @param mode 変換モード * @param source 変換元の文字列 * @return 変換後の文字列 */ QByteArray translate(WORD mode, const QByteArray &source) const; protected: WORD bufferSize_; }; // ソース部 TranslateLmbcs::TranslateLmbcs(WORD bufferSize) : bufferSize_(bufferSize) {} QByteArray TranslateLmbcs::translate( WORD mode, const QByteArray &source ) const { CharArrayPtr buffer(new char[bufferSize_]); WORD len = OSTranslate( mode , source.constData() , static_cast<WORD>(source.size()) , buffer.data() , bufferSize_ ); return QByteArray(buffer.data(), static_cast<int>(len)); }
CharArrayPtr
CharArrayPtr
は別名で、本来は次のようなシグネチャを持っています。
/** * @brief 文字列配列用スコープドスマートポインタ */ using CharArrayPtr = QScopedPointer<char, QScopedPointerArrayDeleter<char>>;
QScopedPointer
は、Qt版std::unique_ptr
です。std::unique_ptr
はC++11からの仕様です。スマートポインタと呼ばれる、割り当てたメモリをプログラマに代わって解放してくれる賢いヤツです。char
だけなら簡単な記述が、配列になるとこんな長いシグネチャになってしまうので、短い別名を付けました。変換後の文字列のバッファ用に使用しています。
notespp::LmbcsToUnicode、notespp::UnicodeToLmbcs関数オブジェクト
TranslateLmbcs
を継承して、読んで字のごとく、LMBCS文字列からUnicode文字列へ、Unicode文字列からLMBCS文字列へ変換してくれます。
// ヘッダー部 /** * @brief LMBCS文字列からUnicode(UTF-16)に変換する関数オブジェクト */ class NOTESPPSHARED_EXPORT LmbcsToUnicode : public TranslateLmbcs { public: LmbcsToUnicode(WORD bufferSize = MAXWORD - 1); QByteArray operator ()(const QByteArray &source) const; }; /** * @brief Unicode(UTF-16)からLMBCS文字列に変換する関数オブジェクト */ class NOTESPPSHARED_EXPORT UnicodeToLmbcs : public TranslateLmbcs { public: UnicodeToLmbcs(WORD bufferSize = MAXWORD - 1); QByteArray operator ()(const QByteArray &source) const; }; // ソース部 LmbcsToUnicode::LmbcsToUnicode(WORD bufferSize) : TranslateLmbcs(bufferSize) {} QByteArray LmbcsToUnicode::operator ()(const QByteArray &source) const { return translate(OS_TRANSLATE_LMBCS_TO_UNICODE, source); } UnicodeToLmbcs::UnicodeToLmbcs(WORD bufferSize) : TranslateLmbcs(bufferSize) {} QByteArray UnicodeToLmbcs::operator ()(const QByteArray &source) const { return translate(OS_TRANSLATE_UNICODE_TO_LMBCS, source); }
notespp::Stringクラス
以上を踏まえて、LMBCS対応文字列クラス、notespp::String
の実装を見てみましょう。
// ヘッダー部 /** * @brief LMBCS文字列 * @class String */ class NOTESPPSHARED_EXPORT String { public: /** * @brief デフォルトコンストラクタ */ String(); /** * @brief バイナリから作成するコンストラクタ * @param pData LMBCS文字列へのポインタ * @param size LMBCS文字列のバイト長、ヌル終端していれば省略可能 */ String(const char *pData, int size = -1); /** * @brief QStringに変換する。 * @return 変換したQString */ QString toQString() const; /** * @brief 文字数を返す。 * @return LMBCS文字列の文字数(バイト数ではない) */ int charSize() const; /** * @brief バイナリデータへのポインタを返す。 * @return バイナリデータへのポインタ */ const char *constData() const; /** * @brief QStringからString文字列を作成する。 * @param qstr 変換するQStringオブジェクト * @return 変換したStringオブジェクト */ static String fromQString(const QString &qstr); private: QByteArray bytes_; }; // ソース部 String::String() : bytes_() {} String::String(const char *pData, int size) : bytes_(pData, size) {} QString String::toQString() const { // 必要なバッファサイズを割り出す。 WORD bufferSize = static_cast<WORD>(std::min<int>( charSize() * sizeof(ushort), static_cast<int>(MAXWORD - 1) )); // 関数オブジェクトLmbcsToUnicodeでUnicode(UTF-16)に変換する。 QByteArray rawUnicode = LmbcsToUnicode(bufferSize)(bytes_); // バイナリ状のUTF-16からQStringオブジェクトを作成する。 return QString::fromUtf16( reinterpret_cast<ushort*>(const_cast<char*>(rawUnicode.constData())), rawUnicode.size() / sizeof(ushort) ); } int String::charSize() const { QByteArray s = bytes_.left(MAXWORD - 1); WORD numChars = 0; NLS_STATUS status = NLS_string_chars( reinterpret_cast<const BYTE*>(s.constData()) , NLS_NULLTERM , &numChars , OSGetLMBCSCLS() ); if (status != NLS_SUCCESS) return -1; return static_cast<int>(numChars); } const char *String::constData() const { return bytes_.constData(); } String String::fromQString(const QString &qstr) { // 必要なバッファサイズを割り出す。 WORD bufferSize = static_cast<WORD>(std::min<int>( qstr.size() * 3, static_cast<int>(MAXWORD - 1) )); // QStringをUnicode(UTF-16)にする。 QByteArray rawUnicode( reinterpret_cast<const char*>(qstr.utf16()), qstr.size() * 2 ); // LMBCS化してStringオブジェクトに変換して返す。 QByteArray rawLmbcs = UnicodeToLmbcs(bufferSize)(rawUnicode); return String(rawLmbcs.constData(), rawLmbcs.size()); }
NCAPI & C++11+ & RX & Qt #10 ~LMBCS
はじめに
タイトル、短縮しました。とにかく長かった。
僭越ながら、名著をアップデート
私にとってのバイブル的存在、「Notes/Domino APIプログラミング」(2000年 津田義史著)を改めて紹介します。
Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミング
- 作者: 津田義史
- 出版社/メーカー: オーム社
- 発売日: 2000/03
- メディア: 単行本
- 購入: 1人 クリック: 18回
- この商品を含むブログを見る
Notes C APIに携わる数少ない(であろう)日本人の一人として、18年経った今でも拠り所となる本です。とはいえ、です。そろそろアップデートしないと厳しいところも出てきているのも事実です。この本で学んだことを元に、お気に入りのフレームワークQtと、手になじんでいる範囲でのC++11以降のスタイルと、今年お気に入りとなったReactiveXを混ぜ込んで、私なりのアップデートを試みていきたいと思います。
LMBCS
このブログでは何度か取り上げている、Notes/Domino独自のマルチバイト文字セット「LMBCS(リンビックス)」。前著にも「2.7 LMBCS文字コードと日本語処理」として、初期化/終了処理、マルチスレッドに続く早い段階でAPIが紹介されています。このLMBCSの処理方法は、APIでプログラムを書く上でも早々に確立しておかないと、何もできなくなってしまいます。
シフトJIS
日本語Windowsの文字コードは長らくシフトJISでしたが、最近ではUnicode(UTF-16)が使われているらしいです。WindowsのAPIでもchar
型(シングルバイト文字列とシフトJISなどのマルチバイト文字列)とwchat_t
型(ワイド文字列)と、同じAPIでも扱う文字列によって2種類の関数が用意されています(前者をANSI版、後者をUnicode版と呼称します)。VCでは両者を区別して書かなくてもいいように、_TCHAR
型という型を用意して、宣言次第でどちらでも扱えるようにしています。ただし、このANSI版とUnicode版はまったく機能差がないわけではないようで、例えばパス文字列などの最大文字数などで、ANSI版では短いパス長しか扱えないことがあります。
なお、前著では、LMBCSとシフトJISを、std::string
上で相互変換する関数を紹介しています。
UnicodeとQt文字列
WindowsでもUnicode(UTF-16)が優位になってきていますが、マルチプラットフォームC++フレームワークであるQtでは、文字列はどのように扱っているのでしょうか。最近のUnix系(Linux、Mac)はUnicodeでもUTF-8が主流のようですが、Qtが扱う文字列クラスQString
の内部文字コードは、ずいぶん前からUTF-16のようです。シフトJISもUTF-8も、文字によって長さが変わる文字コードは扱いにくいんでしょうか。
ただ、Notes C APIから受け取る文字列をQString
に統一しておくと、マルチプラットフォームでの開発はとても楽になります。また、Notes C API関数には、LMBCSとUnicode(UTF-16)との相互変換関数が用意されているので、LMBCS文字列をラップするクラスと、QString
と相互に変換できるメソッドを用意することにします。
NLS
前著では、C++側の文字列をシフトJISとしていたので、変換時に扱うバイト数は、「半角カナ文字1バイトをLMBCSにすると3バイトになる」という法則をもって用意するバッファの大きさを決めていました。原因は、LMBCS、シフトJISともに1文字当たりのバイト数が変化するところでしょう。
QString
は1文字当たりのバイト数は2バイトと固定です。課題はLMBCSということになります。前著では紹介されませんでしたが、現在Notes C APIには「NLS(National Language Service)」というものがあり(以前紹介しました)、LMBCSを含む対応文字セットの文字数を数える機能があります(NLS_string_chars
関数)。これを使えば、LMBCS→Unicode変換時にムダのないバッファサイズを決めることができます。ただしUnicode→LMBCS変換時は、相変わらず推測計算するしかないですが。
LMBCS→文字数を取得→2倍にする→QString(Unicode)バイト数が判明
QString(Unicode)→(半角英数0.5倍、全角1.5倍、半角カナ1.5倍)→文字数×2バイト×1.5→LMBCS推定バイト数を算出
(続く)
Notes C API & C++11+ & ReactiveX & Qt #9 ~RxでNotes C API関数をラップ(実装編)
前回の続きになります。
ソースコードは、いつものようにこちらをgit clone
して、git checkout v0.0.2
してください。
実装コード
前回目標にした、「Notes C API関数をRx的にラップする」実装側です。
rx::observable<GetServerLatency::ReturnValues> GetServerLatency::operator ()( const QByteArray &serverName, DWORD timeout ) { return rx::observable<>::create<GetServerLatency::ReturnValues>( [this, &serverName, &timeout](rx::subscriber<GetServerLatency::ReturnValues> o) { try { Status status = NSFGetServerLatency( const_cast<char*>(serverName.constData()), timeout, clientToServer_.pValue(), serverToClient_.pValue(), version_.pValue() ); if (status.hasError()) throw status; GetServerLatency::ReturnValues values; values.version_ = version_.value(); values.clientToServer_ = clientToServer_.value(); values.serverToClient_ = serverToClient_.value(); o.on_next(values); o.on_completed(); } catch (...) { o.on_error(std::current_exception()); } }); }
GetServerLatency::ReturnValues
について補足します。これは、GetServerLatencyクラス内で定義されている内部構造体です。
/** * @brief 出力用データ型 * @struct ReturnValues */ struct ReturnValues { WORD version_; // サーババージョン DWORD clientToServer_; // クライアントからサーバへの待ち時間 DWORD serverToClient_; // サーバからクライアントへの待ち時間 };
Notes C API関数のNSFGetServerLatency
が返す4つの値の内、STATUS値を除く3つの値を1個のデータとして扱えるようにした構造体です。ReactiveXでは、原則流す値は1個です。複数の値を流すときは配列や連想配列、構造体やクラス、タプルなどのコンテナにまとめます。このReturnValues
構造体もそのためです。
さて、かっこ演算子をオーバーロードしたこのメソッドは、このReturnValues
を呼び出し元に返すのが目的ですが、実際には次のように定義されています。
rx::observable<GetServerLatency::ReturnValues>
rx
は名前空間の別名で、実際にはrxcpp
とrxcpp::operators
を表しています。nx::observable
の実際のシグネチャはrxcpp::observable
になります。ここでは、「ReturnValues
型のデータを扱うコンテナのようなもの」程度に思っていてください。
次にメソッド内を見てみると、いきなりreturn
文が書かれていますが、引数を省略すると、次のようになります。
return rx::observable<>::create<GetServerLatency::ReturnValues>(/*引数*/);
実際にはたった1文しかない実装になります。observable::create
メソッドが、「引数を元にobservable
(監視可能)なものを作って返す」ということですね。rx::observable::create<ReturnValues>
は、ReturnValues
型のデータを流す「水源」を作るメソッドです。「水源」から流れた川を流れるのは「水」ですから、observable::create<ReturnValues>
から流れてくるのはReturnValues
です。
まだちんぷんかんぷんかもしれませんが、今はまだなんとなくでいいです。
では、ReturnValues
をどうやって流すのか、それを決めるのがcreate
に渡される「引数」です。引数にはロジック(関数やラムダ式)を渡します。いろいろ簡略化すると、以下のようなことになります。
create((subscriber o) { try { o.on_next(/* ReturnValues型の値 */); o.on_completed(); } catch (...) { o.on_error(/* 例外 */); } });
ロジックの引数にはrx::subscriber<ReturnValues>
型なる値が渡されます。subscriber
を直訳すると、「加入者」とか「愛読者」とか出てきますが、Rxの世界ではsubscribe
(購読する)という言葉を使うので、個人的には「購読者」という理解が近いと思っています。購読者にon_next
メソッドを使ってReturnValues
型の値を流すと、購読者は値を読めるということになります。
整理してみましょう。冒頭のソースコードに表されているのは、以下のようなことになります。
NSFGetServerLatency
を実行する。- ステータス値が正常でなければ、
subscriber.on_error
を呼び出す。 - ステータス値が正常であれば、出力値を
ReturnValues
にまとめ、subscriber.on_next
で流す。
以上のロジックを持つ・・・
rx::observable
オブジェクトを作成して返す。
ロジックを目の当たりにしてしまうので、Rxや関数型プログラミングに慣れていない人はよく混乱するんですが、observable::create
がしていることは、あくまでobservable
オブジェクトを返すことだけです。ロジックは一切実行されていません。
nx::GetServerLatency()(pServer);
と実行しても、NSFGetServerLatency
関数は実行されません。前述のコードは、もともとこういう意味になります。
rx::observable<ReturnValues> ob = nx::GetServerLatency()(pServer);
変数ob
が作成されただけです。NSFGetServerLatency
を含んだロジックはどこにいったのか?そうです。このob
変数にしまわれています。では、どうしたらこのob
変数にしまわれてしまったロジックを実行できるのか?それが、先程も出てきたsubscribe
(購読する)メソッドです。先程のob
変数では、次のようにします。
ob.subscribe(/* ReturnValuesを受け取りロジック */);
メソッドsubscribe
は、observable
オブジェクトが持っているロジックを活性化して、データを流す役割があり、流れてきたデータは、引数に指定された受け取りロジックに渡します。ソースコードncl/main.cpp
ではこのように書いています。
nx::GetServerLatency getServerLatency(true, true, true); getServerLatency(pServer, 0).subscribe( [&pServer](nx::GetServerLatency::ReturnValues values) { // 標準出力 QTextStream out(stdout, QIODevice::WriteOnly); out << QObject::tr("Build version of '%1'").arg(pServer) << ": " << values.version_ << endl << QObject::tr("Latency time for client to server") << ": " << QString("%1 ms").arg(values.clientToServer_) << endl << QObject::tr("Latency time for server to client") << ": " << QString("%1 ms").arg(values.serverToClient_) << endl; } , [](std::exception_ptr ep) { try {std::rethrow_exception(ep);} catch (nx::Status status) { // 標準エラー出力 QTextStream out(stderr, QIODevice::WriteOnly); out << QObject::tr("NSFGetServerLatency status") << ": " << status.error() << endl; } });
抜き出して簡略化すると、こうなります。
nx::GetServerLatency()(pServer).subscribe(/* 正常ロジック */, /* 異常ロジック */);
pServer
でサーバ名を渡した時点では、NSFGetServerLatency
は実行しておらず、そこで作られたobservable<ReturnValues>
型のオブジェクトで、subscribe
したときに実行されます。活性化されたロジックは、NSFGetServerLatency
関数を実行し、正常に実行できれば、ReturnValues
構造体を作り、subscriber
(購読者)のon_next
メソッドに渡します。subscriber
は、いわばsubscribe
メソッドが放った「エージェント」の様な存在で、on_next
に渡された値は、結局のところsubscribe
メソッドの引数「正常ロジック」に渡されます。
ついていけていませんか?心配ありません。今までのパラダイムでプログラミングしていれば、こんなパラダイムをすぐ理解できる方がまれじゃないでしょうか。今は、「subscribe
して初めてロジック全体が実行される」ということだけ覚えていただければよいです。
回りくどいですか?ひどく同感です。しかし、こうすることで実際には多くの恩恵が潜んでいるらしいです。私自身は、エラー処理が楽になるとか、ロジックの変更がしやすいとか、単体テストがしやすいとか、同期/非同期の差がなくなるとか、まだまだその程度の実感です。
ReactiveXは、理屈をいくら重ねても理解しにくいかもしれませんが、実装例を見て「こうしたらこうなる」「こうしてみたらこう変わる」というのを体感していく方がよいでしょう。ReactiveXが実装されている言語はJava、JavaScript、.NET、Rubyなど多くの言語で展開されています。自身が得意な言語で、新しいパラダイムに触れてみてはいかがでしょうか。
P.S. あいにくLotusScriptのReactiveXは、私が個人的に実装した例以外には見当たらないようですが。(^^;)どこかでお披露目できればと思っています。
Notes C API & C++11+ & ReactiveX & Qt #8 ~RxでNotes C API関数をラップ
お題「Notes C API関数をReactiveXでラップする」
今回はこのお題について考えます。なお、今回のコードは、前回ご紹介した以下のサイトを使います。
Gitを使ってクローンして、v0.0.2
をチェックアウトします。
git clone https://github.com/Chiburu/notespp.git
cd notespp
git checkout v0.0.2
そもそもReactiveXとは?
という方は、ぜひとも「ReactiveXとは?」でググってみてください。つい3~4ヶ月前まで「ReactiveX」の「り」の字も知らなかった私を含め、デザインパターン「Observer」や「Iterator」、関数型プログラミングなどに触れたことがない人にとっては、「ググってもわからない」というのが正直なところでしょう。わからなかったことがなんとなくわかるようになったことは、この連載の#1でも触れています。あの記事から2ヶ月が経ちましたが、わからないことはまだまだ多い!・・・ですが、もうRxなしは考えられない状況です。先日はLotusScriptでもRxっぽいライブラリを作ってしまいました。この連載でも「なんとなく使ってみると、こんな風になりました」という感じで紹介できたらと思っています。
目標コード
今回の目標は、以下のコードを・・・
WORD serverVersion = 0; DWORD clientToServer_ms, serverToClient_ms; STATUS status = NSFGetServerLatency( const_cast<char*>(pServer), 0, &clientToServer_ms, &serverToClient_ms, &serverVersion ); out << QObject::tr("NSFGetServerLatency status") << ": " << ERR(status) << endl; if (ERR(status) != NOERROR) { return 1; } out << QObject::tr("Build version of '%1'").arg(pServer) << ": " << serverVersion << endl << QObject::tr("Latency time for client to server") << ": " << QString("%1 ms").arg(clientToServer_ms) << endl << QObject::tr("Latency time for server to client") << ": " << QString("%1 ms").arg(serverToClient_ms) << endl;
次のようにすることです。
nx::GetServerLatency()(pServer, 0).subscribe( [&pServer](nx::GetServerLatency::ReturnValues values) { QTextStream out(stdout, QIODevice::WriteOnly); out << QObject::tr("Build version of '%1'").arg(pServer) << ": " << values.version_ << endl << QObject::tr("Latency time for client to server") << ": " << QString("%1 ms").arg(values.clientToServer_) << endl << QObject::tr("Latency time for server to client") << ": " << QString("%1 ms").arg(values.serverToClient_) << endl; } , [](std::exception_ptr ep) { try {std::rethrow_exception(ep);} catch (nx::Status status) { QTextStream out(stderr, QIODevice::WriteOnly); out << QObject::tr("NSFGetServerLatency status") << ": " << status.error() << endl; } });
もう少し短くしてみますね。
STATUS status = NSFGetServerLatency(const_cast<char*>(pServer), 0, &clientToServer_ms, &serverToClient_ms, &serverVersion); if (ERR(status) != NOERROR) { /*異常処理*/ } else { /*正常処理*/ }
こんな風に書いているのを、
GetServerLatency()(pServer, 0).subscribe( [](ReturnValues values) { /* 正常処理 */ }, [](std::exception_ptr ep) { /* 異常処理 */ });
こんな風にリファクタリングしてみようということです。
あらためてNSFGetServerLatency
この関数の仕事は、目的のサーバと通信するときの待ち時間の計測と、サーバのバージョンの取得です。
入力
- サーバ名
- タイムアウト時間
- クライアントからサーバへの待ち時間を取得するなら変数へのポインタ、取得しないならヌルポインタ
- サーバからクライアントへの待ち時間を取得するなら変数へのポインタ、取得しないならヌルポインタ
- サーババージョンを取得するなら変数へのポインタ、取得しないならヌルポインタ
出力
- ステータス値、正常に取得できればNOERROR(0)、異常があれば非ゼロ値
- クライアントからサーバへの待ち時間(取得のための変数へのポインタを指定した場合)
- サーバからクライアントへの待ち時間(取得のための変数へのポインタを指定した場合)
- サーババージョン(取得のための変数へのポインタを指定した場合)
面倒なことは隠そう
この関数の面倒なところは、何と言ってもヌルポインタによる「取得したくない」の意思表示です。3つの値を取得するかしないかは、デフォルト「取得する」でいいのではないかと思うわけです。取得したくないときは、そのデフォルトを変更できるようにしてあげれば、関数の機能としても型落ちすることがありません。
また、戻り値について、3つの値のための変数をひとつひとつ用意するのは面倒です。構造体を用意して、3つの値を一括で返してあげれば、受け取りも楽ですね。
というわけで、こんなクラスを考えてみました。
// notespp/notespp/server.h /** * @brief サーバへの待ち時間、サーバからの待ち時間、サーバのバージョンを取得する関数オブジェクト。 * @class GetServerLatency */ class NOTESPPSHARED_EXPORT GetServerLatency { public: /** * @brief 内部管理用値クラス * @class InnerValue * @tparam T データ型 */ template <typename T> class InnerValue { public: InnerValue(const T &value, bool enabled) : value_(value) , enabled_(enabled) {} T *pValue() { return (enabled_) ? &value_ : nullptr; } T value() const { return value_; } private: T value_; bool enabled_; }; /** * @brief 出力用データ型 * @struct ReturnValues */ struct ReturnValues { WORD version_; DWORD clientToServer_; DWORD serverToClient_; }; /** * @brief コンストラクタ * @param enableVersion バージョン取得を有効にする * @param enableClientToServer クライアントtoサーバを有効にする * @param enableServerToClient サーバtoクライアントを有効にする */ GetServerLatency( bool enableVersion = true, bool enableClientToServer = true, bool enableServerToClient = true ); /** * @brief 関数を実行する。 * @param serverName サーバ名 * @param timeout タイムアウト時間(0ならデフォルト) * @return ReturnValuesを流すObservable */ rx::observable<ReturnValues> operator ()( const QByteArray &serverName, DWORD timeout = 0 ); private: InnerValue<WORD> version_; InnerValue<DWORD> clientToServer_; InnerValue<DWORD> serverToClient_; };
このクラスは、俗に「関数オブジェクト」というタイプになります。簡単な例を示します。
class Plus { public: Plus(int a):a_(a){} int operator()(int b){ return a_ + b; } private: int a_; }; int main() { Plus plus_one(1); printf("1 + 2 = %d\n", plus_one(2)); // => 1 + 2 = 3 printf("3 + 4 = %d\n", Plus(3)(4)); // => 3 + 4 = 7 }
main
関数の1行目のplus_one
はPlus
クラスの変数ですが、2行目では、あたかも変数を関数のようにして使っています。実体は「かっこ演算子」をオーバーロードしたインスタンスメソッドなワケですが、このように関数のごとく扱えるクラスのことを「関数オブジェクト」と呼びます。変数の宣言も省略すると、3行目のようなPlus(3)(4)
と書くこともできます。
GetServerLatencyクラスのコンストラクタは、次のようになっています。
GetServerLatency( bool enableVersion = true, bool enableClientToServer = true, bool enableServerToClient = true );
どれを取得して、どれを取得しないということを、コンストラクタで決めています。ただし、すべての値にデフォルト値true
が設定されているので、変更しなくてよいのであれば、すべての入力値を省略することもできます。
実際の呼び出しは、かっこ演算子が担当しますが、「タイムアウト時間」もデフォルト値を指定できるので、これも既定値と考えると、次のようなメソッドになります。
rx::observable<ReturnValues> operator ()( const QByteArray &serverName, DWORD timeout = 0 );
すべての既定値を活かして書くと、次のように書くこともできます。
GetServerLatency()("YourServer");
とてもコードがスッキリしました。ところで、この関数オブジェクトはrx::observable<ReturnValus>
を返します。rx::observable
を返すには、どんな書き方をすればいいのか、また、受け取り側はどんな風に受け取ればいいのか、次回説明します。