原创声明

原文地址:https://codeleading.com/article/54435809443/本文在其上新增若干内容。


1. FFI

虽然高级编程语言功能丰富,表达能力强,但对底层操作的支持不完善,因此需要调用其它编程语言实现该类操作。调用其它编程语言的接口,被称为 Foreign Function Interface,直译为外部功能接口。该接口通常用于调用 C 语言实现的外部功能模块,因为 C 语言接近于全能,几乎能实现任何功能,正如使用汇编语言也可以实现很多功能一样,但开发效率低。很多脚本语言提供 FFI 功能,比如 Python、PHP 和 JIT 版本的 Lua 解析器等。同样地,Rust 也提供 FFI 接口,其为标准库中的功能模块。本文不讨论该模块的使用方法。本文记录笔者编写一个简单的 C 语言动态库,并且通过 Rust 调用动态库导出的函数。另外,笔者直接使用 Rust 官方提供的 libc 库,替代笔者编写的 C 语言动态库,以避免重复造轮子。


2. UDP 套接字的读超时

Rust 标准库中的 UDP 网络功能提供设置套接字读超时的函数 set_read_timeout,了解 C 网络编程的开发人员应该知道相应的底层调用为 setsockopt(SO_RCVTIMEO)。假设 Rust 标准库中 UDP 模块未提供该函数,那么就需要编写 C 代码,将其编译成动态库,尝试将 Rust 链接到该库,并且调用其中定义的函数。笔者编写的代码如下:

通过以下命令生成动态库 libsetsock.so

笔者使用 Rust 编写的简单 UDP 服务端代码如下:

短短 50 多行代码即可实现简单的 UDP 服务端,作为系统编程语言的 Rust 开发效率可见一斑。不过该 UDP 服务端的读操作是阻塞的,它将一直等待网络数据的到来:


3. Rust 调用 C 语言动态库中的函数

与 C 类似,Rust 使用 extern 关键字可实现对外部函数的声明,不过调用代码需要用 unsafe 关键字包成代码块。以下是笔者对上面 Rust 代码的修改:

修改后的主代码增加 #[link] 属性,指示 Rust 编译器在链接时加入 -lsetsock 链接选项。再次编译,发现链接命令失败:

这说明虽然编译正常,但在链接时找不到 libsetsock.so 动态库。解决方法是在工程根目录下增加控制编译的 Rust 代码,文件名为 build.rs,给出动态库所在的目录:

再次执行 cargo build 编译工程,链接可以成功。使用 patchelfnm 等命令行工具察看生成的可执行文件,可见其依赖动态库 libsetsock.so,并且引用其导出的函数符号 normal_setsock_timeout

此时运行 UDP 服务端程序,可以确定我们增加的套接字读超时功能能够正常工作:


4. 避免重复造轮子,使用 Rust 官方 C 库

以上我们用 C 编写简单的动态库,导出可设置套接字读超时的函数。该功能过于简单,大费周折编写动态库显得得不偿失。另一个解决方案是直接使用 Rust 官方提供的 C 库,该库提供很多变量和函数(与 glibc 提供的宏定义和库函数、系统调用有很多重叠),可以直接添加 setsockopt 等系统调用的代码。修改 UDP 服务端代码:

除以上修改外,还需要在 Cargo.toml 文件中加入 C 库的依赖,这里笔者使用的 libc 版本为 0.2.98:

以上代码与之前相同的是调用 C 库提供的函数也需要使用 unsafe 代码块。而不再需要工程根目录下的编译相关的控制代码 build.rs。编译生成的 UDP 服务端也将在 5 秒无数据时退出。最后,能够调用 C 编写的动态库意味着使用 Rust 语言进行嵌入式系统软件的开发是具备可行性的技术方案。


5. Rust 调用静态库中的函数

按照如下步骤操作调整项目:

  1. 将 UDP 服务端代码恢复成第 3 节。去掉 extern "C" { 上面的 #[link(name = "setsock")](不去掉亦可)

  2. mysetsock.c 放到项目根目录下

  3. Cargo.toml 中增加:

  4. 在项目根目录下创建 build.rs

将项目根目录下的 target/ 目录删除后,执行 cargo build 进行编译。通过 patchelf 命令可以看到,二进制程序不再依赖 libsetsock.so

最终,二进制程序可以正常运行: