C++20で導入が決まったモジュールですが、「リンクのことは処理系依存」というわけで規格上DLLとは何の関係もありません。
ただし、DLLとモジュールを連動させるような実装はありえるし、実現すればうれしいところだと思います。
今回は規格の外側、DLLとモジュールの関係をコンパイラの挙動から探ってみました。
処理系は、一番早くからモジュールを実装していた(要出典)MSVCを使用しました。
- cl.exe バージョン 19.20.27508.1
ライブラリを分けない場合
まずは普通にモジュールを使うコードをMicrosoftのブログを参考に書きました。
export module calc;
export namespace calc
{
int add(int a, int b)
{
return a + b;
}
}
import calc;
//import <iostream>; // => Header units; まだ実装していなかった
#include <iostream>
int main()
{
int x,y;
std::cin >> x >> y;
std::cout << calc::add(x, y) << std::endl;
return 0;
}
- モジュールを宣言しても名前空間は作られません
- Header Unitsは未実装
ビルドはとりあえずMakefileで。
CXX = cl /nologo /EHsc /std:c++latest /experimental:module
main.exe: main.cpp calc.ifc calc.obj
$(CXX) main.cpp calc.obj
calc.ifc: calc.ixx
$(CXX) /c calc.ixx
calc.obj: calc.ifc
clean:
del main.exe
del main.obj
del calc.ifc
del calc.obj
check: main.exe
echo 1 2 | main.exe
- モジュールは依存順にビルドする必要があります(ここでは、calc→mainの順)
- MSVCではビルド済みモジュールの拡張子は
.ifc
のようです
> nmake /nologo check
echo 1 2 | main.exe
3
無事に動きました。
ライブラリを分ける場合
問題はライブラリを分ける場合です。DLLとモジュールは実装上何らかの関係があるのでしょうか。
レガシーライブラリ
初めにモジュール以前の方法でDLLを作ってみます。
#pragma once
#ifdef FOO
#define EXPORT __declspec(dllexport)
#else
#define EXPORT __declspec(dllimport)
#endif
struct EXPORT foo
{
static int foovar;
static int getvar();
};
#define FOO
#include "foo.h"
int foo::foovar;
int foo::getvar(){ return foovar; }
#include <iostream>
#include "foo.h"
int main()
{
foo::foovar = 99;
std::cout << foo::getvar() << std::endl;
return 0;
}
CXX = cl /nologo /EHsc /std:c++latest
main.exe: main.cpp foo.lib
$(CXX) main.cpp foo.lib
foo.lib: foo.cpp foo.h
$(CXX) /DFOO /LD foo.cpp
モジュールへ移行
前節のレガシーライブラリをモジュールへ移行するには、Header Unitsで次のようにラップすれば良いです。
export module foo;
export import "foo.h";
しかし、現時点では実装されていません。代わりに次のようにします。
export module foo;
#include "foo.h"
export struct foo;
利用側も修正します。
import foo;
#include <iostream>
int main()
{
foo::foovar = 99;
std::cout << foo::getvar() << std::endl;
return 0;
}
CXX = cl /nologo /EHsc /std:c++latest /experimental:module
main.exe: main.cpp foo.ifc foo.lib
$(CXX) main.cpp foo.lib
foo.lib: foo.cpp ifoo.ixx
$(CXX) /LD /DFOO foo.cpp ifoo.ixx
foo.ifc: foo.lib
これはコンパイルでき、動作は前節と同じでした。
モジュールと dllexport
しかし、以下の分岐はどうなるのでしょうか?
#ifdef FOO
#define EXPORT __declspec(dllexport)
#else
#define EXPORT __declspec(dllimport)
#endif
モジュールは利用するより前にビルドしています。プリプロセッサはモジュールのビルド時には使えますが、ヘッダーファイルとは違って利用側ではもはや実行されません。
※ 関数に対する __declspec(dllimport)
の使用は任意なので注意しましょう。foo
に変数があるのはそのためです。モジュールだと省略できるのかと思って時間を失いました。
一見すると利用側でも__declspec(dllexport)
になりそうですが、コンパイルできるという事実から次の予測が成り立ちます。
- モジュールユニットで
__declspec(dllexport)
を記述するとインポート側では__declspec(dllimport)
に見える
ヘッダーファイルと違って、モジュール自身をコンパイルしているのか、コンパイル済みのモジュールをインポートしているのかは、コンパイラからすれば簡単にわかるはずなので十分あり得えそうです。
このことはエラーメッセージを使って確認できます。
変数 foo::foovar
を再定義してコンパイルしてみましょう。
import foo;
#include <iostream>
int foo::foovar = 0;
int main()
{
foo::foovar = 99;
std::cout << foo::getvar() << std::endl;
return 0;
}
main.cpp(5): error C2491: 'foo::foovar': dllimport スタティック データ メンバー の定義は許されません。
当たりのような気がします。
まとめ
- 挙動から判断すると、モジュールユニットで
dllexport
した場合、インポート側ではdllimport
として扱われるようです。 - 従って、モジュールを使えば
dllimport
とdllexport
の使い分けが不要になります。
より連携を強くするのであれば、__declspec(dllexport) export module foo;
などとモジュールに対して指定できると便利かもしれません。
参考文献
謝辞
この記事はC++20を相談しながら調べる会 #1の結果として書かれました。