Following these answers, I've currently defined a Rust 1.0 function as follows, in order to be callable from Python using 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
}
And I'm setting up the Python part like so:
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)
This seems to be OK, but I'm:
- Not sure how to transform
combined
(aVec<(i32, i32)>
) to a C-compatible structure, so it can be returned to my Python script. - Not sure whether I should be returning a reference (
return &combined
?) and how I would have to annotate the function with the appropriate lifetime specifier if I did
The most important thing to note is that there is no such thing as a tuple in C. C is the lingua franca of library interoperability, and you will be required to restrict yourself to abilities of this language. It doesn't matter if you are talking between Rust and another high-level language; you have to speak C.
There may not be tuples in C, but there are
struct
s. A two-element tuple is just a struct with two members!Let's start with the C code that we would write:
We've defined two
struct
s — one to represent our tuple, and another to represent an array, as we will be passing those back and forth a bit.We will follow this up by defining the exact same structs in Rust and define them to have the exact same members (types, ordering, names). Importantly, we use
#[repr(C)]
to let the Rust compiler know to not do anything funky with reordering the data.We must never accept or return non-
repr(C)
types across the FFI boundary, so we pass across ourArray
. Note that there's a good amount ofunsafe
code, as we have to convert an unknown pointer to data (c_void
) to a specific type. That's the price of being generic in C world.Let's turn our eye to Python now. Basically, we just have to mimic what the C code did:
Forgive my rudimentary Python.
I'm sure an experienced Pythonista could make this look a lot prettier!Thanks to @eryksun for some nice advice on how to make the consumer side of calling the method much nicer.A word about ownership and memory leaks
In this example code, we've leaked the memory allocated by the
Vec
. Theoretically, the FFI code now owns the memory, but realistically, it can't do anything useful with it. To have a fully correct example, you'd need to add another method that would accept the pointer back from the callee, transform it back into aVec
, then allow Rust to drop the value. This is the only safe way, as Rust is almost guaranteed to use a different memory allocator than the one your FFI language is using.No, you don't want to (read: can't) return a reference. If you could, then the ownership of the item would end with the function call, and the reference would point to nothing. This is why we need to do the two-step dance with
mem::forget
and returning a raw pointer.