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」関数についての対応策について書いた。
これについて、その後特に問題もなく順調にいっていたのだが、つい先日、とうとうこの対応が別の形で悪さをする事案に陥った。
以前の記事の概略
前回の対応をかいつまんで説明する
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を操作できるようにする。
公式ガイドはこちら。
日本語で参考にしたのはこちら。
環境
- Windows 10
- Visual Studio 2015 Express
日本語で参考にした記事にはVS2017を使ったとあるが、後述するようにVS2017ではビルドできなかったので、VS2015を使う。
準備
Visual C++ 2015をインストール
CMakeをインストール
VS用のMakefileを作成するために使用する。
Boostをインストール
MongoDB C++ DriverではC++17の機能を使用するため、対応していないコンパイラではC++17ポリフィルが必要になる。公式ガイドによると、MSVCではBoostが唯一の道だとある。Boost C++ Librariesは基本的にソースコードだけでいいはずだが、プリコンパイル版もあるので、そちらを使うことにする。
上記サイトから、最新版のboost_x_y_z-msvc-14.0-64.exe
をダウンロードして、インストールしておく。デフォルトのままインストールすれば、C:/local/boost_x_y_x
にインストールされる。
MongoDB C DriverとBSON libraryをインストールする。
2種類入れるような書き方だが、操作自体は単一。できあがるDLLが2つになるイメージ。
公式ガイドはこちら。
ダウンロードサイトはこちら。
日本語で参考にしたのはこちら。
ダウンロードした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リンクからをダウンロードして展開する。
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だけ違うレコードが複数表示される。
MongoDBクライアントCompassで見るとご覧の通り。
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; } }
- コンパイルして、DLLを作成する。例えば、
hello
というアドインであれば、nhello.dll
という名前でDLLを作る。頭文字のn
はWindows用のプリフィックス。 nhello.dll
をDominoサーバのプログラムディレクトリにコピーする(他に必要なモジュールも同様にプログラムディレクトリに置く)。- サーバ文書かWeb設定のDSAPI欄に、
hello
と記述する(n
と.dll
は書かない)。 - Dominoサーバを再起動する。
すると、Dominoコンソールには以下のように表示される。
続いて、ブラウザから次のようなリクエストを送る。
http://localhost/hello
すると、以下のようなレスポンスが返ってくる。
自分用メモ: Notes C APIアプリをマルチプラットフォーム開発するときのマクロ定義
- Windows 32ビット版
-DNT -DW32 -DW -D_X86_ -DND32 -DDTRACE -D_CRT_SECURE_NO_WARNINGS -DPRODUCTION_VERSION -DDUMMY
- Windows 64ビット版
-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
- Linux 32ビット版
-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
- Linux 64ビット版
-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
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; }