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;
  }
}
  1. コンパイルして、DLLを作成する。例えば、helloというアドインであれば、nhello.dllという名前でDLLを作る。頭文字のnWindows用のプリフィックス
  2. nhello.dllをDominoサーバのプログラムディレクトリにコピーする(他に必要なモジュールも同様にプログラムディレクトリに置く)。
  3. サーバ文書かWeb設定のDSAPI欄に、helloと記述する(n.dllは書かない)。
  4. Dominoサーバを再起動する。

すると、Dominoコンソールには以下のように表示される。

f:id:takahide-kondoh:20190708222937p:plain

続いて、ブラウザから次のようなリクエストを送る。

http://localhost/hello

すると、以下のようなレスポンスが返ってくる。

f:id:takahide-kondoh:20190708223147p:plain

自分用メモ: Notes C APIアプリをマルチプラットフォーム開発するときのマクロ定義

-DNT -DW32 -DW -D_X86_ -DND32 -DDTRACE -D_CRT_SECURE_NO_WARNINGS -DPRODUCTION_VERSION  -DDUMMY
-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
-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
-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

SlideShare

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_ptrC++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による実践的プログラミング

Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミング

Notes C APIに携わる数少ない(であろう)日本人の一人として、18年経った今でも拠り所となる本です。とはいえ、です。そろそろアップデートしないと厳しいところも出てきているのも事実です。この本で学んだことを元に、お気に入りのフレームワークQtと、手になじんでいる範囲でのC++11以降のスタイルと、今年お気に入りとなったReactiveXを混ぜ込んで、私なりのアップデートを試みていきたいと思います。

LMBCS

このブログでは何度か取り上げている、Notes/Domino独自のマルチバイト文字セット「LMBCS(リンビックス)」。前著にも「2.7 LMBCS文字コードと日本語処理」として、初期化/終了処理、マルチスレッドに続く早い段階でAPIが紹介されています。このLMBCSの処理方法は、APIでプログラムを書く上でも早々に確立しておかないと、何もできなくなってしまいます。

シフトJIS

日本語Windows文字コードは長らくシフトJISでしたが、最近ではUnicode(UTF-16)が使われているらしいです。WindowsAPIでも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系(LinuxMac)はUnicodeでもUTF-8が主流のようですが、Qtが扱う文字列クラスQStringの内部文字コードは、ずいぶん前からUTF-16のようです。シフトJISUTF-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してください。

chiburusystems.hatenablog.com

実装コード

前回目標にした、「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名前空間の別名で、実際にはrxcpprxcpp::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型の値を流すと、購読者は値を読めるということになります。

整理してみましょう。冒頭のソースコードに表されているのは、以下のようなことになります。

  1. NSFGetServerLatencyを実行する。
  2. ステータス値が正常でなければ、subscriber.on_errorを呼び出す。
  3. ステータス値が正常であれば、出力値をReturnValuesにまとめ、subscriber.on_nextで流す。

以上のロジックを持つ・・・

  1. 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が実装されている言語はJavaJavaScript、.NET、Rubyなど多くの言語で展開されています。自身が得意な言語で、新しいパラダイムに触れてみてはいかがでしょうか。

ReactiveX

P.S. あいにくLotusScriptのReactiveXは、私が個人的に実装した例以外には見当たらないようですが。(^^;)どこかでお披露目できればと思っています。

Notes C API & C++11+ & ReactiveX & Qt #8 ~RxでNotes C API関数をラップ

お題「Notes C API関数をReactiveXでラップする」

今回はこのお題について考えます。なお、今回のコードは、前回ご紹介した以下のサイトを使います。

github.com

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

この関数の仕事は、目的のサーバと通信するときの待ち時間の計測と、サーバのバージョンの取得です。

入力

  1. サーバ名
  2. タイムアウト時間
  3. クライアントからサーバへの待ち時間を取得するなら変数へのポインタ、取得しないならヌルポインタ
  4. サーバからクライアントへの待ち時間を取得するなら変数へのポインタ、取得しないならヌルポインタ
  5. サーババージョンを取得するなら変数へのポインタ、取得しないならヌルポインタ

出力

  1. ステータス値、正常に取得できればNOERROR(0)、異常があれば非ゼロ値
  2. クライアントからサーバへの待ち時間(取得のための変数へのポインタを指定した場合)
  3. サーバからクライアントへの待ち時間(取得のための変数へのポインタを指定した場合)
  4. サーババージョン(取得のための変数へのポインタを指定した場合)

面倒なことは隠そう

この関数の面倒なところは、何と言ってもヌルポインタによる「取得したくない」の意思表示です。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_onePlusクラスの変数ですが、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を返すには、どんな書き方をすればいいのか、また、受け取り側はどんな風に受け取ればいいのか、次回説明します。