Skip to content

Commit

Permalink
更新与 C 交互内容
Browse files Browse the repository at this point in the history
  • Loading branch information
jinzhongjia committed Dec 5, 2023
1 parent 7f2c9a1 commit 336b7d8
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 43 deletions.
2 changes: 1 addition & 1 deletion learn/advanced/assembly.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,4 @@ AssemblerTemplate

:::

了解更多?可以查看我的这篇文章 [Handle Interrupt on x86-64 kernel with zig](https://blog.nvimer.org/2023/11/12/handle-interrupt-on-x86-64-kernel-with-zig/),这是一篇关于如何在 x86-64 内核上利用 zig 的特性实现中断处理的文章,在这其中我用了内联汇编,算是一个比较巧妙的例子。
了解更多?可以查看我的这篇文章 [Handle Interrupt on x86-64 kernel with zig](https://blog.nvimer.org/2023/11/12/handle-interrupt-on-x86-64-kernel-with-zig/),这是一篇关于如何在 x86-64 内核上利用 zig 的特性实现中断处理的文章,在这其中我用了内联汇编,算是一个比较巧妙的例子。
196 changes: 194 additions & 2 deletions learn/advanced/interact-with-c.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ zig 定义了几个对应 C ABI 的基本类型:

对应 C `void` 类型的时候,使用 `anyopaque` (大小为止的类型)。

## 导入 C Header
## C Header 导入

C 语言共享类型通常是通过引入头文件实现,这点在 zig 中可以无缝做到,得益于 zig 的 **translate-c** 功能。

Expand Down Expand Up @@ -67,4 +67,196 @@ pub fn main() void {

## `C Translation CLI`

TODO
zig 提供了一个命令行工具 `zig translate-c` 供我们使用,它可以将 C 的代码翻译为 zig 的代码,并将其输出到标准输出。

::: warning

注意:当前 zig 的 `translate-c` 功能并不完善,可能存在某些 bug,使用时请注意查询 issue!

:::

### 命令行参数

- `-I`:指定 `include` 文件的搜索目录,可多次使用,相当于 clang 的 `-I` 标志,默认不包含当前目录(仅添加参数 `-I` 来添加当前目录)。
- `-D`:定义预处理器宏,相当于 clang 的 `-D` 标志。
- `-cflags [flags] --`:将任意附加命令行参数传递给 clang(注意:最后一定要加 `--`)。
- `-target`:zig 的构建目标三元组,缺省则使用本机作为构建目标。

::: info 🅿️ 提示

完整的构架目标三元组可以在这里 _[查看](https://ziglang.org/documentation/master/#Targets)_

在使用翻译功能时,需要保证 target 和传递的 cflags 是正确的,否则可能会出现解析失败或者与 C 代码链接时出现微妙的 ABI 不兼容问题。

:::

### `@cImport` vs `translate-c`

事实上,这两个东西的底层实现是一样的,`@cImport` 一般用于使用 C 库时引入头文件,而 `translate-c` 通常是为了修改翻译后的代码,例如:将 `anytype` 修改为更加精确的类型、将 `[*c]T` 指针修改为 `[*]T` 或者 `*T` 来提高类型安全性、启动或者禁用某些运行时的安全性功能。

## C 翻译缓存

C 翻译功能(无论是通过 `zig translate-c` 还是 `@cImport` 使用)与 Zig 缓存系统集成。使用相同源文件、目标和 `cflags` 的后续构建将使用缓存,而不是重复翻译相同的代码。

要在编译使用 `@cImport` 引入的代码时打印缓存文件的存储位置,请使用 `--verbose-cimport` 参数:

```zig
// 示例文件
const c = @cImport({
@cDefine("_NO_CRT_STDIO_INLINE", "1");
@cInclude("stdio.h");
});
pub fn main() void {
_ = c;
}
```

```sh
$ zig build-exe test.zig -lc --verbose-cimport
info(compilation): C import source: /home/username/.cache/zig/o/6f35761b17b87ee4c9f26e643a06e289/cimport.h
info(compilation): C import .d file: /home/username/.cache/zig/o/6f35761b17b87ee4c9f26e643a06e289/cimport.h.d
info(compilation): C import output: /home/username/.cache/zig/o/86899cd499e4c3f94aa141e400ac265f/cimport.zig
```

`cimport.h` 包含要翻译的文件(通过调用 `@cInclude``@cDefine``@cUndef` 构建),`cimport.h.d` 是文件依赖项列表,`cimport.zig` 包含翻译后的代码。

## C 翻译错误

针对某些 C 的结构,zig 会无法翻译,如:`goto`、使用位域(**bitfields**)的结构体、拼接(**token-pasting**)宏,zig 会暂时简单处理一下它们以继续翻译任务。

处理方式有三种:`opaque``extern``@compileError`

1. 无法被正确翻译的 C 结构体和联合类型会被翻译为 [`opaque{}`](../basic/advanced_type/opaque)
2. 包含 `opaque` 类型或者代码结构无法被翻译的函数会使用 `extern` 标记为外部连接函数,仅存在函数的声明,没有具体的定义。只要编译器知道去哪里找到函数的具体实现,那就可以正常使用。
3. 当顶层空间(全局变量、函数原型、宏)无法转换或处理时,zig 会使用 `@compileError` ,但得益于 zig 针对顶级声明使用惰性分析,故只有在使用它们时才会报告编译错误。

## C Macro

关于 C 中的宏,zig 会尽量将类似函数的宏定义转为对应的 zig 函数,但由于宏是在词法分析的级别上生效,并非所有宏均可以转为函数。无法翻译的宏会被转为 `@compileError` 错误。

::: info 🅿️ 提示

请注意,使用了宏的 C 代码转换并不会出问题,这是因为 zig 会在经过预处理器加工后的代码上进行翻译,只是翻译宏可能失败(但不排除当前因为 bug 导致翻译出错)。

```c
#define MAKELOCAL(NAME, INIT) int NAME = INIT
int foo(void) {
MAKELOCAL(a, 1);
MAKELOCAL(b, 2);
return a + b;
}
```
```zig
pub export fn foo() c_int {
var a: c_int = 1;
_ = &a;
var b: c_int = 2;
_ = &b;
return a + b;
}
pub const MAKELOCAL = @compileError("unable to translate C expr: unexpected token .Equal");
```

例如,以上代码翻译完成后,函数 `foo` 是正常可以工作的,仅仅是宏 `MAKELOCAL` 无法正常使用!

:::

## C 指针

应当避免使用此类型,通常它仅出现在翻译输出代码中。

导入 C 头文件后,zig 并不知道如何处理指针(因为 C 的指针可以同时作为单项指针和多项指针使用),这会导致歧义,故 zig 引入一种新类型 `[*]T`,作为一种折中方案,新类型 `[*]T` 具有以下特点:

1. 支持 zig 普通指针(`*T` 和 `[*]T`)的全部语法。
2. 可以强制转换为其他的任意指针类型,当然也包括可选指针类型(当被转换为非可选指针时,如果地址为 0,此时会触发安全检查的保护机制,报错并通知出现了未定义行为)。
3. 允许地址为 0,在非 `freestanding`(可以简单看作裸机器,通常编写内核会使用这个)目标上,不允许取消引用地址为 0 的指针(会触发未定义行为)。可选的 C 指针引入一个位来跟踪 `null`,但通常它没有这样做,可以直接使用普通的可选指针。
4. 支持与整数进行强制转换。
5. 支持和整数进行比较。
6. 不支持 zig 的指针特性,例如对齐(align)方式,如果要设置这些,请转换为普通指针后再进行操作!

当 C 指针指向一个结构体时,此时它是单项指针,则可以这样解引用:

```zig
ptr_to_struct.*.struct_member
```

当 C 指针指向一个数组时,此时它是一个多项指针,则可以这样解引用:

```zig
ptr_to_struct_array[index].struct_member
```

## C 可变参数函数

zig 支持外部(`extern`)可变参数函数:

```zig
// 这是对应 C printf 的声明
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;
```

可变参数的访问可以使用 [`@cVaStart`](https://ziglang.org/documentation/master/#cVaStart)[`@cVaEnd`](https://ziglang.org/documentation/master/#cVaEnd)[`@cVaArg`](https://ziglang.org/documentation/master/#cVaArg)[`@cVaCopy`](https://ziglang.org/documentation/master/#cVaCopy) 来实现:

```zig
const std = @import("std");
// 使用 callconv 声明函数调用约定为 C
fn add(count: c_int, ...) callconv(.C) c_int {
// 对应 C 的宏 va_start
var ap = @cVaStart();
// 对应 C 的宏 va_start
defer @cVaEnd(&ap);
var i: usize = 0;
var sum: c_int = 0;
while (i < count) : (i += 1) {
// 对应 C 的宏 va_arg
sum += @cVaArg(&ap, c_int);
}
return sum;
}
```

## 额外内容

以下是经过实践和总结出来的额外信息,官方的 ziglang 并未提供!

### 为什么 zig 可以做到比 c 更好的编译

实际上,zig 本身实现了一个 C 的编译器,当然不仅仅如此,zig 还提供了一个比较 **_magic_** 的东西—— [`glibc-abi-tool`](https://github.com/ziglang/glibc-abi-tool),这是一个收集每个版本的 glibc 的 `.abilist` 文件的存储库,还包含一个将它们组合成一个数据集的工具。

所以,zig 本身所谓的 “**_ships with libc_**” 并不准确,它的确分发 libc,但它只携带每个版本的符号库,仅依赖这个符号库,zig 就可以实现在没有 libc 的情况下仍然正确地进行动态链接!

::: info 🅿️ 提示

由于这种特性,这导致 zig 尽管携带了 40 个 libc,却仍然能保持 45MB(linux-x86-64)左右的大小,作为对比 llvm 分发的 clang 完整的工具链的大小多达好几百 M。

关于更多的细节,你可以参考以下链接:

- [process_headers tool](https://github.com/ziglang/zig/blob/0.4.0/libc/process_headers.zig)
- [Updating libc](https://github.com/ziglang/zig/wiki/Updating-libc)
- [hacker news](https://news.ycombinator.com/item?id=29538264)

:::

### zig 能静态链接 libc 吗?

能,又不能!

zig 支持静态链接 musl(针对linux的另一个 libc,目标为嵌入式系统与移动设备),其他仅支持动态链接。受益于这种特性,我们可以将它作为 C 编译器的替代品使用,它可以提供更加完善的工具链。

举个比较*剑走偏锋*的例子,go 的 cgo 特性一直为人们所吐槽,一旦使用了它,基本就要和 go 宣称的非常方便的交叉编译说拜拜了,但我们可以使用 zig 来帮助我们实现 cgo 的交叉编译:

```sh
CC='zig cc -target x86_64-linux-gnu' CXX='zig cc -target x86_64-linux-gnu' go build
```

设置 zig 作为 C 编译器来供 go 使用,只要对 zig 和 go 设置正确的target,就可以在本机实现完善的交叉编译。

再进一步,我们还可以构建出 linux 的使用 cgo 的静态链接的二进制可执行文件:

```sh
CC='zig cc -target x86_64-linux-musl' CXX='zig cc -target x86_64-linux-musl' CGO_CFLAGS='-D_LARGEFILE64_SOURCE' go build -ldflags='-linkmode=external -extldflags -static'
```

上方的 `CGO_CFLAGS` 是为了防止编译失败,`ldfalgs` 是为了指定静态链接!
40 changes: 0 additions & 40 deletions learn/more/miscellaneous.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,3 @@ outline: deep
# 杂项

本部分是关于 zig 一些额外知识的补充,暂时还没有决定好分类到何处!

## 为什么 zig 可以做到比 c 更好的编译

实际上,zig 本身实现了一个 C 的编译器,当然不仅仅如此,zig 还提供了一个比较 **_magic_** 的东西—— [`glibc-abi-tool`](https://github.com/ziglang/glibc-abi-tool),这是一个收集每个版本的 glibc 的 `.abilist` 文件的存储库,还包含一个将它们组合成一个数据集的工具。

所以,zig 本身所谓的 “**_ships with libc_**” 并不准确,它的确分发 libc,但它只携带每个版本的符号库,仅依赖这个符号库,zig 就可以实现在没有 libc 的情况下仍然正确地进行动态链接!

::: info 🅿️ 提示

由于这种特性,这导致 zig 尽管携带了 40 个 libc,却仍然能保持 45MB(linux-x86-64)左右的大小,作为对比 llvm 分发的 clang 完整的工具链的大小多达好几百 M。

关于更多的细节,你可以参考以下链接:

- [process_headers tool](https://github.com/ziglang/zig/blob/0.4.0/libc/process_headers.zig)
- [Updating libc](https://github.com/ziglang/zig/wiki/Updating-libc)
- [hacker news](https://news.ycombinator.com/item?id=29538264)

:::

## zig 能静态链接 libc 吗?

能,又不能!

zig 支持静态链接 musl(针对linux的另一个 libc,目标为嵌入式系统与移动设备),其他仅支持动态链接。受益于这种特性,我们可以将它作为 C 编译器的替代品使用,它可以提供更加完善的工具链。

举个比较*剑走偏锋*的例子,go 的 cgo 特性一直为人们所吐槽,一旦使用了它,基本就要和 go 宣称的非常方便的交叉编译说拜拜了,但我们可以使用 zig 来帮助我们实现 cgo 的交叉编译:

```sh
CC='zig cc -target x86_64-linux-gnu' CXX='zig cc -target x86_64-linux-gnu' go build
```

设置 zig 作为 C 编译器来供 go 使用,只要对 zig 和 go 设置正确的target,就可以在本机实现完善的交叉编译。

再进一步,我们还可以构建出 linux 的使用 cgo 的静态链接的二进制可执行文件:

```sh
CC='zig cc -target x86_64-linux-musl' CXX='zig cc -target x86_64-linux-musl' CGO_CFLAGS='-D_LARGEFILE64_SOURCE' go build -ldflags='-linkmode=external -extldflags -static'
```

上方的 `CGO_CFLAGS` 是为了防止编译失败,`ldfalgs` 是为了指定静态链接!

0 comments on commit 336b7d8

Please sign in to comment.