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を使いやすくするライブラリを目指していきます。

W64APIにまたしても!DNParseの落とし穴

いつぞやのNSFItemInfoNext関数に続き、またしてもW64で正常に動かないC APIを発見しました。今回は、DNParse関数です。まず、DNParse関数とはなんぞや?というところから。

DNParse関数の接頭辞、「DN」は「Distinguished Name」の略で、識別名を表します。Notesでいうところの、

CN=Taro Yamada/O=Acme/C=JP

というID名を指します。ユーザ名、サーバ名などがそれにあたります。実際に捌ける要素はこれだけにとどまらないようですが、ここでは割愛します。ここでのポイントは、「DN」を冠した関数は識別名を操作する関数ということです。

DNParseは、その識別名をパーツごとに分解することができます。例えば、前出の例を元に分解すると、

Common Name => Taro Yamada
Org Name => Acme
Country Name => JP

となります。ヘッダーでは、DNParse関数は次のように定義されています。

#include <dname.h>

STATUS LNPUBLIC DNParse(
    DWORD Flags,
    const char far *TemplateName,
    const char far *InName,
    DN_COMPONENTS far *Comp,
    WORD CompSize);

FlagsとTemplateNameは0を渡します。 InNameに分解したい識別名へのポインタを入力します。 続く「DN_COMPONENTS」という構造体に、分解された情報が入ってくるので、事前にDN_COMPONENTS変数を用意して、そのポインタをCompに、変数サイズをCompSizeに与えれば取得できます。 問題なく分解できれば、NOERRORを返します。

DN_COMPONENTS dn;
STATUS result = DNParse(0, 0, "CN=admin/O=acme", &dn, sizeof(dn));

ヘッダーでDN_COMPONENTSを見ると、Ver3はここまで、Ver4はここから、みたいなコメントがあり、拡張が繰り返されてきたことが伺えます。

#include <dname.h>

typedef struct {
  DWORD Flags;                    /* Parsing flags */
/* (中略) */
  WORD CLength;                   /* Country name length */
  char far *C;                    /* Country name pointer */
  WORD OLength;                   /* Organization name length */
  char far *O;                    /* Organization name pointer */
  WORD OULength[DN_OUNITS];       /* Org Unit name lengths */
  /*  OULength[0] is rightmost org unit */
  char far *OU[DN_OUNITS];        /* Org unit name pointers */
  /*  OU[0] is rightmost org unit */
  WORD CNLength;                  /* Common name length */
  char far *CN;                   /* Common name pointer */
  WORD DomainLength;              /* Domain name length */
  char far *Domain;               /* Domain name pointer */

  /* Original V3 structure ended here.  The following fields were added in V4 */

  WORD PRMDLength;                /* Private management domain name length */
  char far *PRMD;                 /* Private management domain name pointer */
/* (中略) */
} DN_COMPONENTS;

実際にDNParseをDomino Win32bitで動かすと、DN_COMPONENTSには以下のような情報が返ってきます。

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

ハイフンを挟んで左が文字数、右が文字列へのポインタです。Country name、Org name、Common nameのそれぞれに文字数2、文字数4、文字数11(0x0B)と、文字列が格納されているポインタが格納され、Org Unit name0〜3にはデータがないのがわかります。

全く同じコードをWin64用にコンパイルして動かすと以下のようになります。

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

「あれれ〜おかしいなあ〜。ポインタっぽい値が文字数変数に入り込んでいるぞ〜。」という感じになっています。

このズレは何なのか、いろいろ考えてみました。単純に、メモリ配置的に、ポインタが4バイトから8バイトになったというだけでは説明が付きませんでした。試行錯誤の結果、国名(C)、組織名(O)共通名(CN)までの変数であれば、次のようにパディングすることで解決しました。

#ifdef W64

typedef struct {
  DWORD Flags;                    /* Parsing flags */
/* (中略) */
  WORD CLength;                   /* Country name length */
  WORD c_padding; // パティング
  char far *C;                    /* Country name pointer */
  WORD OLength;                   /* Organization name length */
  WORD o_padding[3]; // パティング
  char far *O;                    /* Organization name pointer */
  WORD OULength[DN_OUNITS];       /* Org Unit name lengths */
  /*  OULength[0] is rightmost org unit */
  char far *OU[DN_OUNITS];        /* Org unit name pointers */
  /*  OU[0] is rightmost org unit */
  WORD CNLength;                  /* Common name length */
  WORD cn_padding[3]; // パティング
  char far *CN;                   /* Common name pointer */
  WORD DomainLength;              /* Domain name length */
  char far *Domain;               /* Domain name pointer */

  /* Original V3 structure ended here.  The following fields were added in V4 */

  WORD PRMDLength;                /* Private management domain name length */
  char far *PRMD;                 /* Private management domain name pointer */
/* (中略) */
} DN_COMPONENTS;

#else

/* (オリジナルのDN_COMPONENTS定義をここに) */

#endif

CLengthとCの間にWORD一つ分、OLengthとOの間にWORD3つ分、CNLengthとCNの間も同じく3つ分パディングすると、以下のように正しく変数に格納されたのが確認できました。

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

Domain以下の変数に対しては試せていませんが、ズレているのであれば、逐次直していくしかないでしょう。

まとめ

Notes/Dominoは登場初期からマルチプラットフォーム指向で、C APIも早い段階から提供され、Notes/Dominoのバージョンとともに更新されてきました。ただ残念なことに、C APIに目を向ける人口が少ないせいか、時々このようにお粗末なところが放置され、それを指摘するような利用者サイドも皆無となっています。 また、こういう関数が放置されているのにWin64 Dominoサーバがほぼ問題なく動いているのは、Undocumentedな関数で実装されていることは想像に難くなく、Windows ToolのDependency Walkerにかければそれっぽい関数名が浮かび上がります。 C APIのドキュメントもきちんと整備し、Documentableになった関数からユーザに開放してくれることを切に願うばかりです。

注意: コードの利用においてチブル・システムズは一切の責任を負いません。自己責任でご利用をお願いいたします。

DominoサーバにOAuth2認証機能をアドインする「DOAP」

先日、Facebookページで開発を表明したDominoのOAuth2アドインですが、一部の機能を残して0.1.0-betaバージョンが完成しました。

名称は「DOAP」Domino OAuth2 Providerの略です。

このDOAPは、Dominoにアカウントを持っているユーザが、他のWebサービスからDominoのリソースにアクセスしたいときに、OAuth2の仕組みでシームレスに「認可」することができます。

通常、DominoはBasic認証かフォーム認証で、REST APIでさえこの認証が必要になります。 Basic認証の場合は常にユーザ名、パスワードをどこかに保持しておかなければなりません。 フォーム認証の場合も同様で、さらにセッションが切れたとき、ログイン画面をHTMLで返してきます。 このとき厄介なのが「HTTP/1.1 200 OK」と、認証が切れていることをリターンコードだけでは判断できないのです。

これを一気に解決する手立てがOAuth2認証であり、その実装が「DOAP」です。

この問題が解決されると、OAuth2に対応したWebサイト構築言語の間口が途端に開けることになります。 Node.js、PHPRubyPython、.NETなどなど。 Dominoを「認可サーバ」「リソースサーバ」という形で、言語にとらわれずに活用することが可能になります。

構想こそ長くボヤッとしていたものの、「もしやできるのは?」と思い立ったのが、このGW前。 1ヶ月ほどでこのような形になったのも、応援して下さった方々のおかげです。

Lotus Notes R3.0Jに出会い、およそ25年。 C APIC/C++で開発する人が周りにいない中、C APIで初めて製品を構築したのが約5年前。 それからも2つの製品開発に応え、このOAuth2開発に至りました。

現在、関係者とDOAPの公開方法について模索しているところです。 広く使ってもらいたいところですが、認証というセンシティブな一面もあるので、なかなか頭を悩ませています。 正式な発表は別のところからになりますが、その折にはここでもリンクさせていただきます。

NSFItemInfoNext関数を64bit Windows Dominoサーバで使用するとクラッシュする

追記 2019-11-24

  • 記事のリンクを追加しました。
  • ソースコードにcpp指定をしました。

本編

私が、とあるプロジェクトでDominoサーバのアドインを作ったとき、奇妙な現象に悩まされた。

その現象というのは、NSFItemInfoNextというNotes C APIの関数を使うと、32bit WindowsのNotesクライアントとDominoサーバ、64bitのMacOS、32bit Linuxではまったく問題がないのに、64bit WindowsのDominoサーバでのみクラッシュするという事象である。

そもそも「NSFItemInfoNext」とは何なのか。

NSFItemInfoNext関数は、通常NSFItemInfo関数とペアで使用する。「NSFItem○○」という関数はアイテム(フィールドと同義)に関する操作を行う種類のもので、NSFItemInfoはアイテムの情報を取得するのに使用する。アイテムは、名前、型、値、属性の4つで構成されていて、NSFItemInfoは属性以外の情報を取得できる。アイテムは文書中名前(フィールド名)で区別されるため、通常1文書に1つだが、1アイテム中に保持できる値の大きさが限られているため、同名アイテムが複数保管されることがある。その時、2つめ以降の同名アイテムを取得するのに使われるのが、NSFItemInfoNext関数というわけである。

例えば、次のようにコーディングになる。

STATUS error = NSFItemInfo(hNote, "Body", strlen("Body"), &bItem, &wType, &bValue, &dwLength);
if (ERR(error) != ERR_ITEM_NOT_FOUND) {
  if (error) return;
  bPrevItem = bItem;
  error = NSFItemInfoNext(hNote, bPrevItem, "Body", strlen("Body"), &bItem, &wType, &bValue, &dwLength);
}

バイト数が決まっている数値型(NUMBER)、日時型(TIMEDATE)やシンプルなテキストデータくらいであれば、同名アイテムを2つ以上に分けて保存することはめったにないが、64キロバイトを超えるデータは文書に保存できないため、同名アイテム保存を使用する。Notesクライアントは、約40キロバイトを目安に同名アイテムを2つ以上に分けて保存する。

このように、NSFItemInfoNext関数は何か特殊な関数というわけではなく、APIとしては極めてベーシックな存在の関数なのだが、それだけにDominoサーバx64Winでクラッシュし、その原因がNSFItemInfoNextだとわかった時は、首をひねるばかりだった。

Notes C APIを使用したコードをコンパイルする場合、Windows 32ビットでは、少なくとも次の3つを指定する。

-DW -DW32 -DNT

Windows 64ビットでは、これらに加えて、以下の3つも必要になる。

-DW -DW32 -DNT -DW64 -DND64 -D_AMD64_

これら以外に識別子の過不足がないか調べてみたが、特に問題ない。

ネット上で調べているうちに、興味深い記述を見つけた。NSFItemInfoNext関数は以下のような引数を取る。

STATUS LNPUBLIC NSFItemInfoNext (
  NOTEHANDLE hNote,
  BLOCKID NextItem,
  const char far *Name,
  WORD NameLength,
  BLOCKID far *retbhItem,
  WORD far *retDataType,
  BLOCKID far *retbhValue,
  DWORD far *retValueLength
);

その記事では、この関数をLotusScript内から呼び出した時に、BLOCKID型の引数を倍精度小数点数(double型)の幅8バイトで指定する必要があるということだった。

C APIの世界では、BLOCKIDはプールハンドル(4バイト)とブロックハンドル(2バイト)の2つの値を持つ6バイト構造体だ。これを8バイトにするには、2バイトのブロックハンドルを4バイトにすることになる。

Notes C APIでブロックハンドルを定義しているのは、global.hヘッダーファイルだ。

// global.hの1076行目
typedef WORD BLOCK;

これを次のように書き換える。

#ifdef W64
typedef DWORD BLOCK;
#else
typedef  WORD BLOCK;
#endif

こうすることで、NSFItemInfoNext関数はクラッシュしなくなり、 BLOCKIDを使用するほかの関数も問題なく動作している(ように見える)。この手法が他の関数に本当に影響はないのか予断を許さないが、64bit WindowsのDominoサーバでクラッシュに困ったら、NSFItemInfoNext関数に起因していないか調べ、上記方法を試してみるのもよいかもしれない。

この修正による影響事案

この記事で提案した修正について、私自身以下の事案に遭遇しました。よろしければこちらもご参照下さい。

chiburusystems.hatenablog.com