将Rust元组向量转换为C兼容结构体

17

参考这些回答,我目前已经定义了一个Rust 1.0函数如下,以便可以使用ctypes从Python中调用:

use std::vec;

extern crate libc;
use libc::{c_int, c_float, size_t};
use std::slice;

#[no_mangle]
pub extern fn convert_vec(input_lon: *const c_float, 
                          lon_size: size_t, 
                          input_lat: *const c_float, 
                          lat_size: size_t) -> Vec<(i32, i32)> {
    let input_lon = unsafe {
        slice::from_raw_parts(input_lon, lon_size as usize)
    };
    let input_lat = unsafe {
        slice::from_raw_parts(input_lat, lat_size as usize)
    };

    let combined: Vec<(i32, i32)> = input_lon
        .iter()
        .zip(input_lat.iter())
        .map(|each| convert(*each.0, *each.1))
        .collect();
    return combined
}

我正在这样设置Python部分:

from ctypes import *

class Int32_2(Structure):
    _fields_ = [("array", c_int32 * 2)]

rust_bng_vec = lib.convert_vec_py
rust_bng_vec.argtypes = [POINTER(c_float), c_size_t, 
                         POINTER(c_float), c_size_t]
rust_bng_vec.restype = POINTER(Int32_2)

这看起来没问题,但我有以下两个疑问:

  • 不确定如何将combined(一个Vec<(i32, i32)>)转换为C兼容结构,以便可以返回给我的Python脚本。
  • 不确定是否应该返回引用(return &combined?),如果是的话,我将如何使用适当的生命周期注释函数。

请勿在您的问题中提供解决方案。如果您想分享适用于您的代码,可以发布自己的答案。 - nobody
1个回答

19
请注意的最重要的一点是,在C语言中没有元组这个概念。C语言是库互操作的通用语言,你需要限制自己只使用该语言的能力。无论你是在Rust和另一种高级语言之间交流,你都必须使用C语言。
虽然C语言中没有元组,但是有结构体。一个包含两个元素的元组就是一个有两个成员的结构体!
让我们从我们将要编写的C代码开始:
#include <stdio.h>
#include <stdint.h>

typedef struct {
  uint32_t a;
  uint32_t b;
} tuple_t;

typedef struct {
  void *data;
  size_t len;
} array_t;

extern array_t convert_vec(array_t lat, array_t lon);

int main() {
  uint32_t lats[3] = {0, 1, 2};
  uint32_t lons[3] = {9, 8, 7};

  array_t lat = { .data = lats, .len = 3 };
  array_t lon = { .data = lons, .len = 3 };

  array_t fixed = convert_vec(lat, lon);
  tuple_t *real = fixed.data;

  for (int i = 0; i < fixed.len; i++) {
    printf("%d, %d\n", real[i].a, real[i].b);
  }

  return 0;
}

我们定义了两个“struct”——一个用于表示我们的元组,另一个用于表示数组,因为我们将会在它们之间传递一些数据。
接下来,我们将在Rust中定义完全相同的结构体,并将它们定义为具有完全相同的成员(类型、顺序、名称)。重要的是,我们使用“#[repr(C)]”来让Rust编译器知道不要对数据重新排序。
extern crate libc;

use std::slice;
use std::mem;

#[repr(C)]
pub struct Tuple {
    a: libc::uint32_t,
    b: libc::uint32_t,
}

#[repr(C)]
pub struct Array {
    data: *const libc::c_void,
    len: libc::size_t,
}

impl Array {
    unsafe fn as_u32_slice(&self) -> &[u32] {
        assert!(!self.data.is_null());
        slice::from_raw_parts(self.data as *const u32, self.len as usize)
    }

    fn from_vec<T>(mut vec: Vec<T>) -> Array {
        // Important to make length and capacity match
        // A better solution is to track both length and capacity
        vec.shrink_to_fit();

        let array = Array { data: vec.as_ptr() as *const libc::c_void, len: vec.len() as libc::size_t };

        // Whee! Leak the memory, and now the raw pointer (and
        // eventually C) is the owner.
        mem::forget(vec);

        array
    }
}

#[no_mangle]
pub extern fn convert_vec(lon: Array, lat: Array) -> Array {
    let lon = unsafe { lon.as_u32_slice() };
    let lat = unsafe { lat.as_u32_slice() };

    let vec =
        lat.iter().zip(lon.iter())
        .map(|(&lat, &lon)| Tuple { a: lat, b: lon })
        .collect();

    Array::from_vec(vec)
}

我们必须始终拒绝或返回FFI边界之外的非“repr(C)”类型,因此我们通过传递我们的“Array”来进行传递。请注意,有相当数量的“unsafe”代码,因为我们必须将未知指针转换为数据(“c_void”)到特定类型。这是在C世界中通用的代价。
现在让我们转向Python。基本上,我们只需要模仿C代码所做的:
import ctypes

class FFITuple(ctypes.Structure):
    _fields_ = [("a", ctypes.c_uint32),
                ("b", ctypes.c_uint32)]

class FFIArray(ctypes.Structure):
    _fields_ = [("data", ctypes.c_void_p),
                ("len", ctypes.c_size_t)]

    # Allow implicit conversions from a sequence of 32-bit unsigned
    # integers.
    @classmethod
    def from_param(cls, seq):
        return cls(seq)

    # Wrap sequence of values. You can specify another type besides a
    # 32-bit unsigned integer.
    def __init__(self, seq, data_type = ctypes.c_uint32):
        array_type = data_type * len(seq)
        raw_seq = array_type(*seq)
        self.data = ctypes.cast(raw_seq, ctypes.c_void_p)
        self.len = len(seq)

# A conversion function that cleans up the result value to make it
# nicer to consume.
def void_array_to_tuple_list(array, _func, _args):
    tuple_array = ctypes.cast(array.data, ctypes.POINTER(FFITuple))
    return [tuple_array[i] for i in range(0, array.len)]

lib = ctypes.cdll.LoadLibrary("./target/debug/libtupleffi.dylib")

lib.convert_vec.argtypes = (FFIArray, FFIArray)
lib.convert_vec.restype = FFIArray
lib.convert_vec.errcheck = void_array_to_tuple_list

for tupl in lib.convert_vec([1,2,3], [9,8,7]):
    print tupl.a, tupl.b

请原谅我简陋的Python。 我相信有经验的Pythonista可以让这个看起来更漂亮! 感谢@eryksun提供一些不错的建议,使调用方法的消费方面变得更加美好。

关于所有权和内存泄漏的说明

在这个示例代码中,我们泄漏了Vec分配的内存。理论上,FFI代码现在拥有这段内存,但实际上它无法对其进行任何有用的操作。要有一个完全正确的示例,您需要添加另一个方法,该方法将接受来自被调用者的指针,将其转换回Vec,然后允许Rust删除该值。这是唯一安全的方式,因为Rust几乎肯定会使用与您的FFI语言使用的不同的内存分配器。

不确定是否应该返回引用,如果我这样做,我将如何注释函数以使用适当的生命周期说明符

不,你不想(读作:不能)返回一个引用。如果可以的话,那么项目的所有权将在函数调用结束时结束,并且引用将指向空。这就是为什么我们需要使用mem::forget和返回原始指针进行两步操作的原因。

这非常详尽。非常感谢你。 - urschrei
这里是from_paramerrcheck的文档链接:from_paramerrcheck - Eryk Sun
@eryksun,您能否提供一个基本示例,包含您的建议? - urschrei
@eryksun,我已经更新了,再次感谢!我最不确定的部分是errcheck函数;按照我写的方式是否有意义? - Shepmaster
1
我会使用 return ctypes.cast(array.data, ctypes.POINTER(FFITuple * array.len))[0] 来返回一个 FFITuple 数组。另外,我忘记了在 from_param 中应该传递类的实例,例如 return arg if isinstance(arg, cls) else cls(arg) - Eryk Sun
@Shepmaster +1 好主意。关于你的陈述:“为了有一个完全正确的例子,你需要添加另一个方法,该方法将接受来自被调用者的指针,将其转换回Vec,然后允许Rust丢弃该值” - 在Rust中是否可行?你能从Python返回相同的指针,并允许Rust捕获相同的并且丢弃它吗? - Waffle's Crazy Peanut

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接