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

我们知道 Rust 专门提供了 4 个字节 char 类型来表示 unicode 字符 , 但对于外部导出函数来说,使用 char 是不安全的,所以直接使用 u8 和 u32 就行 。
编译之后 , Python 调用:
from ctypes import *py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")# u8 除了可以使用 c_byte 包装之外,还可以使用 c_char# 并且 c_byte 里面只能接收整数,而 c_char 除了整数,还可以接收长度为 1 的字节串print(c_byte(97))print(c_char(97))print(c_char(b"a"))"""c_byte(97)c_char(b'a')c_char(b'a')"""# 以上三者是等价的,因为 char 说白了就是个 u8# 指定返回值为 c_byte,会返回一个整数py_lib.get_char.restype = c_byte# c_byte(97)、c_char(97)、c_char(b"a") 都是等价的# 因为它们本质上都是 u8 , 至于 97 也可以解析为 u8print(py_lib.get_char(97))  # 98# 指定返回值为 c_char,会返回一个字符(长度为 1 的 bytes 对象)py_lib.get_char.restype = c_charprint(py_lib.get_char(97))  # b'b'py_lib.get_unicode.restype = c_wcharprint(py_lib.get_unicode(c_wchar("嘿")))  # 嘿# 直接传一个 u32 整数也可以,因为 unicode 字符底层就是个 u32print(py_lib.get_unicode(ord("憨")))  # 批以上就是字符类型的操作,比较简单 。

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

文章插图
 字符串类型再来看看字符串,我们用 Rust 实现一个函数,它接收一个字符串 , 然后返回大写形式 。
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 {    // 将 *const c_char 转成 &CStr    let s = unsafe {        CStr::from_ptr(s)    };    // 将 &CStr 转成 &str    // 然后调用 to_uppercase 转成大写,得到 String    let s = s.to_str().unwrap().to_uppercase();    // 将 String 转成 *mut char 返回    CString::new(s).unwrap().into_raw()}解释一下里面的 CStr 和 CString,在 Rust 中,CString 用于创建 C 风格的字符串(以  结尾),拥有自己的内存 。关键的是,CString 拥有值的所有权 , 当实例离开作用域时,它的析构函数会被调用,相关内存会被自动释放 。
而 CStr,它和 CString 之间的关系就像 str 和 String 的关系,所以 CStr 一般以引用的形式出现 。并且 CStr 没有 new 方法,不能直接创建,它需要通过 from_ptr 方法从原始指针转化得到 。
然后指针类型是 *const 和 *mut,分别表示指向 C 风格字符串的首字符的不可变指针和可变指针,它们的区别主要在于指向的数据是否可以被修改 。如果不需要修改,那么使用 *const 会更安全一些 。
我们编写 Python 代码测试一下 。
from ctypes import *py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")s = "hello 古明地觉".encode("utf-8")# 默认是按照整型解析的,所以不指定返回值类型的话,会得到脏数据print(py_lib.to_uppercase(c_char_p(s)))"""31916096"""# 指定返回值为 c_char_p,表示按照 char * 来解析py_lib.to_uppercase.restype = c_char_pprint(    py_lib.to_uppercase(c_char_p(s)).decode("utf-8"))"""HELLO 古明地觉"""从表面上看似乎挺顺利的 , 但背后隐藏着内存泄露的风险,因为 Rust 里面创建的 CString 还驻留在堆区 , 必须要将它释放掉 。所以我们还要写一个函数,用于释放字符串 。
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 free_cstring(s: *mut c_char) {    unsafe {        if s.is_null() { return }        // 基于原始指针创建 CString,拿到堆区字符串的所有权        // 然后离开作用域,自动释放        CString::from_raw(s)    };}


推荐阅读