C++
C++20

C++ ModulesとDLLの関係 (MSVCの場合)

C++20で導入が決まったモジュールですが、「リンクのことは処理系依存」というわけで規格上DLLとは何の関係もありません。

ただし、DLLとモジュールを連動させるような実装はありえるし、実現すればうれしいところだと思います。

今回は規格の外側、DLLとモジュールの関係をコンパイラの挙動から探ってみました。

処理系は、一番早くからモジュールを実装していた(要出典)MSVCを使用しました。


  • cl.exe バージョン 19.20.27508.1


ライブラリを分けない場合

まずは普通にモジュールを使うコードをMicrosoftのブログを参考に書きました。


calc.ixx

export module calc;

export namespace calc
{
int add(int a, int b)
{
return a + b;
}
}



main.cpp

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で。


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を作ってみます。


foo.h

#pragma once

#ifdef FOO
#define EXPORT __declspec(dllexport)
#else
#define EXPORT __declspec(dllimport)
#endif

struct EXPORT foo
{
static int foovar;
static int getvar();
};


foo.cpp

#define FOO

#include "foo.h"

int foo::foovar;
int foo::getvar(){ return foovar; }


main.cpp

#include <iostream>

#include "foo.h"

int main()
{
foo::foovar = 99;
std::cout << foo::getvar() << std::endl;
return 0;
}


Makefile

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で次のようにラップすれば良いです。


ifoo.ixx

export module foo;

export import "foo.h";

しかし、現時点では実装されていません。代わりに次のようにします。


ifoo.ixx

export module foo;

#include "foo.h"
export struct foo;

利用側も修正します。


main.cpp

import foo;

#include <iostream>

int main()
{
foo::foovar = 99;
std::cout << foo::getvar() << std::endl;
return 0;
}


Makefile

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 を再定義してコンパイルしてみましょう。


main.cpp

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として扱われるようです。

  • 従って、モジュールを使えば dllimportdllexport の使い分けが不要になります。

より連携を強くするのであれば、__declspec(dllexport) export module foo; などとモジュールに対して指定できると便利かもしれません。


参考文献


謝辞

この記事はC++20を相談しながら調べる会 #1の結果として書かれました。