Преобразование вектора ржавчины кортежей в C-совместимую структуру

Следуя этим , я в настоящее время определил функцию Rust 1.0 следующим образом, чтобы быть вызванным из Python с помощью ctypes:

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 (a Vec<(i32, i32)>) в C-совместимую структуру, поэтому его можно вернуть на мой Python script.
  • Не уверен, должен ли я возвращать ссылку (return &combined?) и как мне пришлось бы аннотировать функцию с соответствующим спецификатором времени жизни, если бы я сделал

Ответы

Ответ 1

Самое главное отметить, что не такая вещь, как кортеж в C. C является лингва-франкой библиотечной совместимости, и вам придется ограничить себя возможностями этого язык. Неважно, если вы говорите между Rust и другим языком высокого уровня; вы должны говорить C.

В C не может быть кортежей, но есть struct s. Двухэлементный кортеж - это просто структура с двумя членами!

Начнем с кода 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)
}

Мы не должны принимать или возвращать типы не repr(C) по границе FFI, поэтому мы проходим через наш Array. Обратите внимание, что существует хороший код unsafe, так как мы должны преобразовать неизвестный указатель на данные (c_void) в определенный тип. Это цена родового в мире C.

Теперь повернемся к Питону. В принципе, нам просто нужно подражать тому, что сделал код 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 и вернуть необработанный указатель.