0%

Revisit C++20 Module and CMake

由於打算寫本電子書,所以重新審視了C++20 Moudle的部分
語法的不是這篇的重點,這篇講的是

  • 如何跟CMake搭配使用
  • 測試環境是Linux + Clang20 + CMake 3.28

Prerequisites

首先先clone git repo,所有的變化都由範例legacy開始,這是沒有Module之前的做法

CMake

CMake已經是事實上的標準

Case1 Normal case

詳細內容請觀看 module_1目錄,這邊只講我覺得重要的地方
這個Case就是legacy直接翻譯成Module版本
首先看MathFunctions的CMakeLists.txt的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
target_sources(MathFunctions
PUBLIC
FILE_SET primary_interface
TYPE CXX_MODULES
FILES
MathFunctions.cppm
PRIVATE
FILE_SET implementaion_units
TYPE CXX_MODULES
FILES
src/mysqrt.cppm
PRIVATE
src/MathFunctions.cxx
)

這邊有兩個FILE_SET

  • primary_interface:也就是我們要對外提供的Primary module interface unit
  • implementaion_units:內部的partion unit,不對外輸出
    所以在安裝的時候,只會將MathFunctions.cppm複製到安裝的目錄下

    Case2: Multiple Primary Module Interface Units

    接著我們稍微修改MathFunctions.cppm的內容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    module;

    export module Math;

    export import :detail;
    export namespace mathfunctions
    {
    double sqrt(double);
    }
    我們也將detail的內容也輸出了,因此我們需要做以下的修改
  • detail module和namespace需要標記成 export
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    module;
    #include <math.h>

    export module Math:detail;

    export namespace mathfunctions::detail {
    double sqrt(double x) {
    return ::sqrt(x);
    }
    }
    修改我們的CMakeLists.txt的部分
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    target_sources(MathFunctions
    PUBLIC
    FILE_SET primary_interface
    TYPE CXX_MODULES
    FILES
    MathFunctions.cppm
    src/mysqrt.cppm
    PRIVATE
    src/MathFunctions.cxx
    )
    現在我們有了兩個Primary Module Interface Units,在安裝的時候也要同時複製兩個檔案
    Math.detailMath:detail的情況類似,所以就不說了

接著來研究Mitgrate的部分,這是參考clang Transitioning to modules的部分

Case3: Mitgrate legacy to module (Part1)

看一下transform_1的目錄
這邊主要的差別在於CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
target_sources(MathFunctions
PUBLIC
FILE_SET export_headers
TYPE HEADERS
BASE_DIRS include/
FILES include/MathFunctions.h
PUBLIC
FILE_SET primary_interface
TYPE CXX_MODULES
FILES
MathFunctions.cppm
PRIVATE
src/MathFunctions.cxx
src/mysqrt.h
src/mysqrt.cxx
)
install(TARGETS MathFunctions
EXPORT MathFunctionsTargets
ARCHIVE
FILE_SET export_headers
FILE_SET primary_interface
DESTINATION lib/cmake/MathFunctions/src
)

既保留原有的leagcy code,更新增了一個Primary Module Interface Units
MathFunctions.cppm的內容則是

1
2
3
4
5
6
7
8
module;
#include "MathFunctions.h"

export module Math;

export namespace mathfunctions {
using mathfunctions::sqrt;
}

將Global Module Fragment中的內容導出到Module中
這種方法不會破壞原有leagcy code,殺傷力最小

Case4: Mitgrate legacy to module (Part2)

看一下transform_21的目錄,CMakeLists.txt跟上面一樣不變
改變的是MathFunctions.cppmMathFunctions.h
此時的MathFunctions.cppm長這樣

1
2
3
4
5
6
7
8
9
module;

export module Math;

#define IN_MODULE_INTERFACE

extern "C++" {
#include "MathFunctions.h"
}

MathFunctions.h的內容則是

1
2
3
4
5
6
7
8
9
10
11
#pragma once

#ifdef IN_MODULE_INTERFACE
#define EXPORT export
#else
#define EXPORT
#endif

namespace mathfunctions {
EXPORT double sqrt(double x);
}

由於只有在Module狀態下,IN_MODULE_INTERFACE才會發揮作用,因此leagcy code的情況下會維持不變
這個方法雖然比上面麻煩,不過可以順利遷移到下一個階段

Case5: Mitgrate legacy to module (Part3)

所有方案中最麻煩的一種
主要思想是在implemtation unit當中切開legacymodule的實作,強迫Consumer只能使用其中一種,例如原先的Header可能要加上export

1
2
3
4
5
6
7
8
9
10
11
#pragma once

#ifdef IN_MODULE_INTERFACE
#define EXPORT export
#else
#define EXPORT
#endif

namespace mathfunctions {
EXPORT double sqrt(double x);
}

以及Implementation的部分也要隔開

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef IN_MODULE_INTERFACE
#include "MathFunctions.h"
#include "mysqrt.h"
#else
module Math;
#endif

namespace mathfunctions {
double sqrt(double x) {
return detail::sqrt(x);
}
}

在這裡我選擇對CMakeLists.txt動手腳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
if (ENABLE_MODULE_BUILD)
target_sources(MathFunctions
PUBLIC
FILE_SET export_headers
TYPE HEADERS
BASE_DIRS include/
FILES
include/MathFunctions.h
include/mysqrt.h
PUBLIC
FILE_SET primary_interface
TYPE CXX_MODULES
FILES
MathFunctions.cppm
PRIVATE
src/MathFunctions.cxx
src/mysqrt.cxx
)
else()
target_sources(MathFunctions
PUBLIC
FILE_SET export_headers
TYPE HEADERS
BASE_DIRS include/
FILES
include/MathFunctions.h
PRIVATE
include/mysqrt.h
src/MathFunctions.cxx
src/mysqrt.cxx
)
endif()

target_compile_definitions(MathFunctions
PRIVATE
$<$<BOOL:${ENABLE_MODULE_BUILD}>:IN_MODULE_INTERFACE>
)

if (ENABLE_MODULE_BUILD)
install(TARGETS MathFunctions
EXPORT MathFunctionsTargets
ARCHIVE
FILE_SET export_headers
FILE_SET primary_interface
DESTINATION lib/cmake/MathFunctions/src
)
else()
install(TARGETS MathFunctions
EXPORT MathFunctionsTargets
ARCHIVE
FILE_SET export_headers
)
endif()

當我們指定ENABLE_MODULE_BUILD的時候,會自動處理細節的部分
不過這邊也遇到了clang文件中的問題

Minor issue

由於我們之前的mysqrt.h是經由src/MathFunctions.cxx所include的,改成Module之後,這個相依性被切斷了
因此我們需要在MathFunctions.cppm強迫加入

1
2
3
4
5
6
7
module;

export module Math;
#include "MathFunctions.h"

module: private;
#include "mysqrt.h"

這樣沒有問題,不過

  • 原來的mysqrt.h不需要公開,現在變成強迫要公開了
  • 更好的方法是直接使用Module Partition Unit,也就是要改寫