Compile Principle Part 0 : Before Everything Begins

Docker

maxXing 提供为实验提供了 docker 镜像,所以我们只需要将 docker 下载下来拉取镜像即可。

使用 pacman 下载 docker

1
sudo pacman -S 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 直接进入 dockermake 进行编译。

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 拉过来使用。

没想到 fmtprint 还好用。

我的实现有一个缺陷,为了避免引入依赖,我直接使用 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 提供了一个干净的可复现环境(本项目用不到),并且为你做出的修改提供自动化测试。

会长寻找灵感中...
使用 Hugo 构建
主题 StackJimmy 设计
Published 33 aritcles · Total 91.31k words