C的下一代替代语言:Ziglang 简明教程

文摘   科技   2024-08-03 21:37   江苏  
这份zig简明教程适合没有编程经验但是懂得善用搜索引擎的同学,在本文中,通过理论与示例相结合的方式来帮助初学者进行Zig语言的学习


译|zouyee


为了帮助读者深入了解Kubernetes在各种应用场景下所面临的挑战和解决方案,以及如何进行性能优化。我们推出了<<Kubernetes经典案例30篇>>



What‘s Ziglang


Zig是一款开源的规模最小、功能齐全的系统编程语言,其被视作较C更友好的替代品。它具有类似Rust的极简语法,同时保持了C的简单性。

Zig的目标是通过一种新的、受Rust语法影响的C语法的现代化方法来解决C开发人员面临的问题。它提供了一个高效的C互操作解决方案,让C开发人员可以将他们的C代码迁移到Zig。

Zig不仅仅是一种语言——其具备是一套完整的、功能齐全的工具链,这也意味着您可以使用Zig来创建、开发、测试和构建程序/库,而无需第三方自动化构建工具。Zig工具链还可以交叉编译C/C++以及Rust项目,因此您可以有效地使用Zig工具链来构建您现有的C/C++项目。Zig被设计为一种低级别、硬件友好的系统编程语言,但其高效、开发者友好的语法和功能使其更适合构建任何现代软件系统。

Zig项目最初由Andrew Kelley发起,现在由Zig软件基金会(ZSF)维护。




Zig的突出特点


Zig致力于成为一个更好的C语言替代品,其不仅适用于低级系统编程,还适用于开发通用软件系统,具有以下突出特点:

设计简单 

现代化语言的设计目标是提供一套设计良好的语法,而不像汇编语言那样原子化。如果语言的抽象过于接近汇编语言,开发人员可能需要编写冗长的代码。另一方面,当语言被抽象成接近人类可读时,它可能与硬件相距甚远,可能不适合系统编程的需求。

Zig提供了轻量级的、类Rust的语法,其大多数C提供的能力都已具备,但是它不提供Rust和C++那些复杂的功能集和语法,而是提供了一个像Go那样简单性为先的开发路径。

性能和安全性 

性能和安全性是选择的关键因素。语言的性能通常取决于其标准库、核心运行时功能的性能,以及编译器生成的二进制文件的质量。同时,安全设计实现边界检查、溢出处理和内存范围,并帮助开发人员减少关键安全漏洞。

Zig构建系统提供了四种构建模式,开发人员可以根据其性能和安全性要求使用。Zig还可以在编译时理解变量溢出。

此外,它可以生成带有运行时安全检查的优化二进制文件,就像Rust一样,也可以生成不带运行时安全检查的超轻量级二进制文件,就像C一样。Zig官方文档声称,由于其基于LLVM的优化和改进的未定义行为,Zig在理论上比C更快!

完整的系统编程解决方案

大多数编程语言都有一个或多个标准编译器和标准库实现。例如,您可以使用以下编译C:

- GNU C 

- Apple Clang 

- 带有libc、BSD-libc和Microsoft C运行时的MSVC编译器 

但是这两个组件对于现代系统编程需求来说还不够。程序员通常需要建立工具、包管理器和交叉编译工具等。

因此,在C生态系统中,像CMake、Ninja、Meson这样的构建工具以及类似Conan这样的包管理器逐渐流行,而像Go和Rust这样的现代语言官方内置了包管理器、构建工具及API、交叉编译支持和测试集成等。

与Go及Rust等现代语言一样,Zig内置了包管理器、构建系统及API、支持交叉编译和测试集成,这提高了Zig成为更好的C的机会,因为它解决了C(和C++)开发人员面临的关键系统编程问题。从语言设计的角度来看,Zig提供了C开发人员期望的现代语言的所有功能,因此C程序员可以逐步将他们的系统迁移到现代Zig,而无需重新编写他们遗留的代码库


为什么学习Zig

虽然编程语言千千万,但只有其中的一小部分能够在开发者社区中流行开来。未来还会有更多的编程语言出现,它们具有各种各样的特性,旨在取代现有的语言。

我们不需要学习所有开发语言,但是,如果有那么一种语言具有理想中未来语言的意味,并且提供了强有力、有效和经过验证的论据,说明它可以作为现有语言的替代品,那么学习它的内部设计理念无疑比完全忽视它要好。Go 1.0在2012年发布,作为一种新的极简语言,现在已经成为主要技术公司的依赖。在1990年代,Python是一种新的实验性脚本语言,但现在数字世界依赖于它。

类似地,Zig最初出现于2016年,2017年发布了首个预发布版本,展示了它作为C的现代替代品的能力。Zig甚至提供了一个完整的系统编程工具链,经过几年的积极开发,并确立了一个充满希望的未来。鉴于它与C的相似性和互操作性,它还可以为您打开更广泛的开发机会,包括人工智能开发、游戏开发等领域。

学习Zig不仅为您的技能组合增加了一种有前途的与C相关的语言,而且由于其聪明、性能安全平衡的设计,还提高了您对系统编程的了解。


谁在使用Zig?

以下流行的开源项目和技术公司使用Zig语言及其工具链:

主要项目

主要公司



学习Zig 

现在您已经了解了Zig及其引入原因,让我们通过学习语言语法、概念和特性的方式开始使用Zig进行开发。

设置开发环境

与任何其他开源项目一样,您可以从官方网站下载Zig,或者从源代码构建,但最简单、最现代的方法是通过系统软件包管理器安装它。在Ubuntu上使用以下Snap命令(以sudo运行)安装了Zig开发工具链:

snap install zig --beta --classic

有关Zig安装方法的更多信息,请参阅官方安装指南。

安装Zig后,请在终端中输入zig验证您的安装状态

Zig初体验

我们已经安装了Zig工具链,现在让我们开始在Zig中编写程序。我们将编写一个(经典)的Hello, World!类型的程序,以了解基本的源代码结构和工具链基础知识。

创建一个名为hello.zig的新文件,并添加以下源代码:


const std = @import("std");
pub fn main() void { std.debug.print("Hello Zig!\n", .{});}

在这里,我们在第一行导入了标准库,并将其引用加载到std常量中。然后,main函数(返回void)使用print函数在终端上打印调试消息。

使用以下命令运行上述代码:

zig run hello.zig

您将在终端中看到Hello Zig!。print函数提供了类似于C的printf函数的API,让我们通过第二个参数使用格式化字符串:

const std = @import("std");
pub fn main() void { std.debug.print("Hello {s}! {d}\n", .{"Zig", 2024}); // Hello Zig! 2024}

您还可以省略原子的格式类型,如下所示:

std.debug.print("Hello {}\n", .{2024}); // Hello 2024

编译Zig二进制文件

任何软件发布都需创建一个二进制文件。Zig提供四种模式构建配置基于性能和安全性需求来交叉编译二进制文件。

为您的Hello, World!程序创建一个二进制文件:

zig build-exe --name hello-bin hello.zig

上述命令将使用默认的Debug模式构建生成一个名为hello-bin的调试二进制文件,因此我们可以通过./hello-bin来执行。

就像 GNU C 一样,默认情况下,Zig 会为当前目标(CPU 和操作系统)生成二进制文件,因此上述命令在我的计算机上为我生成了 x86 Linux 二进制文件。此外,可以使用 -target 标志进行交叉编译二进制文件。


例如,以下命令会为 x64 Windows 系统交叉编译一个 .exe 文件:

zig build-exe -target x86_64-windows --name hello-bin.exe hello.zig


使用构建器 API 编译 Zig

Zig 的编译命令行界面与 GNU C 的命令行标志相同,因此我们可能需要经常重复使用编译命令。在 C 中,我们可以通过编写用于构建过程的 shell 脚本或 CMake来解决这个问题。

构建系统允许您将编译器标志存储在配置文件中,甚至添加自定义构建步骤。Zig 通过 std 包暴露了一个内置的构建器 API,作为独立的、第三方构建系统。它还提供了 zig build 命令来执行存储在 build.zig 文件中的构建步骤。

您可以根据自己的喜好使用以下命令搭建一个 Zig 构建项目:

  • zig init-exe:初始化一个基于 Zig 构建器的可执行应用程序

  • zig init-lib:初始化一个基于 Zig 构建器的库

之前的演示应用程序是一个可执行类型的应用程序,所以我们可以使用第一个命令来了解 Zig 构建系统:

mkdir zig-exe-democd zig-exe-demo
zig init-exe

上述命令创建了一个新的可执行程序,其中包含 src/main.zig 中的演示源代码。它向 build.zig 文件添加了几个构建步骤,我们可以使用 zig build 命令执行它们,而不是使用 zig run 或 zig build-exe。

例如,您可以通过执行以下命令来运行程序:

zig build run

您可以使用 install 步骤创建二进制文件:

zig build install
./zig-out/bin/zig-exe

就像在其他构建系统(如 CMake)中所做的那样,您可以通过修改 build.zig 文件来添加更多的构建步骤或中间处理。通过zig build 运行所有的构建自动化步骤,这无疑简化了 Zig 开发过程。

现在,您知道了如何编写一个基本的 Zig 程序,使用 Zig 编译器进行编译,并使用 Zig 的内置构建系统简化开发过程。让我们开始学习语法和特性吧!

Zig 基本类型

与 C 类似,Zig 支持各种形式的整数、浮点数和指针。让我们学习一些您应该了解的基本类型。

以下代码片段定义了一些整数和浮点数:

const std = @import("std");
pub fn main() void { var x: i8 = -100; // 有符号 8 位整数 var y: u8 = 120; // 无符号 8 位整数 var z: f32 = 100.324; // 32 位浮点数
std.debug.print("x={}\n", .{x}); // x=-100 std.debug.print("y={}\n", .{y}); // y=120 std.debug.print("z={d:3.2}\n", .{z}); // z=100.32}

由于上述标识符值永远不会更改,我们可以使用 const 关键字而不是 var:

const x: i8 = -100;// ...// ...

作为现代语言,布尔类型也得到了支持:

var initialized: bool = true;

您可以将字符存储在无符号字节(8 位整数)中,如下所示:

const std = @import("std");
pub fn main() void { const l1: u8 = 'Z'; const l2: u8 = 'i'; const l3: u8 = 'g';
std.debug.print("{c}-{c}-{c}\n", .{l1, l2, l3}); // Z-i-g}

您还可以定义变量而不写明其数据类型,如下所示。然后,Zig 将使用 comptime 类型将它们存储起来,保证编译时评估:

Zig 还支持原生 C 类型(即 c_char、c_int 等)。可以在官方文档的表格中查看所有支持的类型。Zig 中没有内置的字符串类型,因此我们必须使用字节数组。我们将在本教程的另一部分讨论数组。

枚举

Zig 提供了一个简单的语法来定义和访问枚举。看看以下示例源代码:

const std = @import("std");
pub fn main() void { const LogType = enum { info, err, warn };
const ltInfo = LogType.info; const ltErr = LogType.err;
std.debug.print("{}\n", .{ltInfo}); // main.main.LogType.info std.debug.print("{}\n", .{ltErr}); // main.main.LogType.err}

Zig 允许覆盖枚举的序数值,如下所示:

const LogType = enum(u32) {    info = 200,    err = 500,    warn = 600};

数组和切片

Zig 建议将数组用于编译时已知值(compile-time-known),切片用于运行时已知值(runtime-known)。例如,我们可以将英语元音存储在常量字符数组中,如下所示:

const std = @import("std");
pub fn main() void { const vowels = [5]u8{'a', 'e', 'i', 'o', 'u'};
std.debug.print("{s}\n", .{vowels}); // aeiou std.debug.print("{d}\n", .{vowels.len}); // 5}

在这里,我们可以省略大小,因为它在编译时已知:

const vowels = [_]u8{'a', 'e', 'i', 'o', 'u'}; // 注意 "_"

您不需要使用这种方法来定义字符串,因为 Zig 允许您以 C 风格定义字符串,如下所示:

const std = @import("std");
pub fn main() void { const msg = "Ziglang";
std.debug.print("{s}\n", .{msg}); // Zig std.debug.print("{}\n", .{@TypeOf(msg)}); // *const [7:0]u8}

一旦将硬编码字符串存储在标识符中,Zig 将自动使用空终止数组引用(数组的指针)*const [7:0]u8 来存储元素。在这里,我们使用了 @TypeOf() 内置函数来获取变量的类型。您可以在官方文档中浏览所有支持的内置函数。

数组可以使用 ** 和 ++ 运算符进行重复或连接:

const std = @import("std");
pub fn main() void { const msg1 = "Zig"; const msg2 = "lang";
std.debug.print("{s}\n", .{msg1 ** 2}); // ZigZig std.debug.print("{s}\n", .{msg1 ++ msg2}); // Ziglang}

Zig 切片几乎与数组一样,但用于存储在运行时已知而不在编译时已知的值。看看下面的例子,它从数组中创建一个切片:

const std = @import("std");
pub fn main() void { const nums = [_]u8{2, 5, 6, 4}; var x: usize = 3; const slice = nums[1..x];
std.debug.print("{any}\n", .{slice}); // { 5, 6 } std.debug.print("{}\n", .{@TypeOf(slice)}); // []const u8}

在这里,如果 x 是一个运行时已知的变量,slice 标识符就会变成一个切片。如果对 x 使用 const,则 slice 将变成一个数组指针(*const [2]u8),因为 x 在编译时已知。我们将在后面的章节中讨论指针。

结构体和联合体

结构体是用于存储多个值的有用数据结构,甚至可以用来实现面向对象编程(OOP)概念。

您可以创建结构体并使用以下语法访问其内部字段:

const std = @import("std");
pub fn main() void { const PrintConfig = struct { id: *const [4:0] u8, width: u8, height: u8, zoom: f32 };
const pc = PrintConfig { .id = "BAX1", .width = 200, .height = 100, .zoom = 0.234 };
std.debug.print("ID: {s}\n", .{pc.id}); // ID: BAX1 std.debug.print("Size: {d}x{d} (zoom: {d:.2})\n", .{pc.width, pc.height, pc.zoom}); // Size: 200x100 (zoom: 0.23)}

在 Zig 中,结构体也可以具有方法,所以当我们讨论函数时,下面将展示一个示例。

Zig 联合体类似于结构体,但一次只能有一个活动字段。看下面的示例:

const std = @import("std");
pub fn main() void { const ActionResult = union { code_int: u8, code_float: f32 };
const ar1 = ActionResult { .code_int = 200 }; const ar2 = ActionResult { .code_float = 200.13 };
std.debug.print("code1 = {d}\n", .{ar1.code_int}); // code1 = 200 std.debug.print("code2 = {d:.2}\n", .{ar2.code_float}); // code2 = 200.13 // std.debug.print("code2 = {d:.2}\n", .{ar2.code_int}); // 错误!}

使用控制结构

每种编程语言通常都提供控制结构来处理程序的逻辑流程。Zig 支持所有常见的控制结构,如 if、switch、for 等。

看看以下 if…else 语句的示例代码片段:

const std = @import("std");
pub fn main() void { var score: u8 = 100;
if(score >= 90) { std.debug.print("Congrats!\n", .{}); std.debug.print("{s}\n", .{"*" ** 10}); } else if(score >= 50) { std.debug.print("Congrats!\n", .{}); } else { std.debug.print("Try again...\n", .{}); }}

下面是 switch 语句的一个示例:

const std = @import("std");
pub fn main() void { var score: u8 = 88;
switch(score) { 90...100 => { std.debug.print("Congrats!\n", .{}); std.debug.print("{s}\n", .{"*" ** 10}); }, 50...89 => { std.debug.print("Congrats!\n", .{}); }, else => { std.debug.print("Try again...\n", .{}); } }}

下面是while 语句的一个示例:

const std = @import("std");
pub fn main() void { var x: u8 = 0; while(x < 11) { std.debug.print("{}\n", .{x}); x += 1; }}

下面是for 语句的一个示例:

const std = @import("std");
pub fn main() void { const A = [_]u8 {2, 4, 6, 8};
for (A) |n| { std.debug.print("{d}\n", .{n}); }}


函数

函数通过允许我们使用可调用标识符来命名每个代码段来帮助我们创建可重用的代码段。我们已经使用了main 来启动我们的应用程序,让我们创建更多函数并进一步学习函数。

下面是一个简单的函数,它返回两个整数的总和:

const std = @import("std");
fn add(a: i8, b: i8) i8 { return a + b;}
pub fn main() void { const a: i8 = 10; const b: i8 = -2; const c = add(a, b);
std.debug.print("{d} + {d} = {d}\n", .{a, b, c}); // 10 + -2 = 8}

递归也是 Zig 提供的一种编程功能,与许多其他通用语言一样:

const std = @import("std");
fn fibonacci(n: u32) u32 { if(n == 0 or n == 1) return n; return fibonacci(n - 1) + fibonacci(n - 2);}
pub fn main() void { std.debug.print("{d}\n", .{fibonacci(2)}); // 1 std.debug.print("{d}\n", .{fibonacci(12)}); // 144}

与 Go 一样,Zig 允许您在结构体中创建方法,并将它们用作 OOP 方法,如下例所示:

const std = @import("std");
const Rectangle = struct { width: u32, height: u32, fn calcArea(self: *Rectangle) u32 { return self.width * self.height; }};
pub fn main() void { var rect = Rectangle { .width = 200, .height = 25 }; var area = rect.calcArea(); std.debug.print("{d}\n", .{area}); // 5000}

指针

Zig 作为硬件友好的语言,其支持类似 C 的指针。看看下面的基本整数指针:

const std = @import("std");
pub fn main() void { var x: u8 = 10; var ptr_x = &x; ptr_x.* = 12;
std.debug.print("{d}\n", .{x}); // 12 std.debug.print("{d}\n", .{ptr_x}); // u8@...mem_address std.debug.print("{}\n", .{@TypeOf(ptr_x)}); // *u8}

在这里,C/C++ 开发人员需要注意,我们使用 ptr.* 语法对指针进行解引用,而不是像在 C/C++ 中那样使用 *ptr。指向数组元素和指向整个数组的指针也能按预期工作,如下所示的代码片段:

const std = @import("std");
pub fn main() void { var A = [_]u8 {2, 5, 6, 1, 1}; var ptr_x = &A[1]; ptr_x.* = 12;
std.debug.print("{d}\n", .{A[1]}); // 12 std.debug.print("{d}\n", .{ptr_x}); // u8@...mem_address std.debug.print("{}\n", .{@TypeOf(ptr_x)}); // *u8
var ptr_y = &A; ptr_y[2] = 11; std.debug.print("{any}\n", .{A}); // { 2, 12, 11, 1, 1 } std.debug.print("{}\n", .{@TypeOf(ptr_y)}); // *[5]u8}


高级语言特性

以下是您应该了解的一些 Zig 高级语言特性的摘要:

  1. 通过分配器和 defer 关键字

  2. 支持手动内存管理

  3. 使用简单的语法支持泛型

  4. 提供高效的关键字(async、suspend 和 resume)进行现代异步编程(后续版本已经撤回该特性)

  5. 提供自动类型转换和手动类型转换,使用内置的 @as Zig 的 C-interop 允许您调用 C API。使用 -lc 标志进行以下运行以链接到 libc:

const std = @import("std");const c = @cImport({    @cInclude("stdio.h");});
pub fn main() void { const char_count = c.printf("Hello %s\n", "C..."); // Hello C...
std.debug.print("{}\n", .{@TypeOf(char_count)}); // c_int std.debug.print("{}\n", .{char_count}); // 11}

可以通过关注官方 Zig 新闻页面,及时了解最新功能和高级概念。

Zig 中的标准库 API

我们已经讨论了前面示例中的 Zig 语言语法和特性,但这些概念不足以开发通用程序 —— 我们经常需要使用复杂的数据结构、数学公式和操作系统级别的 API。Zig 通过 std 命名空间提供了一个功能齐全但又精简的标准库。

我们将编写一个简单的 CLI 程序来学习几个 Zig 标准库的特性。将以下代码添加到一个新的 Zig 文件中:

const std = @import("std");const stdout = std.io.getStdOut().writer();
fn print_help() !void { try stdout.print("{s}\n" , .{"-" ** 25}); try stdout.print("0: 退出\n", .{}); try stdout.print("1: 显示帮助\n", .{}); try stdout.print("2: 打印 Node.js 版本\n", .{}); try stdout.print("{s}\n" , .{"-" ** 25});}
fn print_node_version() !void { const cmd_res = try std.ChildProcess.exec(.{ .allocator = std.heap.page_allocator, .argv = &[_][]const u8{ "node", "--version", }, }); try stdout.print("{s}\n", .{cmd_res.stdout});}
fn ask_action() !i64 { const stdin = std.io.getStdIn().reader(); var buf: [10]u8 = undefined;
try stdout.print("输入操作:", .{});
if (try stdin.readUntilDelimiterOrEof(buf[0..], '\n')) |user_input| { return std.fmt.parseInt(i64, user_input, 10); } else { return @as(i64, 0); }}
pub fn main() !void { try print_help(); while(true) { const action = ask_action() catch -1; switch(action) { 0 => { std.debug.print("再见!\n", .{}); break; }, 1 => { try print_help(); }, 2 => { try print_node_version(); }, else => { std.debug.print("无效的操作:{d}\n", .{action}); } } }}

这个演示的 CLI 支持三种整数操作:

  • 0:退出程序

  • 1:显示程序帮助

  • 2:通过 Zig 子进程 API 打印当前 Node.js 版本

在这里,我们使用了一些错误处理基础知识以及标准库的 io 命名空间和 ChildProcess 结构。您可以在官方标准库参考文档中查看所有可用的命名空间和结构。



Zig 生态系统


Zig 是一种新的语言,因此开源软件包的可用性和开发者资源仍在不断增长。请查看以下流行的开源 Zig 库:

  • zigzap/zap:用于构建 Web 后端的微型框架

  • JakubSzark/zig-string:用于 Zig 的字符串库

  • kooparse/zalgebra:游戏和实时图形的线性代数库

  • zigimg/zigimg:用于读写不同图像格式的 Zig 库

  • ziglibs/ini:用于 Zig 的简单 INI 解析器 您还可以从 awesome-zig 存储库中了解更多与 Zig 有关的开发内容。



Zig vs. C vs. Rust




总结


现在我们已经掌握了相当大的Zig基础知识。没有覆盖的一些(非常重要的)内容包括:

  • 测试(Zig使得编写测试非常容易)

  • 标准库

  • 内存模型(Zig在分配器方面没有倾向性)

在本教程中,我们学习了Zig编程语言开发背后的概念、目标和设计技术。通过测试通用的、通用的编程知识来学习Zig语言,这些知识可以用来构建现代计算机程序。

Zig仍然是一种新语言,ZSF仍在定期实现和测试更多功能。学习Zig是一个很好的决定,因为它作为一种更好的C语言,有着光明的前景。




由于笔者时间、视野、认知有限,本文难免出现错误、疏漏等问题,期待各位读者朋友、业界专家指正交流。

参考文献
1.https://ziglang.org/documentation/master/.
2.https://gist.github.com/ityonemo/769532c2017ed9143f3571e5ac104e50




真诚推荐你关注



Kubernetes经典案例30篇

ziglang30分钟速成

Rust vs. Zig:究竟谁更胜一筹?性能、安全性等全面对决!




来个“分享、点赞、在看”👇

DCOS
CNCF 云原生基金会大使,CoreDNS 开源项目维护者。主要分享云原生技术、云原生架构、容器、函数计算等方面的内容,包括但不限于 Kubernetes,Containerd、CoreDNS、Service Mesh,Istio等等