Skip to main content

Chapter 4 - 使用C工作

Zig的设计从一开始就把C语言的互操作作为一个首要功能。在这一节中,我们将介绍如何工作.

ABI

ABI (application binary interface)是一种标准,与以下方面有关:

  • 类型的内存布局(即一个类型的大小、对齐方式、偏移量和它的字段的布局)
  • 符号的内存命名(例如,名称混用)
  • 函数的调用约定(即函数调用在二进制水平上如何工作)

通过定义这些规则并且不破坏它们,ABI被认为是稳定的,这可以被用来,例如,可靠地连接多个库、可执行文件或单独编译的对象(可能在不同的机器上,或使用不同的编译器)。这允许FFI (外部函数接口)的发生,我们可以在编程语言之间共享代码。

Zig原生支持用于 "外部 "事物的C ABI;使用哪种C ABI取决于你所编译的目标(如CPU架构、操作系统)。这允许与不是用Zig编写的代码进行近乎无缝的互操作;使用C ABI是编程语言中的标准。

Zig内部不使用ABI,这意味着在需要可复制和定义的二进制级别行为时,代码应明确地符合C ABI。

C原始类型

Zig提供了特殊的c_前缀类型,以符合C ABI的要求。这些类型没有固定的大小,而是根据所使用的ABI来改变大小.

TypeC EquivalentMinimum Size (bits)
c_shortshort16
c_ushortunsigned short16
c_intint16
c_uintunsigned int16
c_longlong32
c_ulongunsigned long32
c_longlonglong long64
c_ulonglongunsigned longlong64
c_longdoublelong doubleN/A
c_voidvoidN/A

注意:C的void(和Zig的c_void)有一个未知的非零大小。Zig的void是一个真正的零尺寸类型.

调用约定

调用约定描述了函数的调用方式。这包括如何向函数提供参数(即它们的位置--在寄存器中还是在堆栈中,以及如何),以及如何接收返回值。

在Zig中,"callconv "属性可以被赋予给一个函数。可用的调用约定可以在std.biltin.CallingConvention中找到。这里我们使用了cdecl的调用约定 .

fn add(a: u32, b: u32) callconv(.C) u32 {
return a + b;
}

当你从C语言中调用Zig时,用C语言的调用惯例来标记你的函数是至关重要的.

外部结构

Zig中的普通结构没有定义布局;当你希望结构的布局与C ABI的布局相匹配时,就需要extern结构。

我们来创建一个extern结构。这个测试应该在x86_64gnuABI下运行,可以用-target x86_64-native-gnu完成.

const expect = @import("std").testing.expect;

const Data = extern struct { a: i32, b: u8, c: f32, d: bool, e: bool };

test "hmm" {
const x = Data{
.a = 10005,
.b = 42,
.c = -10.5,
.d = false,
.e = true,
};
const z = @ptrCast([*]const u8, &x);

try expect(@ptrCast(*const i32, z).* == 10005);
try expect(@ptrCast(*const u8, z + 4).* == 42);
try expect(@ptrCast(*const f32, z + 8).* == -10.5);
try expect(@ptrCast(*const bool, z + 12).* == false);
try expect(@ptrCast(*const bool, z + 13).* == true);
}

这就是我们的x值里面的内存的样子.

Fieldaaaabccccde
Bytes152700002A000000000028C100010000

请注意中间和末尾的空隙--这被称为 "填充"。这个填充物中的数据是未定义的内存,不会一直为零.

由于我们的 "x "值是一个外部结构,我们可以安全地将它传递给一个期望有 "数据 "的C函数,前提是该C函数也是用相同的 "gnu "ABI和CPU架构编译的.

对齐

由于电路的原因,CPU在内存中以一定的倍数访问原始值。例如,这可能意味着一个f32'值的地址必须是4的倍数,这意味着f32'的对齐方式是4。这种所谓的原始数据类型的 "自然对齐 "取决于CPU架构。所有的对齐方式都是2的幂。

一个较大的对齐方式的数据也有每个较小的对齐方式;例如,一个对齐方式为16的值也有8、4、2和1的对齐方式。

我们可以通过使用align(x)属性来制作特殊对齐的数据。这里我们制作的是具有更大对齐度的数据 .

const a1: u8 align(8) = 100;
const a2 align(8) = @as(u8, 100);

而制作对齐度较小的数据。注意:创建较小对齐方式的数据并不是特别有用.

const b1: u64 align(1) = 100;
const b2 align(1) = @as(u64, 100);

const一样,align也是指针的一个属性.

test "aligned pointers" {
const a: u32 align(8) = 5;
try expect(@TypeOf(&a) == *align(8) const u32);
}

让我们利用一个期望对齐指针的函数.

fn total(a: *align(64) const [64]u8) u32 {
var sum: u32 = 0;
for (a) |elem| sum += elem;
return sum;
}

test "passing aligned data" {
const x align(64) = [_]u8{10} ** 64;
try expect(total(&x) == 640);
}

打包的结构

默认情况下,Zig中的所有结构字段都是自然对齐的,即@alignOf(FieldType)(ABI大小),但没有定义布局。有时你可能希望结构字段的布局与你的C ABI不一致。packed结构允许你对结构字段进行极其精确的控制,允许你逐位放置字段。

在打包的结构中,Zig的整数在空间中占用其位宽(即u12@bitSizeOf为12,意味着它在打包的结构中会占用12位)。傻瓜也占用1位,这意味着你可以很容易地实现位标志.

const MovementState = packed struct {
running: bool,
crouching: bool,
jumping: bool,
in_air: bool,
};

test "packed struct size" {
try expect(@sizeOf(MovementState) == 1);
try expect(@bitSizeOf(MovementState) == 4);
const state = MovementState{
.running = true,
.crouching = true,
.jumping = true,
.in_air = true,
};
_ = state;
}

目前Zig的打包结构有一些长期存在的编译器错误,目前在很多使用情况下都不能使用.

位对齐的指针

与对齐的指针类似,位对齐的指针在其类型中具有额外的信息,告知如何访问数据。当数据不是字节对齐的时候,这些信息是必要的。位对齐信息通常需要用于寻址打包结构中的字段.

test "bit aligned pointers" {
var x = MovementState{
.running = false,
.crouching = false,
.jumping = false,
.in_air = false,
};

const running = &x.running;
running.* = true;

const crouching = &x.crouching;
crouching.* = true;

try expect(@TypeOf(running) == *align(1:0:1) bool);
try expect(@TypeOf(crouching) == *align(1:1:1) bool);

try expect(@import("std").meta.eql(x, .{
.running = true,
.crouching = true,
.jumping = false,
.in_air = false,
}));
}

C 指针

到现在为止,我们已经使用了以下几种指针:

  • 单项指针 - *T
  • 多项指针 - [*]T
  • 切片 - []T

与上述指针不同,C语言指针不能处理特别对齐的数据,可以指向地址0。C指针在整数之间来回递增,也可以递增到单项和多项指针。当一个值为0的C指针被胁迫到一个非选择的指针,这是可以检测到的非法行为。

在自动翻译的C代码之外,使用[*c]几乎总是一个坏主意,而且几乎不应该被使用 .

Translate-C

Zig提供了zig translate-c命令,用于自动翻译C源代码。

创建main.c文件,内容如下.

#include <stddef.h>

void int_sort(int* array, size_t count) {
for (int i = 0; i < count - 1; i++) {
for (int j = 0; j < count - i - 1; j++) {
if (array[j] > array[j+1]) {
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}

运行命令zig translate-c main.c以获得相当于Zig代码的输出到你的控制台(stdout)。你可以用zig translate-c main.c > int_sort.zig将其导入一个文件中(对windows用户的警告:在powershell中的管道将产生一个编码不正确的文件--用你的编辑器来纠正)。

在另一个文件中,你可以使用@import("int_sort.zig")来使用这个函数。

当前产生的代码可能是不必要的冗长,尽管translate-c成功地将大多数C代码翻译成Zig。你可能希望在将其编辑成更习惯的代码之前使用 translate-c 来生成 Zig 代码;在一个代码库中从 C 逐步转移到 Zig 是一个支持的使用案例 .

cImport

Zig@cImport内建程序很特别,因为它接收一个表达式,这个表达式只能接收@cInclude@cDefine@cUndef。它的工作原理与translate-c类似,将C代码翻译成Zig。

@cInclude接收一个路径字符串,可以将该路径添加到包含列表中。

@cDefine@cUndef为导入的东西进行定义和取消定义。

这三个函数的工作方式与你期望它们在C代码中的工作方式完全一致。

@import 类似,它返回一个带有声明的结构类型。通常建议在一个应用程序中只使用一个@cImport的实例,以避免符号冲突;在一个cImport中生成的类型将不等同于在另一个中生成的类型。

cImport 仅在链接 libc 时可用。

链接 libc

链接 libc 可以通过命令行中的 -lc 来完成,或者通过 build.zig 使用 exe.linkLibC(); 来完成。使用的libc是编译目标的libc;Zig为许多目标提供libc .

Zig cc, Zig c++

Zig的可执行文件中嵌入了Clang,以及为其他操作系统和架构进行交叉编译所需的库和头文件。

这意味着,zig cczig c++不仅可以编译C和C++代码(与Clang兼容的参数),而且还可以在尊重Zig的目标三参数的情况下进行编译;你所安装的单个Zig二进制文件有能力为多个不同的目标进行编译,而无需安装多个版本的编译器或任何附加组件。使用zig cczig c++还可以利用Zig的缓存系统来加速你的工作流程.

使用Zig,人们可以很容易地为使用C和/或C++编译器的语言构建一个交叉编译工具链。

一些野外的例子:

第四章结束

这一章是不完整的。在未来,它将包含诸如以下内容:

  • 从Zig调用C代码,反之亦然
  • 使用zig build混合使用C和Zig代码

欢迎大家提供反馈意见和PR。