< 返回版块

Mike Tang 发表于 2020-04-23 22:27

Tags:rust,ffi,libc

前文警示:如果对 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 了。

[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 标准库中没有提供创建子进程的设施,不过可以创建一个子线程。作为对比演示,我们就创建一个新线程吧:

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!");
}

以上代码会输出:

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

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

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!");                                                           
        }                                                                                                
    }                                                                                                    
}    

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

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)。文档中明确注释了:

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 而感到幸福。

评论区

写评论
jellybobbin 2020-04-24 10:51

赞!!

chenwei767 2020-04-24 10:33
先无脑顶, 再看.
whfuyn 2020-04-24 02:29

赞!

1 共 3 条评论, 1 页