Zig 構建系統解析 - 第二部分
原文鏈接: https://zig.news/xq/zig-build-explained-part-2-1850
API 適配到 Zig 0.11.0 版本
註釋
從現在起,我將只提供一個最精簡的 build.zig,用來說明解決一個問題所需的步驟。如果你想了解如何將所有這些文件粘合到一個構建文件中,請閱讀本系列第一篇文章 [1]。
在命令行上編譯 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 生成的項目,我們可以用下面的命令行來構建它:
zig cc -o example buffer.c main.c
這將爲我們創建一個名爲 example 的可執行文件(在 Windows 系統中,應使用 example.exe 代替 example)。與普通的 clang 不同,Zig 默認會插入一個 -fsanitize=undefined,它將捕捉你使用的未定義行爲。
如果不想使用,則必須通過 -fno-sanitize=undefined 或使用優化的發佈模式(如 -O2)。
使用 zig cc 進行交叉編譯與使用 Zig 本身一樣簡單:
zig cc -o example.exe -target x86_64-windows-gnu buffer.c main.c
如你所見,只需向 -target 傳遞目標三元組,就能調用交叉編譯。只需確保所有外部庫都已準備好進行交叉編譯即可!
使用 zig build-exe 和其他工具
使用 Zig 工具鏈構建 C 項目的另一種方法與構建 Zig 項目的方法相同:
zig build-exe -lc main.c buffer.c
這裏的主要區別在於,必須明確傳遞 -lc 才能鏈接到 libc,而且可執行文件的名稱將從傳遞的第一個文件中推導出。如果想使用不同的可執行文件名,可通過 --name example 再次獲取示例文件。
交叉編譯也是如此,只需通過 -target x86_64-windows-gnu 或其他目標三元組即可:
zig build-exe -lc -target x86_64-windows-gnu main.c buffer.c
你會發現,使用這條編譯命令,Zig 會自動在輸出文件中附加 .exe 擴展名,並生成 .pdb 調試數據庫。如果你在此處傳遞 --name example,輸出文件也會有正確的 .exe 擴展名,所以你不必考慮這個問題。
用 build.zig 創建 C 代碼
那麼,我們如何用 build.zig 來構建上面的兩個示例呢?
首先,我們需要創建一個新的編譯目標:
// 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 語言文件:
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 代碼中的未定義行爲,就必須在調用時添加選項:
exe.addCSourceFile(.{.file = std.build.LazyPath.relative("buffer.c"), .flags = &.{"-fno-sanitize=undefined"}});
使用外部庫
通常情況下,C 項目依賴於其他庫,這些庫通常預裝在 Unix 系統中,或通過軟件包管理器提供。
爲了演示這一點,我們創建一個小工具,通過 curl 庫下載文件,並將文件內容打印到標準輸出:
#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 集成:
// 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 調用它
zig build
./zig-out/bin/downloader https://mq32.de/public/ziggy.txt
配置路徑
由於我們不能在交叉編譯項目中使用 pkg-config,或者我們想使用預編譯的專用庫(如 BASS 音頻庫),因此我們需要配置包含路徑和庫路徑。
這可以通過函數 addIncludePath 和 addLibraryPath 來完成:
//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 編譯器標誌,因此我們也可以在這裏手動設置包含目錄。
//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:
//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:
//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 編譯系統可以輕鬆處理這兩種變體:
//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++ 標誌放入各自的變量中:
//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。它使用的不是文件名,而是可編譯的所有源文件的文件名片段。這樣,我們就可以收集某個文件夾中的所有文件:
//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 程序並添加框架了:
//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 中的第二個參數設置爲文件名,然後點擊編譯!
//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 中組合多個項目,以創建便捷的構建體驗。
敬請期待!
參考資料
[1]
第一篇文章: https://zigcc.github.io/post/2023/12/24/zig-build-explained-part1/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/7tCo_2bGFcifR_O8dwrJGg