Closed27

C++20 module に関する調査

maimai

概要

目的

module 機能を使用することで成果物にどう影響を与えるのかについて重点的に調査したい。

コーディングや仕様については省略することがある。
コメントの順番がめちゃくちゃで申し訳ない

環境

$ g++ --version
g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ clang --version
Ubuntu clang version 14.0.0-1ubuntu1.1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

clang++ を利用する際、'iostream' file not found とエラーが表示され、コンパイルが失敗することがある。エラーを解消するだけであれば、 g++-12 を追加でインストールすることで解消できる。詳細は以下の記事を参照。
https://askubuntu.com/questions/1441844/todays-ubuntu-22-04-updates-seem-to-break-clang-compiler

maimai

参考資料
https://cpprefjp.github.io/lang/cpp20/modules.html

翻訳単位の分類

モジュールユニットとグローバルモジュール(=モジュールでない従来のもの)に区別される。
モジュールユニットは以下のようにさらに細分化される。

モジュールインターフェースユニット モジュール実装ユニット
モジュール本体 プライマリモジュールインターフェース モジュール実装本体ユニット
パーティション モジュールインターフェースパーティション モジュール実装パーティション

モジュール宣言(翻訳単位の開始)は以下の通り

モジュールインターフェースユニット モジュール実装ユニット
モジュール本体 export module A module A
パーティション export module A:z module A:z

パーティションとは、モジュールを構成するファイルをさらに分割するためのもの。

『パーティションを使わない場合モジュールは1ファイルで書き切る必要があるということ?』

maimai

フラグメント

フラグメントは、モジュールとはまた独立した単位である。「プライベートモジュールフラグメント」と「グローバルモジュールフラグメント」がある。
『名前は似てるけど用途は全く別物っぽいね。』

プライベートモジュールフラグメント

https://cpprefjp.github.io/lang/cpp20/modules.html
プライベートモジュールフラグメントは、1ファイルでモジュールを定義しつつインターフェースと実装を分離するための機能である。
プライベートモジュールフラグメントを記述する場合、そのモジュールは翻訳単位(必然的にモジュールインターフェースユニット)を1つしか持つことができない。

『モジュールを真面目に実装しようとすると、複数の翻訳単位から構成する必要がある。プライベートモジュールフラグメントは、モジュールインターフェースユニットだけでモジュールを書き切りたいときに使う。っぽい。』

グローバルモジュールフラグメント

https://cpprefjp.github.io/lang/cpp20/modules.html
モジュール宣言の前にグローバルモジュールの実装を書ける。これをグローバルモジュールフラグメントという。
グローバルモジュールフラグメント内の宣言や定義は、後続のモジュールではなくグローバルモジュールに属する。
グローバルモジュールフラグメントにはプリプロセッサディレクティブ以外を書くことはできない。

includeヘッダを書くための区間。
#include が記述できて、プリプロセッサ以外の記述が出来ないとは?インクルード先のコードに制限は無い?』

maimai

リンケージ

外部から呼び出せるかどうか。

従来のC++(C)では主に外部リンケージと内部リンケージがある。

外部リンケージは、宣言さえあればその実装に参照できる。
内部リンケージは、宣言があっても実装は隠蔽され参照できない。(static や無名空間付けるやつ)

モジュールにおいて、
外部リンケージは、 export を付加して宣言したもの。
内部リンケージは、従来同様 static などを付与したもの。
export も static なども付与しないものは、「モジュールリンケージ」として扱われる。モジュールリンケージは、モジュール同士であればアクセス可能。

『Javaっぽい』

maimai

Hello world を書いてみよう…といったところで拡張子の問題がある

モジュールファイルの拡張子

コンパイラ・処理系によって思想が異なる。

https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Modules.html
No new source file suffixes are required. A few suffixes preferred for module interface units by other compilers (e.g. ‘.ixx’, ‘.cppm’) are supported, but files with these suffixes are treated the same as any other C++ source file.

https://clang.llvm.org/docs/StandardCPlusPlusModules.html
File name requirements
By convention, importable module unit files should use .cppm (or .ccm, .cxxm, or .c++m) as a file extension. Module implementation unit files should use .cpp (or .cc, .cxx, or .c++) as a file extension.

以降はモジュールインターフェースについては cppm を、それ以外は cpp を選択。

コーディングスタイル

普段 Google C++ Style Guide に従って書いているので、今回もこちらに従おうと思う。

https://google.github.io/styleguide/cppguide.html#modules
Do not use C++20 Modules.

そうですか…

Clang であれば、C++ module のサンプルコードとして信頼性がありそう。

https://clang.llvm.org/docs/StandardCPlusPlusModules.html

maimai

module を clang でビルドする

プライマリモジュールインターフェース のみから成るモジュールと、それを使用するコードでプログラムを組んでみる。

base.cppm
module;

#include <vector>
#include <iostream>

export module Base;  // プライマリモジュールインターフェース

namespace base {

export class Table {
public:
  Table(int height, int width)
      : height_(height), width_(width), data_(height * width) {}
  int operator()(int y, int x) const { return data_[y * width_ + x]; }
  int &operator()(int y, int x) { return data_[y * width_ + x]; }

  void print() const {
    for (std::size_t y = 0; y < height_; ++y) {
      for (std::size_t x = 0; x < width_; ++x) {
        std::cout << (*this)(y, x) << ' ';
      }
      std::cout << '\n';
    }
  }

private:
  std::size_t height_, width_;
  std::vector<int> data_;
};

} // namespace base
main.cpp
#include <iostream>

import Base;

int main() {
  base::Table table(3, 3);
  table(0, 0) = 1;
  table(0, 2) = 3;
  table(1, 1) = 1;
  table(2, 2) = 1;

  table.print();

  return 0;
}

ビルドコマンドは以下の通り

(mkdir -p out && cd out \
  && clang++ -std=c++20 ../base.cppm --precompile -o base.pcm \
  && clang++ -std=c++20 -fmodule-file=Base=base.pcm ../main.cpp -o main)

実行すると以下の通り

$ ./out/main 
0 1 3 
0 0 1 
2 0 0
maimai

-fmodule-file の代わりに -fprebuilt-module-path でディレクトリごと指定できる。
clangにおいては、このどちらかの方法でモジュールを指定することになる。

$ (mkdir -p out && cd out \
  && clang++ -std=c++20 ../base.cppm --precompile -o base.pcm \
  && clang++ -std=c++20 -fprebuilt-module-path=. ../main.cpp -o main)
../main.cpp:3:8: fatal error: module 'Base' not found
import Base;
~~~~~~~^~~~
1 error generated.

import Base; と記述すると、Base.pcm を探しにいくらしい。

$ cd out
$ mv base.pcm Base.pcm
$ clang++ -std=c++20 -fprebuilt-module-path=. ../main.cpp -o main
$ ./main
0 1 3 
0 0 1 
1 0 0 

この挙動に従うならば、モジュール名とファイル名は同一のものにしたほうがよさそう。
ドキュメント見たら既に書いてあった。

https://clang.llvm.org/docs/StandardCPlusPlusModules.html#file-name-requirements
A BMI should use .pcm as a file extension. The file name of the BMI for a primary module interface unit should be module_name.pcm. The file name of a BMI for a module partition unit should be module_name-partition_name.pcm.

Base.pcm のような中間ファイルは Built Module Interface (BMI) と呼ばれる。

maimai

ファイルサイズを見てみる。

$ ls -g -G
total 6092
-rw-rw-r-- 1 6216528  916 17:23 Base.pcm
-rwxrwxr-x 1   20472  916 17:24 main

ちなみにモジュールとして記述しない場合、オブジェクトファイルのサイズは、

-rw-rw-r-- 1 1134948  916 18:00 base.o

Base.pcm 自体がヘッダに関する情報を保有していると考えられるが

maimai

two-phase compilation というキーワードが出てくるのでこれについて調べる。

BMI の生成には --precompile を指定する方法(従来の lib.o を生成するのと同じ)と -fmodule-output を指定する方法がある。--precompile の方を two-phase compilation-fmodule-output の方を one-phase compilation と呼ぶ。

-fmodule-output は手元の clang では使えないので省略。

https://clang.llvm.org/docs/StandardCPlusPlusModules.html#how-to-produce-a-bmi によると、オブジェクトファイルの生成までに2段階のビルドが必要であるため、two-phase compilation と呼ばれているらしい。

『後で考える:BMI はあくまでも clang がビルド時に使用する中間ファイルであってオブジェクトファイルではない?C++ライブラリのリリースという観点では BMI の使用は不適切か?』

maimai

C++20 module は Precompiled Headers と深く関係しているようなので、Precompiled Headers についても調べる。

通常ソースファイルをビルドするときは、#include でヘッダファイルをソースファイルに挿入したり、マクロを適用したりするなどプリプロセッサ命令を処理してからコンパイル等が行われる。1つのヘッダファイルが複数のソースファイルから何度もインクルードされていると、その分コンパイル等も多重に行われる。

Precompiled Headers は、よく使うヘッダファイルを予め解析して中間ファイルを生成しておくことで、以降そのヘッダファイルがインクルードされた時はその中間ファイルを代わりにロードし、ヘッダファイルの解析処理を省略する。この機能はビルド手順に関するもので、 C++ 標準で定められたものではない。はず。

ビルド手順等は Module に非常に似ている。Module は Precompiled Headers と同様にヘッダファイルの多重ロードを回避しビルド時間を短縮するために策定されたらしい。

maimai

Clang で Precompiled Headers

https://clang.llvm.org/docs/PCHInternals.html

Clang では Clang’s precompiled headers (PCH) と表現される。生成される中間ファイルの拡張子は .pch。
Clang で PCH を生成するには、-emit-pch を使う。ただし、この引数を使うには内部向け引数である -cc1 が必要。 clang はデフォルトで -target-cpu 等のオプションをいい感じに指定するが、 -cc1 はそれを行わない。

うまく動かせなかったので略

-cc1 が必要と書いてあるが、なくても使えるらしい

GCC で Precompiled Headers

https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html

生成される中間ファイルの拡張子は .gch。
GCC で Precompiled Headers を生成するには、出力ファイルの拡張子を .gch にするだけ。追加の引数は不要。このとき、生成ファイルは、ヘッダファイルと同じ場所かつヘッダファイル名前に .gch をつけたものにする。例えば、ヘッダファイルが /tmp/work/lib.h なら、出力先は /tmp/work/lib.h.gch でなければならない。

gcc -o ../lib.h.gch -c ../lib.h
gcc -o lib.o -c ../lib.c
gcc -o main.o -c ../main.c
gcc lib.o main.o -o main

引数に -H を指定すると、どのヘッダをロードしたかが分かる。.gchも表示される。

$ # .gchがないとき
$ gcc -H -o main.o -c ../main.c
. ../lib.h
$ # .gchがあるとき
$ gcc -H -o main.o -c ../main.c
! ../lib.h.gch
 ../main.c
maimai

CMake で Precompiled Headers

CMake も Precompiled Headers に対応している。 target_precompile_headers で生成できる。

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)

# set(CMAKE_C_COMPILER "/usr/bin/clang" CACHE string "clang compiler" FORCE)
# set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -H")

project(Precompile)

add_library(lib lib.c lib.h)
target_precompile_headers(lib PRIVATE lib.h)

add_executable(main main.c)
target_link_libraries(main PRIVATE lib)
target_precompile_headers(main REUSE_FROM lib)  # libで生成したPCHを使う

ビルドシステムには ninja を使う。

$ mkdir -p out && cd out
$ cmake .. -GNinja
...
$ ninja all
[5/5] Linking C executable main

cmake_pch.h.gch が生成されているのが確認できる。cmake_pch.h にいい感じヘッダを固めてくれる。らしい。

$ tree CMakeFiles/lib.dir/
CMakeFiles/lib.dir/
├── cmake_pch.h
├── cmake_pch.h.c
├── cmake_pch.h.gch
└── lib.c.o

0 directories, 4 files
maimai

ちなみに、CMake かつ Clang で PCH を生成しようとした場合でも、中間ファイルの拡張子は gch になってた。

maimai

-fprebuilt-module-path の仕様により、すべてのモジュールの中間ファイルは1つのディレクトリに配置することになる。ソースファイルの位置は関係なく、出力ディレクトリに配置すれば良いので、PCHよりは使いやすそう。

モジュール名には「識別子または識別子をドットで繋いだもの」が使える。モジュール名は base.math.Matrix のような名前を意図している気がする。

maimai

module を gcc でビルドする

https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Modules.html
G++’s modules support is not complete.

It is independent of the language version selected, although in pre-C++20 versions, it is of course an extension.

実装はまだ不完全らしい。-fmodules-ts を付加する必要がある。C++20 以前のモードであっても -fmodules-ts さえ付加すれば module を扱える。

ビルドコマンドは以下の通り。ソースコードは「module を clang でビルドする」と同じ。

mkdir -p out.gcc && cd out.gcc \
  && g++ -fmodules-ts -std=c++20 -x c++ ../base.cppm -c -o base.o \
  && g++ -fmodules-ts -std=c++20 -O0 -g ../main.cpp -c -o main.o \
  && g++ base.o main.o -o main

.cppm が C++ として認識されないので -x c++ で教える。base.o の通り、GCC の場合はオブジェクトファイルも出力する。そしてこれもリンクしないと、ビルドに失敗することを確認した。

生成物ディレクトリは以下のようになる。base.gcm が生成されている。

$ tree out.gcc/
out.gcc/
├── base.o
├── gcm.cache
│   └── base.gcm
├── main
└── main.o

1 directory, 4 files
maimai

「module を clang でビルドする」で示したソースコードでは、main.cpp では使用していない iostream をインクルードしている。

main.cpp
#include <iostream>  // これ

import Base;

この行をコメントアウトすると、clang++ではビルドは成功するが、実行時にセグメンテーションフォルトが発生する。GCCは取り除いても良い。

ChatGPT(gpt-4) に投げてみたが、「自己完結型であるという考え方」に関する出典が分からず

C++のモジュールシステムは、それぞれのモジュールが自己完結型であるという考え方に基づいています。つまり、モジュールは必要な全てのリソースを自身で提供するべきで、これには使用するヘッダーファイルも含まれます。
したがって、<iostream>のようなヘッダーファイルがモジュール内で使用されている場合、そのモジュールをインポートする全てのコード(この場合、main.cpp)では、それらのヘッダーファイルも直接または間接的に利用可能である必要があります。
その一方で、現在のClang++の実装では、モジュールでインクルードしたヘッダーファイルが自動的にインポート側にエクスポートされないためこの問題が起きています。つまり、モジュール内で使用したインクルードが、そのモジュールを利用するソースコードに対して自動的に適用されないという現象が発生します。その結果、モジュール内で<iostream>が必要な場合、main.cppでもそれをインクルードする必要が発生します。
この問題を回避するためには、main.cpp内で必要なヘッダーファイルを直接インクルードするか、base.cppm内でexport importを用いてインポートされたヘッダーファイルをエクスポートする必要があります。

https://learn.microsoft.com/en-us/cpp/cpp/modules-cpp?view=msvc-170#imported-header-filesself-contained は使われているけど、これはヘッダーに関する記述(ヘッダー単独でコンパイルができるという意味?)。

maimai

インターフェースと実装を分けたコード

プライマリモジュールインターフェースに実装を書いていたが、これを分けてみる。
こちらには、定義のみを書く。

base.cppm
module;

#include <vector>

export module base;

namespace base {

export class Table {
public:
  Table(int height, int width);
  int operator()(int y, int x) const;
  int &operator()(int y, int x);

  void print() const;

private:
  std::size_t height_, width_;
  std::vector<int> data_;
};

} // namespace base

モジュール実装本体ユニットを別のファイルに書く。

base.cpp
module;

#include <iostream>
#include <vector>

module base;

namespace base {

Table::Table(int height, int width)
  : data_(height * width), height_(height), width_(width) {}

int Table::operator()(int y, int x) const {
  return data_[y * width_ + x];
}

int &Table::operator()(int y, int x) {
  return data_[y * width_ + x];
}

void Table::print() const {
  for (std::size_t y = 0; y < height_; ++y) {
    for (std::size_t x = 0; x < width_; ++x) {
      std::cout << (*this)(y, x) << ' ';
    }
    std::cout << '\n';
  }
}

} // namespace base

GCC によるビルドは以下の通り。全部ソースファイルとしてビルドする。

mkdir -p out.gcc && cd out.gcc
g++ -fmodules-ts -std=c++20 -Os -x c++ ../base.cppm -c -o base.cppm.o
g++ -fmodules-ts -std=c++20 -Os -x c++ ../base.cpp -c -o base.o
g++ -fmodules-ts -std=c++20 -Os ../main.cpp -c -o main.o
g++ base.cppm.o base.o main.o -o main

Clang は…ビルド通らない…

mkdir -p out.clang && cd out.clang
clang++ -std=c++20 ../base.cppm --precompile -o base.pcm
clang++ -std=c++20 -Os -fprebuilt-module-path=. ../base.cpp -c -o base.o
clang++ -std=c++20 -Os -fprebuilt-module-path=. ../main.cpp -c -o main.o
clang++ base.o main.o -o main

../base.cpp:6:1: error: definition of module 'base' is not available; use -fmodule-file= to specify path to precompiled module interface
module base;
^
maimai

module を clang でビルドする(2)

https://clang.llvm.org/docs/StandardCPlusPlusModules.html#quick-start

Clang の quick-start を改めて読んだら、 pcm ファイルもオブジェクトにコンパイルしているようだった。clang++ -std=c++20 ./base.pcm -c -o base.module.o のように、ソースファイルと同じように扱える。

リンク時にpcmの情報が入るのでセグメンテーションフォルトが改善されるかと期待したが、エラーは消えない。

clang++ -fmodules-ts -Wall -std=c++20 -Os ../base.cppm --precompile -o base.pcm
clang++ -fmodules-ts -Wall -std=c++20 -Os ./base.pcm -c -o base.module.o
clang++ -fmodules-ts -Wall -std=c++20 -Os -fprebuilt-module-path=. ../main.cpp -c -o main.o
clang++ base.module.o main.o -o main
maimai

module を clang でビルドする(3)

標準ライブラリとして、libc++ (llvm) を使うよう指示すれば、セグメンテーションフォルトなく起動できることが分かった。デフォルトでは libstdc++ (gcc) が使用されるが、こちらは失敗する。

clang++ -std=c++20 -stdlib=libc++ ../hello.cppm --precompile -o hello.pcm
clang++ -std=c++20 -fprebuilt-module-path=. ./hello.pcm -c -o hello.o
clang++ -std=c++20 -stdlib=libc++ -fprebuilt-module-path=. ../main.cpp -c -o main.o
clang++ -stdlib=libc++ hello.o main.o -o main
maimai

module を clang でビルドすると SIGSEGV が発生する問題の調査

iostream に依存する最小構成のモジュールを用意する。

hello.cppm
module;
#include <iostream>
export module hello;
export void greedy() {
  std::cout << "Hello, Modules!" << std::endl;
}
main.cpp
import hello;
int main() {
  greedy();
  return 0;
}
main2.cpp
#include <iostream>
import hello;
int main() {
  greedy();
  return 0;
}

ビルドは以下の通り。main は実行に失敗し、main2は実行に成功するバイナリ。

mkdir -p out.clang.std && cd out.clang.std
clang++ -std=c++20 -v -stdlib=libstdc++ ../hello.cppm --precompile -o hello.pcm
clang++ -std=c++20 -fprebuilt-module-path=. ./hello.pcm -c -o hello.o
clang++ -std=c++20 -stdlib=libstdc++ -fprebuilt-module-path=. ../main.cpp -c -o main.o
clang++ -stdlib=libstdc++ hello.o main.o -o main
clang++ -std=c++20 -stdlib=libstdc++ -fprebuilt-module-path=. ../main2.cpp -c -o main2.o
clang++ -stdlib=libstdc++ hello.o main2.o -o main2
cd -

objdump でバイナリを比較してみる。

objdump -d -l main > main.dump.txt
objdump -d -l main2 > main2.dump.txt

main2 には以下のような __cxx_global_var_init の定義と呼び出しがあるが、main には含まれていない。

0000000000001070 <__cxx_global_var_init>:
__cxx_global_var_init():
    ...略...
00000000000010a0 <_GLOBAL__sub_I_main2.cpp>:
_GLOBAL__sub_I_main2.cpp():
    10a0:	55                   	push   %rbp
    10a1:	48 89 e5             	mov    %rsp,%rbp
    10a4:	e8 c7 ff ff ff       	call   1070 <__cxx_global_var_init>
    10a9:	5d                   	pop    %rbp
    10aa:	c3                   	ret    
    10ab:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

__cxx_global_var_init とは、C++ でグローバル変数のコンストラクタを呼び出したりするための実装。
iostream のグローバル変数の初期化呼び出しが消えてしまったのが原因で、iostream が初期化されておらず、 std::cout の中で SIGSEGV が発生してしまったのでは。


ちなみに、SIGSEGV の発生箇所は lldb で確認できる。

* thread #1, name = 'main', stop reason = signal SIGSEGV: invalid address (fault address: 0xffffffffffffffe8)
    frame #0: 0x00007ffff7d3bf8a libstdc++.so.6`std::ostream::sentry::sentry(std::ostream&) + 26
libstdc++.so.6`std::ostream::sentry::sentry:
->  0x7ffff7d3bf8a <+26>: movq   -0x18(%rax), %rdi
    0x7ffff7d3bf8e <+30>: addq   %rsi, %rdi
    0x7ffff7d3bf91 <+33>: movq   0xd8(%rdi), %rax
    0x7ffff7d3bf98 <+40>: movl   0x20(%rdi), %esi
(lldb) bt
* thread #1, name = 'main', stop reason = signal SIGSEGV: invalid address (fault address: 0xffffffffffffffe8)
  * frame #0: 0x00007ffff7d3bf8a libstdc++.so.6`std::ostream::sentry::sentry(std::ostream&) + 26
    frame #1: 0x00007ffff7d3ca0c libstdc++.so.6`std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) + 60
    frame #2: 0x00007ffff7d3cebb libstdc++.so.6`std::basic_ostream<char, std::char_traits<char> >& std::operator<<<std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) + 43
    frame #3: 0x0000555555555157 main`greedy() + 23
    frame #4: 0x0000555555555174 main`main + 20
    frame #5: 0x00007ffff7829d90 libc.so.6`__libc_start_call_main(main=(main`main), argc=1, argv=0x00007fffffffdb28) at libc_start_call_main.h:58:16
    frame #6: 0x00007ffff7829e40 libc.so.6`__libc_start_main_impl(main=(main`main), argc=1, argv=0x00007fffffffdb28, init=0x00007ffff7ffd040, fini=<unavailable>, rtld_fini=<unavailable>, stack_end=0x00007fffffffdb18) at libc-start.c:392:3
    frame #7: 0x0000555555555075 main`_start + 37
maimai

module を clang でビルドすると SIGSEGV が発生する問題の調査(2)

-stdlib=libc++ を付与してビルドすれば SIGSEGV は発生しないが、こちらのバイナリを objdump -d -l で確認してみると、__cxx_global_var_init は含まれていなかった。libc++ に含まれる iostream がグローバル変数のクラスコンストラクタを使っていないだけかも。

maimai

そもそも clang++ -std=c++20 -fprebuilt-module-path=. ../hello.cppm -c -o hello.o で生成される hello.o__cxx_global_var_init が含まれていない。module でない従来の実装方法であれば含まれるはず。

結局、想定解は分からず…

maimai

生成される .pcm (中間ファイル) は絶対パスを含む

生成される .pcm は絶対パスを保持しているので、ディレクトリを移動した場合は、たぶん再ビルドが必要。

また、中間ファイルに対応する .cppm を削除した状態でそのモジュールを使用するソースファイルをコンパイルすると、エラーが発生する。

../main.cpp:1:1: fatal error: malformed or corrupted AST file: 'could not find file '/tmp/work/out.clang.std/../hello.cppm' referenced by AST file '/tmp/work/out.clang.std/./hello.pcm''
import hello;
^
1 error generated.
maimai

まとめ

  • モジュールは、ビルドの高速化を図る Precompiled Headers を標準っぽくしたもの。
  • モジュールのインターフェースは Built Module Interface (BMI) とか Compiled Module Interface (CMI) とか呼ばれる中間ファイルに変換される。この中間ファイルはキャッシュの一種で、オブジェクトファイルではない。
  • モジュールを利用する手順(普通この手順は CMake 等で隠蔽できる)
    • モジュールインターフェースから中間ファイルを作成する。
    • モジュール実装をコンパイルしてオブジェクトファイルを作る。
    • モジュールを使用するソースファイルをコンパイルしてオブジェクトファイルを作る。
    • オブジェクトファイルをリンクする。
  • (使用したコンパイラが古いだけかもしれないが)まだ発展途上で、挙動がコンパイラに強く依存している。

MSVC も確かめたほうがいいかもしれない

maimai

module を clang でビルドすると SIGSEGV が発生する問題の調査

Clang 19 をダウンロードしてきた

$ clang++ --version
clang version 19.1.0 (/home/runner/work/llvm-project/llvm-project/clang a4bf6cd7cfb1a1421ba92bca9d017b49936c55e4)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /home/mai/Downloads/LLVM-19.1.0-Linux-X64/bin
$ ./out.clang.std/main 
Hello, Modules!

libstdc++でも普通に動くじゃん!コンパイラのバグじゃん!

このスクラップは1ヶ月前にクローズされました