注释
从现在起,我将只提供一个最精简的 build.zig,用来说明解决一个问题所需的步骤。如果你想了解如何将所有这些文件粘合到一个构建文件中,请阅读本系列第一篇文章。
在命令行上编译 C 代码
Zig 有两种编译 C 代码的方法,而且这两种很容易混淆。
使用 zig cc
Zig 提供了 LLVM c 编译器 clang。第一种是 zig cc 或 zig c++,它是与 clang 接近 1:1 的前端。由于我们无法直接从 build.zig 访问这些功能(而且我们也不需要!),所以我将在快速的介绍这个主题。
如前所述,zig cc 是暴露的 clang 前端。您可以直接将 CC 变量设置为 zig cc,并使用 zig cc 代替 gcc 或 clang 来使用 Makefiles、CMake 或其他编译系统,这样您就可以在已有的项目中使用 Zig 的完整交叉编译体验。请注意,这只是理论上的说法,因为很多编译系统无法处理编译器名称中的空格。解决这一问题的办法是使用一个简单的封装脚本或工具,将所有参数转发给 zig cc。
假设我们有一个由 main.c 和 buffer.c 生成的项目,我们可以用下面的命令行来构建它:
1
| zig cc -o example buffer.c main.c
|
这将为我们创建一个名为 example 的可执行文件(在 Windows 系统中,应使用 example.exe 代替 example)。与普通的 clang 不同,Zig 默认会插入一个 -fsanitize=undefined,它将捕捉你使用的未定义行为。
如果不想使用,则必须通过 -fno-sanitize=undefined 或使用优化的发布模式(如 -O2)。
使用 zig cc 进行交叉编译与使用 Zig 本身一样简单:
1
| zig cc -o example.exe -target x86_64-windows-gnu buffer.c main.c
|
如你所见,只需向 -target 传递目标三元组,就能调用交叉编译。只需确保所有外部库都已准备好进行交叉编译即可!
使用 zig build-exe 和其他工具
使用 Zig 工具链构建 C 项目的另一种方法与构建 Zig 项目的方法相同:
1
| zig build-exe -lc main.c buffer.c
|
这里的主要区别在于,必须明确传递 -lc 才能链接到 libc,而且可执行文件的名称将从传递的第一个文件中推导出。如果想使用不同的可执行文件名,可通过 –name example 再次获取示例文件。
交叉编译也是如此,只需通过 -target x86_64-windows-gnu 或其他目标三元组即可:
1
| zig build-exe -lc -target x86_64-windows-gnu main.c buffer.c
|
你会发现,使用这条编译命令,Zig 会自动在输出文件中附加 .exe 扩展名,并生成 .pdb 调试数据库。如果你在此处传递 –name example,输出文件也会有正确的 .exe 扩展名,所以你不必考虑这个问题。
用 build.zig 创建 C 代码
那么,我们如何用 build.zig 来构建上面的两个示例呢?
首先,我们需要创建一个新的编译目标:
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
| // demo2.1
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "example",
// 这块调试了很久。最后的结论是根本不要写
// .root_source_file = .{ .path = undefined },
.target = target,
.optimize = optimize,
});
// 这块调试了很久。API变了不会写,着了很久的文档和看了很久的代码
exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("main.c"), .flags = &.{} });
exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("buffer.c"), .flags = &.{} });
//exe.linkLibC();
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
|
然后,我们通过 addCSourceFile 添加两个 C 语言文件:
1
2
| exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("main.c"), .flags = &.{} });
exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("buffer.c"), .flags = &.{} });
|
第一个参数 addCSourceFile 是要添加的 C 或 C++ 文件的名称,第二个参数是该文件要使用的命令行选项列表。
请注意,我们向 addExecutable 传递的是空值,因为我们没有要编译的 Zig 源文件。
现在,调用 zig build 可以正常运行,并在 zig-out/bin 中生成一个可执行文件。很好,我们用 Zig 构建了第一个小 C 项目!
如果你想跳过检查 C 代码中的未定义行为,就必须在调用时添加选项:
1
| exe.addCSourceFile(.{.file = std.build.LazyPath.relative("buffer.c"), .flags = &.{"-fno-sanitize=undefined"}});
|
使用外部库
通常情况下,C 项目依赖于其他库,这些库通常预装在 Unix 系统中,或通过软件包管理器提供。
为了演示这一点,我们创建一个小工具,通过 curl 库下载文件,并将文件内容打印到标准输出:
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
| #include <stdio.h>
#include <curl/curl.h>
static size_t writeData(void *ptr, size_t size, size_t nmemb, FILE *stream) {
size_t written;
written = fwrite(ptr, size, nmemb, stream);
return written;
}
int main(int argc, char ** argv)
{
if(argc != 2)
return 1;
char const * url = argv[1];
CURL * curl = curl_easy_init();
if (curl == NULL)
return 1;
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeData);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, stdout);
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if(res != CURLE_OK)
return 1;
return 0;
}
|
要编译这个程序,我们需要向编译器提供正确的参数,包括包含路径、库和其他参数。幸运的是,我们可以使用 Zig 内置的 pkg-config 集成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // demo2.2
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "downloader",
.target = target,
.optimize = optimize,
});
exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("download.c"), .flags = &.{} });
exe.linkSystemLibrary("curl");
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
|
让我们创建程序,并通过 URL 调用它
1
2
| zig build
./zig-out/bin/downloader https://mq32.de/public/ziggy.txt
|
配置路径
由于我们不能在交叉编译项目中使用 pkg-config,或者我们想使用预编译的专用库(如 BASS 音频库),因此我们需要配置包含路径和库路径。
这可以通过函数 addIncludePath 和 addLibraryPath 来完成:
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
| //demo 2.3
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "example",
.target = target,
.optimize = optimize,
});
exe.addCSourceFile(.{
.file = std.build.LazyPath.relative("bass-player.c"),
.flags = &.{}
});
exe.linkLibC();
// 还是一步步看源代码,找新的函数,addIncludeDir,addLibDir ->new function
exe.addIncludePath(std.build.LazyPath.relative("bass/linux"));
exe.addLibraryPath(std.build.LazyPath.relative("bass/linux/x64"));
exe.linkSystemLibrary("bass");
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
|
addIncludePath 和 addLibraryPath 都可以被多次调用,以向编译器添加多个路径。这些函数不仅会影响 C 代码,还会影响 Zig 代码,因此 @cImport 可以访问包含路径中的所有头文件。
每个文件的包含路径
因此,如果我们需要为每个 C 文件设置不同的包含路径,我们就需要用不同的方法来解决这个问题:
由于我们仍然可以通过 addCSourceFile 传递任何 C 编译器标志,因此我们也可以在这里手动设置包含目录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| //demo2.4
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "example",
.target = target,
.optimize = optimize,
});
exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("multi-main.c"), .flags = &.{} });
exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("multi.c"), .flags = &.{ "-I", "inc1" } });
exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("multi.c"), .flags = &.{ "-I", "inc2" } });
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
|
上面的示例非常简单,所以你可能会想为什么需要这样的东西。答案是,有些库的头文件名称非常通用,如 api.h 或 buffer.h,而您希望使用两个共享头文件名称的不同库。
构建 C++ 项目
到目前为止,我们只介绍了 C 文件,但构建 C++ 项目并不难。你仍然可以使用 addCSourceFile,但只需传递一个具有典型 C++ 文件扩展名的文件,如 cpp、cxx、c++ 或 cc:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| //demo2.5
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "example",
.target = target,
.optimize = optimize,
});
exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("main.c"), .flags = &.{} });
exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("buffer.cc"), .flags = &.{} });
exe.linkLibCpp();
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
|
如你所见,我们还需要调用 linkLibCpp,它将链接 Zig 附带的 c++ 标准库。
这就是构建 C++ 文件所需的全部知识,没有什么更神奇的了。
指定语言版本
试想一下,如果你创建了一个庞大的项目,其中的 C 或 C++ 文件有新有旧,而且可能是用不同的语言标准编写的。为此,我们可以使用编译器标志来传递 -std=c90 或 -std=c++98:
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
| //demo2.6
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "example",
.target = target,
.optimize = optimize,
});
exe.addCSourceFile(.{
.file = std.build.LazyPath.relative("main.c"),
.flags = &.{"-std=c90"}
});
exe.addCSourceFile(.{
.file = std.build.LazyPath.relative("buffer.cc"),
.flags = &.{"-std=c++17"}
});
exe.linkLibCpp();
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
|
条件编译
与 Zig 相比,C 和 C++ 的条件编译方式非常繁琐。由于缺乏惰性求值的功能,有时必须根据目标环境来包含/排除文件。你还必须提供宏定义来启用/禁用某些项目功能。
Zig 编译系统可以轻松处理这两种变体:
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
| //demo2.7
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const use_platform_io = b.option(bool, "platform-io", "Uses the native api instead of the C wrapper") orelse true;
const exe = b.addExecutable(.{
.name = "example",
.target = target,
.optimize = optimize,
});
exe.addCSourceFile(.{
.file = std.build.LazyPath.relative("print-main.c"),
.flags = &.{}
});
if (use_platform_io) {
exe.defineCMacro("USE_PLATFORM_IO", null);
if (exe.target.isWindows()) {
exe.addCSourceFile(.{
.file = std.build.LazyPath.relative("print-windows.c"),
.flags = &.{}
});
} else {
exe.addCSourceFile(.{
.file = std.build.LazyPath.relative("print-unix.c"),
.flags = &.{}
});
}
}
exe.linkLibC();
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
|
通过 defineCMacro,我们可以定义自己的宏,就像使用 -D 编译器标志传递宏一样。第一个参数是宏名,第二个值是一个可选项,如果不为空,将设置宏的值。
有条件地包含文件就像使用 if 一样简单,你可以这样做。只要不根据你想在构建脚本中定义的任何约束条件调用 addCSourceFile 即可。只包含特定平台的文件?看看上面的脚本就知道了。根据系统时间包含文件?也许这不是个好主意,但还是有可能的!
编译大型项目
由于大多数 C(更糟糕的是 C++)项目都有大量文件(SDL2 有 411 个 C 文件和 40 个 C++ 文件),我们必须找到一种更简单的方法来编译它们。调用 addCSourceFile 400 次并不能很好地扩展。
因此,我们可以做的第一个优化就是将 c 和 c++ 标志放入各自的变量中:
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
| //demo2.8
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "example",
.target = target,
.optimize = optimize,
});
const flags = .{
"-Wall",
"-Wextra",
"-Werror=return-type",
};
const cflags = flags ++ .{"-std=c99"};
const cppflags = cflags ++ .{
"-std=c++17",
"-stdlib=libc++",
"-fno-exceptions",
};
exe.addCSourceFile(.{
.file = std.build.LazyPath.relative("main.c"),
.flags = &cflags,
});
exe.addCSourceFile(.{
.file = std.build.LazyPath.relative("buffer.cc"),
.flags = &cppflags,
});
exe.linkLibC();
exe.linkLibCpp();
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
|
这样就可以在项目的不同组件和不同语言之间轻松共享标志。
addCSourceFile 还有一个变种,叫做 addCSourceFiles。它使用的不是文件名,而是可编译的所有源文件的文件名片段。这样,我们就可以收集某个文件夹中的所有文件:
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
| //demo2.9
const std = @import("std");
pub fn build(b: *std.build.Builder) !void {
var sources = std.ArrayList([]const u8).init(b.allocator);
// Search for all C/C++ files in `src` and add them
{
var dir = try std.fs.cwd().openIterableDir(".", .{ .access_sub_paths = true });
var walker = try dir.walk(b.allocator);
defer walker.deinit();
const allowed_exts = [_][]const u8{ ".c", ".cpp", ".cxx", ".c++", ".cc" };
while (try walker.next()) |entry| {
const ext = std.fs.path.extension(entry.basename);
const include_file = for (allowed_exts) |e| {
if (std.mem.eql(u8, ext, e))
break true;
} else false;
if (include_file) {
// we have to clone the path as walker.next() or walker.deinit() will override/kill it
try sources.append(b.dupe(entry.path));
}
}
}
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "example",
.target = target,
.optimize = optimize,
});
exe.addCSourceFiles(sources.items, &.{});
exe.linkLibC();
exe.linkLibCpp();
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
|
正如您所看到的,我们可以轻松搜索某个文件夹中的所有文件,匹配文件名并将它们添加到源代码集合中。然后,我们只需为每个文件集调用一次 addCSourceFiles,就可以大展身手了。
你可以制定很好的规则来匹配 exe.target 和文件夹名称,以便只包含通用文件和适合你的平台的文件。不过,这项工作留给读者自己去完成。
注意:其他构建系统会考虑文件名,而 Zig 系统不会!例如,在一个 qmake 项目中不能有两个名为 data.c 的文件!Zig 并不在乎,你可以添加任意多的同名文件,只要确保它们在不同的文件夹中就可以了 😏。
编译 Objective C
我完全忘了!Zig 不仅支持编译 C 和 C++,还支持通过 clang 编译 Objective C!
虽然不支持 C 或 C++,但至少在 macOS 上,你已经可以编译 Objective C 程序并添加框架了:
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
| //demo2.10
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "example",
.target = target,
.optimize = optimize,
});
exe.addCSourceFile(.{
.file = std.build.LazyPath.relative("main.m"),
.flags = &.{},
});
exe.linkFramework("Foundation");
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
|
在这里,链接 libc 是隐式的,因为添加框架会自动强制链接 libc。是不是很酷?
混合使用 C 和 Zig 源代码
现在,是最后一章: 混合 C 代码和 Zig 代码!
为此,我们只需将 addExecutable 中的第二个参数设置为文件名,然后点击编译!
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
| //demo2.11
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "example",
.root_source_file = .{ .path = "main.zig" },
.target = target,
.optimize = optimize,
});
exe.addCSourceFile(.{
.file = std.build.LazyPath.relative("buffer.c"),
.flags = &.{},
});
exe.linkLibC();
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
|
这就是需要做的一切!是这样吗?
实际上,有一种情况现在还没有得到很好的支持:
您应用程序的入口点现在必须在 Zig 代码中,因为根文件必须导出一个 pub fn main(…) ……。
因此,如果你想将 C 项目中的代码移植到 Zig 中,你必须将 argc 和 argv 转发到你的 C 代码中,并将 C 代码中的 main 重命名为其他函数(例如 oldMain),然后在 Zig 中调用它。如果需要 argc 和 argv,可以通过 std.process.argsAlloc 获取。或者更好: 在 Zig 中重写你的入口点,然后从你的项目中移除一些 C 语言!
结论
假设你只编译一个输出文件,那么现在你应该可以将几乎所有的 C/C++ 项目移植到 build.zig。
如果你需要一个以上的构建工件,例如共享库和可执行文件,你应该阅读下一篇文章,它将介绍如何在一个 build.zig 中组合多个项目,以创建便捷的构建体验。
敬请期待!