Docker
maxXing
提供为实验提供了 docker 镜像,所以我们只需要将 docker 下载下来拉取镜像即可。
使用 pacman 下载 docker:
关于如何使用基本的 docker 文档里面有足够的讲解
,在此不再赘述。
主要来讲解一下引入 docker 导致宿主机的环境问题。
首先 docker 默认会以 sudo 运行,这涉及到一些历史遗留问题。但客观事实是,这不合常理。所幸,docker 可以通过配置来解决这个问题:
1
2
# 可以将 `docker` 添加进用户组避免 `sudo`。
sudo usermod -aG docker $USER
其次,docker 会改变修改默认的 IP 转发:
1
2
3
4
5
6
sudo iptables -nvL FORWARD
[ sudo] password for anfsity:
Chain FORWARD ( policy DROP 1735 packets, 177K bytes)
pkts bytes target prot opt in out source destination
1735 177K DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
1735 177K DOCKER-FORWARD all -- * * 0.0.0.0/0 0.0.0.0/0
可以看到,docker 将策略改成了 DROP …
如果你之前有跑在宿主机上的类似容器应用,就需要将对应的端口开放给 iptables。
Article
关于 docker 的流量转发我没有做过多的了解,可以看官方文档自行了解。
Networking overview
Packet filtering and firewalls
配置 clangd
如果你使用 c++ 进行 Lab 的话,可能你像我一样使用 clangd 。
但是由于每次运行都是在 docker 里面进行的,这就造成一个问题–如果你使用 cmake 来自动生成 cdb 文件的话,它的路径是 docker 里面的路径而不是宿主机里的路径。
这就导致了 clangd 找不到对应的 cdb ,然后就框框爆红 **file not found 。
这让我很是头疼,网上搜寻了一番,大致有两种思路:
在 docker 里面也装一个 clangd ,然后把 docker 里面的 clangd 通信转发到 vscode 里面来。
这个策略有很多不足,一是折腾起来麻烦;二是就算弄好了 clangd 也没有办法享受我宿主机上的 zsh 环境;三是这只适用于 vscode ,如果我用其他的 IDE 那又要折腾一番了。
把宿主机的目录挂载到 docker 上来,让 docker 的路径和宿主机相同。
这个思路我是在一篇 reddit 的讨论帖上看到的,感觉不错,遂剽窃使用。
为了发扬懒人精神,我把这些命令整合到了 Makefile 中。
我只会 Makefile QAQ,而且它也足够简单(简单吗…?),只要不写太多东西。犹记得初见 Makefile 时的语法,神似鬼画符🤔
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
IMAGE = maxxing/compiler-dev
BUILD_DIR = cmake-build
UID := $( shell id -u)
GID := $( shell id -g)
PWD := $( shell pwd )
all : build
configure :
cmake -S . -B $( BUILD_DIR)
build : configure
cmake --build $( BUILD_DIR) -j12
clean :
rm -rf $( BUILD_DIR)
shell :
docker run -it --rm \
-u $( UID) :$( GID) \
-v " $( PWD) : $( PWD) " \
-w " $( PWD) " \
$( IMAGE) bash
docker-build :
docker run --rm \
-u $( UID) :$( GID) \
-v " $( PWD) : $( PWD) " \
-w " $( PWD) " \
$( IMAGE) \
sh -c "cmake -S . -B $( BUILD_DIR) && cmake --build $( BUILD_DIR) -j12"
你可以在根目录下 make shell 直接进入 docker,make 进行编译。
CMake
谈到 CMake ,只能说又爱又恨。众所周知,C++ 没有像 rs,py 那样好用的包管理器,目前流行的包管理器各有各的缺陷。
不过包管理器相关的知识太过庞杂,而且我也并不熟悉,就不在这里展开叙述了。
我们来魔改一下 maxXing 的 CMakelists 👍
按照现代 CMake 的思想,一切皆为 target 和模块化,我们来调整一下 CMakelists。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
~ anfsity main zsh
tree -d
.
├── debug
├── include
│ ├── backend
│ └── ir
├── scripts
├── src
│ ├── backend
│ ├── frontend
│ └── ir
└── tests
11 directories
我们在顶层目录和 src/include 目录都放一个 CMakelists 来管理。
这是我学习 Cmake 的入门视频
。
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
# root CMakelists.txt
cmake_minimum_required ( VERSION 3.20 )
project (
compiler
LANGUAGES CXX
DESCRIPTION "PKU Compile Principle LABs."
VERSION 0.1.0
)
# c++ settings
set ( CMAKE_CXX_STANDARD 23 )
set ( CMAKE_CXX_STANDARD_REQUIRED ON )
set ( CMAKE_EXPORT_COMPILE_COMMANDS ON )
# binary_dir : the output dir like build/cmake-build
set ( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${ CMAKE_BINARY_DIR } )
# library fmt
include ( FetchContent )
FetchContent_Declare (
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 12.1.0
)
FetchContent_MakeAvailable ( fmt )
# Flex & Bsion
find_package ( FLEX REQUIRED )
find_package ( BISON REQUIRED )
add_subdirectory ( include )
add_subdirectory ( src )
enable_testing ()
file ( GLOB_RECURSE test_cases "tests/*.c" )
foreach ( test_file ${ test_cases } )
get_filename_component ( test_name ${ test_file } NAME_WE )
get_filename_component ( parent_dir ${ test_file } DIRECTORY )
get_filename_component ( group_name ${ parent_dir } NAME )
add_test (
NAME ${ group_name } / ${ test_name }
COMMAND python3 ${ CMAKE_SOURCE_DIR } /scripts/test_runner.py
$< TARGET_FILE:compiler >
${ test_file }
)
endforeach ()
看了一下 docker 里面的环境配置:
Tool
Version
Status/Notes
CMake
3.28.3
现代版本,但离目前的 head 还是稍旧。
Python3
3.12.3
最新的稳定版本之一。
Rust Toolchain (Cargo)
1.91.1
版本非常新 (构建日期 2025-10-10),处于前沿。
flex
2.6.4
标准版本。
bison
3.8.2
标准版本 (GNU Bison)。
GCC
13.3.0
构建于 Ubuntu 24.04。支持 C++20 标准。
Clang
21.1.6
版本极新。但是可能由于 libc++ 限制,可能无法使用 std::print 。
LLVM
21.1.6
Clang 的底层框架,版本与 Clang 一致。
环境可以说是非常现代,但是很遗憾无法使用 print 库。
我早受够用 cout 的 <</>> 来输出字符串了,真的很难用,便把 print 的原型库 fmt 拉过来使用。
没想到 fmt 比 print 还好用。
我的实现有一个缺陷,为了避免引入依赖,我直接使用 cmake 拉取 fmt 仓库。这导致每次测试的时候都要进行一次拉取。如果网络好的时候还算顺畅,但是校园网时常抽风,偶尔要等待半天。
不过也可以指定输出目录进行增量编译,这也不算是什么大问题了。
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
# src/CMakelists.txt
# generate lexer/parser
set ( LEXER_SRC frontend/sysy.lx )
set ( YACC_SRC frontend/sysy.y )
# generate the lexer and parser files
flex_target ( Lexer ${ LEXER_SRC } ${ CMAKE_CURRENT_BINARY_DIR } /sysy.lex.cpp )
bison_target ( Parser ${ YACC_SRC } ${ CMAKE_CURRENT_BINARY_DIR } /sysy.tab.cpp )
add_flex_bison_dependency ( Lexer Parser )
message ( STATUS "[INFO] Generated lexer: ${CMAKE_CURRENT_BINARY_DIR}/sysy.lex.cpp" )
message ( STATUS "[INFO] Generated parser: ${CMAKE_CURRENT_BINARY_DIR}/sysy.tab.cpp" )
message ( STATUS "[INFO] Generated lexer outpus ${FLEX_Lexer_OUTPUTS}" )
message ( STATUS "[INFO] Generated parser outpus ${BISON_Parser_OUTPUT_SOURCE}" )
set ( CORE_SOURCES
ir/ast.cpp
backend/backend.cpp
${ FLEX_Lexer_OUTPUTS }
${ BISON_Parser_OUTPUT_SOURCE }
)
add_library ( compiler_core STATIC ${ CORE_SOURCES } )
target_include_directories ( compiler_core PRIVATE
${ CMAKE_CURRENT_BINARY_DIR } # cmake-build/src/* for generated lexer/parser
)
# compiler core link libraries
target_link_libraries ( compiler_core PUBLIC
koopa
pthread
dl
fmt::fmt
headers
)
# complie options
target_compile_options ( compiler_core PRIVATE -O2 -Wall -Wno-register -Wextra )
# executable
add_executable ( compiler main.cpp )
target_compile_options ( compiler PRIVATE -O2 -Wall -Wno-register -Wextra )
target_include_directories ( compiler PRIVATE $ENV{ CDE_INCLUDE_PATH } )
# compiler link libraries
target_link_libraries ( compiler PRIVATE compiler_core )
target_link_directories ( compiler PRIVATE $ENV{ CDE_LIBRARY_PATH } /native )
1
2
3
4
5
6
7
8
9
# include/CMakeLists.txt
add_library ( headers INTERFACE )
target_include_directories ( headers INTERFACE
# include/
${ CMAKE_CURRENT_SOURCE_DIR }
)
message ( STATUS "[INFO] Compiler Headers Target created: headers" )
这是我目前做了一阵子的目录结构,测试还没写完,从别处剽窃了一些测试过来。
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
tree
.
├── CMakeLists.txt
├── debug
│ ├── hello.asm
│ ├── hello.koopa
│ └── test_temp
│ └── **
├── include
│ ├── backend
│ │ ├── backend.hpp
│ │ └── koopawrapper.hpp
│ ├── CMakeLists.txt
│ ├── ir
│ │ ├── ast.hpp
│ │ ├── ir_builder.hpp
│ │ ├── symbol_table.hpp
│ │ └── type.hpp
│ ├── koopa.h
│ └── Log
│ └── log.hpp
├── Makefile
├── scripts
│ └── test_runner.py
├── src
│ ├── backend
│ │ └── backend.cpp
│ ├── CMakeLists.txt
│ ├── frontend
│ │ ├── sysy.lx
│ │ └── sysy.y
│ ├── ir
│ │ ├── ast.cpp
│ │ └── codegen.cpp
│ └── main.cpp
└── tests
├── hello.c
└── resources
├── functional
│ └── **
└── hidden_functional
└── **
16 directories, 620 files
模块
什么?都 2026 了,我们还在使用传统 cpp 的 pch Σ(゚∀゚ノ)ノ
modules 现在处于一个很尴尬的处境,大家都夸他,但是没人用。
关于模块,已经有人系统的介绍了,下面的内容引用自模块化功能的实现者许传奇的博文 C++20 Modules 用户视角下的最佳实践
。
C++20 Modules 的好处
在介绍实践方式之前,我们先介绍下 C++20 Modules 的好处有哪些,为之后介绍不同的实践方式的原因做铺垫。C++20 Modules 的设计目的主要有:
更快的编译速度
避免 ODR Violation
控制 API 可见性
避免宏污染
其中更快的编译速度和避免 ODR Violation 两个目的都是通过 C++20 Modules 可以为每一个声明提供唯一一个归属的 TU 来达到的。
更快的编译速度 (和更小的代码体积)
之前有人认为 C++20 Modules 不过是标准化的 PCH 或者标准化的 Clang Header Modules。这都不对。PCH 或 Clang Header Modules 通过避免不同 TU 重复的预处理/语法分析以减少编译时间。
而 C++20 Modules 在此之上,还可以避免相同声明在编译器中后端的重复优化与编译。而对于很多项目而言,编译器中后端的优化和编译才是耗时的主要来源。
例如
1
2
3
4
// a.h
inline void func_a () {
...
}
这个写法会让每一个包含 a.h 且引用到了 func_a() 的 TU 都对 func_a() 做优化以及代码生成。
而使用 Modules 的写法
1
2
3
4
export module a ;
export int func_a () {
...
}
无论有多少 TU 引用了 func_a(),这些 TU 被编译时都不会再对 func_a() 做重复的优化和代码生成。这是 C++20 Modules 相比于 PCH 或 Clang Header Modules 能提升更多编译速度的一个点。
比起全局函数,更常见的是 in class inline function,即:
1
2
3
4
class A {
public :
void a () { ... }
};
C++20 标准规定,位于 Named Modules 中的 in class inline function 不再是 implicitly inline。即当 A::a() 位于 Named Modules 中时, A::a() 的定义只应该被放到 Named Modules 对应的 Object File 中,而不会被不同的 Consumer 重复优化/编译。
而除了这样显式的函数定义之外,诸如虚表和 debug info 等信息,都应该遵循相同的原则,即此类信息应该只在相关定义对应的 Named Modules 中生成,避免在各个 Consumer 中都生成一遍,即浪费时间还浪费空间。是的,我们在实践中发现,应用 C++20 Modules 不但可以减少编译时间,对于减少构建产物的体积也有显著帮助。
避免 ODR Violation
ODR(One Definition Rule)指的是一个程序中每个实体都应该只有一个相同的定义。当一个实体有多个不同的定义时,这个程序是就违反了 ODR,称为 ODR Violation,此时程序是 ill-formed。
实践中,若一个实体的多个定义是强符号,则会在链接时报错并提示 multiple definition。而如果一个实体的多个定义全是弱符号,则会在链接时挑选任意一个定义,实践上链接器一般会选择遇到的第一个定义。(忽略一个强符号多种弱符号的情况,这种情况一般是特意设计的)。两种情况相比,在链接时报错比在运行时报错要强很多,安全很多。
头文件机制因为其自身不是 TU 却要被许多 TU 共享的特征,天然地会将头文件内的几乎所有符号都设计为弱符号,为 ODR 安全埋下了很大的隐患。当一个大项目因为各种原因引入了同一个三方库的不同版本时,可能就陷入了 ODR Violation 的潜在危机中。
而 C++20 Modules 基于每一个实体都有唯一的 Owning TU 的原则,会为每一个实体都提供强符号,天然地可以避免这类 ODR Violation。
此外 C++20 Modules 还引入了独特的 Mangling 机制,为 Named Modules 中的每个实体添加和 Module 名强相关的后缀,可以避免不同库开发人员之间不经意的重名冲突。例如
export module M;
namespace NS {
export int foo();
}
NS::foo() 的链接名在 Demangle 后为显示为 NS::foo@M()。进一步降低和其他 Module 中的 foo() 函数重名的概率。
至于 Module 的重名,C++20 Modules 要求每个 Module Unit 都生成一个 Module Initializer 用于初始化其内部状态(哪怕这个 Module 内部实际上不需要初始化任何东西),这个 Module Initializer 是一个强符号。从这个角度我们可以避免一个程序中出现重名的 Module Unit。
就简要放一段介绍了,如果你对模块感兴趣,可以前往许传奇的博客了解,也可以参阅 reddit 的社区讨论,或是草案。
简单的日志打印
一个小巧且漂亮的日志打印可以很好的帮助你进行 debug,在 cpp 20 (还是 23 ?我忘了)引进了 source_location,它可以很好的取代部分宏调试的功能,使用起来更加方便和舒适。
fmt 库的强大功能中包含了颜色调节,这是标准库还没有实现的功能。fmt 看起来比较麻烦,但用起来意外的舒服,很符合“人体工学”。
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* @file log.hpp
* @brief Logging and error handling utilities for the compiler.
*/
#pragma once
#include <fmt/color.h>
#include <fmt/core.h>
#include <source_location>
#include <string>
namespace detail {
/**
* @brief Formats a message with source location information.
* @param loc The source location.
* @param fmt_str Format string.
* @param args Format arguments.
* @return Formatted string including location info.
*/
template < typename ... Args >
static auto format_msg ( const std :: source_location & loc ,
std :: string_view fmt_str , Args && ... args )
-> std :: string {
std :: string user_msg =
fmt :: format ( fmt :: runtime ( fmt_str ), std :: forward < Args > ( args )...);
return fmt :: format ( fmt :: fg ( fmt :: color :: alice_blue ), "{} (at {}:{} in {})" , user_msg , loc . file_name (),
loc . line (), loc . function_name ());
}
/**
* @brief Custom exception for compilation errors.
*/
class CompileError : public std :: runtime_error {
public :
explicit CompileError ( const std :: string & message )
: std :: runtime_error ( message ) {}
};
} // namespace detail
/**
* @brief Static logging utility.
*/
class Log {
public :
/**
* @brief Reports a fatal error, prints debug info, and throws a CompileError.
*
* @param fmt_str Format string for the error message.
* @param args Arguments for the format string.
* @param loc Source location (defaults to caller site).
*/
template < typename ... Args >
static auto
panic ( std :: string_view fmt_str , Args && ... args ,
const std :: source_location & loc = std :: source_location :: current ())
-> void {
fmt :: print ( stderr , fmt :: emphasis :: bold | fmt :: fg ( fmt :: color :: red ),
"[PANIC] " );
std :: string msg =
fmt :: format ( fmt :: runtime ( fmt_str ), std :: forward < Args > ( args )...);
fmt :: println ( stderr , "{}" , msg );
fmt :: print ( stderr , fmt :: fg ( fmt :: color :: slate_gray ), " --> {}:{}:{} \n " ,
loc . file_name (), loc . line (), loc . function_name ());
throw detail :: CompileError (
detail :: format_msg ( loc , fmt_str , std :: forward < Args > ( args )...));
}
/**
* @brief Prints a trace message for debugging.
*
* @param fmt_str Format string for the trace message.
* @param args Arguments for the format string.
* @param loc Source location (defaults to caller site).
*/
template < typename ... Args >
static auto
trace ( std :: string_view fmt_str , Args && ... args ,
const std :: source_location & loc = std :: source_location :: current ())
-> void {
fmt :: print ( stdout , fmt :: fg ( fmt :: color :: cyan ), "[TRACE] " );
fmt :: print ( stdout , "{} " ,
fmt :: format ( fmt :: runtime ( fmt_str ), std :: forward < Args > ( args )...));
fmt :: print ( stdout , fmt :: fg ( fmt :: color :: dark_violet ), "[{}] \n " ,
loc . function_name ());
}
};
代码风格
代码注释
尽可能的写注释……写全写明白…..否则你就会像我一样,一个星期不看就看不懂要重新把所有源码再看一遍…
为什么会一两个星期没看呢,因为要期末考试…
Anyway,就算不是因为这个原因,良好风格的注释在项目中也是非常重要的,
TODO
内存管理
TODO
额外内容(CI)
完成此项目不需要配置任何 CI,仅仅是为了好玩我才配置了 CI
x)除此之外,为了将来的 IR 优化做准备,配置 CI 也是一种考量。
CI 提供了一个干净的可复现环境(本项目用不到),并且为你做出的修改提供自动化测试。