likes
comments
collection
share

VS Code + Meson搭建C开发环境

作者站长头像
站长
· 阅读数 1

本文以Arch Linux为例介绍如何基于VS Code和Meson搭建一套C语言开发环境。VS Code和Meson都是跨平台的,其他平台的读者可以参考本文自行摸索,大体流程都是差不多的。C++用户也可以参考本文,只有个别参数的不同,需要变动的地方我都会标出来。

Meson介绍

依据其官方网站的介绍:

Meson 是一个开源构建系统,不仅速度极快,而且更重要的是,尽可能用户友好。

Meson 的设计要点是,开发人员花在编写或调试构建定义上的每一刻都是浪费。等待构建系统实际开始编译代码的每一秒也是如此。

同样依据其官方网站的介绍,Meson有以下特性:

  • 对 Linux、macOS、Windows、GCC、Clang、Visual Studio 等的多平台支持
  • 支持的语言包括 C、C++、D、Fortran、Java、Rust
  • 以非常可读且用户友好的非图灵完备 DSL 构建定义
  • 对许多操作系统以及裸机的交叉编译支持
  • 针对完整和增量构建进行了优化,速度极快而不牺牲正确性
  • 内置的多平台依赖提供程序,可与发行版包一起使用
  • 乐趣!

其中有两点很重要,一是跨平台,与CMake看齐;二是配置文件非图灵完备,这是用户友好的重要原因。非图灵完备简单来说,就是它的配置文件更像是配置文件,而不是编程语言。

Meson使用Apache 2许可证。

Meson的默认后端是Ninja,也支持Visual Studio、Xcode后端,但不支持Make。因为Make太慢,Makefile语法不行,支持成本高。

Meson是用Python写的,配置文件的语法也基于Python。用Python编写算是Meson的一个劣势,意味着依赖比较庞大。目前有一些其他语言的实现,但还没达到普遍可用的状态。

目前使用Meson的主要是Gnome、Systemd等红帽系项目,但PostgreSQL、QEMU、mpv等多个无关项目也在使用,说明其能力还是被广泛认可的。

环境安装

首先需要安装VS Code与Meson,在Arch Linux上,可以通过以下命令完成:

yay -Sy visual-studio-code-bin meson

如果还没有编译器和调试器,在Arch Linux上,可以通过以下命令安装:

sudo pacman -Sy gcc gdb

之后安装必需的VS Code扩展:

Meson的VS Code扩展是官方推出的,使用体验很差,但没有更好的替代品。要使用扩展的格式化功能,还必需安装另一个程序muon

Meson扩展的内嵌提示建议关掉,没有价值,还影响阅读。

VS Code + Meson搭建C开发环境

第一个项目

VS Code以目录为工作区,先创建一个目录,以demo为例,然后用VS Code打开这个目录。

再创建源文件,以main.c为例,随便写点什么,比如著名的"hello, world"。

#include <stdio.h>

int main(void) {
    puts("hello, world");
    return 0;
}

现在我们已经有了完备的源代码,可以构建出一个可执行程序。接下来编写构建配置文件。

首先创建一个名为meson.build的文件,这是Meson的配置文件,并且文件名是特定的。

VS Code + Meson搭建C开发环境

这时Meson扩展会弹出一条提示,询问你是否配置Meson项目,其实就是是否执行meson setup命令。这时先不用管,因为我们的配置文件还没写完,肯定会失败。

VS Code + Meson搭建C开发环境

和Python一样,Meson的行注释也是以#开头。除注释以外,Meson配置文件必须以project()开头,表明项目的名字、语言、版本、默认选项等。其中名字语言是必需的,依次是函数的前两个位置参数。

# Meson configuration for demo

project('demo', 'c',
    version: '0.0.1',
    default_options: {
        'c_std': 'c17',
        'warning_level': '3',
        'werror': true,
        'optimization': 'g',
        'strip': true,
    },
)

如果项目存在多个语言,可以同时指定,比如project('demo', 'c', 'cpp')

Meson的值是有类型的,基本类型有字符串、数字、bool、列表、字典。字符串以单引号包裹,不支持双引号。

Meson大部分的可变参数都是扁平化的,可以把任意嵌套的列表展开为连续的参数。project('demo', 'c', 'cpp')project('demo', ['c', 'cpp'])是等价的,为了提高可读性,本文都会使用后一种写法。

default_options是对所有构建目录都生效的默认选项,之后还可以针对特定的构建目录执行meson configure命令来改变这些选项。

这些选项有的会添加编译器参数,有的会添加链接参数,有的会改变安装时的行为。

  • c_std选择C语言的标准版本,比如c17就为编译器添加-std=c17参数。如果编程语言为C++,选项名为cpp_std。如果没有指定这个选项,则语言版本是由编译器决定的。
  • warning_level指定警告等级,不同等级对应的编译器参数可对照下表。因为有everything这么个值的存在,所以选项的类型是字符串。需要注意的是,gcc并没有-Weverything这么个参数,所以不要使用everything这个警告等级。警告等级3包含了-Wpedantic,这会对不符合语言标准的写法发出警告,如果设置了c_stdwarning_level建议设置为3。默认是1
Warning levelGCC/ClangMSVC
0
1-Wall/W2
2-Wall -Wextra/W3
3-Wall -Wextra -Wpedantic/W4
everything-Weverything/Wall
  • werror为编译器添加-Werror参数。这会让所有的警告变成错误,从而让编译失败。我强烈建议开启这个选项,让开发者不再忽略警告,从而消灭隐患。如果是故意为之,也可以显式消除警告,提高代码可读性。默认是false
  • optimization改变编译器的优化等级,g代表参数-Og。Meson的默认构建类型是debug,等价于参数-g -O0-Og可以提升调试体验。如果meson setupmeson configure命令指定buildtyperelease,等价于-O3optimization会被覆盖掉,所以不必担心optimization选项对meson setupmeson configure命令造成影响。
  • strip选项表示在执行meson install命令时对二进制文件进行strip,这会减少二进制文件的体积。默认是false.

接着我们指定此次构建的目标,以及它依赖哪些源文件。

executable('demo', sources: 'main.c', install: true)

executable的第一个位置参数表明了可执行文件的名字,关键字参数sources表明了编译哪些源文件,install表明了在执行meson install命令时是否安装这个可执行文件。

最后的meson.build文件长这样:

# Meson configuration for demo

project('demo', 'c',
    version: '0.0.1',
    default_options: {
        'c_std': 'c17',
        'warning_level': '3',
        'werror': true,
        'optimization': 'g',
        'strip': true,
    },
)

executable('demo', sources: 'main.c', install: true)

此时就可以完成构建了,点击扩展提示的Yes,会创建一个名为builddir的目录。

VS Code + Meson搭建C开发环境

如果这个提示消失了,也可以在项目根目录下手动运行以下命令生成构建目录。

meosn setup builddir

目录名不能改,因为这是Meson扩展的默认目录名,后续很多操作都依赖于各个名字。

VS Code + Meson搭建C开发环境

这时还会弹出一个提示,问你是否下载语言服务器,这个必须下载,不然扩展功能少一半。

VS Code + Meson搭建C开发环境

Yes后,会在你的全局设置里加一条"mesonbuild.downloadLanguageServer": true

生成构建目录的同时也会生成一系列配置文件,比如build.ninja就是Ninja的配置文件,.gitignore.hgignore分别是GitMercurial版本控制系统的忽略文件,builddir下的所有内容都不会添加到版本控制系统中。

VS Code + Meson搭建C开发环境

除此之外,Meson扩展还能与C/C++扩展集成,通过compile_commands.json文件配置C/C++扩展的代码提示,体现为自动生成工作区配置。

VS Code + Meson搭建C开发环境

这时我们执行meson compile -C builddir命令,或者使用VS Code命令Meson: Build就可以在builddir下生成一个名为demo的可执行文件。

VS Code + Meson搭建C开发环境

VS Code + Meson搭建C开发环境

调试

Meson扩展提供了一系列任务,但没有提供调试配置。

VS Code + Meson搭建C开发环境

如果我们编辑Meson: Build all targets,可以看到里面的内容是这样的。

{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "meson",
            "mode": "build",
            "problemMatcher": [
                "$meson-gcc"
            ],
            "group": "build",
            "label": "Meson: Build all targets"
        }
    ]
}

执行这个任务,在终端可以看到实际执行的命令。

VS Code + Meson搭建C开发环境

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "debug",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/${config:mesonbuild.buildFolder}/demo",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}/rundir",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": false
                }
            ],
            "preLaunchTask": "Meson: Build all targets"
        }
    ]
}

这里我把cwd设置为一个单独的目录,避免扰乱代码树。preLaunchTask可以在每次调试前都执行一次构建。

现在万事俱备,可以按F5开始调试了。

多文件项目

当然我们不会为了一个源文件而使用构建工具,现在我们提升项目复杂度。创建两个子目录ab,每个子目录下都有对应的源文件和头文件。让main.c 依赖 a.h,让a.c 依赖 b.h

VS Code + Meson搭建C开发环境

// main.c

#include <stdio.h>

#include "a/a.h"

int main(void) {
    puts("hello, world");
    fn_a();
    return 0;
}
// a.h

extern void fn_a(void);
// a.c

#include <stdio.h>

#include "b/b.h"

void fn_a(void) {
    puts("fn_a");
    fn_b();
}
// b.h

extern void fn_b(void);
// b.c

#include <stdio.h>

void fn_b(void) {
    puts("fn_b");
}

文件准备就绪,现在我们需要对meson.build进行一点修改。

# Meson configuration for demo

project('demo', 'c',
    version: '0.0.1',
    default_options: {
        'c_std': 'c17',
        'warning_level': '3',
        'werror': true,
        'optimization': 'g',
        'strip': true,
    },
)

sources = [
    'main.c',
    'a/a.c',
    'b/b.c',
]

executable('demo', sources: sources, install: true)

改动点只有把原来的'main.c'扩充成3个源文件,为了提高可读性,我们还把源文件列表独立成一个变量。

观察上面的几个文件,有两个关键信息:

  1. meson.build里只有源文件,没有头文件,也没有表明任何头文件依赖。这是因为Ninja可以借助编译器产生的信息自动分析头文件依赖,并且在必要的时候重新编译源文件。
  2. 源文件的#include指令并不是相对于自身的路径,而是相对于项目根目录的。这是因为meson.build里的executable()函数会自动添加包含路径,一个是当前meson.build所在的目录,另一个是builddir中对应可执行文件所在的目录。

修改meson.build后,不需要重新执行meson setup,可以直接进行构建,build.ninja里有钩子,自动读取meson.build并且更新builddir里的配置文件。

修改源文件或头文件的内容(注意不是添加源文件)后,也不需要重新执行meson setup,构建时Ninja会自动分析依赖并重新编译需要的文件。

执行结果:

hello, world
fn_a
fn_b

依赖管理

如果我们的项目依赖第三方提供的库,需要加一些编译参数或链接参数。比如libgcrypt,熟悉gcc的朋友应该知道要加-lgcrypt参数。不过直接加参数既不好管理,又不可移植。Meson有内置的依赖管理,可以借助pkg-config查找依赖并自动添加编译和链接参数。

可以使用add_project_dependencies()函数为整个项目的所有构建目标都添加依赖,也可以通过构建目标的dependencies参数单独添加依赖。

# Meson configuration for demo

project('demo', 'c',
    version: '0.0.1',
    default_options: {
        'c_std': 'c17',
        'warning_level': '3',
        'werror': true,
        'optimization': 'g',
        'strip': true,
    },
)

libgcrypt = dependency('libgcrypt')

# add_project_dependencies(libgcrypt, language: 'c')
executable('demo', sources: 'main.c', dependencies: libgcrypt, install: true)

然后编写我们的源文件。

// main.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <gcrypt.h>

int main(void) {
    puts("hello, world");

    enum gcry_md_algos algo = GCRY_MD_MD5;
    unsigned int digest_len = gcry_md_get_algo_dlen(algo);
    char source[] = "password";
    void *result = malloc(digest_len);
    gcry_md_hash_buffer(algo, result, source, strlen(source));
    for (unsigned int iter = 0; iter < digest_len; iter++) {
        printf("%02x", ((unsigned char*)result)[iter]);
    }
    putchar('\n');
    free(result);

    return 0;
}

构建并运行:

hello, world
5f4dcc3b5aa765d61d8327deb882cf99

可以在终端的输出里看到查找依赖的过程。

The Meson build system
Version: 1.3.0
Source dir: /home/lonble/demo
Build dir: /home/lonble/demo/builddir
Build type: native build
Project name: demo
Project version: 0.0.1
C compiler for the host machine: cc (gcc 13.2.1 "cc (GCC) 13.2.1 20230801")
C linker for the host machine: cc ld.bfd 2.41.0
Host machine cpu family: x86_64
Host machine cpu: x86_64
Found pkg-config: YES (/usr/bin/pkg-config) 2.1.0
Run-time dependency libgcrypt found: YES 1.10.3-unknown
Build targets in project: 1

我们还可以指定依赖版本,以及是否使用静态库。

libgcrypt = dependency('libgcrypt', version: '>=1.5', static: false)

Meson还对线程库进行了封装。

threads = dependency('threads')

如果一些库既没有pkg-config文件,也没有Meson封装,还可以通过编译器手动查找。这些库一般都是glibc从C标准库里拆分出来的,以数学库libm为例:

cc = meson.get_compiler('c')
math = cc.find_library('m', required: false)

find_library()的返回值类型和dependency()相同,所以返回值的用法也是相同的。因为除Linux以外的其他平台都没有单独拆分的数学库,所以设置了required: false,如果没找到这个库也不会报错。

自定义编译和链接参数

project()函数的default_options参数里,就有名为c_argsc_link_args的键,对应的C++版本为cpp_argscpp_link_args。这两个键分别设置编译和链接参数。

project('demo', 'c',
    default_options: {
        'c_args': ['-ansi', '-Wmain'],
        'c_link_args': ['-s', '-static'],
    },
)

强烈不建议default_options中添加编译和链接参数,原因如下:

  1. default_options在修改后不会自动同步到builddir,如果要强行覆盖,必须执行meson setup --wipe builddir,这会清空已经生成的目标文件
  2. 在这里设置编译和链接参数会覆盖CFLAGS环境变量

为整个项目添加编译参数的推荐方法是add_project_arguments()函数。

add_project_arguments(['-ansi', '-Wmain'], language: 'c')

这里的language参数不能省略。

同样可以使用add_project_link_arguments()函数为整个项目添加链接参数,用法和add_project_arguments()是一样的。

还可以针对每个构建目标单独设置参数。

executable('demo',
    sources: 'main.c',
    install: true,
    c_args: ['-ansi', '-Wmain'],
    cpp_args: ['-Weffc++', '-Wnamespaces'],
    link_args: ['-s', '-static']
)

需要注意,自定义参数不利于程序的可移植性,因为这些参数都是特定于编译器或平台的。我们可以针对不同平台使用不同的参数,尽可能保证程序的可移植性。

args = []

arg_syntax = meson.get_compiler('c').get_argument_syntax()
if arg_syntax == 'gcc'
    args += ['-Wall', '-Wextra']
elif arg_syntax == 'msvc'
    args += '/W3'
endif

add_project_arguments(args, language: 'c')

安装产物

Meson对产物安装也有一套标准流程。首先只有标记install: true的构建目标才会被安装,构建目标有多个种类,每个种类都有对应的安装目录。

这是从官网扒下来的默认目录表。

OptionDefault valueDescription
prefixsee belowInstallation prefix
bindirbinExecutable directory
datadirshareData file directory
includedirincludeHeader file directory
infodirshare/infoInfo page directory
libdirsee belowLibrary directory
licensedirLicenses directory
libexecdirlibexecLibrary executable directory
localedirshare/localeLocale data directory
localstatedirvarLocalstate data directory
mandirshare/manManual page directory
sbindirsbinSystem executable directory
sharedstatedircomArchitecture-independent data directory
sysconfdiretcSysconf data directory
  • prefix在Windows上是C:/,其他平台是/usr/local
  • libdir是根据平台自动推测的,不同发行版也有区别

prefix设置为某些特定值时,其他选项的默认值会改变。

  • prefix/usr时,sysconfdir默认为/etclocalstatedir默认为/varsharedstatedir默认为/var/lib
  • prefix/usr/local时,localstatedir默认为/var/localsharedstatedir默认为/var/local/lib

如果安装目录是相对路径,则是相对于prefix的;如果是绝对路径,则忽略prefix

这些选项的默认值可以通过project()函数的default_options参数改变,也可以执行meson configure命令手动修改。

每个构建目标也可以通过install_dir参数改变自己的安装目录。

executable('demo',
    sources: 'main.c',
    install: true,
    install_dir: 'exec'
)

以我们最简单的"hello, world"程序为例。此时我们没有改变任何安装目录,所以可执行文件demo的安装目录是/usr/local/bin。如果安装时缺少权限,Meson会向你索要权限。

# Meson configuration for demo

project('demo', 'c',
    version: '0.0.1',
    default_options: {
        'c_std': 'c17',
        'warning_level': '3',
        'werror': true,
        'optimization': 'g',
        'strip': true,
    },
)

executable('demo', sources: 'main.c', install: true)

Meson默认的buildtypedebug,我们当然不能安装debug版本。执行下面的命令生成release构建目录,默认开启-O3优化。同时修改安装目录。接着就可以执行meson install命令安装,安装前会检查是否需要构建,保证产物最新。

meson setup --buildtype=release --prefix="$HOME/.local" build_release
meson install -C build_release

从终端输出可以看到demo被安装到了$HOME/.local/bin目录下,并且执行了strip

ninja: Entering directory `/home/lonble/demo/build_release'
[2/2] Linking target demo
Installing demo to /home/lonble/.local/bin
Stripping target 'demo'