Rust应用调用C语言动态库

2023-11-11

外部功能接口FFI

虽然高级(脚本)编程语言的功能丰富,表达能力强,但对底层的一些特殊操作的支持并不完善,就需要以其他编程语言来实现。调用其他编程语言的接口,被称为Foreign Function Interface,直译为外部功能接口。该接口通常是调用C语言实现的外部功能模块,因为C语言接近于全能,几乎任何功能都能够实现;正如同使用汇编语言也可以实现很多功能一样,但开发效率低下。很多脚本语言提供了FFI功能,例如PythonPHP和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语言来进行嵌入式系统软件的开发,是一种具备可行性的技术方案。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Rust应用调用C语言动态库 的相关文章

  • X 按键/释放事件捕获,与焦点窗口无关

    我想记录所有传入的按键事件 无论哪个窗口处于焦点状态或指针位于何处 我编写了一个示例代码 它应该捕获当前焦点窗口的按键事件 include
  • sudo pip install python-Levenshtein 失败,错误代码 1

    我正在尝试在 Linux 上安装 python Levenshtein 库 但每当我尝试通过以下方式安装它时 sudo pip install python Levenshtein 我收到此错误 命令 usr bin python c 导入
  • “./somescript.sh”和“. ./somescript.sh”有什么区别

    今天我按照一些说明在 Linux 中安装软件 有一个需要首先运行的脚本 它设置一些环境变量 指令告诉我执行 setup sh 但是我执行时犯了一个错误 setup sh 所以环境没有设置 最后我注意到了这一点并继续进行 我想知道这两种调用脚
  • PHP 日志文件颜色

    我正在编写一个 PHP 日志文件类 但我想为写入文件的行添加颜色 我遇到的问题是颜色也会改变终端的颜色 我想要实现的是仅更改写入日志文件的行的颜色 class logClass extends Singleton private funct
  • 在Linux中创建可执行文件

    我计划做的一件事是编写 非常简单的 Perl 脚本 并且我希望能够在不从终端显式调用 Perl 的情况下运行它们 我明白 要做到这一点 我需要授予他们执行权限 使用 chmod 执行此操作非常简单 但它似乎也是一个稍微费力的额外步骤 我想要
  • 使用 terminfo 的终端颜色?

    我正在编写一个 C 类 允许在终端中使用颜色 我希望它适用于每个终端 在支持真彩色 24 位 的终端上打印 在支持它的终端上具有 256 色 6x6x6 其他都是基本的 16 种颜色 我曾经使用 termcap 编写过一次 C 函数 并且我
  • Linux Shellcode“你好,世界!”

    我有以下可用的 NASM 代码 global start section text start mov eax 0x4 mov ebx 0x1 mov ecx message mov edx 0xF int 0x80 mov eax 0x1
  • 有没有办法只安装mysql客户端(Linux)? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 有没有不需要安装整个mysql db安装包的Linux mysql命令行工具 我想做的是从服务器 1 应用程序服务器 执行将在服务器 2
  • 声明 for 循环变量时 &mut 会做什么吗?

    考虑以下 愚蠢的 程序 fn main let mut array mut 1u8 2u8 3u8 for mut value in array 它编译并运行正常 尽管如预期的那样警告未使用的变量 不必要的可变性 但有什么作用 mut做在f
  • 如何防止 CMake 在构建时(而不是安装时)为共享库创建符号链接?

    我正在使用 CMake 在 Linux 上使用 Bullet3 构建一个项目 在构建整个解决方案时 它会构建附加了 SOVERSION 的 Bullet 输出库 并创建一个不带版本的符号链接 对于我的特定场景 我不喜欢这种行为 并且我不想编
  • 如何获取在 Rust 中实现特定特征的类型列表?

    我想知道一个实现的结构std io Write 在某些文件中有描述吗 当你查找API for std https doc rust lang org std 您可以在那里搜索您的特质 例如std io Write https doc rus
  • 是否可以在类型别名上实现方法?

    考虑以下实现 pub struct BST root Link type Link Option
  • 如何限制Cargo.toml中的测试线程数?

    我的测试共享公共资源并且无法同时执行 这些测试失败cargo test 但与RUST TEST THREADS 1 cargo test 我可以修改测试以等待全局互斥体 但如果有任何更简单的方法来强制 我不想让它们混乱cargo为我设置这个
  • 为什么在比较时需要取消引用变量,但在进行算术运算时不需要?

    我有以下代码 fn example known primes i32 number i32 prime i32 limit i32 let mut is prime true for prime in known primes if num
  • 如何重载“新”方法?

    我刚刚开始学习 Rust 我想知道是否有方法重载方法 首先 我创建了一个结构并使用 impl 来实现基本的 新 方法 然后我想添加带有一些参数的 新 方法 并且我尝试使用 Trait 来实现这一点 以下代码已成功编译 但是当我尝试将 new
  • 尝试编译 git 但在 linux 中找不到 libcurl

    我想编译支持 http https 的 git 我有 ls usr include curl curlbuild h curl h curlrules h curlver h easy h mprintf h multi h stdchea
  • 有没有办法改变vim的默认模式

    有谁知道如何更改vim的默认模式 它的默认模式是命令模式 但是我可以将其更改为插入模式吗 只需将以下行添加到您的 vimrc 中 start Vim s default mode will be changed to Insert mode
  • bash.sh 运行 cron 的权限被拒绝

    如何在这里使用 bash 脚本运行 cron 我做了如下操作 这里有错误 我想知道如何在 ubuntu 中做到这一点 我现在对它感到震惊 bash sh 文件 bin bash cd var www Controller usr bin p
  • 检查 Linux 中给定进程的打开 FD 限制

    我最近有一个 Linux 进程 泄露 了文件描述符 它打开了文件描述符 但没有正确关闭其中一些文件描述符 如果我对此进行监控 我就可以提前得知该过程已达到其极限 有没有一种很好的 Bash 或 Python 方法来检查 Ubuntu Lin
  • 无法使用 tar -cvpzf 解压完整目录

    把我的头敲在这上面 I used tar cvpzf file tar gz压缩一个完整的目录 我将文件移动到另一台服务器 并尝试解压缩复制存档的目录 无法使其发挥作用 bash 3 2 tar xvpzf news tar gz tar

随机推荐

  • WEB前端网页设计-Bootstrap5 弹出框 & JavaScript 函数

    目录 Bootstrap5 弹出框 如何创建弹出框 指定弹出框的位置 关闭弹出框 JavaScript 函数 JavaScript 函数语法 调用带参数的函数 带有返回值的函数 语法 局部 JavaScript 变量 全局 JavaScri
  • TensorBoard 启动protobuf报错

    libprotobuf ERROR external protobuf archive src google protobuf descriptor database cc 334 Invalid file descriptor data
  • c语言成绩管理系统

    关注微信公众号每日新觉 私聊作者获取提取密码点击下面链接 私聊作者获取提取密码 https mp weixin qq com s QgRQ3YB2JqCvNOGQHQ57rA 作者每日新觉 是一名热爱技术和编程的年轻程序员 他在计算机科学和
  • js中,清空对象(删除对象的属性)

    在项目中 有些对象用完后需要重置 下面简单介绍下JS中清除对象的方法 方法如下 方法一 字面量定义对象 第一步 定义一个空对象并打印出来 代码和效果 代码 const student console log student 打印结果 第二步
  • 二进制ASCII转换(KeilC+Proteus) 跟书学程序系列

    给定的一个字节二进制数 转换成ASCII码 将累加器A中的值拆为两个ASCII码 并存入从RWESULT开始的两个单元中 示例程序给A赋值 1AH 代码如下 RESULT EQU 30H ORG 00HSTART MOV A 1AH CAL
  • Linux安装失败Grub-install问题超详细解决,傻瓜教学!!

    先说一下我安装Linux的过程 这里只是我无力地吐槽不想看直接往后跳 一年前接触Linux 软件安装GJ公众号上看到的 然后在虚拟机上跑 最近认识了一个大佬 双 爽 系统Ubuntu18 04 win10 突发奇想我也来装个双系统 双系统就
  • 常用端口对照表

    TCP 端口 静态端口 端口类型 端口号 注释 TCP 0 Reserved TCP 1 TCP Port Service Multiplexer TCP 2 Death TCP 5 Remote Job Entry yoyo TCP 7
  • Windows环境下利用Wget批量下载Earthdata数据

    使用wget下载Earthdata数据 首先是下载wget exe 然后保存在自己预设的目录中 同时将Earthdata上获取的download txt也放在该目录中 想要批量下载的数据的路径就保存在该txt文件中 接着win R打开cmd
  • js如何将选中图片文件转换成Base64字符串?

    如何将input type file 选中的文件转换成Base64的字符串呢 1 首先了解一下为什么要把图片文件转换成Base64的字符串 在常规的web开发过程中 大部分上传文件都是在web页面端通过表单直接提交 再由服务器端捕获请求来进
  • HBuilderX连接MuMu模拟器

    1 打开MuMu多开器 启动MuMu模拟器 然后点击ADB查看模拟器的端口号 2 在HBuilderX中配置模拟器端口号和adb路径 以自己的实际路径为准 3 配置环境变量 因为需要运行adb命令 所以需要配置环境变量 在MuMu的安装路径
  • 关于Android Studio检测不到模拟器/真机

    问题表现 初始化ADB不成功 无法连接到ADB unable to establish a connection to adb 打开选择设备界面 检测不到任何模拟器or真机 或许还有其他问题 通常的解决方法 按照网上大部分流传的 查看是否是
  • ES2015 Iterable(可迭代的)接口

    一 为什么ES2015提供Iterable 可迭代的 接口 ES中能够表示有结构的数据类型越来越多 以前有数组 对象等等 ES2015中新增了Map和Set对象 为了给各种各样的数据结构提供统一遍历方式 ES2015提供了Iterable
  • C语言--两数之和

    两数之和 给定一个整数数组 nums 和一个目标值 target 请你在该数组中找出和为目标值的那 两个 整数 并返回他们的数组下标 你可以假设每种输入只会对应一个答案 但是 你不能重复利用这个数组中同样的元素 示例 给定 nums 2 7
  • 前端后端的区别(超详细版)

    前端与后端开发中技术差异的全面对比 什么是前端开发 网站和移动应用的前端 物联网的前端 什么是后端开发 网站和移动应用后端 物联网后端 前端 Vs 后端有什么区别 前端与后端技术栈 前端 Vs 后端 谁是最好的 前端与后端开发人员 前端 V
  • docker安装rabbitmq

    启动脚本 docker stop rabbitmq docker rm rabbitmq docker run name rabbitmq restart always p 5670 5670 p 15670 15670 p 5674 56
  • 英文时间点的表达

    1 所有时间都可以用 小时 分钟 直接读 8 30 eight thirty 2 40 two forty 2 时间在半小时之内的 可以用 分钟 past 小时 6 10 ten past six 4 20 twenty past four
  • springboot使用aop进行全局事务管理

    依赖
  • C++新特性21_shared_ptr与weak_ptr(大量使用,仅看此篇即可;使用方法;注意事项:不能用同一指针去初始化两个shared_ptr;循环引用问题;weak_pt用于解决循环引用问题)

    前面介绍了智能指针中早期的两个版本 auto ptr 已废弃 unique ptr 不常用 auto ptr的升级 限制了某些操作避免了一些问题 本篇介绍在开发中真正可能被大量使用的指针shared ptr weak ptr是对shared
  • 概率神经网络(PNN)

    概率神经网络 Probabilistic Neural Network 是由D F Speeht博士在1989年首先提出 是径向基网络的一个分支 属于前馈网络的一种 它具有如下优点 学习过程简单 训练速度快 分类更准确 容错性好等 从本质上
  • Rust应用调用C语言动态库

    外部功能接口FFI 虽然高级 脚本 编程语言的功能丰富 表达能力强 但对底层的一些特殊操作的支持并不完善 就需要以其他编程语言来实现 调用其他编程语言的接口 被称为Foreign Function Interface 直译为外部功能接口 该