原文地址:https://codeleading.com/article/54435809443/。本文在其上新增若干内容。
虽然高级编程语言功能丰富,表达能力强,但对底层操作的支持不完善,因此需要调用其它编程语言实现该类操作。调用其它编程语言的接口,被称为 Foreign Function Interface
,直译为外部功能接口。该接口通常用于调用 C 语言实现的外部功能模块,因为 C 语言接近于全能,几乎能实现任何功能,正如使用汇编语言也可以实现很多功能一样,但开发效率低。很多脚本语言提供 FFI
功能,比如 Python、PHP 和 JIT 版本的 Lua 解析器等。同样地,Rust 也提供 FFI 接口,其为标准库中的功能模块。本文不讨论该模块的使用方法。本文记录笔者编写一个简单的 C 语言动态库,并且通过 Rust 调用动态库导出的函数。另外,笔者直接使用 Rust 官方提供的 libc 库,替代笔者编写的 C 语言动态库,以避免重复造轮子。
Rust 标准库中的 UDP 网络功能提供设置套接字读超时的函数 set_read_timeout
,了解 C 网络编程的开发人员应该知道相应的底层调用为 setsockopt(SO_RCVTIMEO)
。假设 Rust 标准库中 UDP 模块未提供该函数,那么就需要编写 C 代码,将其编译成动态库,尝试将 Rust 链接到该库,并且调用其中定义的函数。笔者编写的代码如下:
x
/* export function to set socket receive timeout */
extern int normal_setsock_timeout(int sockfd,
unsigned long timeout);
int normal_setsock_timeout(int sockfd,
unsigned long timeout)
{
int ret, err_n;
struct timeval tv;
tv.tv_sec = (time_t) (timeout / 1000);
tv.tv_usec = (timeout % 1000) * 1000;
ret = setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO,
&tv, sizeof(tv));
if (ret == -1) {
err_n = errno;
fprintf(stderr, "Error, setsockopt(%d, RECVTIMEO, %lu) has failed: %s\n",
sockfd, timeout, strerror(err_n));
fflush(stderr);
errno = err_n;
return -1;
}
return 0;
}
通过以下命令生成动态库 libsetsock.so
:
x
gcc -Wall -O2 -fPIC -D_GNU_SOURCE -shared -o libsetsock.so -Wl,-soname=libsetsock.so mysetsock.c
笔者使用 Rust 编写的简单 UDP 服务端代码如下:
xxxxxxxxxx
use std::net::UdpSocket;
use chrono::{DateTime, Local};
fn get_local_time() -> String {
let nowt: DateTime<Local> = Local::now();
nowt.to_string()
}
fn main() -> std::io::Result<()> {
let usock = UdpSocket::bind("127.0.0.1:2021");
if usock.is_err() {
let errval = usock.unwrap_err();
println!("Error, failed to create UDP socket: {:?}", errval);
return Err(errval);
}
// get the UdpSocket structure
let usock = usock.unwrap();
// create 2048 bytes of buffer
let mut buffer = vec![0u8; 2048];
println!("{} -> Waiting for UDP data...", get_local_time());
// main loop
loop {
let res = usock.recv_from(&mut buffer);
if res.is_err() {
println!("{} -> Error, failed to receive from UDP socket: {:?}",
get_local_time(), res.unwrap_err());
break;
}
let (rlen, peer_addr) = res.unwrap();
println!("{} -> received {} bytes from {:?}:{}",
get_local_time(), rlen, peer_addr.ip(), peer_addr.port());
}
// just return ok
Ok(())
}
短短 50 多行代码即可实现简单的 UDP 服务端,作为系统编程语言的 Rust 开发效率可见一斑。不过该 UDP 服务端的读操作是阻塞的,它将一直等待网络数据的到来:
# cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/test_udp`
2023-10-10 06:40:48.851903141 +00:00 -> Waiting for UDP data...
与 C 类似,Rust 使用 extern
关键字可实现对外部函数的声明,不过调用代码需要用 unsafe 关键字包成代码块。以下是笔者对上面 Rust 代码的修改:
x
diff --git a/src/main.rs b/src/main.rs
index 304c7dc..5921106 100644
--- a/src/main.rs
+++ b/src/main.rs
use std::net::UdpSocket;
use chrono::{DateTime, Local};
+use std::os::raw::c_int;
+use std::os::unix::io::AsRawFd;
+
+#[link(name = "setsock")]
+extern {
+ pub fn normal_setsock_timeout(sock_fd: c_int, timo: usize) -> c_int;
+}
fn get_local_time() -> String {
let nowt: DateTime<Local> = Local::now();
let mut buffer = vec![0u8; 2048];
println!("{} -> Waiting for UDP data...", get_local_time());
+ // set UDP socket receive timeout
+ unsafe {
+ normal_setsock_timeout(usock.as_raw_fd() as c_int, 5000);
+ }
+
// main loop
loop {
let res = usock.recv_from(&mut buffer);
修改后的主代码增加 #[link]
属性,指示 Rust 编译器在链接时加入 -lsetsock
链接选项。再次编译,发现链接命令失败:
# cargo run
Compiling test_udp v0.1.0 (/root/test_udp)
error: linking with `cc` failed: exit status: 1
这说明虽然编译正常,但在链接时找不到 libsetsock.so
动态库。解决方法是在工程根目录下增加控制编译的 Rust 代码,文件名为 build.rs
,给出动态库所在的目录:
xxxxxxxxxx
fn main() {
println!(r"cargo:rustc-link-search=native=/root/test_udp");
}
再次执行 cargo build
编译工程,链接可以成功。使用 patchelf
和 nm
等命令行工具察看生成的可执行文件,可见其依赖动态库 libsetsock.so
,并且引用其导出的函数符号 normal_setsock_timeout
:
x
# cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
# patchelf --print-needed ./target/debug/test_udp
libsetsock.so
libgcc_s.so.1
libpthread.so.0
libdl.so.2
libc.so.6
ld-linux-x86-64.so.2
# nm --undefined-only target/debug/test_udp | grep normal_setsock
U normal_setsock_timeout
此时运行 UDP 服务端程序,可以确定我们增加的套接字读超时功能能够正常工作:
xxxxxxxxxx
# LD_LIBRARY_PATH=/root/test_udp target/debug/test_udp
2023-10-10 07:11:58.193780151 +00:00 -> Waiting for UDP data...
2023-10-10 07:12:03.422391045 +00:00 -> Error, failed to receive from UDP socket: Os { code: 11, kind: WouldBlock, message: "Resource temporarily unavailable" }
以上我们用 C 编写简单的动态库,导出可设置套接字读超时的函数。该功能过于简单,大费周折编写动态库显得得不偿失。另一个解决方案是直接使用 Rust 官方提供的 C 库,该库提供很多变量和函数(与 glibc 提供的宏定义和库函数、系统调用有很多重叠),可以直接添加 setsockopt
等系统调用的代码。修改 UDP 服务端代码:
x
diff --git a/src/main.rs b/src/main.rs
index 5921106..3f4bc84 100644
--- a/src/main.rs
+++ b/src/main.rs
use chrono::{DateTime, Local};
use std::os::raw::c_int;
use std::os::unix::io::AsRawFd;
-
-#[link(name = "setsock")]
-extern {
- pub fn normal_setsock_timeout(sock_fd: c_int, timo: usize) -> c_int;
-}
+use libc;
fn get_local_time() -> String {
let nowt: DateTime<Local> = Local::now();
let mut buffer = vec![0u8; 2048];
println!("{} -> Waiting for UDP data...", get_local_time());
- // set UDP socket receive timeout
unsafe {
- normal_setsock_timeout(usock.as_raw_fd() as c_int, 5000);
+ let time_val = libc::timeval {
+ tv_sec: 5,
+ tv_usec: 0,
+ };
+
+ // set socket receive timeout via extern create, libc
+ libc::setsockopt(usock.as_raw_fd() as c_int,
+ libc::SOL_SOCKET, libc::SO_RCVTIMEO,
+ &time_val as *const libc::timeval as *const libc::c_void,
+ std::mem::size_of_val(&time_val) as libc::socklen_t);
}
除以上修改外,还需要在 Cargo.toml
文件中加入 C 库的依赖,这里笔者使用的 libc 版本为 0.2.98:
x
diff --git a/Cargo.toml b/Cargo.toml
index f802b0d..eb0b78e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+libc = "0.2.98"
chrono = "0.4.19"
以上代码与之前相同的是调用 C 库提供的函数也需要使用 unsafe
代码块。而不再需要工程根目录下的编译相关的控制代码 build.rs
。编译生成的 UDP 服务端也将在 5 秒无数据时退出。最后,能够调用 C 编写的动态库意味着使用 Rust 语言进行嵌入式系统软件的开发是具备可行性的技术方案。
按照如下步骤操作调整项目:
将 UDP 服务端代码恢复成第 3 节。去掉 extern "C" {
上面的 #[link(name = "setsock")]
(不去掉亦可)
将 mysetsock.c
放到项目根目录下
在 Cargo.toml
中增加:
xxxxxxxxxx
[build-dependencies]
cc = "1.0"
在项目根目录下创建 build.rs
:
xxxxxxxxxx
fn main() {
let out_dir = std::env::var("OUT_DIR").unwrap();
cc::Build::new()
.file("mysetsock.c")
.flag("-Wall")
.opt_level(2)
.pic(true)
.define("_GNU_SOURCE", None)
.shared_flag(true)
.out_dir(out_dir)
.compile("setsock");
println!("cargo:rerun-if-changed=mysetsock.c");
}
将项目根目录下的 target/
目录删除后,执行 cargo build
进行编译。通过 patchelf
命令可以看到,二进制程序不再依赖 libsetsock.so
:
xxxxxxxxxx
# patchelf --print-needed target/debug/test_udp
libgcc_s.so.1
libpthread.so.0
libdl.so.2
libc.so.6
ld-linux-x86-64.so.2
最终,二进制程序可以正常运行:
xxxxxxxxxx
# target/debug/test_udp
2023-10-10 08:42:15.280537371 +00:00 -> Waiting for UDP data...
2023-10-10 08:42:20.384330129 +00:00 -> Error, failed to receive from UDP socket: Os { code: 11, kind: WouldBlock, message: "Resource temporarily unavailable" }