Rust 实战练习 - 7. FFI, 库, ABI, libc

FFI

FFI(Foreign Function Interface)是这样一种机制:用一种编程语言写的程序能调用另一种编程语言写的函数(routines)。

调用约定,类型表示和名称修饰这三者的统称,即是众所周知的应用二进制接口(ABI)。

如果所有的语言在调用时都能认识同样一套 ABI 规范,那么就能完全畅通的调用了。可惜,世界不会像我们人为想象的那样干净。

在计算机技术发展的过程中,出现了各种 ABI 规范:

  • cdecl
  • syscall
  • optlink
  • pascal
  • register
  • stdcall
  • fastcall
  • thiscall
  • winapi
  • Intel ABI
  • System V

等。详情可参考:X86调用约定。

而 Rust 目前支持如下 ABI 约定:

  • stdcall
  • aapcs
  • cdecl
  • fastcall
  • vectorcall
  • Rust
  • rust-intrinsic
  • system
  • C
  • win64
  • sysv64

目前我们 IT 工业的基石,绝大部分是由 C 语言写成。于是自然而然,绝大多数库都遵循 cdecl(或 C)规范。所以我们可以专注于 C 规范来讨论问题。

目前所知,能(较方便地)对其它语言提供 FFI 库支持的语言有:

  • C
  • C++(通过定义 C 接口)
  • Rust(通过使用 C 约定)
  • Ada
  • Fortran

通用方案

在开发的过程中,要一个一个对大量的 C/C++ 库写绑定来进行 FFI,毕竟是一项费时费力的活儿。聪明的程序员们就开始构想一些“通用”的方案,实现批量快速绑定。

SWIG

以下定义来自 https://zh.wikipedia.org/wiki/SWIG:

简单包装界面产生器(SWIG)是一个开源软件工具,用来将C语言或C++写的计算机程序或函式库,连接脚本语言,例如Lua, Perl, PHP, Python, R, Ruby, Tcl, 和其它语言,例如C#, Java, JavaScript, Go, D, OCaml, Octave, Scilab以及Scheme. 也可以输出成XML格式。

也就是说,使用了 SWIG 这套工具和规范,就可以直接在上层语言(动态语言居多)中调用 C/C++ 库了,省却大量烦恼。但在实际使用中,还会有一些细节问题,往往需要人工调整。所以也不是那么完美。

Gnome 的 GI 规范

Gnome/Gtk 那一帮理想主义青年,发明了 GI(GObject Introspection)。用于对基于 glib/gobject 生态的众多软件(C 代码库)自动生成完整的接口描述文件(及 typelib),然后其它语言只要实现了对 Gir 这一个标准的支持,那么就可以无缝调用所有经过 Gir 化处理的 C 库。而不再需要单独为每一个 C 库做绑定了。这样就大大简化了 FFI 接口项目的编写工作。

目前这一杰出创意的重量级工作成果有 cairo, pango, gtk 等库。

更多信息请参考:https://gi.readthedocs.io/en/latest/。

另一种思路——基于字节码的平台级路线

语言间的相互调用,历史的发展提供了另一条路线:建立一个共同的字节码平台,这个平台之上的所有语言,皆可便捷地相互调用。

  • JVM 平台语言之间的 FFI
  • WASM 平台的 FFI

https://rustcc.cn/search?q=Rust%20FFI%20%E7%BC%96%E7%A8%8B

https://rustcc.cn/article?id=44a47b42-184c-4e2f-9318-be07ee5a516c

Rust 语言主要在关键字和标准库两个方面对 FFI 提供了支持,具体如下:

  • 关键字 extern
    • 属性 #[no_mangle]
    • 外部块 ExternBlock 及其属性 linklink_name
  • 标准库
    • std:os:raw 模块
    • std:ffi 模块

Rust编写lib给别人用

通过使用 ABI 字符串来指定具体的 ABI,其中有三个 ABI 字符串是跨平台的:

  • extern "Rust",默认的 ABI,在 Rust 代码中对任何普通函数fn foo()声明时都将使用该 ABI。
  • extern "C",指定使用 C-ABI,类似extern fn foo(),无论 C 编译器支持哪种默认设置。
  • extern "system",通常类似extern "C",但在 Win32 平台上,它是"stdcall",或用于链接到 Windows API。

crate-type: [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro]

  • bin 二进制可执行文件,main函数为入口。
  • lib Rust Library的编译工具默认值,一般为rlib。由编译器推荐其值。
  • rlib Rust Library的静态库,满足Rust API。 *.rlib 实现为一个 ar 归档文件。
  • dylib Rust Library的动态库,在编译的时候,生成动态库(Linux 上为 .so, MacOS 上为 .dylib, Windows 上为 .dll)。
  • cdylib 满足C调用规范的动态链接库。在编译的时候,生成动态库(Linux 上为 .so, MacOS 上为 .dylib, Windows 上为 .dll)。
  • staticlib 满足C调用规范的静态链接库。编译会生成 .a 文件(在 Linux 和 MacOS 上),或 .lib 文件(在 Windows 上)。
  • proc-macro 过程宏crate.
// Rust定义函数给外部使用
#[no_mangle]
pub extern "C" fn addtwo1(a: u32, b: u32) -> u32 {let c = a + b;println!("print in rust, sum is: {}", c);c
}#[no_mangle]
pub extern "C" fn sum_of_array(array: *const u32, len: usize) -> u32 {let array = unsafe {assert!(!array.is_null());slice::from_raw_parts(array, len)};array.iter().sum()
}// 在 Cargo.toml 中,加入如下两行
[lib]
crate-type = ["cdylib"]

C代码

#include <stdio.h>
#include <stdint.h>extern uint32_t addtwo1(uint32_t, uint32_t);
extern uint32_t sum_of_array(const uint32_t *numbers, size_t length);int main(void) {uint32_t sum = addtwo1(10, 20);printf("print in c, sum is: %d\n", sum);uint32_t numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};unsigned int length = sizeof(numbers) / sizeof(*numbers);uint32_t sum = sum_of_array(numbers, length);printf("print in c, sum is: %d\n", sum);
}

build

编译 rust so:cargo build
编译 c binary:gcc -o ./ccode03 ./ccode03.c -L ./ -lrustffi3
运行:LD_LIBRARY_PATH=. ./ccode03

结构体导出

只有当#[repr(C)] / #[repr(C, packed)]属性应用于一个struct时,Rust 才能保证该结构的布局与平台的 C 语言表示兼容

#[repr(C)]
pub struct Foo {a: isize,b: isize
}// Struct的方法
impl Foo {// 成员函数,需要this指针pub fn method1(&self) -> isize{self.a+self.b}// 成员函数pub fn method2(&self, x: isize) -> isize {x + self.a + self.b}// 静态成员函数,不需要thispub fn method3(x: isize) -> isize {x*2}
}

对应的C代码大概是这样子

struct Foo {int a;int b;
}void foo_method1(Foo* foo);
int foo_method2(Foo* foo, int x);
int foo_method1(int x);

然而,这种映射是不能自动转换的(毕竟只是我们自己的约定),需要手动写出来。

#[no_mangle]
pub unsafe extern "C" fn foo_method1(foo: *mut Foo) {let foo = &*foo;foo.method1();
}#[no_mangle]
pub unsafe extern "C" fn foo_method2(foo: *const Foo, x: isize) -> isize {let foo = &*foo;foo.method2(x)
}#[no_mangle]
pub unsafe extern "C" fn foo_method3(x: isize) -> isize {Foo::method3(x)
}

编译cargo build --release, 得到

$ nm -D --defined-only libd7.so 
0000000000001100 T foo_method1
0000000000001110 T foo_method2
0000000000001120 T foo_method3

最后,C代码:

#include <stdio.h>struct Foo
{long a;long b;
};extern void foo_method1(struct Foo *f);
extern long foo_method2(struct Foo *f, long x);
extern long foo_method3(long x);int main(int argc, char* argv){struct Foo f;f.a = 10;f.b = 100;long val = -1;foo_method1(&f);val = foo_method2(&f, 2);printf("m2: %ld\r\n", val);val = foo_method3(3);printf("m3: %ld\r\n", val);return 0;
}// gcc -o ../target/release/c_ffi main.c -L ../target/release/ -ld7
// LD_LIBRARY_PATH=. ./c_ffi
m2: 112
m3: 6

完美!

Rust调用别人写好的C库

// Rust使用外部的库
#[cfg(all(target_os = "win32", target_arch = "x86"))]
#[link(name = "c_library",kind="cdylib" )]
#[allow(non_snake_case)]
extern "C" {// 访问全局变量static rl_readline_version: libc::c_int; // 可以使用link_name映射外部函数名#[link_name = "c_function_name"]fn c_function(input: i32) -> i32;
}// 标准库<stdlib.h>内置的abs函数
extern "C" {#[link_name = "abs"]fn abs_in_rust(input: i32) -> i32;
}fn main() {unsafe {println!("abs(-1) is {}", abs_in_rust(-1));}
}

kind是一个可选值,通过它来指定原生库的类型,它有以下三种可选的值:

  • dylib,表示为动态库。如果未指定kind,则它为默认值。
  • static,表示为静态库。
  • framework,表示 macOS 的框架,这仅对 macOS 目标有效。

对于互传递参数,Rust 标准库std::os::rawstd::ffi 这两个模块提供了这方面的支持。

参考链接

[1] 外部块支持的 ABI 字符串,https://doc.rust-lang.org/reference/items/external-blocks.html

[2] 标准库 std::os::raw 模块,https://doc.rust-lang.org/stable/std/os/raw/index.html

[3] 标准库 std::ffi 模块,https://doc.rust-lang.org/std/ffi/index.html

[4] Rust 中 String 与 UTF-8 编码,https://mp.weixin.qq.com/s/ZX_0G6JcNMusLz6JJOkNSg

libc

libc 是对各平台的系统库的原始 FFI 绑定。其代码地址在:https://github.com/rust-lang/libc。可以看到,这是 Rust 官方维护的一个库。

libc 提供了与 Rust 支持的各平台上的最基础系统 C 库打交道的所有必要设施。它导出了底层平台的类型、函数和常量。

libc 会导出底层 C 库的这些东西:

  • C 类型,比如 typedefs, 原生类型,枚举,结构体等等
  • C 常量,比如使用 #define 指令定义的那些常量
  • C 静态变量
  • C 函数(按它们的头文件中定义的函数签名来导出)
  • C 宏,在 Rust 中会实现为 #[inline] 函数

另外,libc 中导出的所有 C struct 都已经实现了 CopyClone trait.

libc 的所有函数调用,都必须放进 unsafe 块中。因为它的所有调用都是 unsafe 的;

Rust 默认与libclibm链接。

与C的库区别:

熟悉 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 进行开发。

比如:Rust 标准库中没有提供创建子进程的设施,不过可以创建一个子线程。而libc 可以创建一个子进程。

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)。

nix库

nix 库 旨在提供对各种类 Unix 平台(Linux,Darwin等)API 的友好绑定(bindings),其代码地址在:https://github.com/nix-rust/nix。

它通过使用强制合法或安全的类型对 libc 库进行了一次封装,相对于 libc 库暴露的 unsafe API,它具有两个特点:

  • 用户代码中尽量没有 unsafe
  • Rust 风格的错误处理

不过尽管 nix 库尝试支持 libc 库支持的所有平台,但由于技术或人力限制,仅支持其中的某些平台。可能这也是一些底层库(比如:tokio项目中的mio)在版本v0.6.3之后 移除 对 nix 库依赖的一个原因吧。

nix 库中的模块大致如下:

  • dir,相对标准库中的std::fs::ReadDir更底层的目录接口。
  • errno, nix 库中处理各种类 Unix 系统的错误类型,对于 FreeBSD,IOS,MacOS 系统直接封装的 libc 库中的。
  • fcntl, Unix 系统中文件 IO 的数据结构,以及对文件的各种操作接口。
  • features,用于操作系统级功能的测试。
  • ifaddrs,使用 Linux 或 BSD 中的函数getifaddrs获取网络接口及地址列表。
  • kmod,包含加载和卸载内核模块的功能。
  • mount,包含设备文件的挂载操作,mountumount
  • mqueue, 对应 POSIX 规范中消息队列 API 的功能。
  • net,涉及网络接口的功能。
  • poll,在特点文件描述符上触发 wait 事件。
  • pty,创建主从虚拟伪终端 PTYs。
  • sched,提供 Linux 系统的调度接口。
  • sys,这个模块包括各种系统相关的功能:POSIX 异步 I/O,文件系统事件的监控 API,Socket接口函数等。
  • ucontext,提供协程上下文相关的借接口。
  • unistd,在 libc 库unistd.h头文件中函数的 safe 封装。

Demo

use nix::unistd::*;fn main() {match fork() {Ok(ForkResult::Parent { child }) => {// 在父进程中println!("Hello, I am parent thread: {}", getpid());}Ok(ForkResult::Child) => {// 在子进程中println!("Hello, I am child thread: {}", getpid());println!("My parent thread: {}", getppid());            }Err(errno) => {// fork 创建子进程失败println!("Fork creation failed!");}}  
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/771118.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

云数据库认识

云数据库概述 说明云数据库厂商概述Amazon 云数据库产品Google 的云数据库产品Microsoft 的云数据库产品 云数据库系统架构UMP 系统概述UMP 系统架构MnesiaRabbitMQZooKeeperLVSController 服务器Proxy 服务器Agent 服务器日志分析服务器 UMP 系统功能容灾 读写分离分库分表资源…

刚删除的文件怎么找回?刚删除的文件重新找回方法

电脑是我们办公、生活和娱乐必不可缺少的设备,里面存储着我们很多重要的文件。但是,有时候我们不小心误删了文件,或者是不小心删除了一些重要的数据等,要怎么进行恢复呢?今天,小编就专门给大家讲下刚删除的文件重新找回方法。 方法一: 文件不小心删除了,请立即停止对硬…

【洛谷 P8680】[蓝桥杯 2019 省 B] 特别数的和 题解(暴力枚举+数学)

[蓝桥杯 2019 省 B] 特别数的和 题目描述 小明对数位中含有 2 2 2、 0 0 0、 1 1 1、 9 9 9 的数字很感兴趣&#xff08;不包括前导 0 0 0&#xff09;&#xff0c;在 1 1 1 到 40 40 40 中这样的数包括 1 1 1、 2 2 2、 9 9 9、 10 10 10 至 32 32 32、 39 39 39 和 …

JavaParser 手动安装和配置

目录 前言 一、安装 Maven 工具 1.1 Maven 软件的下载 1.2 Maven 软件的安装 1.3 Maven 环境变量配置 1.4 通过命令检查 Maven 版本 二、配置 Maven 仓库 2.1 修改仓库目录 2.2 添加国内镜像 三、从 Github 下载 JavaParser 3.1 下载并解压 JavaParser 3.2 从路径打…

Java中垃圾回收有什么目的?什么时候进行垃圾回收?

该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:Java中垃圾回收有什么目的?什么时候进行垃圾回收? 在Java中,垃圾回收(Garbage Collection,GC)是一种自动管理内存的机制,它的主要目的是在…

APIFY集成客服系统:提升用户运营效率

{连接电商平台和客服系统} APIFY提供了无代码开发的解决方案&#xff0c;轻松实现系统的自动化连接和集成。商家可以通过APIFY将客服系统与电商平台的数据库、库存管理系统、订单处理系统等进行数据同步&#xff0c;实现信息的实时更新&#xff0c;提高响应速度和顾客满意度。…

蓝桥杯单片机快速开发笔记——利用定时器计数器设置定时器

一、基本原理 参考本栏http://t.csdnimg.cn/iPHN0 二、具体步骤 三、主要事项 如果使用中断功能记得打开总中断EA 四、示例代码 void Timer0_Isr(void) interrupt 1 { }void Timer0_Init(void) //10毫秒12.000MHz {AUXR & 0x7F; //定时器时钟12T模式TMOD & 0xF0;…

python网络爬虫实战教学——requests的使用(2)

文章目录 专栏导读1、POST请求2、响应3、Cookie设置 专栏导读 ✍ 作者简介&#xff1a;i阿极&#xff0c;CSDN 数据分析领域优质创作者&#xff0c;专注于分享python数据分析领域知识。 ✍ 本文录入于《python网络爬虫实战教学》&#xff0c;本专栏针对大学生、初级数据分析工程…

Typecho 博客文章评论添加显示 UserAgent(UA)的功能

本篇文章实现了为 Typecho 博客文章评论添加显示 UserAgent&#xff08;UA&#xff09;的功能本功能可替代 UserAgent 插件&#xff0c;更美观、简洁且好看 效果显示 大概就是这样了&#xff0c;实际效果请看我的评论&#xff01; 目前可以识别的操作系统以及浏览器 食用方…

Qt教程 — 3.5 深入了解Qt 控件:Display Widgets部件(1)

目录 1 Display Widgets简介 2 如何使用Display Widgets部件 2.1 QLabel组件-显示图像或文本 2.2 QCalendarWidget组件-日历简单的使用 2.3 QLCDNumber组件-控件作时钟的显示 2.4 QProgressBar组件-模拟手机电池充电 2.5 QFrame组件-绘制水平/垂直线 Display Widgets将分…

【C++练级之路】【Lv.16】红黑树(冰与火的碰撞,红与黑的史诗)

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《C语言》《数据结构世界》《进击的C》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、红黑树的概念二、红黑树的模拟实现2.1 结点2.2 成员变量2.3 插入情况一&#xff1a;uncle在左&#xff…

【Python】Scrapy整合FastAPI实现爬虫API 附大量示例

文章目录 前言1. 网页分析入门1.1 基本原理1.2 Scrapy 原理 2. 创建项目2.1 创建Scrapy项目2.2.1 创建Scrapy项目2.2.2 创建Spider2.2.3 执行Demo 2.2 引入FastAPI 2. 获取Cookie3. 数据建模3.1 Scrapy 数据建模3.2 SQLAlchemy 创建实体类 3. 分析网页3.1 xpath 分析3.2 css 分…

Kotlin协程CoroutineScope命名空间CoroutineName,Kotlin

Kotlin协程CoroutineScope命名空间CoroutineName&#xff0c;Kotlin import kotlinx.coroutines.*fun main(args: Array<String>) {val myName CoroutineName("fly")runBlocking {CoroutineScope(Dispatchers.IO).launch {repeat(3) {val name coroutineCont…

厨余垃圾处理设备工业监控PLC连接APP小程序智能软硬件开发之功能结构篇

厨余垃圾处理设备工业监控PLC连接APP小程序智能软硬件开发之功能结构篇 好几年前&#xff0c;应朋友之邀&#xff0c;为其工厂的厨余垃圾处理设备研发一套用于对现场的生产及维护进行远程查看、管理和质量监控的厨余垃圾处理设备工业监控PLC连接APP小程序智能软硬件系统。 因为…

9.串口通信

串口基本认识 串行接口简称串口&#xff0c;也称串行通信接口或串行通讯接口&#xff08;通常指COM接口&#xff09;&#xff0c;是采用串行通信方 式的扩展接口。串行接口&#xff08;Serial Interface&#xff09;是指数据一位一位地顺序传送。其特点是通信线路简 单&#x…

搭建 canal 监控mysql数据到RabbitMQ

项目需求&#xff1a; 使用canal监控mysql某个库某个表&#xff0c;或者多个库&#xff0c;多个表---- update/inster/create 操作&#xff0c; 系统版本mysql版本java版本canal版本rabbitMQ版本Rocky 9.2MySQL 8.0.26openjdk 11.0.221.1.6rabbitmq-server 3.12.4 mysql 配置…

C++(7): std::list的使用

std::list 容器介绍及用法 1. std::list std::list是 C 标准模板库&#xff08;STL&#xff09;中的一个顺序容器适配器&#xff0c;它提供了双向链表的数据结构。与 std::vector 不同&#xff0c;std::list 不存储连续的元素&#xff0c;因此它可以高效地在中间插入和删除元…

Rust 实战练习 - 4. 网络 TCP/UDP/Channel

目标&#xff1a; tcp 服务器和客户端udp服务器和客户端多线程实现服务器&#xff0c;异步并发channel tcp server & client use std::{prelude::*, net, thread, env}; use std::io::{Read, Write};fn main() {let args env::args().into_iter().collect::<Vec<…

淘宝扭蛋机小程序开发:开启线上扭蛋新体验

随着科技的飞速发展和移动互联网的普及&#xff0c;线上娱乐方式也变得越来越多样化。为了满足广大用户对于新鲜、有趣的娱乐体验的需求&#xff0c;我们决定开发一款淘宝扭蛋机小程序&#xff0c;为用户带来全新的线上扭蛋乐趣。 淘宝扭蛋机小程序将结合淘宝平台的优势资源&a…

flask_restful渲染模版

渲染模版就是在 Flask_RESTful 的类视图中要返回 html 片段代码&#xff0c;或 者是整个html 文件代码。 如何需要浏览器渲染模板内容应该使用 api.representation 这个装饰器来定 义一个函数&#xff0c; 在这个函数中&#xff0c;应该对 html 代码进行一个封装&#xff…