モジュール化されたC++プログラムをビルドする


モジュール機能を使ったC++ソースファイルをコンパイルするためには、そのソースファイルがインポートするモジュールのCMI[^1]が必要になります。CMIはモジュールインターフェース単位から作成されます。

CMIに関する仕様は処理系によってまちまちなので、モジュール機能を使う場合はこのあたりの仕様に注意を払うとよいでしょう。

[^1]: Compiled Module Interface (コンパイル済みモジュールインターフェース)

# 環境

- GCC 14.2.0
- Clang 18.1.8

# サンプルプログラム

```c++:main.cpp
import <string_view>;

import bbb;

int main()
{
    greet_twice("world");
}
```

```c++:aaa.cppm
export module aaa;

import <string_view>;

export void greet(std::string_view);
```

```c++:aaa.cpp
module aaa;

import <iostream>;

void greet(std::string_view name)
{
    std::cout << "hello, " << name << std::endl;
}
```

```c++:bbb.cppm
export module bbb;

import <string_view>;

export void greet_twice(std::string_view);
```

```c++:bbb.cpp
module bbb;

import aaa;

void greet_twice(std::string_view name)
{
    greet(name);
    greet(name);
}
```


# GCC

GCCではCMIはモジュールインターフェース単位をコンパイルすることで作成されます。CMIは作業ディレクトリ直下の`gcm.cache`ディレクトリに保存されます。モジュール名とCMIファイル名は次のように対応します。

| モジュール名 | CMIファイル名 |
|:-|:-|
| `abc` | `abc.gcm` |
| `abc.def` | `abc.def.gcm` |
| `abc:xyz` | `abc-xyz.gcm` |

CMIは必要になった際に自動的に`gcm.cache`から探されるので、コンパイラに明示的にCMIファイル名を渡す必要はありません。

上のサンプルプログラムをビルドするためのコマンドは次のようになります。

```sh
CXX=g++
CXXFLAGS='-std=c++20 -fmodules-ts -fPIC'
LDFLAGS='-pie'

$CXX $CXXFLAGS -x c++-system-header -c iostream
$CXX $CXXFLAGS -x c++-system-header -c string_view

$CXX $CXXFLAGS -x c++ -c -o aaa.cppm.o aaa.cppm
$CXX $CXXFLAGS -c -o aaa.cpp.o aaa.cpp
$CXX $CXXFLAGS -x c++ -c -o bbb.cppm.o bbb.cppm
$CXX $CXXFLAGS -c -o bbb.cpp.o bbb.cpp
$CXX $CXXFLAGS -c -o main.cpp.o main.cpp

$CXX $LDFLAGS -o greet-twice main.cpp.o aaa.cppm.o aaa.cpp.o bbb.cppm.o bbb.cpp.o
```

<dl>
  <dt><code>-x <i>lang</i></code></dt>
  <dd>ソースファイルの種類を指定する。</dd>
  
  <dt><code>-fmodules-ts</code></dt>
  <dd>C++のモジュール機能を使用する。</dd>
</dl>

# Clang

ClangではCMI[^2]はモジュールインターフェース単位をプリコンパイルすることで作成されます。CMIは拡張子が`.pcm`であるという点を除いてGCCと同じように命名されます。GCCの場合とは異なりCMIが格納される既定の場所は存在しないので、CMIの場所をコンパイラに伝える必要があります。

また、モジュールインターフェース単位のオブジェクトファイルはCMIをさらにコンパイルすることで作成されます。

上のサンプルプログラムをClangを使ってビルドするためのコマンドは次のようになります。

```sh
CXX=clang++
CXXFLAGS='-std=c++20 -fprebuilt-module-path=. -fPIC'
LDFLAGS='-pie'

$CXX $CXXFLAGS -x c++-system-header --precompile -o iostream.pcm iostream
$CXX $CXXFLAGS -x c++-system-header --precompile -o string_view.pcm string_view

CXXFLAGS+=' -fmodule-file=iostream.pcm'
CXXFLAGS+=' -fmodule-file=string_view.pcm'

$CXX $CXXFLAGS --precompile -o aaa.pcm aaa.cppm
$CXX $CXXFLAGS --precompile -o bbb.pcm bbb.cppm

$CXX $CXXFLAGS -c -o aaa.cppm.o aaa.pcm
$CXX $CXXFLAGS -c -o bbb.cppm.o bbb.pcm

$CXX $CXXFLAGS -c -o aaa.cpp.o aaa.cpp
$CXX $CXXFLAGS -c -o bbb.cpp.o bbb.cpp
$CXX $CXXFLAGS -c -o main.cpp.o main.cpp

$CXX $LDFLAGS -o greet-twice main.cpp.o aaa.cppm.o aaa.cpp.o bbb.cppm.o bbb.cpp.o
```

<dd>
  <dt><code>--precompile</code></dt>
  <dd>モジュールインターフェース単位のプリコンパイルを行う。</dd>

  <dt><code>-fprebuilt-module-path=<i>path</i></code></dt>
  <dd>CMI探索パスを追加する。</dd>

  <dt><code>-fmodule-file=<i>file</i></code></dt>
  <dd>使用するCMIを追加する。</dd>
</dd>

現段階ではヘッダーユニットのCMIは`-fmodule-file`を使って指定することしかできませんが、この制限は将来的には取り払われる予定とのことです。その場合は次の２行は必要なくなります。

```sh
CXXFLAGS+=' -fmodule-file=iostream.pcm'
CXXFLAGS+=' -fmodule-file=string_view.pcm'
```

`-fmodule-output`オプションを使えばGCCのようにCMIの作成とオブジェクトファイルの作成を同時に行うことができます。

```sh
$CXX $CXXFLAGS -fmodule-output=aaa.pcm -c -o aaa.cppm.o aaa.cppm
$CXX $CXXFLAGS -fmodule-output=bbb.pcm -c -o bbb.cppm.o bbb.cppm
```

この場合CMIのコンパイルは必要なくなりますが、ビルド処理の並列化可能性が少しだけ悪化します。

[^2]: Clangの用語法では Built Module Interface (ビルド済みモジュールインターフェース)