使用 VSCode + XMake + LLVM 开发现代 C++!(llvm-mingw + clangd + modules)
使用 VSCode + XMake + LLVM 开发现代 C++!(llvm-mingw + clangd + modules)
现在已经 4202 年了,C++ 模块已经经过了长足的发展,但就模块是否得到工具支持仍然有历史遗留的争议。
本文将说明在 VSCode 下配合 XMake 和 Clangd 是可行方案,并给出指导。
先说结论,配置 xmake.lua
加上 compile_commands.json
就足够了,不需要别的操作。
Info
本文一些细节图文内容待补充
环境
使用 llvm-mingw:https://github.com/mstorsjo/llvm-mingw,版本为 19.1。编译器对标准库模块支持应当已经很充分了。
XMake:v2.9.5
VSCode 安装插件:clangd,其他 C++ 语言服务相关的均不需要。尤其是 C/C++ Clang Command Adapter
,这玩意不仅没用还会添乱。
基础
首先考虑最基本的模块用法,不考虑标准库和构建工具。
先看 clang 官方文档:https://clang.llvm.org/docs/StandardCPlusPlusModules.html#quick-start
使用文档中的示例程序:
// Hello.cppm
module;
#include <iostream>
export module Hello;
export void hello() {
std::cout << "Hello World!\n";
}
// use.cpp
import Hello;
int main() {
hello();
return 0;
}
编译:
$ clang++ -std=c++20 Hello.cppm --precompile -o Hello.pcm
$ clang++ -std=c++20 use.cpp -fmodule-file=Hello=Hello.pcm Hello.pcm -o Hello.out
$ ./Hello.out
Hello World!
是可以正常运行的。
加上 Clangd
不幸的是 clangd 会出点问题,大量提示语法错误:
原因是没启用对应编译命令。解决方法:在 compile_flags.txt
中加上:
-xc++
-std=c++20
-stdlib=libc++
-fexperimental-library
或者在 .clangd
中加入:
CompileFlags:
Add:
- -xc++
- -std=c++20
- -stdlib=libc++
- -fexperimental-library
(当然更推荐用 compile_commands,这个我们后面再说)
然后重启 VSCode 的 clangd,可以看到 Hello.cppm
的错误已经全部消除了。
但事情并没那么顺利, use.cpp
提示找不到模块:
原因是没加入到模块搜索路径。接下来我们用构建工具一并解决这个问题。
使用 XMake
本节参考官方文档:https://xmake.io/#/zh-cn/guide/project_examples?id=c20-模块
add_rules("mode.debug", "mode.release")
target("test")
set_kind("binary")
add_files("*.cpp", "*.cppm")
set_languages("c++20")
set_policy("build.c++.modules", true)
注意,.cppm
也是源代码文件一部分。如果没加入,会提示:
error: module dependency Hello required for use.cpp not found
事实上因为已经有了 .cppm
文件,XMake 会认为这是一个模块项目,这样就不用写 set_policy
了。
正确编译后,输出如下:
[ 42%]: <test> compiling.module.debug Hello
[ 71%]: compiling.debug use.cpp
[ 85%]: linking.debug test.exe
[100%]: build ok, spent 1.25s
warning: std and std.compat modules not found ! disabling them for the build, maybe try to add --sdk=<PATH/TO/LLVM>
Executing task: xmake run test
Hello World!
这里提示找不到 std modules,没关系,我们还未用到。
然后需要将 .vscode/compile_commands.json
(若没有请使用 XMake Update Intellisense)加入到 settings.json
中的 clangd 参数(其他实用参数可自行添加):
{
"clangd.arguments": ["--compile-commands-dir=${workspaceFolder}/.vscode"]
}
若没有这个,use.cpp
里会提示找不到 Hello
模块。
这样,大功告成!
因为有 compile_commands.json
了,.clangd
或 compile_flags.txt
也不需要了。
标准库模块
下面,我们来启用标准库模块。
修改代码如下:
module;
export module Hello;
import std;
export void hello() { std::cout << "Hello World!\n"; }
Note
一开始我直接把 #include
行换成了 import
,出现了如下错误:
后来才知道是 export module
前面只能出现预处理指令。
按照 clang 说法是开箱即用的,并没有给出详细使用教程。
llvm-mingw 是提供了 std.cppm
的,位于:llvm-mingw/share/libc++/v1/std.cppm
。并不需要手动编译整个工具链。
但如果直接写 import std
,会提示 error: module dependency std required for Hello not found
。
尝试按照指导,如下编译 std.pcm
,是可以成功的,说明库文件正常。
clang++ -std=c++23 -stdlib=libc++ --precompile -o std.pcm "path/to/llvm-mingw/share/libc++/v1/std.cppm"
尝试直接把 std.cppm
加入到编译文件中:
add_files("C:/Program Files/llvm-mingw/share/libc++/v1/*.cppm")
很长的错误输出
[ 0%]: <test> generating.module.deps C:\Program Files\llvm-mingw\share\libc++\v1\std.cppm
error: In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:222:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/array.inc:16:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
16 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:233:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/chrono.inc:44:32: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
44 | using std::chrono::operator<=>;
| ^
|
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/chrono.inc:297:51: error: invalid suffix on literal; C++11 requires a space between literal and identifier [-Wreserved-user-defined-literal]
297 | using std::literals::chrono_literals::operator""d;
| ^
|
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/chrono.inc:300:51: error: invalid suffix on literal; C++11 requires a space between literal and identifier [-Wreserved-user-defined-literal]
300 | using std::literals::chrono_literals::operator""y;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:243:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/coroutine.inc:20:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
20 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:256:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/deque.inc:15:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
15 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:264:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/forward_list.inc:15:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
15 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:276:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/iterator.inc:166:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
166 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:279:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/list.inc:15:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
15 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:281:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/map.inc:15:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
15 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:283:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/memory.inc:132:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
132 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:289:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/optional.inc:28:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
28 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:292:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/queue.inc:20:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
20 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:297:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/regex.inc:59:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
59 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:300:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/set.inc:15:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
15 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:306:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/stack.inc:20:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
20 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:312:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/string.inc:19:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
19 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:313:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/string_view.inc:21:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
21 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:316:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/system_error.inc:34:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
34 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:318:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/thread.inc:33:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
33 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:319:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/tuple.inc:45:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
45 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:325:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/utility.inc:68:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
68 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:327:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/variant.inc:33:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
33 | using std::operator<=>;
| ^
|
In file included from C:\\Program Files\\llvm-mingw\\share\\libc++\\v1\\std.cppm:328:
C:\\Program Files\\llvm-mingw\\share\\libc++\\v1/std/vector.inc:15:22: warning: '<=>' is a single token in C++20; add a space to avoid a change in behavior [-Wc++20-compat]
15 | using std::operator<=>;
| ^
|
22 warnings and 2 errors generated.
warning: std and std.compat modules not found ! disabling them for the build, maybe try to add --sdk=<PATH/TO/LLVM>
warning: add -v for getting more warnings ..
似乎并不可行?
事实上,自行编译是可以通过的:
clang++ -std=c++26 "path/to/llvm-mingw/share/libc++/v1/std.cppm" --precompile -o std.pcm
clang++ -std=c++26 Hello.cppm -fmodule-file=std="std.pcm" --precompile -o Hello.pcm
clang++ -std=c++26 use.cpp -fmodule-file=Hello="Hello.pcm" -fmodule-file=std="std.pcm" Hello.pcm -o Hello.exe
但 XMake 硬是说:
error: module dependency std required for Hello not found
warning: std and std.compat modules not found ! disabling them for the build, maybe try to add --sdk=<PATH/TO/LLVM>
所以是 XMake 的锅?。
在一个 discussion 中我们看到,llvm 已经合并了一个必需的 pr,但 XMake 似乎还没实现对应功能。
曲线救国
考虑到,既然 std
编译流程和普通一样,那么我们能不能把它作为一个 target 呢?
target("std")
set_kind("static")
add_files("path/to/llvm-mingw/share/libc++/v1/*.cppm", { public = true })
target("test")
set_kind("binary")
add_files("*.cpp", "*.cppm")
编译,成功!虽然 warning 仍然在,但我们确实成功使用了 std
……
最终解决方案
翻了一下 xmake issue,找到了这个回复:https://github.com/xmake-io/xmake/issues/5615#issuecomment-2358776939
考虑到 llvm-mingw 是 cross 的,XMake 可能没有使用正确的那个默认平台库(只有它有 std.cppm
),设置 runtime 也有道理。
xmake f --runtimes=c++_shared
(这里换成 c++_static
也可)
然后困扰了很久的问题成功解决……
另外可以加上自动更新 compile_commands.json
的 builtin plugin。
完整版本的 xmake.lua
:
add_rules("mode.debug", "mode.release")
add_rules("plugin.compile_commands.autoupdate", { outputdir = "./.vscode" })
set_plat("mingw")
set_toolchains("clang")
set_runtimes("c++_static")
set_languages("c++latest")
target("test")
set_kind("binary")
add_files("*.cpp", "*.cppm")
注:plat、toolchain、runtime 也可在命令行设置,此处手动是为了防止乱掉。