Skip to content

Latest commit

 

History

History
180 lines (133 loc) · 7.46 KB

File metadata and controls

180 lines (133 loc) · 7.46 KB

00 —— mcpp 是什么

背景:C++ 工程侧的工具现状

一个 C++ 工程需要四样东西同时成立:一份构建描述、一组依赖、一个新到足以编译这份 代码的编译器,以及一个能让产物真正运行起来的环境。C++ 没有任何一个工具同时负责 这四样,它们由四类互不隶属的工具分别承担,而把它们对齐是工程自己的工作。

CMake 是其中第一样的事实标准。事实标准陈述的是采用率,不是使用体验:CMake 不解析 依赖、不安装编译器、也不描述运行期环境,而这三件恰好是新参与者在第一天遇到的。 补齐它们意味着再引入一个包管理器、发行版的软件包,以及 README 里的一段环境说明 —— 三套新的模型,而它们之间的对齐仍然落在工程身上。

最经常失效的是环境这一层,因为只有它没有任何东西在检查。构建描述会在配置阶段 报错,包管理器会解析失败,而「机器上的库不是这份代码构建时所依据的那一份」在 链接之前不产生任何信号,有时到程序运行时才产生。

模块给这个结构再加一条约束。C++20 的模块改变的是一个翻译单元的代价:接口只声明 一次并被 import,而不是被每个需要它的文件从头文件里重新解析一遍;消费者看到的是 作者导出的东西,不是头文件恰好 include 进来的一切。代价是这个语言特性由构建系统 实现:构建要扫描源里的 import、据此给编译定序、缓存编译好的接口并正确地让它 失效,而且要在一个新到具备这个特性的编译器上完成这些。import std 再加一条: 标准库自己的模块必须先被构建出来,别的东西才能用它。

于是采用模块的工程需要上述四层全部成立,而不是其中三层。这个落差是具体的。在写 这一章的这台机器上:

$ g++ --version
g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0

这个编译器编不了 import std。工程本身没有任何问题;是这台机器不是这个工程需要 的那台机器。

这一现状的代价

对人,代价是一次搭建,每台机器一次、每个新参与者一次:装一个更新的编译器、 确定哪些构建旗标把模块打开、取得依赖,然后在链接失败时判断坏的是这三者中的哪一 件。这个代价不随工程成熟而下降,它按人数和机器数重复。

对 agent,代价是上下文,并且在写下第一行代码之前就消耗在三处:读构建描述以 确定它究竟做了什么、重建它所假定的环境,以及顺着一个头文件穿过传递 include 去 确定究竟声明了什么。

模块消掉了第三处,因为接口是显式的,import 陈述了用到什么。mcpp 消掉另外两处, 并把第一处收敛为一条命令。

mcpp 的组成

mcpp = 通用构建系统
     + 构建插件
     + 包管理
     + 工具链管理
     + 环境与运行时(xlings)

多数 C++ 工程要把这五个部分从不同工具里拼起来,而上面那份搭建代价正是拼装本身的 代价:构建文件假定机器上有某个编译器,包管理器假定一份不是它写的构建文件,而环境 是 README 里的一段话。

mcpp 是一个程序,五个部分共用同一套模型。

对于已经在用相应工具的读者:

组成部分 在 mcpp 中的形式 可对照的工具
通用构建系统 mcpp.toml、模块图、ninja 后端 CMake、Meson
构建插件 build.mcpp、规则包 build.zig、xmake rules
包管理 [dependencies]mcpp.lock、索引 Conan、vcpkg
工具链管理 编译器作为被安装并钉住的载荷 Zig 自带的工具链、rustup、手工安装 GCC / LLVM / MSVC
环境与运行时 [xlings]、载荷、运行期搜索路径 Nix、conda

整体上最接近的两个类比是 Cargo 与 Zig,二者各对应同一个想法的一半。Cargo 是一个 程序同时充当构建、包管理、锁文件与测试运行器,于是一个 Rust 工程克隆下来直接构建、 没有前置步骤。Zig 把工具链随工具一起发布,并且默认支持交叉编译,于是编译器不是 机器必须先具备的东西。

mcpp 在 C++ 上是这个形状,外加二者都没有的一部分:环境层 —— 工程通过它声明构建 所需的非编译器工具。

这张表为各部分定位,不宣称等价。 表中每个工具在它自己的领域里做的都比 mcpp 多,需要那种深度的工程使用它。

核心保证

拿到任何一个 mcpp 工程,mcpp build 就能构建 —— 不需要自行安装编译器、 配置环境,也不需要寻找依赖。

这里写下两条边界,以便这句话可以被依赖:面向设备的工程第一次构建仍会下载该设备的 工具包;而这台机器服务不了的目标会被点名拒绝,不会被错误地构建出来。

最小示例

还是那台机器,唯一的 C++ 编译器是上面那个 GCC 13。

$ mcpp new hello
Created bin package 'hello' at /tmp/zero-demo/hello
Next: cd hello && mcpp build && mcpp run  (or `mcpp test`)

四个文件,manifest 五行:

[package]
name        = "hello"
version     = "0.1.0"
description = "A modular C++23 package"
license     = "Apache-2.0"

没有声明编译器、没有声明语言档位、没有声明任何依赖。 这就是一个读者在改动 这个工程之前需要理解的全部。而源码用的正是这台机器的编译器不具备的那个特性:

import std;

int main() {
    std::println("Hello from hello!");
}
$ mcpp run
    Inferred target hello (bin from src/main.cpp)
   Compiling hello v0.1.0 (.)
    Finished dev [unoptimized + debuginfo] in 0.64s
     Running `target/x86_64-linux-gnu/0946988e9e4b52ba/bin/hello`

Hello from hello!
Built with import std + std::println on modular C++23.

墙钟 1.25 秒,含首次运行。完成这次构建的编译器:

$ mcpp self env
default toolchain   = gcc@16.1.0

mcpp 安装了 GCC 16 并使用它。宿主上没有任何东西被改动,而克隆这个工程的同事 —— 或者 agent —— 得到的是同一个编译器,不是那台机器恰好自带的那个。

增加一个依赖是一行,不需要其他步骤:

[dependencies]
"mcpplibs.cmdline" = "^0.0.1"

mcpp build 会解析它、取回它、构建它、链接它。

适用范围

mcpp 围绕 C++20/23 模块与较新的语言特性建立,它维护的生态由此而来,而不是 来自一个泛化的目标:

模块化 C++ import std 零配置、模块扫描、跨工程 BMI 缓存
嵌入式与裸机 freestanding 目标、板级支持包、从源码到一个可运行镜像只需一条命令
异构计算与 GPU CUDA、HIP、SYCL、Vulkan/SPIR-V 与 Ascend C,每一条都是规则包而不是引擎特性
图形 着色器作为构建的一部分被编译,并以模块到达
内核与底层 零 libc 档、显式的链接模型、没有隐藏的宿主依赖

不需要其中任何一项的工程,同样得到上述保证;需要其中之一的工程,不必离开这个 工具去得到它。

阅读路径

运行第一个程序 01 —— 快速开始
判断 mcpp 是否适用于当前工作 02 —— 场景
阅读一个结构相近的工程 03 —— 示例项目

本章之后的一切都是参考:manifest 可以陈述什么、依赖怎样解析、目标怎样命名。本章 按设计不写任何字段与旗标。