W64API不具合レポート〜ExtMgr編

果たして、Lotus Notes C API ToolkitのHCL版はリリースされるのか?

今回のWindows 64ビット版コンパイル時の不具合については簡潔に報告します。

Extension Manager(通称ExtMgr)において、コールバック関数にはEMRecordという構造体へのポインタが渡されます。

typedef struct
  {
  EID           EId;                    /* identifier */
  WORD      NotificationType;       /* EM_BEFORE or EM_AFTER */
  STATUS        Status;                 /* core error code */
  VARARG_PTR    Ap;                     /* ptr to args */
  } EMRECORD;

このときの EID 型は、定義によると以下のようになります。

typedef WORD EID;

しかし、Windows 64では不十分で、このまま使うと以下のメンバ変数が2バイトずつズレてしまい、最後の Ap が指すポインタを操作すると、Dominoサーバがクラッシュします。

そのため、 EID の定義を以下のように修正する必要があります。

// extmgr.h
#if defined(NT) && defined(_AMD64_)
typedef DWORD EID;
#else
typedef WORD EID;
#endif

それでは、素敵なNotes C APIライフをお過ごしください。

Gitで共有しているコードをローカル固有環境で動かす

Gitリモートリポジトリ経由でWebサーバアプリケーションなどを開発している時、URL等の関係でどうしても自身のローカル環境に設定を合わせたい時がある。

今いちばんよい方法だと思っているのは、その設定ファイルをGitの対象ファイルから外す方法。

Node.jsのWebアプリケーションフレームワークSails.js | Realtime MVC Framework for Node.jsでは、ひな形を作ると config/local.js というファイル作成して、そのファイルを .gitignore 内に含めておいてくれるので、ローカルでの設定を他者とファイルを競合することなく開発することができる。

では、仕組みなどの理由でその方法が採れない時はどうすればいいか。

一時しのぎの感は否めないが、 git stash を使う方法もある。

クローンを作成し、自身のローカル用にURLを config.js に書くとする。 もちろん、このファイルは他者と共有するので、変更内容をアップロードすることは避けたい。 そんな場合に、以下の方法を取るといい。

# 変更内容をすべてステージング
git add .

# 特定のファイルをステージングから戻す
git reset HEAD config.js

# コミット
git commit -m HogeHoge

# プッシュ
git push origin master

リモートリポジトリの変更内容をローカルにプルするときは、以下の手順を取る。

# 現在の変更を退避(config.jsは変更前の状態になる)
git stash save

# リモートリポジトリから最新コードをプル
git pull origin master

# 直近の退避内容を戻す
git stash apply

退避内容の戻し方いろいろ

# 特定の退避内容を戻す(stash@{0}はスタッシュ名)
git stash apply stash@{0}

# 内容を戻すと同時に退避リストから削除したい場合(stash@{0}はスタッシュ名)
git stash pop stash@{0}

これで、はれて最新コードをローカル環境下で動かすことができる。 git stash は他にもいろいろコマンドがあるので、興味のある方はググってみてください。

NSFItemInfoNext対応の悪影響

1年半前の記事で、「NSFItemInfoNext」関数についての対応策について書いた。

chiburusystems.hatenablog.com

これについて、その後特に問題もなく順調にいっていたのだが、つい先日、とうとうこの対応が別の形で悪さをする事案に陥った。

以前の記事の概略

前回の対応をかいつまんで説明する

NSFItemInfoNext関数の第2引数はBLOCKIDという構造体を指定する。

typedef WORD BLOCK; /* pool block handle */

typedef struct /* Pointer to any block in any pool */
{
  DHANDLE pool; /* pool handle */
  BLOCK block; /* block handle */
} BLOCKID;

ところが、DominoサーバWindows64ビット版においては、この「BLOCK」は2バイトの「WORD」型ではなく、4バイトの「DWORD」型を指すという事実だ。

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

こうしないと、Win64においては必ずクラッシュする。幸い、Win32版、Mac版やLinux版ではこれを意識しなくてもいい。

今回発生した問題

このまま1年半使い続けてきたが、極めて限定的な条件の時にのみ、この対応がDominoサーバのクラッシュを生むらしい。私の認識している範囲で言えば、レプリカ直後の文書において、「特殊な」アクセスを試みたときに起こる(詳細は割愛する)。

NSFItemInfo関数は、文書内のアイテム情報(フィールド)を取得する。NSFItemInfoNext関数は、同一フィールド名の2番目以降を取得する。添付ファイルなどの特殊な場合を除き、1つのアイテムには64キロバイトの制限があるので、大きいサイズのフィールドはいくつかのブロックに分割して保存する。前出の「BLOCKID」はそれらを識別するための仕組みである。

ブロックからフィールドデータを得るためには、ブロックをロックして、ポインタを取得する。

void far * LNPUBLIC OSLockObject (DHANDLE Handle);

#define OSLock(blocktype,handle) ((blocktype far *) OSLockObject(handle))

#define    OSLockBlock(type,blockid) \
  ((type far *)(OSLock(char,(blockid).pool) + (blockid).block))

OSLockObject関数でハンドルからポインタを得る。

OSLockマクロは、単に得られたポインタを指定した型のポインタにキャストする。

OSLockBlockマクロは、BLOCKIDのpoolハンドルから得たcharポインタから、blockバイト分移動した場所を指すようにして返す。

一般的な使い方であればこれで問題がなかった。しかし、「特殊なアクセス」でこのOSLockBlockマクロを使ってポインタを得ると、とんでもないことになる。

問題の根本は、BLOCKID.blockの型を2バイトから4バイトにしたことにある。特殊なアクセスをすると、この4バイトの上位2バイトに得体の知れない値「0xc000」が含まれてしまうのである。元々blockは2バイトなので、OSLockBlockマクロが64キロバイト以上移動した場所を指すことはない。しかし、NSFItemInfoNext関数の「実装バグ」対応でBLOCKID.blockを4バイトにしたWin64 C APIプログラムでは、4バイトのblock値を受け取ってしまう。上位に「0xc000」という値が入ってしまえば、OSLockBlockマクロはとんでもない場所をポインタとして返してしまう。もちろんこのポインタでメモリアクセスすれば、保護違反でクラッシュする。幸い今まで上位2バイトに0以外が入ったことがなかっただけで、0以外が入らない保証はなく、この危険性は大いにあったわけである。

DBLOCK、DBLOCKIDの存在

この事案に直面した折、あらためてC APIヘッダファイルを確認したところ、BLOCK、BLOCKID以外に4バイト版とも言うべき以下の型を発見した。いずれもpool.hヘッダファイルである。

typedef DWORD DBLOCK; /* dpool block handle */

typedef struct /* Pointer to any block in any pool */
{
  DHANDLE pool; /* pool handle */
  DBLOCK block; /* block handle */
} DBLOCKID;

これらがWindows64ビット版で活用されてもいいところだが、残念ながら、APIツールキット9.0.1上では、これらは宣言されているだけで、NSFItemInfoNext関数などには全く利用されていない。

う〜む、またしても小手先で対応しなければならないではないか!

対応策

今回も、Windows64ビット版限定の対応とする。

// pool.hの54行目
#define    OSLockBlock(type,blockid) \
  ((type far *)(OSLock(char,(blockid).pool) + (blockid).block))

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

#ifdef W64
#define    OSLockBlock(type,blockid) \
  ((type far *)(OSLock(char,(blockid).pool) + ((blockid).block & 0x0000ffff)))
#else
#define    OSLockBlock(type,blockid) \
  ((type far *)(OSLock(char,(blockid).pool) + (blockid).block))
#endif

BLOCKID.blockの値上位ビットをビットマスクでバッサリ切り捨てる。そもそもマクロやビットマスクの対応も、C++11以降のモダンC++の世界でどうかと思うが、まあこれはCのためのものなので、出しゃばったまねはしないようにする。

まとめ

クラッシュの原因特定まで時間を要した。「特殊なアクセス」という限定的な条件のときだけ起こるクラッシュ。その差分からとんちんかんな方向にも時間を費やした。最終的に、自分が書いた昔の記事に助けられた。こんな状況、海外も含めて同志がいないのもなかなかさみしいものだが、また一つ、自分の成長を実感できたことは嬉しい。

最後にもう一つ、HCLのNotes/Domino担当の皆さん、C APIのサポートってどうなっていますか?

mongodb c++ driver(windows 64)をビルド/インストールする。

目標

MongoDB C++ Driverをインストールして、C++からMongoDBを操作できるようにする。

公式ガイドはこちら。

mongocxx.org

日本語で参考にしたのはこちら。

qiita.com

環境

日本語で参考にした記事にはVS2017を使ったとあるが、後述するようにVS2017ではビルドできなかったので、VS2015を使う。

準備

Visual C++ 2015をインストール

visualstudio.microsoft.com

CMakeをインストール

VS用のMakefileを作成するために使用する。

cmake.org

Boostをインストール

MongoDB C++ DriverではC++17の機能を使用するため、対応していないコンパイラではC++17ポリフィルが必要になる。公式ガイドによると、MSVCではBoostが唯一の道だとある。Boost C++ Librariesは基本的にソースコードだけでいいはずだが、プリコンパイル版もあるので、そちらを使うことにする。

sourceforge.net

上記サイトから、最新版のboost_x_y_z-msvc-14.0-64.exeをダウンロードして、インストールしておく。デフォルトのままインストールすれば、C:/local/boost_x_y_xにインストールされる。

MongoDB C DriverとBSON libraryをインストールする。

2種類入れるような書き方だが、操作自体は単一。できあがるDLLが2つになるイメージ。

公式ガイドはこちら。

mongoc.org

ダウンロードサイトはこちら。

github.com

日本語で参考にしたのはこちら。

qiita.com

ダウンロードしたtar.gzファイルを、ここではC:¥tmp以下に展開する。 (Source codeリンクからダウンロードしたものは、中身は似ているが、cmakeでエラーとなってしまうので、必ずmongo-c-driver-x.y.z.tar.gzのリンクからダウンロードするように)

Visual Studio 2015のx64 Native Toolsコマンドプロンプトを起動して、以下のように進める。

cd /d C:\tmp\mongo-c-driver-1.15.1
mkdir cmake-build
cd cmake-build
"C:\Program Files\CMake\bin\cmake.exe" -G "Visual Studio 14 2015 Win64" "-DCMAKE_INSTALL_PREFIX=C:\mongo-c-driver" "-DCMAKE_PREFIX_PATH=C:\mongo-c-driver" ..

最後の .. が非常に重要なので、忘れないようにする。 C:\mongo-c-driverの部分は任意で変えられる。インストール先を変えたい場合は変更する。

  • 追記2020-1-15

このままだとデバッグビルドになる。リリースビルドにしたい場合は、以下のオプションを追加する。

-DCMAKE_BUILD_TYPE=Release

続いてビルドする。

msbuild.exe /p:Configuration=RelWithDebInfo ALL_BUILD.vcxproj

大量の警告が出てもエラーが0なら問題ない(と思う)。 続いてインストールする。インストールは、バイナリ(DLL)、インクルードファイル(ヘッダファイル)、ライブラリファイルを環境に配置する作業。

msbuild.exe INSTALL.vcxproj

これでC:\mongo-c-driverにMongoDB C Driverがインストールされた。

MongoDB C++ Driverをインストールする。

インストールの方法は、ほぼC Driverと同じ。下記リンクから最新版のSource codeリンクからをダウンロードして展開する。

github.com

Visual Studio 2015のx64 Native Toolsコマンドプロンプトを起動して、以下のように進める。

cd /d C:\tmp\mongo-cxx-driver-r3.4.0\build
"C:\Program Files\CMake\bin\cmake.exe" -G "Visual Studio 14 2015 Win64" -DCMAKE_INSTALL_PREFIX=C:\mongo-cxx-driver -DCMAKE_PREFIX_PATH=C:\mongo-c-driver -DBOOST_ROOT=C:\local\boost_1_71_0 ..

C Driverインストール時にC:\mongo-c-driverを変更していたら、こちらでもそれに対応しておくこと。 また、C:\mongo-cxx-driverの部分は任意で変更可能。 実行すると、最終行で-- Build files have been written to: C:/tmp/mongo-cxx-driver-r3.4.0/buildのような出力が表示されるはず。

  • 追記2020-1-15

こちらも同様。このままだとデバッグビルドになる。リリースビルドにしたい場合は、以下のオプションを追加する。

-DCMAKE_BUILD_TYPE=Release

続いてビルド、インストールする。

msbuild.exe ALL_BUILD.vcxproj
msbuild.exe INSTALL.vcxproj

VS2017

Visual Studio 2017で一連の作業を進めたところ、C++ Driverのビルドmsbuild.exe ALL_BUILD.vcxprojでエラーが出力されて、全く進めなくなった。ENABLE_EXTENDED_ALIGNED_STORAGEに関するエラーが出ているが、Boostをやめてもムダで、結局VS2015を使ってみたらエラーが出なくなった。バイナリさえできてしまえば、あとはVS2017で開発できるが、この疑問は解消されないまま。まあ、公式ガイドにVS2017のことは書いていないので、正しいといえば正しいのかな。

初めてのMongoDB C++によるレコードの作成。

私のC++開発環境はQt Creatorなので、Qt寄りで説明する。

公式ガイドのサンプルコードをアレンジして、次のようなコードを作成する。

#include <iostream>

#include <bsoncxx/builder/stream/document.hpp>
#include <bsoncxx/json.hpp>

#include <mongocxx/client.hpp>
#include <mongocxx/instance.hpp>

int main(int, char**) {
  try {
    mongocxx::instance inst{};
    mongocxx::client conn{mongocxx::uri{}};

    bsoncxx::builder::stream::document document{};

    auto collection = conn["mydb"]["mycollection"];
    document << "say" << "Hello, MongoDB C++ Driver!";

    collection.insert_one(document.view());
    auto cursor = collection.find({});

    for (auto&& doc : cursor) {
        std::cout << bsoncxx::to_json(doc) << std::endl;
    }
  }
  catch (std::exception &ex) {
    std::cerr << ex.what() << std::endl;
  }
}

mongocxx::uri{}のところで接続したいMongoDBへのURIを書くが、ローカルに接続したいときは空で構わない。

Qtプロジェクトファイルには、インクルードパスとライブラリパスを定義しておく。Boostにもインクルードパスを通す。

INCLUDEPATH += C:/mongo-cxx-driver/include/bsoncxx/v_noabi C:/mongo-cxx-driver/include/mongocxx/v_noabi C:/local/boost_1_71_0
LIBS += -LC:/mongo-cxx-driver/lib -lmongocxx -lbsoncxx

実行時のPATH環境変数には、C++ DriverとC DriverのDLLがあるbinパスを通しておく。C++のDLLがCのDLLを参照しているため。

...;C:\mongo-cxx-driver\bin;C:\mongo-c-driver\bin

実行すると、次のように書き込まれたレコードが表示される。何回も実行すれば、IDだけ違うレコードが複数表示される。

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

MongoDBクライアントCompassで見るとご覧の通り。

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

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;
}