解密 Python 如何调用 Rust 编译生成的动态链接库(二)

简介: 解密 Python 如何调用 Rust 编译生成的动态链接库(二)

接上篇:https://developer.aliyun.com/article/1617449?spm=a2c6h.13148508.setting.14.72964f0eZeRnl9


给函数传递指针



如果扩展函数里面接收的是指针,那么 Python 要怎么传递呢?

#[no_mangle]
pub extern "C" fn add(a: *mut i32, b: *mut i32) -> i32 {
    // 定义为 *mut,那么可以修改指针指向的值,定义为 *const,则不能修改
    if a.is_null() || b.is_null() {
        0
    } else {
        let res = unsafe {
            *a + *b
        };
        unsafe {
            // 这里将 *a 和 *b 给改掉
            *a = 666;
            *b = 777;
        }
        res
    }
}

定义了一个 add 函数,接收两个 i32 指针,返回解引用后相加的结果。但是在返回之前,我们将 *a 和 *b 的值也修改了。

from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
a = c_int(22)
b = c_int(33)
# 计算
print(py_lib.add(pointer(a), pointer(b)))  # 55
# 我们看到 a 和 b 也被修改了
print(a, a.value)  # c_int(666) 666
print(b, b.value)  # c_int(777) 777

非常简单,那么问题来了,能不能返回一个指针呢?答案是当然可以,只不过存在一些注意事项。

由于 Rust 本身的内存安全原则,直接从函数返回一个指向本地局部变量的指针是不安全的。因为该变量的作用域仅限于函数本身,一旦函数返回,该变量的内存就会被回收,从而出现悬空指针。

为了避免这种情况出现,我们应该在堆上分配内存,但这又出现了之前 CString 的问题。Python 在拿到值之后,堆内存依旧驻留在堆区。因此 Rust 如果想返回指针,那么同时还要定义一个释放函数。

#[no_mangle]
pub extern "C" fn add(a: *const i32, b: *const i32) -> *mut i32 {
    // 返回值的类型是 *mut i32,所以 res 不能直接返回,因此它是 i32
    let res = unsafe {*a + *b};
    // 创建智能指针(将 res 装箱),然后返回原始指针
    Box::into_raw(Box::new(res))
}
#[no_mangle]
pub extern "C" fn free_i32(ptr: *mut i32) {
    if !ptr.is_null() {
        // 转成 Box<i32>,同时拿到所有权,在离开作用域时释放堆内存
        unsafe { let _ = Box::from_raw(ptr); }
    }
}

然后 Python 进行调用:

from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
a, b = c_int(22), c_int(33)
# 指定类型为 c_void_p
py_lib.add.restype = c_void_p
# 拿到指针保存的地址
ptr = py_lib.add(pointer(a), pointer(b))
# 将 c_void_p 转成 POINTER(c_int) 类型,也就是 c_int *
# 通过它的 contents 属性拿到具体的值
print(cast(ptr, POINTER(c_int)).contents)  # c_int(55)
print(cast(ptr, POINTER(c_int)).contents.value)  # 55
# 释放堆内存
py_lib.free_i32(c_void_p(ptr))

这样我们就拿到了指针,并且也不会出现内存泄露。但是单独定义一个释放函数还是有些麻烦的,所以 Rust 自动提供了一个 free 函数,专门用于释放堆内存。举个例子:

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn to_uppercase(s: *const c_char) -> *mut c_char {
    let s = unsafe {
        CStr::from_ptr(s)
    };
    let s = s.to_str().unwrap().to_uppercase();
    CString::new(s).unwrap().into_raw()
}
#[no_mangle]
pub extern "C" fn add(a: *const i32, b: *const i32) -> *mut i32 {
    let res = unsafe {*a + *b};
    Box::into_raw(Box::new(res))
}

这是出现过的两个函数,它们的内存都申请在堆区,但我们将内存释放函数删掉了,因为 Rust 自动提供了一个 free 函数,专门用于堆内存的释放。

from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
# 返回值类型指定为 c_void_p,表示万能指针
py_lib.to_uppercase.restype = c_void_p
py_lib.add.restype = c_void_p
ptr1 = py_lib.to_uppercase(
    c_char_p("Serpen 老师".encode("utf-8"))
)
ptr2 = py_lib.add(
    pointer(c_int(123)), pointer(c_int(456))
)
# 函数调用完毕,将地址转成具体的类型的指针
print(cast(ptr1, c_char_p).value.decode("utf-8"))
"""
SERPEN 老师
"""
print(cast(ptr2, POINTER(c_int)).contents.value)
"""
579
"""
# 释放堆内存,直接调用 free 函数即可,非常方便
py_lib.free(c_void_p(ptr1))
py_lib.free(c_void_p(ptr2))

以上我们就实现了指针的传递和返回,但对于整数、浮点数而言,直接返回它们的值即可,没必要返回指针。


传递数组



下面来看看如何传递数组,由于数组在作为参数传递的时候会退化为指针,所以数组的长度信息就丢失了,使用 sizeof 计算出来的结果就是一个指针的大小。因此将数组作为参数传递的时候,应该将当前数组的长度信息也传递过去,否则可能会访问非法的内存。

我们实现一个功能,Rust 接收一个 Python 数组,进行原地排序。

use std::slice;
#[no_mangle]
pub extern "C" fn sort_array(arr: *mut i32, len: usize) {
    assert!(!arr.is_null());
    unsafe {
        // 得到一个切片 &mut[i32]
        let slice = slice::from_raw_parts_mut(arr, len);
        slice.sort();  // 排序
    }
}

然后 Python 进行调用:

from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
# 一个列表
data = [3, 2, 1, 5, 4, 7, 6]
# 但是列表不能传递,必须要转成 C 数组
# Array_Type 就相当于 C 的 int array[len(data)]
Array_Type = c_int * len(data)
# 创建数组
array = Array_Type(*data)
print(list(array))  # [3, 2, 1, 5, 4, 7, 6]
py_lib.sort_array(array, len(array))
print(list(array))  # [1, 2, 3, 4, 5, 6, 7]

排序实现完成,这里的数组是 Python 传过去的,并且进行了原地修改。那 Rust 可不可以返回数组给 Python 呢?从理论上来说可以,但实际不建议这么做,因为你不知道返回的数组的长度是多少?

如果你真的想返回数组的话,那么可以将数组拼接成字符串,然后返回。

use std::ffi::{c_char, CString};
#[no_mangle]
pub extern "C" fn create_array() -> *mut c_char {
    // 筛选出 1 到 50 中,能被 3 整除的数
    // 并以逗号为分隔符,将这些整数拼接成字符串
    let vec = (1..=50)
        .filter(|c| *c % 3 == 0)
        .map(|c| c.to_string())
        .collect::<Vec<String>>()
        .join(",");
    CString::new(vec).unwrap().into_raw()
}

编译之后交给 Python 调用。

from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
# 只要是需要释放的堆内存,都建议按照 c_void_p 来解析
py_lib.create_array.restype = c_void_p
# 此时拿到的就是指针保存的地址,在 Python 里面就是一串整数
ptr = py_lib.create_array()
# 由于是字符串首字符的地址,所以转成 char *,拿到具体内容
print(cast(ptr, c_char_p).value.decode("utf-8"))
"""
3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48
"""
# 此时我们就将数组拼接成字符串返回了
# 但是堆区的 CString 还在,所以还要释放掉,调用 free 函数即可
# 注意:ptr 只是一串整数,或者说它就是 Python 的一个 int 对象
# 换句话说 ptr 只是保存了地址值,但它不具备指针的含义
# 因此需要再使用 c_void_p 包装一下(转成指针),才能传给 free 函数
py_lib.free(c_void_p(ptr))

因此虽然不建议返回数组,但将数组转成字符串返回也不失为一个办法,当然除了数组,你还可以将更复杂的结构转成字符串返回


传递结构体



结构体应该是 Rust 里面最重要的结构之一了,它要如何和外部交互呢?

use std::ffi::c_char;
#[repr(C)]
pub struct Girl {
    pub name: *mut c_char,
    pub age: u8,
}
#[no_mangle]
pub extern "C" fn create_struct(name: *mut c_char, age: u8) -> Girl {
    Girl { name, age }
}

因为结构体实例要返回给外部,所以它的字段类型必须是兼容的,不能定义 C 理解不了的类型。然后还要设置 #[repr(C)] 属性,来保证结构体的内存布局和 C 是兼容的。

下面通过 cargo build 命令编译成动态库,Python 负责调用。

from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
class Girl(Structure):
    _fields_ = [
        ("name", c_char_p),
        ("age", c_uint8),
    ]
# 指定 create_struct 的返回值类型为 Girl
py_lib.create_struct.restype = Girl
girl = py_lib.create_struct(
    c_char_p("S 老师".encode("utf-8")),
    c_uint8(18)
)
print(girl.name.decode("utf-8"))  # S 老师
print(girl.age)  # 18

调用成功,并且此时是没有内存泄露的。

当通过 FFI 将数据从 Rust 传递到 Python 时,如果传递的是指针,那么会涉及内存释放的问题。但如果传递的是值,那么它会复制一份给 Python,而原始的值(这里是结构体实例)会被自动销毁,所以无需担心。

然后是结构体内部的字段,虽然里面的 name 字段是 *mut c_char,但它的值是由 Python 传过来的,而不是在 Rust 内部创建的,因此没有问题。

但如果将 Rust 代码改一下:

use std::ffi::{c_char, CString};
#[repr(C)]
pub struct Girl {
    pub name: *mut c_char,
    pub age: u8,
}
#[no_mangle]
pub extern "C" fn create_struct() -> Girl {
    let name = CString::new("S 老师").unwrap().into_raw();
    let age = 18;
    Girl { name, age }
}

这时就尴尬了,此时的字符串是 Rust 里面创建的,转成原始指针之后,Rust 将不再管理相应的堆内存(因为 into_raw 将所有权转移走了),此时就需要手动堆内存了。

from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
class Girl(Structure):
    _fields_ = [
        ("name", c_char_p),
        ("age", c_uint8),
    ]
# 指定 create_struct 的返回值类型为 Girl
py_lib.create_struct.restype = Girl
girl = py_lib.create_struct()
print(girl.name.decode("utf-8"))  # S 老师
print(girl.age)  # 18
# 直接传递 girl 即可,会释放 girl 里面的字段在堆区的内存
py_lib.free(girl)

此时就不会出现内存泄露了,在 free 的时候,将变量 girl 传进去,释放掉内部字段占用的堆内存。

当然,Rust 也可以返回结构体指针,通过 Box 实现。

#[no_mangle]
pub extern "C" fn create_struct() -> *mut Girl {
    let name = CString::new("S 老师").unwrap().into_raw();
    let age = 18;
    Box::into_raw(Box::new(Girl { name, age }))
}

注意:之前是 name 字段在堆上,但结构体实例在栈上,现在 name 字段和结构体实例都在堆上。

然后 Python 调用也很简单,关键是释放的问题。

from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
class Girl(Structure):
    _fields_ = [
        ("name", c_char_p),
        ("age", c_uint8),
    ]
# 此时返回值类型就变成了 c_void_p
# 当返回指针时,建议将返回值设置为 c_void_p
py_lib.create_struct.restype = c_void_p
# 拿到指针(一串整数)
ptr = py_lib.create_struct()
# 将指针转成指定的类型,而类型显然是 POINTER(Girl)
# 调用 POINTER(T) 的 contents 方法,拿到相应的结构体实例
girl = cast(ptr, POINTER(Girl)).contents
# 访问具体内容
print(girl.name.decode("utf-8"))  # S 老师
print(girl.age)  # 18
# 释放堆内存,这里的释放分为两步,并且顺序不能错
# 先 free(girl),释放掉内部字段(name)占用的堆内存
# 然后 free(c_void_p(ptr)),释放掉结构体实例 girl 占用的堆内存
py_lib.free(girl)
py_lib.free(c_void_p(ptr))

不难理解,只是在释放结构体实例的时候需要多留意,如果内部有字段占用堆内存,那么需要先将这些字段释放掉。而释放的方式是将结构体实例作为参数传给 free 函数,然后再传入 c_void_p 释放结构体实例。


回调函数



最后看一下 Python 如何传递函数给 Rust,因为 Python 和 Rust 之间使用的是 C ABI,所以函数必须遵循 C 的标准。

// calc 接收三个参数,前两个参数是 *const i32
// 最后一个参数是函数,它接收两个 *const i32,返回一个 i32
#[no_mangle]
pub extern "C" fn calc(
    a: *const i32, b: *const i32,
    op: extern "C" fn(*const i32, *const i32) -> i32
) -> i32
{
    op(a, b)
}

然后看看 Python 如何传递回调函数。

from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
# 基于 Python 函数创建 C 函数,通过 @CFUNCTYPE() 进行装饰
# CFUNCTYPE 第一个参数是返回值类型,剩余的参数是参数类型
@CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
def add(a, b):  # a、b 为 int *,通过 .contents.value 拿到具体的值
    return a.contents.value + b.contents.value
@CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
def sub(a, b):
    return a.contents.value - b.contents.value
@CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
def mul(a, b):
    return a.contents.value * b.contents.value
@CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
def div(a, b):
    return a.contents.value // b.contents.value
a = pointer(c_int(10))
b = pointer(c_int(2))
print(py_lib.calc(a, b, add))  # 12
print(py_lib.calc(a, b, sub))  # 8
print(py_lib.calc(a, b, mul))  # 20
print(py_lib.calc(a, b, div))  # 5

成功实现了向 Rust 传递回调函数,当然例子举得有点刻意了,比如参数类型指定为 i32 即可,没有必要使用指针。


小结



以上我们就介绍了 Python 如何调用 Rust 编译的动态库,再次强调一下,通过 ctypes 调用动态库是最方便、最简单的方式。它和 Python 的版本无关,也不涉及底层的 C 扩展,它只是将 Rust 编译成 C ABI 兼容的动态库,然后交给 Python 进行调用。

因此这也侧面要求,函数的参数和返回值的类型应该是 C 可以表示的类型,比如 Rust 函数不能返回一个 trait 对象。总之在调用动态库的时候,库函数内部的逻辑可以很复杂,但是参数和返回值最好要简单。

如果你发现 Python 代码存在大量的 CPU 密集型计算,并且不怎么涉及复杂的 Python 数据结构,那么不妨将这些计算交给 Rust。

以上就是本文的内容,后续有空我们介绍如何用 Rust 的 PyO3 来为 Python 编写扩展。PyO3 的定位类似于 Cython,用它来写扩展非常的方便,后续有机会我们详细聊一聊。

相关文章
|
15天前
|
网络协议 数据库连接 Python
python知识点100篇系列(17)-替换requests的python库httpx
【10月更文挑战第4天】Requests 是基于 Python 开发的 HTTP 库,使用简单,功能强大。然而,随着 Python 3.6 的发布,出现了 Requests 的替代品 —— httpx。httpx 继承了 Requests 的所有特性,并增加了对异步请求的支持,支持 HTTP/1.1 和 HTTP/2,能够发送同步和异步请求,适用于 WSGI 和 ASGI 应用。安装使用 httpx 需要 Python 3.6 及以上版本,异步请求则需要 Python 3.8 及以上。httpx 提供了 Client 和 AsyncClient,分别用于优化同步和异步请求的性能。
python知识点100篇系列(17)-替换requests的python库httpx
|
3天前
|
数据采集 数据可视化 数据处理
如何使用Python实现一个交易策略。主要步骤包括:导入所需库(如`pandas`、`numpy`、`matplotlib`)
本文介绍了如何使用Python实现一个交易策略。主要步骤包括:导入所需库(如`pandas`、`numpy`、`matplotlib`),加载历史数据,计算均线和其他技术指标,实现交易逻辑,记录和可视化交易结果。示例代码展示了如何根据均线交叉和价格条件进行开仓、止损和止盈操作。实际应用时需注意数据质量、交易成本和风险管理。
16 5
|
2天前
|
存储 数据挖掘 数据处理
Python数据分析:Pandas库的高效数据处理技巧
【10月更文挑战第26天】Python 是数据分析领域的热门语言,Pandas 库以其高效的数据处理功能成为数据科学家的利器。本文介绍 Pandas 在数据读取、筛选、分组、转换和合并等方面的高效技巧,并通过示例代码展示其实际应用。
11 1
|
11天前
|
数据可视化 数据挖掘 Python
Seaborn 库创建吸引人的统计图表
【10月更文挑战第11天】本文介绍了如何使用 Seaborn 库创建多种统计图表,包括散点图、箱线图、直方图、线性回归图、热力图等。通过具体示例和代码,展示了 Seaborn 在数据可视化中的强大功能和灵活性,帮助读者更好地理解和应用这一工具。
26 3
|
16天前
|
Linux Android开发 开发者
【Python】GUI:Kivy库环境安装与示例
这篇文章介绍了 Kivy 库的安装与使用示例。Kivy 是一个开源的 Python 库,支持多平台开发,适用于多点触控应用。文章详细说明了 Kivy 的主要特点、环境安装方法,并提供了两个示例:一个简单的 Hello World 应用和一个 BMI 计算器界面。
25 0
|
3月前
|
Rust 安全 Go
揭秘Rust语言:为何它能让你在编程江湖中,既安全驰骋又高效超车,颠覆你的编程世界观!
【8月更文挑战第31天】Rust 是一门新兴的系统级编程语言,以其卓越的安全性、高性能和强大的并发能力著称。它通过独特的所有权和借用检查机制解决了内存安全问题,使开发者既能享受 C/C++ 的性能,又能避免常见的内存错误。Rust 支持零成本抽象,确保高级抽象不牺牲性能,同时提供模块化和并发编程支持,适用于系统应用、嵌入式设备及网络服务等多种场景。从简单的 “Hello World” 程序到复杂的系统开发,Rust 正逐渐成为现代软件开发的热门选择。
61 1
|
2天前
|
Rust 安全 编译器
编程语言新宠:Rust语言的特性、优势与实战入门
【10月更文挑战第26天】Rust语言诞生于2006年,由Mozilla公司的Graydon Hoare发起。作为一门系统编程语言,Rust专注于安全和高性能。通过所有权系统和生命周期管理,Rust在编译期就能消除内存泄漏等问题,适用于操作系统、嵌入式系统等高可靠性场景。
11 2
|
3天前
|
Rust 安全 云计算
Rust语言入门:安全性与并发性的完美结合
【10月更文挑战第25天】Rust 是一种系统级编程语言,以其独特的安全性和并发性保障而著称。它提供了与 C 和 C++ 相当的性能,同时确保内存安全,避免了常见的安全问题。Rust 的所有权系统通过编译时检查保证内存安全,其零成本抽象设计使得抽象不会带来额外的性能开销。Rust 还提供了强大的并发编程工具,如线程、消息传递和原子操作,确保了数据竞争的编译时检测。这些特性使 Rust 成为编写高效、安全并发代码的理想选择。
8 0
|
20天前
|
Rust 安全 网络安全
在 Rust 语言中,寻找企业上网行为管理软件的突破
在数字化企业环境中,上网行为管理软件对于保障网络安全和提升工作效率至关重要。Rust 语言凭借其安全性、高性能和并发性,为开发此类软件提供了新机遇。本文通过几个 Rust 代码示例,展示了如何实现网址检查、访问频率统计及访问控制等功能,旨在探索 Rust 在企业上网行为管理中的应用潜力。
29 0
|
3月前
|
Rust 安全 编译器
初探 Rust 语言与环境搭建
Rust 是一门始于2006年的系统编程语言,由Mozilla研究员Graydon Hoare发起,旨在确保内存安全而不牺牲性能。通过所有权、借用和生命周期机制,Rust避免了空指针和数据竞争等问题,简化了并发编程。相较于C/C++,Rust在编译时预防内存错误,提供类似C++的语法和更高的安全性。Rust适用于系统编程、WebAssembly、嵌入式系统和工具开发等领域。其生态系统包括Cargo包管理器和活跃社区。学习资源如&quot;The Book&quot;和&quot;Rust by Example&quot;帮助新手入门。安装Rust可通过Rustup进行,支持跨平台操作。
145 2
初探 Rust 语言与环境搭建