C++にて、DLLからunique_ptrのオブジェクトを返す

経緯など

かれこれC++にて、拡張機能として異なる種類のオブジェクトを生成するプログラムを作っておりました。従いまして、プログラム本体中ではオブジェクトは基底クラスのポインタとして扱い、実際には派生クラスとして生成しております。DLLで後から別の派生クラスを追加できる設計として、DLL側でオブジェクトを生成し、基底クラスのポインタとして返しておりました。メイン側のプログラムで示すと、以下のような感じです。

// メイン側プログラム
class BaseObject
{
public:
BaseObject(void) {};
virtual ~BaseObject(void) {};
virtual int getValue(void) = 0;
virtual void setValue(void) = 0;
};

class MainObject: public BaseObject
{
public:
MainObject(void) {};
~MainObject(void) {};
int getValue(void) {return val};
void setValue(int val) {value = val;};
private:
int value;
};

一方で、DLL側で生成するオブジェクトは以下のような感じです。サンプルなので内容的にはMainObjectとDllObjectは全く同じになっていますが、実際には機能的の異なるものを想定しています。DLL側でのオブジェクトの定義を以下に示します。

// DLL側プログラム
class DllObject: public BaseObject
{
public:
DllObject(int val) {};
~DllObject(void) {};
int getValue(void) {return val};
void setValue(int val) {value = val;};
private:
int value;
};

実体はMainObjectとDllObjectと異なるものをBaseObjectのポインタとしてメインプログラム側から統一的に扱いたい、というのがここでの趣旨です。

将来的にはWindows以外も対象としたいのですが、Windowsしか動作させることができていませんので、現状ではunique_ptrを扱う部分はWindowsに限定しております。またWindows以外は動作チェックできておりませんので悪しからず。

生ポインタで扱う手順

あちこち調べますと、オブジェクトの生成をどちらか一方で行えば大丈夫のようです。従いまして、メイン側オブジェクト(MainObject)の生成削除はメイン側で、DLL側オブジェクト(DllObject)の生成削除はDLL側で行えばいいことになります。メイン側のオブジェクト生成はごくごく普通に

// メイン側プログラム
BaseObject * ptrObj = new MainObject();
delete ptrObj;

のようになると思います。一方で、DLL側オブジェクトを扱うには少々準備が必要です。まず、DLL側で、DllObjectを生成、破棄する関数を作る必要があります。その関数は、メイン側から呼ぶ必要があるので、少々ややこしいですが、以下のように書くことができます。

// DLL側プログラム
#if defined(_WIN32)
#define DLL_API extern "C" __declspec(dllexport)
#define CALL_CONVENTION __stdcall
#else
#define DLL_API extern "C"
#define CALL_CONVENTION __attribute__((stdcall))
#endif

DLL_API BaseObject * CALL_CONVENTION createObject(void)
{
return new DllObject();
}

DLL_API BaseObject * CALL_CONVENTION deleteObject(BaseObject *ptrObj)
{
delete ptrObj;
}

これらをメイン側から呼ぶためには(少々変な実装ですが)

// メイン側プログラム
#if defined(_WIN32)
#include <windows.h>
#else
#include <dlfcn.h>
#endif

using CreateObjectFunctionPtr = BaseObject * (CALL_CONVENTION *)(void);
using DeleteObjectFunctionPtr = void (CALL_CONVENTION *)(BaseObject *);
std::string filename = "dllfile";
void *ptrHandle;

// 共有ファイルの読み込み
#if defined(_WIN32)
std::string filename2 = filename + ".dll";
std::wstring filename3(filename2.begin(), filename2.end());
ptrHandle = LoadLibrary(filename3.c_str());
#else
std::string filename2 = filename + ".so"; // or ".bundle"
ptrHandle = dlopen(filename2.c_str(), RTLD_LAZY);
#endif

// 関数ポインタ取得
CreateObjectFunctionPtr ptrCreateFunction;
DeleteObjectFunctionPtr PtrDeleteFunction;
#if defined(_WIN32)
ptrCreateFunction = (CreateObjectFunctionPtr)GetProcAddress(static_cast<HINSTANCE>(ptrHandle), "createObject");
ptrDeleteFunction = (DeleteObjectFunctionPtr)GetProcAddress(static_cast<HINSTANCE>(ptrHandle), "deleteObject");
#else
ptrCreateFunction = (CreateObjectFunctionPtr)dlsym(ptrHandle, "createObject");
ptrDeleteFunction = (DeleteObjectFunctionPtr)dlsym(ptrHandle, "deleteObject");
#endif

// オブジェクトの生成、削除
BaseObject * ptrObj = ptrCreateFunction();
ptrCeleteFunction(ptrObj);

// 後片付け
#if defined(_WIN32)
FreeLibrary(static_cast<HINSTANCE>(ptrHandle));
#else
dlclose(ptrHandle);
#endif

このような感じで、恐らくはWindows、macOS、Linuxで動作するものと思います。ソースファイルを大幅に簡略化しているため、実際の動作チェックはしておりません。

unique_ptrでの受け渡しへの変更

ここまで書いてかなり力尽きそうなのですが、実は「#define DLL_API extern “C”」が曲者でして、C++11特有のunique_ptrを返すことができません。そこで、「extern “C”」を除けばいいのですが、そうすると今度は名前修飾によって、DLL側の関数名が変更され、調べてその名前に一々変更するのも少々大変です(とはいえ、同じコンパイラであれば頻繁に名前収修飾規則は変更されないはずなので調べてべた書きも選択肢としてはありだと思います)。そこで、DLL側に「Library.def」なるファイルを生成し、以下のような記述を行います。DLLのファイル名はDllLibrary.dllであると仮定しいます。「@1」の指定によって、番号での関数ポインタの取得も可能ですが、今回は特には使っておりません。VisualStudioですと、「新しい項目の追加」(右クリックのメニュー)で「コード」の中の「モジュール定義ファイル」で追加すると自動的に「リンカー」設定内に「入力」項目の「モジュール定義ファイル」にファイル名を設定してくれます。

Library DllLibraryEXPORTS    createObject @1

DLL側のファイルを以下のように修正します。unique_ptrなので削除する関数が不要になります。

// DLL側プログラム
#include <memory>
#if defined(_WIN32)
#define DLL_API
#define CALL_CONVENTION __stdcall
#else
#define DLL_API
#define CALL_CONVENTION __attribute__((stdcall))
#endif

DLL_API std::unique_ptr<BaseObject> CALL_CONVENTION createObject(void)
{
return std::make_unique<DllObject>();
}

メイン側のプログラムは変更点のところのみ記述すると、以下のようになると思います。

// メイン側プログラム
using CreateObjectFunctionPtr = std::unique_ptr<BaseObject>(CALL_CONVENTION *)(void);

unique_ptrの動作により、自動的にDLL側でdeleteが呼ばれているようで、今のところこの実装でメモリリーク等は検出されておりません。使い方は、メイン側から以下のようにどちらのオブジェクトも統一的に扱えるようになっていると思います。

// メイン側プログラム
std::unique_ptr<BaseObject> ptrObj = std::make_unique<MainObject>();
std::unique_ptr<BaseObject> ptrObj2 = ptrCreateFunction();

私はC++の仕様について詳しいわけではなく、上記コードで動いていることは確認しておりますが、これが正しいことまでは保証できておりませんので、その辺りはご注意ください。もう少し時間が取れればソースファイルをまとめて動作チェックまでするのですが、そこまで時間が割けず、まずは自分自身が情報を探してあまり見付からず、試行錯誤してなんとか動く書き方を見つけたところを、他の悩んでいる方々にヒントとして共有するところまでに留めさせてください。またshared_ptrについては実験しておりませんのであしからず。

参考にさせていただいたサイト

コメント

タイトルとURLをコピーしました