diff --git a/learn/advanced/assembly.md b/learn/advanced/assembly.md index 7c02ce82..6aaf8e52 100644 --- a/learn/advanced/assembly.md +++ b/learn/advanced/assembly.md @@ -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 的特性实现中断处理的文章,在这其中我用了内联汇编,算是一个比较巧妙的例子。 \ No newline at end of file +了解更多?可以查看我的这篇文章 [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 的特性实现中断处理的文章,在这其中我用了内联汇编,算是一个比较巧妙的例子。 diff --git a/learn/advanced/interact-with-c.md b/learn/advanced/interact-with-c.md index c6499332..0039457e 100644 --- a/learn/advanced/interact-with-c.md +++ b/learn/advanced/interact-with-c.md @@ -31,7 +31,7 @@ zig 定义了几个对应 C ABI 的基本类型: 对应 C `void` 类型的时候,使用 `anyopaque` (大小为止的类型)。 -## 导入 C Header +## C Header 导入 C 语言共享类型通常是通过引入头文件实现,这点在 zig 中可以无缝做到,得益于 zig 的 **translate-c** 功能。 @@ -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` 是为了指定静态链接! diff --git a/learn/more/miscellaneous.md b/learn/more/miscellaneous.md index 33cc5f45..78f8a0be 100644 --- a/learn/more/miscellaneous.md +++ b/learn/more/miscellaneous.md @@ -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` 是为了指定静态链接!