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を返すには、どんな書き方をすればいいのか、また、受け取り側はどんな風に受け取ればいいのか、次回説明します。

Notes C API & C++11+ & ReactiveX & Qt #7 ~ Qtの翻訳システム(コーディング編)

ソースコードサイトの変更

まず最初に、1つご連絡があります。前回までの公開ソースコードを、訳あって以下のサイトに変更しました。

github.com

前回までの解説に使った改訂前のv0.0.1と、このサイトのv0.0.1は、原則合わせてありますので、連載上は問題ないかと思います。そのv0.0.1の説明も、今回が最後になります。

今回の目標「Qtの翻訳システム」

前々回の#5にて、Qtの翻訳システムについては概略を説明しました。今回はもう少し細かいところも説明して、v0.0.1の締めくくりにしたいと思います。

Qtの翻訳システムは、おおよそ次のような手順で行います。

  1. プロジェクトファイルで翻訳編集ファイル名を宣言します。
  2. ソースコード上にあるハードコーディングされたANCI形式のC文字列に印を付けます。
  3. ツールを使って翻訳編集ファイルとして抜き出します。
  4. 各文字列に翻訳を付けます。
  5. 実行時に変換可能な形にします。
  6. 実行形式ファイルの内部、または外部にバンドルします。

それでは、前出のv0.0.1のソースコードを元に順に説明していきます。

プロジェクトファイルで翻訳ファイル名を宣言

Qtアプリケーションやライブラリに使用されるQtプロジェクト(.proファイル)には、翻訳編集ファイル(.tsファイル)を指定する変数TRANSLATIONSがあります。

TRANSLATIONS += ncl.ja_JP.ts

演算子+=は、略式足し算としてよく使いますが、.proファイルでは変数に複数の値を指定するときに使います。このTRANSLATIONS変数も複数の値を指定できます。多言語対応する場合には、ここで国や言語に個別に対応するファイル名を指定することが可能です。複数のファイル名を指定する場合は空白文字で区切ります。

前述の例では、ファイル名nclと、拡張子.tsの間に、.ja_JP(日本語_日本)としています。これは、QtのQLocaleクラスが返すロケール文字列に合うようにしているためです。この箇所を、他の言語や国別の文字列に変えて指定すれば、他言語の翻訳ファイルを作ることができるようになります。

翻訳を実行するコードを書く

Qtを使えば勝手に翻訳してくれるわけではなく、コードを書く必要があります。次に、プログラム上でどのように翻訳する仕組みを動作させるのかを見てみます。

int main(int argc, char *argv[])
{
  QCoreApplication app(argc, argv);
  QTranslator translator; // <= (1)
  translator.load(QLocale(), "ncl", ".", ":/translations", ".qm"); // <= (2)
  app.installTranslator(&translator); // <= (3)

翻訳システムを動作させるには、QCoreApplication(またはそれを継承したクラス)オブジェクトを生成した後、できるだけ早い機会に、「翻訳データをロードしたトランスレータをアプリにインストールする」コードを書く必要があります。

  1. 翻訳オブジェクトを作成
  2. 翻訳ファイルをロード
  3. 翻訳オブジェクトをQCoreApplicationにインストール

ここまでやっておけば、ソースコード内の文字列は、Qtの翻訳システムが文字列を置き換えてくれます。

QTranslator::load

先程のコードで、(2)のところで翻訳データを読み込んでいます。このQTranslator::loadにはいくつかのオーバーロードが存在しますが、前述のメソッドのシグネチャは次のようになります。

bool QTranslator::load(const QLocale &locale, const QString &filename, const QString &prefix = QString(), const QString &directory = QString(), const QString &suffix = QString())

このように、翻訳ファイル名、ディレクトリ名、プレフィックスサフィックスをバラバラに指定することで、いろいろなパターンで翻訳ファイルの指定を支援してくれます。これについては、Qtのヘルプに掲載されている例が、とてもよくわかります。

http://doc.qt.io/qt-5/qtranslator.html#load-1

かいつまんで説明すると、スペイン語(es)、カナダのフランス語(fr-CA)、ドイツ語(de)という優先順位を持つ、Windowsなどのロケール環境で次のQTranslator::loadを呼び出したとします。

translator.load(QLocale(), "foo", ".", "/opt/foolib", ".qm");

すると、次のようなパターンで検索し、最初に見つかったファイルをロードしてくれます。

  1. /opt/foolib/foo.es.qm
  2. /opt/foolib/foo.es
  3. /opt/foolib/foo.fr_CA.qm
  4. /opt/foolib/foo.fr_CA
  5. /opt/foolib/foo.de.qm
  6. /opt/foolib/foo.de
  7. /opt/foolib/foo.fr.qm
  8. /opt/foolib/foo.fr
  9. /opt/foolib/foo.qm
  10. /opt/foolib/foo.
  11. /opt/foolib/foo

検索するファイル名、ディレクトリは変わりませんが、言語名、拡張子名を入れ替えて、ファイルの有無を確認します。

もちろん、この適用ルールに当てはめたくない場合は、他のloadメソッドを使って直接指定すればいいわけです。なお、QLocaleで使われる言語と国の省略形による識別には、ISO639(言語)、ISO3166(国)が使用されています。日本語-日本であればja_JPとなります。

翻訳したい箇所をマーク

ところで、.qmファイルというものが急に出てきました。これを説明するには、ソースコード上の翻訳したい文字列にどうやって印を付けるか、というところから説明します。

例えば、v0.0.1のmain.cppの70行目付近に、以下のようなコードがあります。

out << QObject::tr("Build version of '%1'").arg(pServer)

この中の、QObject::tr()で囲った部分が、翻訳対象の文字列になります。普通に書くより長ったらしいと感じるかもしれません。これは、QObjectクラスを継承していない環境では、こう書かざるを得ませんが、特にQtのGUIアプリケーションなどでは、QObjectを継承している環境下でコーディングすることになるので、短くtr("...")とするだけでよくなります。まあ、慣れですね。

tsファイルの作成(または更新)

このようにして、ソースコード内の文字列にQObject::trメソッドでマークを付けたら、次はlupdateコマンドを使います。lupdateは、.pro(プロジェクト)ファイルを元に、ソースコードを探索するファイルと、登録されている.tsファイル名を使って、.tsファイルをなければ生成、あれば既存のファイルを更新します。.tsファイルは抜き出した文字列と、対となる翻訳文字を管理するもので、中身は単なるXMLファイルです。

Qt Linguist

.tsファイルはXMLなので、テキストエディタでも編集できなくはないですが、ここはQt LinguistというGUIツールを使った方がいいでしょう。

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

Qt Linguistは、単に翻訳文字列を書き込むだけでなく、ソースコードGUIプレビューを確認しながら、状況にあった翻訳を考えられるように、プレビューができるところが大きな利点です。

リリース(バイナリファイルの作成)

一通り翻訳を付け終えたら、.tsファイルそのものを保存し直して、「リリース」作業をします。

リリースとは、.tsファイルから.qmファイルを作成する作業のことをいいます。.qmファイルはバイナリで構成された翻訳データで、QTranslatorがロードするのもこのファイルです。リリース作業はlreleaseコマンドを使いますが、前出のQt Linguistのメニューコマンドにもリリース機能はあるので、Qt Linguistを使って作業をするのであれば、後者の方が便利でしょう。

.qmファイルの保存先-Qtリソースを使った場合

ここまで来ればあと一息です。

.qmファイルは、そのままアプリケーションとともに配布することもできますが、インストーラに組み込むのが面倒な場合もあります。そんな時にお手軽なのが「Qtリソースファイル」として同梱してしまうことです。「Qtリソースファイル」とは、EXEやDLLなどのバイナリファイル内にリソースファイルコンテナを設けて、登録したファイルをあたかもファイルシステムの一部のようにして取り出すことができる仕組みです。私の場合、よく使うのが翻訳ファイル以外に、アイコン画像を取り込んでおくことがよくあります。

「Qtリソースファイル」は.qrc拡張子で作成されるファイルで、これも中身はXMLファイルです。ディレクトリと同じような階層構造を持っています。

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

このスクリーンショットでは、1つの.qmファイル以外にいくつかのアイコンファイル名が登録されています。Qtリソースファイルは、コンパイル、リンクを経てEXEやDLL内に取り込まれます。Qtリソースファイルに登録されたファイル名に、プログラム内からアクセスするには、頭に:を付け、以降はディレクトリと同じ要領でアクセスすることができます。図の中のbirch.ja_JP.qmファイルにアクセスするには、:/translations/birch.ja_JP.qmとすればアクセスできます。

int main(int argc, char *argv[])
{
  QCoreApplication app(argc, argv);
  QTranslator translator;
  translator.load(QLocale(), "ncl", ".", ":/translations", ".qm");
  app.installTranslator(&translator);

前出のコードの内、":/translations"という部分が、ディレクトリ指定になり、ディレクトリの行き先は、リソースファイル内のtranslationsフォルダ内ということになります。

まとめ

以上が、Qtアプリケーションで多言語化を簡単にする翻訳システムの、ソースコードやツールを使った作業の流れになります。QtはマルチプラットフォームGUIアプリケーションの開発支援ツールなので、OSごとにマルチバイト言語の扱いが異なるC++コーディングでは、ソースコードをシングルバイトで共通化しておき、マルチバイト文字列は「翻訳」というプロセスに外部化したのは、ごく自然の流れだったのかもしれません。

おさらい

#3~#7で、Notes C APIとQtを使ったコーディングの初歩を勉強してきました。簡単に振り返っておきます。

Notes C API

  1. APIのヘッダーファイルを読み込む前にOSを識別するシグネチャを宣言する(Windows 32bitならW W32 NT)
  2. APIのヘッダーファイルを読み込む場合、Windowsではアラインメントを1バイトにする。
  3. APIを使用する前に関数NotesInitExtended、終了前に関数NotesTermを実行する。
  4. API関数の大半がSTATUSを返す。この値で関数が正しく動作したかわかる。

Qt

  1. Qtはマルチプラットフォームのアプリケーションを1つのソースコードで開発できる。
  2. プラットフォームごとの差異は、.proファイル内の定義で吸収する。
  3. シングルバイトでソースを書き、翻訳システムでマルチバイト化する。

Notes/Dominoはマルチプラットフォームアプリケーションですから、Qtと相性がいいはずです。次回以降も、実践的なNotes C APIアプリ開発手法を紹介していきます。

Notes C API & C++11+ & ReactiveX & Qt #6 ~ Notes C API関数の基本

前回は、Qtの翻訳機能について少し触れました。今回は、このプログラムで肝となるNotes C API「NSFGetServerLatency」についてお話しします。

v0.0.1のソースコードを実行すると、次のような出力が得られます。

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

2行目、3行目はAPIがユーザにパスワードの入力を促している様子です(GUIアプリの場合はダイアログボックスが表示されます)。 また、以下のシグネチャを・・・

// 待ち時間を読み取る場合はこのシグネチャを有効にする。
//#define GET_LATENCY_TIME

次のようにコメントを外すと・・・

// 待ち時間を読み取る場合はこのシグネチャを有効にする。
#define GET_LATENCY_TIME

このような出力に変化します。

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

目的のサーバのバージョン(ビルド番号)に加えて、2種類のLatency(レイテンシ、待ち時間)をミリ秒で表示するようになります。

では、NSFGetServerLatency関数のシグネチャについて見てみましょう。

// nsfdb.h

STATUS LNPUBLIC NSFGetServerLatency(
  char far *ServerName,
  DWORD Timeout,
  DWORD far *retClientToServerMS,
  DWORD far *retServerToClientMS,
  WORD far *ServerVersion
);
  • ServerName: (入力)目的のサーバ名を指定します。LMBCSですが、マルチバイト文字を含まなければシングルバイト文字列のまま指定できます。
  • Timeout: (入力)サーバからの応答を待機する時間で、ミリ秒で指定します。0を指定すると、環境が持っているデフォルトのタイムアウト時間が使用されます。
  • retClientToServerMS: (出力)クライアントからサーバへの応答時間(ミリ秒)を取得します。
  • retServerToClientMS: (出力)サーバからクライアントへの応答時間(ミリ秒)を取得します。
  • ServerVersion: (出力)サーバのバージョンをビルド番号で取得します。「405」は「9.0.1」を表します。@関数の@Versionと同じです。
  • STATUS戻り値: 実行結果を返します。成功すればNOERROR(0)を、失敗すればエラー番号を返します。

使い方としては次のようになります。

DWORD clientToServerMS, serverToClientMS;
WORD serverVersion;
STATUS status = NSFGetServerLatency(
  const_cast<char*>("Your/Server/Name"),
  static_cast<DWORD>(0),
  &clientToServerMS,
  &serverToClientMS,
  &serverVersion
);

Notes C APIでよくある問題として、「const_cast問題」があります。const_castキャスト演算子とは、定数属性「const」「volatile」などがついた型から定数属性を外します。関数の入力用引数は、出力用引数に比べて変化することがないので、const属性を付加するのが普通ですが、Notes C APIの関数では、理由は不明ですが、しばしば入力値にconst属性がついていないものがあります。上述のように、const_castを使わずに定数ポインタを指定してしまうと、コンパイルエラーを起こしてしまうので、面倒ですが、const_castを追加する必要があります。

Notes C API関数の大部分は、戻り値として「STATUS」値を返します。元をたどると符号なし2バイト整数になります。STATUS値は関数が実行に成功すると0(シグネチャ「NOERROR」)を、失敗すると0以外のエラー値を表します。ただし上位2ビットはフラグとして機能するので、正確なエラー値を取得するにはERRマクロでマスクする必要があります。

STATUS error = ERR(status);

関数の戻り値は、STATUSによる関数実行の成否に使われてしまうため、レイテンシやバージョン番号などの出力要素は、ポインタで指定された引数で取得することになります。

アドインを開発している分にはNotes C API関数は普通に利用できますが、NotesクライアントやDominoサーバから独立したプロセスでプログラムを動かしている場合、Notes C APIは初期化/終了処理を必要とします。

STATUS LNPUBLIC NotesInitExtended (int argc, char far * far *argv);
void   LNPUBLIC NotesTerm (void);

Notes C API関数の実行に先立ち、NotesInitExtendedを実行する必要があります。argc、argvはmain関数の引数をそのまま渡します。プログラム(プロセス)を終了する前にNotesTermを実行します。

(続く)

Notes C API & C++11+ & ReactiveX & Qt #5 ~ Qtの翻訳システム

前回はmain.cppとNotes C APIのアラインメントについてお話ししました。今回はQtのアプリケーションとしての仕組みについて、サクッとご紹介します。

main.cppのmain関数が呼び出された直後に、次のような記述があります。

QCoreApplication app(argc, argv);

一般的に、コンソールベースのアプリケーションを作成する場合、Qtではこの「QCoreApplication」クラスのインスタンスを生成して初期化します。QtではGUIアプリケーションも作成できますが、その場合は「QApplication」で同様の処理をします。面白いのは、ネイティブWindowsアプリのエントリポイントである「WinMain」を、C/C++であっても一切コーディングしないことです。もちろん、その辺はQtがうまく隠しています。

話をQCoreApplicationに戻します。このオブジェクトを使うと、アプリケーション開発に有益ないくつかの仕組みを提供してくれます。Qtのドキュメントを参考にすると、以下のようになります。

  1. イベントループとイベントハンドリング
  2. アプリケーションとライブラリのパス
  3. 国際化と翻訳システム
  4. コマンドライン引数へのアクセス
  5. ロケール設定

コンソールベースのアプリケーションでは、特に「コマンドライン引数へのアクセス」は重宝します。ヘルプ表示とバージョン表示についてはすでに仕組みを持っており、必須引数やオプション引数についても簡単に登録できるようになっています。

コマンドラインへのアクセスは、QCoreApplication::arguments()からアクセスすることもできますが、より便利に使いたいのであれば、QCommandLineParserを使うといいでしょう。ソースコードで出てきた折に改めて触れたいと思います。

個人的によく使うのは「翻訳システム」です。Qtはソースコードにマルチバイト文字を書きにくいんですが、そもそもC/C++のマルチバイト文字の扱いはコンパイラ依存が大きいため、マルチプラットフォームのQtにしてみれば厄介な問題なのかもしれません。その一方で、翻訳機能が充実していて、LinguistというGUIツールもあります。対応言語が多いアプリを作れるというのは魅力的ですし、それを励みに、可能な限り翻訳システムを使うようにしています。

Qtの翻訳システムの流れは、おおよそ以下のようになります。

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

lupdate、lreleaseはコマンドラインツール、LinguistはGUIアプリです。コンパイルのくだりで、もう少し細かく紹介します。

Qtの翻訳システムについては、以下の本がとても役に立ちました。前バージョンのQt4の本ですが、Qt5でも通じる基本から応用までを網羅しています。

入門 Qt 4 プログラミング

入門 Qt 4 プログラミング

なお、前出のQCommandLineParserは、Qt5.2からの実装なので掲載されていません。あしからず。 (続く)