Rust FFI 基础知识

  • Rust FFI 总结, 转载汇总自 rust 中文语言社区

  • 资料来源:

    <>

  • 更新

    1
    2023.11.27 初始

导语

blog 断更了好久, 该总结的有点多… 先从 rust 开始,补上..

接下来会是 rust ffi 相关内容的总结, 汇总自 rust 中文语言社区 ,原来的教程比较零散.

FFI 概述

FFI(Foreign Function Interface)是这样一种机制:用一种编程语言写的程序能调用另一种编程语言写的函数(routines)。

FFI 有两种内涵。一种是是在当前正在使用的语言(host)中,调用由其它语言(guest)提供的库。第二种内涵与第一种方向相反,即,使用当前语言(host)写库,供其它语言(guest)调用。不过,后者不是任何语言都能做到的,有些语言即使能做,也会非常吃力。

FFI 的历史和现状

FFI 这个术语最早来自 Common Lisp 的 规范。目前几乎所有严肃编程的语言都有提供 FFI 的支持,但大多数是单向功能。

不同语言称呼这种语言间调用的功能名字可能不同。Common Lisp、Haskell、Python、Rust 这些叫 FFI,Java 叫 JNI 或 JNA,还有一些其它语言叫 “ 绑定 “。严格来说,FFI 与 绑定,意义并不相同,绑定可以理解为 FFI 中的一种实现。

不同语言实现 FFI 的方式不尽相同。有的语言,比如,要调用 C 库,必须用 C 语言,按那种语言的绑定规范,实现一个 C 项目,用 C 编译器编译并链接,生成库文件,再由这种语言调用(这种语言本身已经实现了加载其定义的规范 C 库的能力)。

有的语言,比如,Rust,要调用 C 库,不再需要使用 C 语言写绑定工程,而是直接使用 Rust 语言写。这样,就有个好处是,你不再需要掌握 C 语言的那么多的繁文缛节和工具链(但是还是必须懂 C 语言)。

FFI 调用原理

为什么不同的语言之间能互相调用呢?

我们知道,计算机的运算,最底层的数据/代码都是以二进制的形式存在。所有的语言在编译后,都会以二进制的形式去执行(即使编译后的代码为字节码,虚拟机在运行的时候,也会继续翻译成 CPU 认识的二进制指令)。这就为不同语言间的调用提供了可能性。

但是,可能归可能。二进制毕竟太底层了。没有大家一致认可的 调用约定,那也是不可能互通的。于是,ABI(应用程序二进制接口) 就出现了。调用约定,类型表示和名称修饰这三者的统称,即是众所周知的应用二进制接口(ABI)。

试想,如果所有的语言在调用时都能认识同样一套 ABI 规范,那么就能完全畅通的调用了。可惜,世界不会像我们人为想象的那样干净。

在计算机技术发展的过程中,出现了各种 ABI 规范,它们有的看起来相似,但在具体编译器的实现上,又有细微不同。所以,这是一件很麻烦的事情。大体来说,有如下规范:

  • cdecl
  • syscall
  • optlink
  • pascal
  • register
  • stdcall
  • fastcall
  • thiscall
  • winapi
  • Intel ABI
  • System V

等。详情可参考:X86调用约定

而 Rust 目前支持如下 ABI 约定

  • stdcall
  • aapcs
  • cdecl
  • fastcall
  • vectorcall
  • Rust
  • rust-intrinsic
  • system
  • C
  • win64
  • sysv64

不过,值得庆幸的是,目前我们 IT 工业的基石,绝大部分是由 C 语言写成。于是自然而然,绝大多数库都遵循 cdecl(或 C)规范。所以我们可以专注于 C 规范来讨论问题。


注: c++ 的函数名默认会有 “ 名称编程 “, 需要在函数加上 extend “C” 才能直接使用原名称查找到函数;


FFI 的困难之处

FFI 实现起来,比想像的要复杂许多,困难体现在:

  • 如果 host 语言(调用主动方)带 GC(垃圾收集器),而 guest 语言(调用被动方)不带,那么可能会在资源管理(创建,释放)上面造成一些问题,需要特别细致地处理;
  • 复杂对象或类型,在映射到两边的时候,可能会有一些不协调甚至失真的现象;
  • 两边要同时引用一个可变对象的时候,可能会遇到问题;
  • 如果两边的语言都是运行在 VM 之上的语言,那么这两个语言之间的直接 FFI 非常困难甚至不可能;
  • 类型系统/对象组合模型/继承机制等其它细节,可能在跨语言的时候,成为障碍;
  • 其它。

所以,虽然都能做 FFI,但是不同语言实现 FFI 的困难程度是不同的。

哪些语言可以方便地对外提供 FFI 库支持

可惜,大部分语言只能单向地 “ 索取 “。目前所知,能(较方便地)对其它语言提供 FFI 库支持的语言有:

  • C
  • C++(通过定义 C 接口)
  • Rust(通过使用 C 约定)
  • Ada
  • Fortran

小编能力所限,如有未列举完整之处,欢迎补充。

偷懒的程序员

在开发的过程中,要一个一个对大量的 C/C++ 库写绑定来进行 FFI,毕竟是一项费时费力的活儿。聪明的程序员们就开始构想一些 “ 通用 “ 的方案,实现批量快速绑定。

SWIG

以下定义来自 https://zh.wikipedia.org/wiki/SWIG :

简单包装界面产生器 (SWIG) 是一个开源软件工具,用来将 C 语言或 C++ 写的计算机程序或函式库,连接脚本语言,例如 Lua, Perl, PHP, Python, R, Ruby, Tcl, 和其它语言,例如 C#, Java, JavaScript, Go, D, OCaml, Octave, Scilab 以及 Scheme. 也可以输出成 XML 格式。

也就是说,使用了 SWIG 这套工具和规范,就可以直接在上层语言(动态语言居多)中调用 C/C++ 库了,省却大量烦恼。但在实际使用中,还会有一些细节问题,往往需要人工调整。所以也不是那么完美。

SWIG 官网:http://swig.org/

Gnome 社区关于构建通用 GI 规范的理想和实践

Gnome/Gtk 那一帮理想主义青年,发明了 GI(GObject Introspection)。用于对基于 glib/gobject 生态的众多软件(C 代码库)自动生成完整的接口描述文件(及 typelib),然后其它语言只要实现了对 Gir 这一个标准的支持,那么就可以无缝调用所有经过 Gir 化处理的 C 库。而不再需要单独为每一个 C 库做绑定了。这样就大大简化了 FFI 接口项目的编写工作。

目前这一杰出创意的重量级工作成果有 cairo, pango, gtk 等库。

更多信息请参考:https://gi.readthedocs.io/en/latest/。

另一种思路——基于字节码的平台级路线

语言间的相互调用,历史的发展提供了另一条路线:建立一个共同的字节码平台,这个平台之上的所有语言,皆可便捷地相互调用。

JVM 平台语言之间的 FFI

Java 发展到现在,已经形成了一个强大的 JVM 生态。JVM 平台上有大量的新语言产生,比如 Scala, Clojure, JRuby, Jython 等。这些语言前端不同,但是共享同一套 JVM 字节码和调用规范。因此,这些语言和 Java 之间,以及这些衍生语言之间,能比较容易地实现相互调用。

JVM 平台的缺点在于,其生态中的成果,被局限在了 JVM 平台内,无法(或很难)被其它语言平台所享用。

WASM 平台的 FFI

Web Assembly(WASM)是一个新的字节码平台,其势头发展很猛。其有着比 JVM 平台更大的野心和联盟。因为是新设计的字节码,故其在设计的时候,就对 JVM 平台的一些问题做了规避(这方面可 Google 查阅相关资料)。

目前几乎所有主流语言都已实现将 WASM 作为编译目标,并且有相当一部分语言能够加载 WASM 库文件,调用其中的函数。不同的语言编译出的 WASM 效能和体积大小也是不同的。目前来看,C、C++、Rust 这些非 GC 语言能够编译出最精简,执行效率最高的 WASM 字节码。

WASM 的规范还在快速完善中。

结语

本篇描述了 FFI (外部程序接口)的概念和基本原理,并对其历史、内在的困难,以及程序员在 FFI 发展上的各种尝试,都做了简单介绍。

本篇大量内容参考 wikipedia 的 Foreign function interface 页面

恕小编能力所限,如有描述不当或不完整之处,欢迎同行指正或补充,感谢!

Rust 语言层面对 FFI 的支持

Rust 语言对 FFI 有比较完善的支持。本节主要讲在基础设施层面,Rust 语言对 FFI 的支持。

Rust 语言主要在关键字和标准库两个方面对 FFI 提供了支持,具体如下:

  • 关键字 extern
    • 属性 #[no_mangle]
    • 外部块 ExternBlock 及其属性 linklink_name
  • 标准库
    • std:os:raw 模块
    • std:ffi 模块

1. 关键字 extern

在 Rust 语言中,使用关键字 extern 可以实现 Rust 语言与其它语言的交互,这是 Rust 外部函数接口 FFI 的基础。

1.1 extern 函数

直接在 Rust 的函数关键字 fn 前使用关键字 extern,可以创建一个允许其他语言调用 Rust 函数的接口。

同时可以通过使用 ABI 字符串 [^1] 来指定具体的 ABI,其中有三个 ABI 字符串是跨平台的:

  • extern "Rust",默认的 ABI,在 Rust 代码中对任何普通函数 fn foo() 声明时都将使用该 ABI。
  • extern "C",指定使用 C-ABI,类似 extern fn foo(),无论 C 编译器支持哪种默认设置。
  • extern "system",通常类似 extern "C",但在 Win32 平台上,它是 “stdcall”,或用于链接到 Windows API。
1
2
3
4
5
// ffi/c-call-rust/src/lib.rs

pub extern "C" fn call_from_rust() {
println!("This is a Rust function for C!");
}

1.2 属性 #[no_mangle]

属性 no_mangle,用来关闭 Rust 的名称修改(name mangling)功能。Mangling 是编译器在解析名称时,修改我们定义的函数名称,增加一些用于其编译过程的额外信息。

但在与其它语言交互时,如果函数名称被编译器修改,程序开发者无法知道修改后的函数名称,其它语言也无法按原名称调用。执行 1.1 中示例代码时,报错信息如下:

1
2
3
4
5
Undefined symbols for architecture x86_64:
"_call_from_rust", referenced from:
_main in main-77cb59.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1

所以为了使 Rust 函数能在其它语言中被调用,必须禁用 Rust 编译器的名称修改功能。通过在 1.1 的示例代码中增加属性 #[no_mangle] ,告诉 Rust 编译器不要修改此函数的名称。

1
2
3
4
5
// ffi/c-call-rust/src/lib.rs
#[no_mangle]
pub extern "C" fn call_from_rust() {
println!("This is a Rust function for C!");
}

1.3 外部块 ExternBlock

在 Rust 语言中,使用关键字 extern 可以声明一个外部块(ExternBlock),通过外部块的形式,可以在 Rust 代码中调用外部代码。

在 Rust 语言参考文档中,使用关键字 extern 声明一个外部块的语法格式如下:

1
2
3
4
extern Abi? {
InnerAttribute*
ExternalItem*
}

其中的 Abi 表示调用库使用的 ABI 标准,可选值为 1.1 节中提到的 ABI 字符串。缺省情况下,外部块默认为标准的 C-ABI。在定义外部块的时候,可以使用 linklink_name 这两个属性,通过它们来控制外部块的行为。

属性 link 用来指定原生库的名称,编译器根据它为外部块链接原生库。它支持的键有:namekindwasm_import_modulename 用来定义要链接的原生库的名称。 kind 是一个可选值,通过它来指定原生库的类型,它有以下三种可选的值:

  • dylib,表示为动态库。如果未指定 kind,则它为默认值。
  • static,表示为静态库。
  • framework,表示 macOS 的框架,这仅对 macOS 目标有效。

如果对属性 link 设定了原生库的类型 kind,则必须包括原生库的名称 name

wasm_import_module 可用于指定 WebAssembly 模块的名称,如果未指定 wasm_import_module,则模块名称默认为 env

1
2
3
4
#[link(name = "c_library")]
extern "C" {
fn c_function(input: i32) -> i32;
}

在外部块内,通过属性 link_name,指定原生库中函数或静态对象的名称,编译器根据它可以为外部块链接原生库并导入该名称定义的函数或静态对象。

1
2
3
4
extern "C" {
#[link_name = "c_function_name"]
fn name_in_rust();
}

外部块中声明的函数在 Rust 代码中是不安全的,因为其他语言不会强制执行 Rust 语言中的语法规则,故无法检查这些代码,所以程序开发者务必要确保这部分代码的安全。

1
2
3
4
5
6
7
8
9
10
11
12
// ffi/rust-call-c/src/main.rs
// 标准库<stdlib.h>内置的abs函数
extern "C" {
#[link_name = "abs"]
fn abs_in_rust(input: i32) -> i32;
}

fn main() {
unsafe {
println!("abs(-1) is {}", abs_in_rust(-1));
}
}

2. 标准库

在实际开发 Rust 语言与其它语言相互调用的程序时,会遇到需要相互传递参数的情况。Rust 标准库 std::os::rawstd::ffi 这两个模块提供了这方面的支持。

2.1 std::os::raw 模块

使用 FFI 进行交互的代码通常会使用到 C 语言提供的基本类型,标准库 std::os::raw 模块 [^2] 提供了一些类型与 C 语言定义的类型相匹配,以便与 C 语言交互的代码引用正确的类型。

类型解释
c_char等同于 C 语言的 char 类型
c_double等同于 C 语言的 double 类型

更多类型可以查见参考链接 [^2]。

2.2 标准库 std::ffi 模块

由于 Rust 语言中字符串与 C 语言字符串的不同之处,标准库 std::ffi 模块 [^3] 提供了一组实用的程序,主要用于外部函数接口 FFI 的绑定,以及用在与其他语言传递类 C 字符串的代码中。

在支持 C-ABI 的语言(如:Python)中传递 UTF-8 字符串 [^4] 时,CStringCStr 很有用。

CStr

在 C 语言中生成的字符串,Rust 使用 CStr 来表示,它和 str 类型对应,表明并不拥有这个字符串的所有权。所以 CStr 表示一个以终止符 \0 结尾的字节数组的引用,如果它是有效的 UTF-8 字符串,则可以将其转换为 Rust 语言中的 &str。实现从 C 语言到 Rust 语言的字符串传递。

CString

在 Rust 语言中生成的字符串,Rust 使用 CString 来表示用以传给 C 程序的字符串。CString 以终止符 \0 结尾,并且没有内部 \0 字符,代码可以首先从 Rust 语言的普通字符串创建 CString 类型,然后将其作为参数传递给使用 C-ABI 约定的字符串函数。实现从 Rust 语言到 C 语言的字符串传递。

1
2
3
4
5
6
7
8
use std::ffi::CString;
use std::os::raw::c_char;

#[no_mangle]
pub extern rust_printer(input: *const c_char) {
let mut hello = String::from("Hello World!");
let c_str_to_print = CString::new(hello).unwrap();
}

注意:因为所有权概念是 Rust 语言特有的,所以在和 C 语言交互时,必须实现一个释放内存的方法供 C 代码调用。

此外在不同操作系统平台传输字符串,或者在捕获外部命令的输出时,OsStringOsStr 很有用。

  • OsString 表示传递给操作系统的拥有所有权的字符串。例如,env::var_os() 用于查询环境变量,它返回一个 Option<OsString>。如果环境变量存在,将获得 Some(os_string),然后可以将其转换为 Rust 字符串。
  • OsStr 表示传递给操作系统的字符串引用,可以按照与 OsString 类似的方式将其转换为 UTF-8 编码的 Rust 字符串切片。

另外,当用作指针时,std::ffi::c_void 等同于 C 语言中的 void 类型。

示例代码:https://github.com/lesterli/rust-practice/tree/master/ffi

Libc Crate

前文警示:如果对 Unix 环境系统编程没有基础知识的话,本文会看得云里雾里。

我们在做 Rust 开发编译的时候,常常能在依赖列表中,看到 libc 这个 crate 的身影。我们一般不会直接依赖这个 crate,但是依赖的依赖(的依赖的依赖……)可能就会用到这个 crate。总的来说,它是 Rust 生态中非常基础非常底层的一个 crate 了。

Libc 是什么

libc 是对各平台的系统库的原始 FFI 绑定。其代码地址在:https://github.com/rust-lang/libc。可以看到,这是 Rust 官方维护的一个库。

libc 提供了与 Rust 支持的各平台上的最基础系统 C 库打交道的所有必要设施。它导出了底层平台的类型、函数和常量。

所有内容都直接放置在 libc 这个命名空间下,没有再分模块了。因此,你可以使用 libc::foo 这种形式访问这个库中的任何导出内容。

它可以与 std 配合使用,也可以在 no_std 环境下使用。

Libc 的导入

在项目的 Cargo.toml 中添加如下配置,就可以导入 libc 了。

1
2
[dependencies]
libc = "0.2"

Libc 的内容分类

libc 会导出底层 C 库的这些东西:

  • C 类型,比如 typedefs, 原生类型,枚举,结构体等等
  • C 常量,比如使用 #define 指令定义的那些常量
  • C 静态变量
  • C 函数(按它们的头文件中定义的函数签名来导出)
  • C 宏,在 Rust 中会实现为 #[inline] 函数

另外,libc 中导出的所有 C struct 都已经实现了 CopyClone trait.

好吧,熟悉 C 的同学,应该已经知道了,C 的接口,无非也就这些东西了。现在 libc 全给导出来了。

导出的结果是什么呢?直接打开 https://docs.rs/libc/0.2.69/libc/index.html 查看,在你面前将会出现一个长长的网页。有

  • Structs 对应 C 中的符号
  • Enums 对应 C 中的枚举
  • Constants 对应 C 中的常量
  • Functions 对应 C 中的函数接口
  • Type Definitions 对应 C 中的 typedef 定义的符号

这些符号,可能 99% 的人都不敢打包票说用过 20% 以上,甚至很多专注于上层开发的同学从没见过这些命名。

这一套东西可不得了,它是计算机工程历史这么多年积累下来的成体系的精华之作。这套精华的体系就叫作 _Unix 环境编程 _。这套体系在 《UNIX环境高级编程(第3版)》 这本书中做了权威讲解。

这套东西的精华核心在于,它不仅仅是一套符号的简单罗列,其内在包含有一套精巧的机制来驱动。对,是一套机制。这套机制又是由若干个不同的部分组成,这些部分之间区分得非常清晰(Unix 的 KISS 原则),但是在设计理念上,又保持了同一种味道。因此,这套东西,我们称其为工程、技术、哲学、甚至艺术。

这套东西是现代 IT 工业,互联网的基石。

Libc 的界限

熟悉 linux 系统开发的同学都知道,linux 系统本身有个 libc 库,是几乎一切应用的基础库。基本上 linux 下 C 语言写的代码都要链接这个库才能运行。

而 Rust 的 libc crate,不完全等价于 C 的 libc 库的封装。具体区别如下:

  • Linux (以及其它 unix-like 平台)下,导出的是 libc, libm, librt, libdl, libutil 和 libpthread 这几个库的符号。
  • OSX 下,导出的是 libsystem_c, libsystem_m, libsystem_pthread, libsystem_malloc 和 libdyld 这几个库的符号。
  • Windows 下,导出的是 VS CRT(VS C RunTime VS C 运行时库)中的符号。但是这些符号,比前两个平台的符号,数量上要少得多。因此,可以直接这样说,Rust libc crate 在 Windows 平台上的功能有限。在 Windows 平台上,建议使用 winapi 这个 crate 进行开发。

举例:使用 Libc 创建子进程

说得那么神乎其神,还是让我们见见 libc 的庐山真面目吧。下面,我们就用一个示例——创建一个子进程——来展示 libc 的用法,以及与 Rust 标准库中线程操作的不同。

Rust 标准库中没有提供创建子进程的设施,不过可以创建一个子线程。作为对比演示,我们就创建一个新线程吧:

1
2
3
4
5
6
7
8
9
10
11
12
use std::thread;

fn main() {
let child = thread::spawn(move || {
println!("hello, I am a new rust thread!");
});

let res = child.join();
println!("{:?}", res);

println!("Hello, I am main thread!");
}

以上代码会输出:

1
2
3
hello, I am a new rust thread!
Ok(())
Hello, I am main thread!

下面我们来看看用 libc 如何创建一个子进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {                                                                                              
unsafe {
let pid = libc::fork();

if pid > 0 {
println!("Hello, I am parent thread: {}", libc::getpid());
}
else if pid == 0 {
println!("Hello, I am child thread: {}", libc::getpid());
println!("My parent thread: {}", libc::getppid());
}
else {
println!("Fork creation failed!");
}
}
}

这段代码会有类似下面的输出:

1
2
3
Hello, I am parent thread: 5722
Hello, I am child thread: 5724
My parent thread: 5722

具体的进程 id 数字,每次运行都可能会变化。

从两个程序的简单对比,可以发现:

  1. libc 的所有函数调用,都必须放进 unsafe 块中。因为它的所有调用都是 unsafe 的;
  2. std 的线程操作封装,好用,形象。libc 的进程操作,与 C 语言系统编程一样,完全是另外一套思路和编程风格;
  3. std 的线程操作虽然简洁,但是也缺少更细颗粒度的控制。而 libc 可以对进程的操作(及后面对子进程的功能扩充,父进程中的信号管理等),做到完全的控制,更加灵活,功能强大;
  4. std 本身无法实现进程 fork 的功能。

以上代码示例地址:https://github.com/daogangtang/learn-rust/tree/master/07libctest

哪些事情是 Rust Std 不能做而 Libc 能做的?

几乎所有底层编程的事情(当然这句话并不严谨)。

随便举几个例子:dup2 标准库有吗?openpty 标准库有吗?ioctl 标准库有吗?

ioctl 没有,那就是跟底层 say byebye 啦(进而跟严肃的嵌入式开发绝缘)。当然,你可以说,那我拿 Rust 自己写操作系统呗。对嘛,你用 Rust 写操作系统,也用不上 std 啊。

应该说,使用 libc,类 Unix 平台上的所有系统编程,之前只能由 C 完成的工作,现在都能用 Rust 来做了。在这一层面上,C 能做到的事情,Rust 都能做到。

通过 libc 这一层,Rust 闯入了系统编程领域。

可能,有的同学又要辩解了,不就是一个库嘛,这没什么大不了的。Python 也有对操作系统基础库的封装,Python 一样的可以做系统开发。这点不足以证明 Rust 是一门系统编程语言,Rust 在这一点上没有什么不同。

其实只需要用一句话就能回击这种质疑:因为我 Rust 的封装是 zero cost (零成本)的。

Yes,就这么简单。零成本抽象赋予了 Rust 系统编程的能力。

Libc 与 std::os::*::raw 的关系?

细心的同学会发现,在标准库的 os 模块下面,有一些东西与 libc 的重复。

页面 https://doc.rust-lang.org/std/os/raw/index.html 包含了 c_char, c_double, c_float, c_int, c_long, c_longlong, c_schar, c_short, c_uchar, c_uint, c_ulong, c_ulonglong, c_ushort

而 libc 中,对这些内容,也重新定义了一份(比如:https://docs.rs/libc/0.2.69/libc/type.c_char.html)。为什么呢?>

std::os::raw 中这些定义,可以用于与一些简单的 C 代码进行交互,比如说不存在系统调用的 C 代码。这个时候,就不需要再引入 libc 库了。

而一旦产生了系统调用或者 Unix 环境编程,那么就得引入 libc 库来操作。

std 下面还有一些 std::os::*::raw 的模块,这些模块现在已经被 Deprecated 了(比如:https://doc.rust-lang.org/std/os/unix/raw/index.html)。文档中明确注释了:>

1
2
3
Deprecated since 1.8.0:

these type aliases are no longer supported by the standard library, the libc crate on crates.io should be used instead for the correct definitions

也就是说,这些东西,去 libc 中找吧,用 libc 来实现这些功能。

总结

我们应该庆幸,Rust 标准库为我们提供的人性化的便捷的编程方式。

同时,我们又应该庆幸,Rust 与 C 的亲密血缘关系,让我们 Rustaceans 可以轻松的几乎没有性能损失的用 C 的方式和思维进行最底层的系统编程。

这种小幸运(可能性),不是谁都能拥有的。

我为能掌握 Rust 而感到幸福。

Nix Crate

在 Rust 中使用 nix 这个库,在某些情况下可以简化 Unix 系统编程。本文主要包括以下内容:

  • 前言:什么是 Unix 系统编程?
  • nix 库介绍
  • nix 库使用示例

什么是 Unix 系统编程?

Unix 系统编程实际上是把底层编程和系统设计两个概念混在了一起,本文将其理解为 “ 操作系统层级的编程 “。在进行 Unix 系统编程时,关键要熟悉 POSIX 规范 中定义的接口函数,以及 Unix/Linux 的 man 手册,以下是一些示例:

  • 进程管理(例如,forkkill
  • 文件处理(例如,readwrite
  • 网络编程(例如,socketlisten
  • 与硬件交互(例如,ioctlmmap
  • Linux 容器(例如,clonemount

Nix 库介绍

nix 库 旨在提供对各种类 Unix 平台(Linux,Darwin 等)API 的友好绑定(bindings),其代码地址在:https://github.com/nix-rust/nix。在其> lib.rs 文件中有如下代码:

1
2
// Re-exported external crates
pub extern crate libc;

它通过使用强制合法或安全的类型对 libc 库进行了一次封装,相对于 libc 库暴露的 unsafe API,它具有两个特点:

  • 用户代码中尽量没有 unsafe
  • Rust 风格的错误处理

以系统调用 gethostname 为例,我们来看一下,libc 和 nix 之间的区别:

1
2
3
4
5
// libc api (unsafe, requires handling return code/errno)
pub unsafe extern fn gethostname(name: *mut c_char, len: size_t) -> c_int;

// nix api (returns a nix::Result<CStr>)
pub fn gethostname<'a>(buffer: &'a mut [u8]) -> Result<&'a CStr>;

不过尽管 nix 库尝试支持 libc 库支持的所有平台,但由于技术或人力限制,仅支持其中的某些平台。可能这也是一些底层库(比如:tokio项目中的mio)在版本 v0.6.3 之后 移除 对 nix 库依赖的一个原因吧。

nix 库中的模块大致如下:

  • dir,相对标准库中的 std::fs::ReadDir 更底层的目录接口。
  • errno, nix 库中处理各种类 Unix 系统的错误类型,对于 FreeBSD,IOS,MacOS 系统直接封装的 libc 库中的。
  • fcntl, Unix 系统中文件 IO 的数据结构,以及对文件的各种操作接口。
  • features,用于操作系统级功能的测试。
  • ifaddrs,使用 Linux 或 BSD 中的函数 getifaddrs 获取网络接口及地址列表。
  • kmod,包含加载和卸载内核模块的功能。
  • mount,包含设备文件的挂载操作,mountumount
  • mqueue, 对应 POSIX 规范中消息队列 API 的功能。
  • net,涉及网络接口的功能。
  • poll,在特点文件描述符上触发 wait 事件。
  • pty,创建主从虚拟伪终端 PTYs。
  • sched,提供 Linux 系统的调度接口。
  • sys,这个模块包括各种系统相关的功能:POSIX 异步 I/O,文件系统事件的监控 API,Socket 接口函数等。
  • ucontext,提供协程上下文相关的借接口。
  • unistd,在 libc 库 unistd.h 头文件中函数的 safe 封装。

Nix 库使用示例

在项目的 Cargo.toml 中添加如下配置,就可以导入 nix 库了。

1
2
[dependencies]
nix = "0.17.0"

用 Nix 如何创建一个子进程

我们用 nix 库重写 libc 文章中创建一个子进程的示例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use nix::unistd::*;

fn main() {
match fork() {
Ok(ForkResult::Parent { child }) => {
// 在父进程中
println!("Hello, I am parent thread: {}", getpid());
}
Ok(ForkResult::Child) => {
// 在子进程中
println!("Hello, I am child thread: {}", getpid());
println!("My parent thread: {}", getppid());
}
Err(errno) => {
// fork 创建子进程失败
println!("Fork creation failed!");
}
}
}

fork/kill 示例

熟悉 POSIX 规范的话,其中的 fork() 函数可以用来创建一个新的进程(子进程),而 kill() 函数可以用来向一个或一组进程发送信号。我们来看如下的一段 C 语言代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <signal.h>
#include <unistd.h>

int main(void)
{
pid_t child = fork();
if (child)
{
sleep(5);
kill(child, SIGKILL);
}
else
{
for (;;)
// 循环直到被 kill 掉
;
}

return 0;
}

这段代码有问题吗?

我们知道 fork() 函数如果执行成功,则向子进程返回 0,并将子进程的进程 ID 返回给父进程。否则,将向父进程返回 -1,不创建子进程,并设置 errno 来标识错误。

上述代码中没有处理 fork() 函数失败时的逻辑,这样则可能将 -1(fork 的错误结果)视为子进程的进程 ID。这时在随后的程序中关闭子进程 kill(child, SIGKILL);,你知道进程 ID 为 -1 时会发生什么吗?

If pid is -1, sig shall be sent to all processes (excluding an unspecified set of system processes) for which the process has permission to send that signal.

如果进程 ID 等于 -1,则将信号发送到调用进程有权发送信号的每个进程,一些系统进程(如 init)除外。

kill(-1, SIGKILL); 等效于 kill 你有权发送信号的所有其他进程。

我们来看 nix 库中的 fork() 函数,其返回值为 Result<ForkResult, Errno> 类型,相比 C 语言中的 fork() 函数,它有两个优点:

  • Rust 的错误处理风格,使用类型 Result 区分成功和失败的情况
  • 使用枚举类型 ForkResult 区分返回父/子进程

这时使用 nix 库来重写上述逻辑,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use nix::sys::signal::*;
use nix::unistd::*;

fn main() {
match fork().expect("fork failed") {
ForkResult::Parent{ child } => {
sleep(5);
kill(child, SIGKILL).expect("kill failed");
}
ForkResult::Child => {
// 直到被 kill 掉
loop {}
}
}
}

以上代码示例地址:https://github.com/lesterli/rust-practice/tree/master/ffi/nix

总结

nix 库通过对 libc 库暴露的 unsafe API 进行封装,为 libc 库支持的某些平台提供了一种 safe 的替代方案。

[^1]: 外部块支持的 ABI 字符串,https://doc.rust-lang.org/reference/items/external-blocks.html
[^2]: 标准库 unsafe 模块,https://doc.rust-lang.org/stable/std/os/raw/index.html
[^3]: 标准库 safe 模块,https://doc.rust-lang.org/std/ffi/index.html
[^4]: Rust 中 String 与 UTF-8 编码,https://mp.weixin.qq.com/s/ZX_0G6JcNMusLz6JJOkNSg