外部功能接口FFI

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

UDP套接字的读超时

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

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
/* 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

gcc -Wall -O2 -fPIC -D_GNU_SOURCE -shared -o libsetsock.so -Wl,-soname=libsetsock.so mysetsock.c

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

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服务器的读操作是阻塞的,它会一直等待网络数据的到来:

udp_socket$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/udp_socket`
2021-07-11 19:38:29.791363796 +08:00 -> Waiting for UDP data...
2021-07-11 19:38:39.721713256 +08:00 -> received 16 bytes from 127.0.0.1:39180
2021-07-11 19:38:48.553386975 +08:00 -> received 16 bytes from 127.0.0.1:58811

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

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

diff --git a/src/main.rs b/src/main.rs
index 304c7dc..5921106 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,12 @@
 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();
@@ -20,6 +27,11 @@ fn main() -> std::io::Result<()> {
     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链接选项。再次编译,会发现链接命令失败:

udp_socket$ cargo build
   Compiling udp_socket v0.1.0 (/home/yejq/program/rust-lang/udp_socket)
error: linking with `cc` failed: exit status: 1

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

fn main() {
    println!(r"cargo:rustc-link-search=native=/home/yejq/program/rust-lang/socket_udp");

再次执行cargo build编译工程,链接就能成功了;使用patchelfnm等命令行工具察看,生成的可执行文件依赖了C语言编写的动态库libsetsock.so,并引用了其导出的函数符号normal_setsock_timeout

udp_socket$ cargo build
   Compiling udp_socket v0.1.0 (/home/yejq/program/rust-lang/udp_socket)
    Finished dev [unoptimized + debuginfo] target(s) in 1.72s
udp_socket$ patchelf --print-needed ./target/debug/udp_socket
libsetsock.so
libgcc_s.so.1
libpthread.so.0
libdl.so.2
libc.so.6
ld-linux-x86-64.so.2
udp_socket$ nm --undefined-only ./target/debug/udp_socket | grep -e normal_setsock
                 U normal_setsock_timeout

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

udp_socket$ LD_LIBRARY_PATH=/home/yejq/program/rust-lang/socket_udp ./target/debug/udp_socket
2021-07-11 19:55:26.279653039 +08:00 -> Waiting for UDP data...
2021-07-11 19:55:29.788948366 +08:00 -> received 16 bytes from 127.0.0.1:43303
2021-07-11 19:55:31.977738660 +08:00 -> received 16 bytes from 127.0.0.1:46854
2021-07-11 19:55:37.179290653 +08:00 -> Error, failed to receive from UDP socket: Os { code: 11, kind: WouldBlock, message: "Resource temporarily unavailable" }

避免重复造轮子,使用Rust官方C语言库

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

diff --git a/src/main.rs b/src/main.rs
index 5921106..3f4bc84 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,11 +2,7 @@ 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;
+use libc;
 fn get_local_time() -> String {
     let nowt: DateTime<Local> = Local::now();
@@ -27,9 +23,17 @@ fn main() -> std::io::Result<()> {
     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:

diff --git a/Cargo.toml b/Cargo.toml
index f802b0d..eb0b78e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,4 +6,5 @@ edition = "2018"
 # 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语言来进行嵌入式系统软件的开发,是一种具备可行性的技术方案。

外部功能接口FFI虽然高级(脚本)编程语言的功能丰富,表达能力强,但对底层的一些特殊操作的支持并不完善,就需要以其他编程语言来实现。调用其他编程语言的接口,被称为Foreign Function Interface,直译为外部功能接口。该接口通常是调用C语言实现的外部功能模块,因为C语言接近于全能,几乎任何功能都能够实现;正如同使用汇编语言也可以实现很多功能一样,但开发效率低下。很多脚本语言提供了FFI功能,例如Python、PHP和JIT版本的Lua解析器等。同样的,Rust也提供了FFI接口,作为标准
这个项目是一个以高度自动化但安全的方式从Rust调用C ++的工具。 Autocxx这个项目是一个以高度自动化但安全的方式从Rust调用C ++的工具。 目的是它具有cxx的所有安全功能,同时使用bindgen的变体从现有C ++头自动生成接口。 将autocxx视为将bindgen插入cxx的胶水。 概述命名空间基础{类Bob {公共:Bob(std :: string name); ... void do_a_thing()const; }使用autocxx :: include_cpp; include_cpp!(
libc-原始FFI绑定到平台的系统 libc提供了所有必需的定义,以便在Rust支持的每个平台上轻松地与C代码(或“类似C的”代码)进行互操作。 这包括类型定义(例如c_int ),常量(例如EINVAL )以及函数头(例如malloc )。 此板条箱导出板条箱根下的所有基础平台类型,函数和常量,因此所有项目都可通过libc::foo进行访问。 所有导出的API的类型和值都与为其编译libc的平台匹配。 有关该设计的更多详细信息,请参见其 。 将以下内容添加到您的Cargo.toml : [ dependencies ] libc = " 0.2 " std :默认情况下, libc链接到标准。 禁用此功能可删除此依赖性,并能够在#![no_std]箱中使用libc 。 extra_traits :在libc中实现的所有struct都是Copy和Clone 。
CXX — Rust和C ++之间的安全FFI 该提供了一种用于从Rust调用C ++代码和从C ++调用Rust代码的安全机制,而不受使用bindgen或cbindgen生成不安全的C样式绑定时事情出问题的多种方式的约束。 这不会改变100%的C ++代码不安全的事实。审核项目时,您将需要审核所有不安全的Rust代码和所有C ++代码。在这种新模型下的核心安全要求是,仅对C ++方面进行审核就可以解决所有问题,即Rust方面可以100%安全。 [ dependencies ] cxx = " 1.0 " [ build-dependencies ] cxx-build = " 1.0 " 编译器支持:需要rustc 1.48+和c ++ 11或更高版本 请参阅以获取教程,参考资料和示例代码。 这个想法是我们定义一起嵌入在一个Rust模块中的FFI边界两侧的签名(下一部分显
dns-lookup-一个简单的dns解析api,非常类似于rust的不稳定api。 还包括用于libc变体的getaddrinfo和getnameinfo包装器。 dns-lookup libc的一个小型包装程序,用于执行简单的DNS查找。 您可以使用lookup_host函数获取给定主机名的IP地址列表,并使用lookup_name函数获取给定IP地址的反向dns条目。 PS:如果只需要一个结果,请考虑使用libstd中的ToSocketAddrs。 该还包含用于getaddrinfo和getnameinfo的安全包装。 文档用法简单的API使用dns_lookup :: {lookup_host,lookup_addr}; {let hostname =“ localhost”
Rust和C语言的相互调用过程中,指针是一个重要的概念。Rust是一种较新的系统级编程语言,具备了内存安全和高性能的特性,而C语言作为一种传统的系统性语言,其指针的概念在Rust中也可以得到支持和使用。 在Rust中,可以使用`ffi`(Foreign Function Interface)功能来与C语言进行交互。这使得Rust可以使用C语言的函数和数据结构。在C语言中,指针用于引用内存中的数据地址。而在Rust中,由于其内存管理的安全性,需要使用特定的语法和关键字来操作和使用指针。 在Rust中,使用`&`和`*`两个符号来进行指针操作。使用`&`可以创建一个指向某个值的引用,并且由Rust自动处理内存的管理。而使用`*`可以通过解引用操作符来取得指针所指向的值。 当Rust与C语言进行相互调用时,指针在两者之间的传递非常重要。在Rust调用C函数时,需要通过`*const`或`*mut`等类型来声明指针。同时,通过`unsafe`关键字来告诉编译器这是一个不安全的操作,需要手动去管理指针所指向的内存。 在C调用Rust函数时,需要考虑Rust的所有权(ownership)机制。Rust的所有权机制确保了内存的安全和有效的内存管理。当C语言调用Rust函数时,需要传递指针给Rust函数,并在合适的时候将指针的所有权返回给C语言。 总之,Rust和C语言的相互调用中,指针是连接两者的重要桥梁。在Rust中,通过特定的语法和关键字进行指针操作,并通过`ffi`功能与C语言进行交互。指针的有效管理是确保内存安全的关键所在。