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からの実装なので掲載されていません。あしからず。 (続く)

Notes C API & C++11+ & ReactiveX & Qt #4 ~ main.cppとアラインメント

前回に引き続き、以下のソースコードについて説明していきます。

github.com

前回のQMakeのプロジェクトファイルに関するトピックはまだまだ尽きません。言語的な要素も持っていて、ビルトインの変数、関数の他に、カスタム変数、関数を定義することもできます。Windows/Mac/Linuxや他のOS、組み込みデバイススマートフォンなどに対応するための仕組みもあります。もちろん、Qt独自の機能(例えば、Qt翻訳システムやリソースシステムなど)を定義するのもこのファイルです(今後もQMakeプロジェクトについては、関連するトピックを取り上げる際に触れていきます)。

これらQMakeとプロジェクトファイルについては、日本語で情報を紹介してくれている方々も多くいらっしゃいますが、やはり全体的には英語が主流です。QMakeの英語マニュアルで基礎から学びたい方で、私のように英語に自信のない方は、翻訳サイトをうまく活用してみてはいかがでしょうか。

それでは、C++ソースコードの説明に移ります。このv0.0.1では、ハードコードしたサーバ名でそのサーバと通信し、待ち時間とサーバのバージョンを取得して表示します。この機能は1つのAPI関数「NSFGetServerLatency」だけで実行することができます。

// main.cpp

#include <QCoreApplication>
#include <QTranslator>
#include <QLocale>
#include <QTextStream>

// Notes C APIのインクルードは、Windowsではアラインメントを1バイトにして読み込む。
#ifdef NT
#pragma pack(push, 1)
#endif

#include <global.h>
#include <nsfdb.h>

#ifdef NT
#pragma pack(pop)
#endif

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

// 自身で到達させたいサーバ名を設定する。
const char *pServer = "Your/server/name";

int main(int argc, char *argv[])
{
  QCoreApplication app(argc, argv);

  QTranslator translator;
  translator.load(QLocale(), "ncl", ".", ":/translations", ".qm");
  app.installTranslator(&translator);

  // 標準出力
  QTextStream out(stdout, QIODevice::WriteOnly);

  // Notes C APIを初期化する。
  STATUS initStatus = NotesInitExtended(argc, argv);
  out << QObject::tr("NotesInitExtended status")
      << ": "
      << ERR(initStatus)
      << endl;

  // 戻り値格納変数を用意する。
  WORD serverVersion = 0;
#ifdef GET_LATENCY_TIME
  DWORD clientToServer_ms, serverToClient_ms;
#endif

  // API関数を実行する。
  STATUS status = NSFGetServerLatency(
        const_cast<char*>(pServer),
        0, // <- 既定のタイムアウト時間を指定
#ifdef GET_LATENCY_TIME
        &clientToServer_ms, &serverToClient_ms,
#else
        nullptr, nullptr,
#endif
        &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;
#ifdef GET_LATENCY_TIME
  out << 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;
#endif

  // Notes C APIを終了する。
  if (ERR(initStatus) == NOERROR)
    NotesTerm();

  return 0;
}

ソースコードについて順を追って説明していきますが、まずは、以下のコードに注目してみます。

// 一部省略
#pragma pack(push, 1)
#include <global.h>
#include <nsfdb.h>
#pragma pack(pop)

インクルードしている「global.h」と「nsfdb.h」は、Notes C APIのヘッダファイルです。それを挟んでいるのは、Visual C++のプラグマディレクティブ(#pragma)です。後ろにつく「pack」というのは、構造体のアラインメントを変更する機能があります。「push,1」は「アラインメントを1バイトに変更しますよ」という意味で、「pop」は「元に戻しますね」という意味です。Visual C++のアラインメントは、既定値は8バイトなんですが、Notes C APIの構造体は一部を除いて1バイトのアラインメントになっています。Mac/Linuxではこのアラインメントの問題は起きません。マルチプラットフォームを意識した書き方をするのであれば、Mac/Linuxでは、このプラグマディレクティブはむしろ不要なので、以下のようにキーワード「NT」を元に判定します(NTはNotes C APIWindowsのみに定義する)。

#ifdef NT
#pragma pack(push, 1)
#endif

#include <global.h>
#include <nsfdb.h>

#ifdef NT
#pragma pack(pop)
#endif

ちなみに、Windowsでも例外的にアラインメントを既定値でインクルードするNotes C APIヘッダファイルがあります。私の経験上、「dsapi.h」だけはアラインメントを1バイトにしてはいけなくて、既定の8バイトのままにします。「dsapi.h」は、HTTPタスクのエクステンション用にインクルードするもので、これを1バイトでパックすると、ひどいエラーを引き起こしました。(続く)

Notes C API & C++11+ & ReactiveX & Qt #3

ここでは、チュートリアルとして簡単なコマンドラインアプリを作り、Notes C APIの基礎を勉強していきます。C/C++C++11以降の仕様、ReactiveX/C++、Qtフレームワークについては、必要に応じて触れていきます。

以降の前提条件として、以下のような要件で進めていきます。

教材は、GitHubにアップしているものを使うので、こちらからソースコードをクローン/プルして、記事ごとにタグをチェックアウトして下さい。

# クローンの作成
> git clone https://github.com/Chiburu/ncl
> cd ncl

今回は、タグ「v0.0.1」を使います。

# タグ「v0.0.1」をチェックアウト
> git checkout v0.0.1

ソースコードが更新されたら、次のようにローカルもアップデートします。

# ローカルリポジトリをリモートリポジトリに合わせる
> git pull origin master

v0.0.1では、一つのNotes C API関数を実行するために、コンパイル、リンクではどんな準備が必要かを見ていきます。

QtはC++のライブラリフレームワークです。

www.qt.io

オープンソース版であれば、無償で利用できます。

Qtの最大の特徴は、PC(Windows/Mac/Linux)のデスクトップアプリケーションを同じソースコードからビルドできるところでしょう。その点、マルチプラットフォームアプリケーションであるNotesととても相性がいいと言えます。事実、昨年までの私の記事でも、3つの環境のアプリを同一ソースから作成していました(※ とある事情でMacのNotesが使えなくなり、UbuntuではNotes C APIの都合でC++11未対応のgccしか使えず、現在はWindowsオンリーという状況です)。

C++マルチプラットフォームアプリケーションが開発できる秘密は、Qtプロジェクトファイル(.pro)と、QMakeというコマンドツールです。Qtプロジェクトファイルは、コンパイル、リンクに関わる設定の他に、プラットフォームごとに必要な準備設定をし、それをQMakeがそれを読み取って、プラットフォームごとのコンパイラ、リンカが動作するのに必要な準備を実施します。

# ncl.pro(Qtプロジェクトファイル)
QT -= gui

CONFIG += c++11 console
CONFIG -= app_bundle

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
        main.cpp

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

# ===== ここまでQtコンソールアプリケーションの既定値 =====

# アプリケーション説明
QMAKE_TARGET_PRODUCT = Notes Commandline
QMAKE_TARGET_DESCRIPTION = Notes Commandline Tool
QMAKE_TARGET_COPYRIGHT = (c) Chiburu Systems

# バージョン設定
VERSION = 0.0.1

# 翻訳ファイル
TRANSLATIONS += ncl.ja_JP.ts

# Notes C APIのインストール先(区切りはWindowsでもスラッシュ)
NOTES_C_API_DIR = Z:/Users/Shared/notesapi

# Notes C APIのインクルードファイルの場所
INCLUDEPATH += $$NOTES_C_API_DIR/include
DEPENDPATH += $$NOTES_C_API_DIR/include

# Windowsの場合
win32 {
  # Windowsに必要なシンボル
  DEFINES += W W32 NT
  # 不要な警告を非表示
  QMAKE_CXXFLAGS += -wd4503 -wd4005
  # 64ビット版
  contains(QMAKE_TARGET.arch, x86_64) {
    DEFINES += W64 ND64 _AMD64_
    LIBS += -L$$NOTES_C_API_DIR/lib/mswin64/
  }
  # 32ビット版
  else {
    LIBS += -L$$NOTES_C_API_DIR/lib/mswin32/
  }
}

# OS共通のリンクライブラリ
LIBS += -lnotes

DISTFILES += \
    ncl.ja_JP.ts \
    Readme.md

RESOURCES += \
    ncl.qrc

この中で、特にNotes C APIに関わってくるところとして、以下のような記述箇所があります。

DEFINES += W W32 NT

「DEFINES」は、ソースコードに#defineシグネチャを追加する、コンパイラの-Dオプションのようなものです。Notes C APIはプラットフォームごとに必要なシグネチャがあり、Windowsの場合はこの「W」「W32」「NT」というシグネチャを、ヘッダファイルを読み込む前に定義しておく必要があります。64ビットWindowsのDominoの場合は、さらに「W64 ND64 AMD64」を追加する必要があります。(続く)

Notes C API & C++11+ & ReactiveX & Qt #2

リアクティブ・エクステンション(Reactive Extensions、ReactiveX、RX)は奥が深く、勉強を始めてから日の浅い私もまだまだわからないことだらけです。ですが、初級レベルくらいならある程度書けるようになってきたので、Notes APIを例に、RXを適用するとこんな風になるというところをご紹介してみます。

ここでは、サーバ名を取得する関数「NSGetServerList」関数を使って、一般的な方法と、RXを使った方法とを比較してみます。NSGetServerList関数については、昨年書いたこちらをご覧下さい。

chiburusystems.hatenablog.com

これをDirというクラスの静的メソッドとして実装したとします。そして、そのメソッドを呼び出して、サーバ名のリストを取得する側のコードを書いてみます。私のように、20年以上オブジェクト指向でプログラムを書いてきた場合、次のようになるでしょう。

namespace nx {
using namespace chiburu::noteslib;
}

QList<nx::String> serverList = nx::Dir::getServerList();
QTextStream sout(stdout);
foreach (nx::String server, serverList) {
  sout << server.toQString() << endl;
}

名前空間やクラス名などのこまかいところはさておき、getServerListで取得したサーバ名リストをQList<nx::String> serverListに保管し、標準出力にサーバ名を出力します。極めて一般的な書き方ではないでしょうか。 (※nx(=chiburu::noteslib)名前空間がついたクラスが独自のもので、特にnx::StringはLMBCS文字列を表しています。)

このnx::Dir::getServerListメソッドを、リアクティブ・エクステンションで実装し直すと、こんな書き方になります。

QTextStream sout(stdout);
nx::Dir::getServerListStream()
  .subscribe([&](nx::String server) {
    sout << server.toQString() << endl;
});

&{...}という書き方は、C++11以降で使える「ラムダ式」で、JavaScriptでいうクロージャのようなもので、リアクティブ・エクステンションに直接関係はないですが、これがないとC++でRXは立ちゆかないでしょう。

getServerList関数は、getServerListStreamと名前を変えました。シグネチャは次のようになります。

#include <rxcpp/rx.hpp>
namespace Rx {
using namespace rxcpp;
}

Rx::observable<nx::String> nx::Dir::getServerListStream(nx::String port = nx::String());

Rx::observable?いきなり出てこられても困りますが、ここでは、「LMBCS文字列を流してくるもの」というふわっとした感じで捉えておいて下さい。「流してくる」ので「捕らえれば」サーバ名を取得できる・・・という具合です。じゃあ、どこで「捕らえる」か?「subscribe」で「捕らえます」。「subscribe」は「observable」のメソッドです。「observable」が流すものを、「subscribe」で捕らえて、指定したラムダ式や関数オブジェクトなどで処理します。

リアクティブ・エクステンションを使うと何が嬉しいか?検索するといろいろ議論になっていますが、ここでは局所的に捉えてみます。

今回のNSGetServerList関数をラップする場合、取得したサーバ名を返すとき、サーバ名が一括で返ってきてしまうので、リストという形でまとめ上げ、それを返すことになります。テンプレートを使ったとしても、すべてのサーバ名を「なんらかのコンテナオブジェクト」に収めてから返すことに変わりはありません。

リアクティブ・エクステンションを使って、取得したサーバ名を、順次呼び出し側に提供するようにすると、コンテナオブジェクトを強要することがなくなりました。呼び出し側は、サーバ名の扱い方を自分で決めることができるようになります。これだけでも十分すごいことだと、実装してみて感じました。

今回のようにNotes APIを使った例では、同期的なプログラムが大半を占めますが、この仕組みは非同期的なプログラムでも使えます。特にHTTPリクエストやUI更新など非同期を多く使う場面では、リアクティブ・エクステンションは威力を発揮するようです。同期、非同期に関係なく使えることはとても大きいと感じています(実際には意識する必要がでてきますが)。

Notes C API & C++11+ & ReactiveX & Qt #1

仕事でAngularを使い始めました。Angularは、ReactやVueなどと比較されるSPAフレームワークです。開発にはJavaScript回りがわかればいいのですが、TypeScriptから入ることをお勧めされます。

https://angular.io/

www.typescriptlang.org

AngularJS(1.x)からAngular(2.x~)に移行したとき、TypeScriptが体に合わず、しばらく遠ざかっていましたが、世の中SPAなしでは生きていけず、またTypeScriptの構文がES6(ES2015)にも通じることから、意を決して取り込むことにしました。

ただ、後1つクリアしなければならないハードルがあります。RX(Reactive Extensions、ReactiveX)です。

http://reactivex.io/

Angularのチュートリアルに取り組んでいると、奇妙な言葉に遭遇します。

https://angular.io/tutorial/toh-pt4#observable-heroservice

「Observable」?オブザーバブル?監視可能なもの?

いろいろ端折りますが、乱暴にまとめれば「あらゆるデータを監視可能なストリーム(流れ)として扱えるようにした仕組み」で、これをAngularは積極的に取り込んでいるので、RX学習はほとんど必須科目になっています。

RXは大変奥が深いようで、私の学習レベルもまだまだですが、七転八倒の末、「なるほどこれがRXか!」というパラダイムシフトが起きて以来、手放せないロジックになっていることは確かです。今までFor文、コールバック、Promiseなどが当たり前だった頭の回路が、RXなしでは成立しないと言っても過言ではないです。私がパラダイムシフトを起こすために助けになったのは、この本のおかげです。

関数型リアクティブプログラミング (Programmer's SELECTION)

関数型リアクティブプログラミング (Programmer's SELECTION)

しかし待てよ、と。JavaScriptでこんな便利な考え方があるなら、C++の世界でも使えないことはなかろう、と。RXはあらゆる言語で展開されているようです。

ReactiveX - Languages

C++もRxCppという名前で展開されていました。となれば、以前から取り組んでいるNotes C APIラッパーライブラリを、RXや最新のC++言語使用を取り入れて、アップデートしてもよいのではないだろうか・・・、と思い立ちました。

もちろん、ベースとなるのは国内唯一のNotes C API本であるこの本です。

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

いまだにバイブルであることに変わりはありません。これに、C++11以降の言語仕様、ReactiveXの仕組み、QtマルチプラットフォームC++GUIフレームワークを組み合わせて、V10時代を迎えるNotes/DominoのC APIを使いやすくするライブラリを目指していきます。