Rust FFI 编程 - 其它语言调用 Rust 代码 - Python

2024-06-22 23:48

本文主要是介绍Rust FFI 编程 - 其它语言调用 Rust 代码 - Python,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

引言

随着 Rust 生态的发展,一些 Rust 语言实现的优秀工具或基础协议库,受到越来越多的企业或开发者青睐。与此同时,使用 Rust 语言对已有产品和工具进行性能优化或安全性提升,以及开发其它语言的扩展,这样的案例也越来越多。像被大家广泛使用的 curl 工具,其开发者 Daniel Stenberg 已采用 Rust 实现的 HTTP 协议库 hyper 来提供内存安全的 curl。

为了不同语言生态中的开发者可以快速地使用 Rust 语言以及 Rust 生态中优秀的工具或库,Rust FFI 编程计划通过编写一系列文章,专门介绍 C 语言之外的其它语言如何调用 Rust 导出库。目前准备介绍的语言列表有 Python,Ruby,Node.js,Go,Java,PHP。

对于每种语言,如果将 Rust 库的公共接口转换为应用程序二进制接口( C ABI),则在其它编程语言中可以相对容易地使用它们,当前列表中的语言都具有某种形式的外部函数接口(C FFI),剩下的就是其它语言和 Rust 类型之间的相互转换。

因此,同之前介绍过的 C 调用 Rust 导出库类似,文章基本上均会先介绍该语言中支持的 FFI 库,然后通过设计一些示例,分别介绍在该语言中调用 Rust 导出库时,如何处理 Rust 中的常见数据类型,包括数值,字符串,数组,结构体等。

Python 中的 FFI 库

目前 Python 中常用来与 FFI 交互的有 ctypes 和 cffi。其中,ctypes 已被包含在 Python 标准库中,成为 Python 内建的用于调用动态链接库函数的功能模块。ctypes的主要问题是,我们必须使用其特定的 API 完全重复 C ABI 的声明。cffi 则是则通过解析实际的 C ABI 声明,自动推断所需的数据类型和函数签名,以避免重写声明。ctypescffi都使用了libffi,通过它实现 Python 动态调用其他语言的库。在本文中的示例,我们采用 cffi 库。

安装

最快捷的安装方式是通过 pip :

pip install cffi

或者通过项目链接 https://pypi.python.org/pypi/cffi,下载源码,编译安装,这里不做介绍,参考链接中有相关的介绍文档。

使用

使用 cffi 的方式有 ABI 模式 和 API 模式 ,前者以二进制级别访问库,而后者使用 C 编译器访问库,所以在运行时,API 模式比 ABI 模式更快。我们的示例中使用 ABI 模式,因为它不需要 C 编译器。

在 cffi 中,我们可以使用 ffi.cdef(source) 解析给定的 C ABI。在其中注册所有函数,类型,常量和全局变量,这些类型可以在其它函数中立即使用。然后通过 ffi.dlopen(libpath) 使用 ABI 模式加载外部库并返回一个该库的对象,这样我们就可以使用库对象来调用先前由 ffi.cdef() 声明的函数,读取常量以及读取或写入全局变量。这种方式的大致代码框架如下:

# 导入 FFI 类
from cffi import FFIffi = FFI()# 声明数据类型和函数原型 
ffi.cdef("""""")# 以 ABI 模式加载外部库并返回库对象
lib = ffi.dlopen("")

Python 调用 Rust 代码示例

我们示例代码的目录结构如下:

example_04
├── Cargo.toml
├── ffi
│   ├── Cargo.toml
│   ├── cbindgen.toml
│   ├── example_04_header.h
│   ├── src
│   │   └── lib.rs
├── .gitignore
├── python
│   └── main.py
├── README.md
├── src
│   └── lib.rs

其中,

  • ffi 目录存放 Rust 代码库暴露给外部的 C ABI 代码;

    • 通过以下命令生成头文件 example_04_header.h:

      cbindgen --config cbindgen.toml --output example_04_header.h
  • python 目录存放在 Python 调用 Rust 代码库的 Python 代码;

  • src 目录存放 Rust 库的代码,lib.rs 中包含了我们设计并实现的几个示例函数:

    • count_char,计算给定字符串的长度;

    • sum_of_even,计算给定整数数组中所有偶数之和;

    • handle_tuple,处理元组包含整数和布尔类型两个元素,将整数加1和布尔取反后返回;

示例 - 整数与字符串

整数在 Rust,C,Python 中都有对应的转换,通常很容易通过 FFI 边界。

字符串则比较复杂,Rust 中的字符串,是一组 u8 组成的 UTF-8 编码的字节序列,字符串内部允许 NUL 字节;但在 C 中,字符串只是指向一个 char 的指针,用一个 NUL 字节作为终止。

我们需要做一些特殊的转换,在 Rust FFI 中使用 std::ffi::CStr,它表示一个 NUL 字节作为终止的字节数组,可以通过 UTF-8 验证转换成 Rust 中的 &str

#[no_mangle]
pub extern "C" fn count_char(s: *const c_char) -> c_uint {let c_str = unsafe {assert!(!s.is_null());CStr::from_ptr(s)};let r_str = c_str.to_str().unwrap();r_str.chars().count() as u32
}

同时,C 的 char 类型对应于 Python 中的单字符字符串,在 Python 中字符串必须编码为 UTF-8,才能通过 FFI 边界。

# coding: utf-8print 'count_char("hello") from Rust: ', lib.count_char("hello")
print 'count_char("你好") from Rust: ', lib.count_char(u"你好".encode('utf-8'))

执行结果为:

count_char("hello") from Rust:  5
count_char("你好") from Rust:  2
示例 - 数组与切片

在 Rust 和 C 中,数组均表示相同类型元素的集合,但在 C 中,其不会对数组执行边界检查,而 Rust 会在运行时检查数组边界。同时在 Rust 中有切片的概念,它包含一个指针和一组元素的数据。

在 Rust FFI 中使用 from_raw_parts 将指针和长度,转换为一个 Rust 中的切片。

#[no_mangle]
pub extern "C" fn sum_of_even(ptr: *const c_int, len: size_t) -> c_int {let slice = unsafe {assert!(!ptr.is_null());slice::from_raw_parts(ptr, len as usize)};let sum = slice.iter().filter(|&&num| num % 2 == 0).fold(0, |sum, &num| sum + num);sum as c_int
}

在 Python 中,并没有明显的 C 数组对等物,它们在 CFFI 中对应于的 cdata 类型。可以通过 ffi.new(cdecl,init=None) ,根据指定的 C 类型分配实例,并返回指向它的指针。

array = ffi.new("int[]", [1, 4, 9, 16, 25])
print 'sum_of_even from Rust: ', lib.sum_of_even(array, len(array))

执行结果为:

sum_of_even from Rust:  20
示例 - 元组与结构体

在 C 中没有元组的概念,我们可以做一个特殊的转换,通过在 Rust FFI 中定义与元组相对应的结构体。

#[repr(C)]
pub struct c_tuple {integer: c_uint,boolean: bool,
}#[no_mangle]
pub extern "C" fn handle_tuple(tup: c_tuple) -> c_tuple {let (integer, boolean) = tup.into();(integer + 1, !boolean).into()
}

与数组类似,在 Python 中,并没有明显的 C 结构体的对等物,它们在 CFFI 中也对应于的 cdata 类型。

py_cdata = ffi.new('c_tuple *')
py_cdata.integer = 100
py_cdata.boolean = True
print('cdata = {0}, {1}'.format(py_cdata.integer, py_cdata.boolean))
new_py_cdata = lib.handle_tuple(py_cdata[0])
print('change cdata = {0}, {1}'.format(new_py_cdata.integer, new_py_cdata.boolean))

执行结果为:

cdata = 100, True
change cdata = 101, False

对于结构体,由于无法查看其实例对象内部,所以通常将其视为不透明的指针(opaque pointer)来处理。可以参考之前系列文章中的介绍(https://mp.weixin.qq.com/s/WkOwKPPmmQOjc4IYwvKOfA)。

小结

通过简单的示例,我们可以整理出其它语言调用 Rust 代码的一般模式或步骤。

  1. 针对 Rust 代码中需要公开的 API,为其编写对应的 C API,对应示例中的 ffi 文件夹;

  2. 通过cbindgen 工具生成 C API 的头文件或手动添加 C API 函数定义;

  3. 在其它语言中,使用其支持调用 C API 的 FFI 模块或库,完成对 Rust 代码的调用。

完整示例代码的 Github 链接:https://github.com/lesterli/rust-practice/tree/master/ffi/example_04

参考链接
  • 内存安全的curl:https://www.abetterinternet.org/post/memory-safe-curl/

  • cbindgen的文档:https://github.com/eqrion/cbindgen/blob/master/docs.md

  • ctypes的中文文档:https://docs.python.org/zh-cn/3/library/ctypes.html

  • cffi 中文文档:https://cffi-zh-cn.readthedocs.io/zh/latest/overview.html

这篇关于Rust FFI 编程 - 其它语言调用 Rust 代码 - Python的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1085754

相关文章

Python将博客内容html导出为Markdown格式

《Python将博客内容html导出为Markdown格式》Python将博客内容html导出为Markdown格式,通过博客url地址抓取文章,分析并提取出文章标题和内容,将内容构建成html,再转... 目录一、为什么要搞?二、准备如何搞?三、说搞咱就搞!抓取文章提取内容构建html转存markdown

Python获取中国节假日数据记录入JSON文件

《Python获取中国节假日数据记录入JSON文件》项目系统内置的日历应用为了提升用户体验,特别设置了在调休日期显示“休”的UI图标功能,那么问题是这些调休数据从哪里来呢?我尝试一种更为智能的方法:P... 目录节假日数据获取存入jsON文件节假日数据读取封装完整代码项目系统内置的日历应用为了提升用户体验,

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Python Websockets库的使用指南

《PythonWebsockets库的使用指南》pythonwebsockets库是一个用于创建WebSocket服务器和客户端的Python库,它提供了一种简单的方式来实现实时通信,支持异步和同步... 目录一、WebSocket 简介二、python 的 websockets 库安装三、完整代码示例1.

揭秘Python Socket网络编程的7种硬核用法

《揭秘PythonSocket网络编程的7种硬核用法》Socket不仅能做聊天室,还能干一大堆硬核操作,这篇文章就带大家看看Python网络编程的7种超实用玩法,感兴趣的小伙伴可以跟随小编一起... 目录1.端口扫描器:探测开放端口2.简易 HTTP 服务器:10 秒搭个网页3.局域网游戏:多人联机对战4.

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

使用C#代码在PDF文档中添加、删除和替换图片

《使用C#代码在PDF文档中添加、删除和替换图片》在当今数字化文档处理场景中,动态操作PDF文档中的图像已成为企业级应用开发的核心需求之一,本文将介绍如何在.NET平台使用C#代码在PDF文档中添加、... 目录引言用C#添加图片到PDF文档用C#删除PDF文档中的图片用C#替换PDF文档中的图片引言在当

C#使用SQLite进行大数据量高效处理的代码示例

《C#使用SQLite进行大数据量高效处理的代码示例》在软件开发中,高效处理大数据量是一个常见且具有挑战性的任务,SQLite因其零配置、嵌入式、跨平台的特性,成为许多开发者的首选数据库,本文将深入探... 目录前言准备工作数据实体核心技术批量插入:从乌龟到猎豹的蜕变分页查询:加载百万数据异步处理:拒绝界面

Python使用自带的base64库进行base64编码和解码

《Python使用自带的base64库进行base64编码和解码》在Python中,处理数据的编码和解码是数据传输和存储中非常普遍的需求,其中,Base64是一种常用的编码方案,本文我将详细介绍如何使... 目录引言使用python的base64库进行编码和解码编码函数解码函数Base64编码的应用场景注意