An apparent calling convention mismatch exists where the position and contents of arguments are incorrect when loading a small function using Python's Ctypes module.
In the example I built up while trying to get something working, one positional argument gets another's value while the other gets garbage.
The Ctypes docs state that cdll.LoadLibrary
expects the cdecl
convention. Resulting standard boilerplate:
# Tell Rustc to output a dynamically linked library
crate-type = ["cdylib"]
// Specify clean symbol and cdecl calling convention
#[no_mangle]
pub extern "cdecl" fn boring_function(
n: *mut size_t,
in_data: *mut [c_ulong],
out_data: *mut [c_double],
garbage: *mut [c_double],
) -> c_int {
//...
Loading our library after build...
lib = ctypes.CDLL("nothing/lib/playtoys.so")
lib.boring_function.restype = ctypes.c_int
Load the result into Python and call it with some initialized data
data_len = 8
in_array_t = ctypes.c_ulong * data_len
out_array_t = ctypes.c_double * data_len
in_array = in_array_t(7, 7, 7, 7, 7, 8, 7, 7)
out_array = out_array_t(10000.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9)
val = ctypes.c_size_t(data_len)
in_array_p = ctypes.byref(in_array)
out_array_p = ctypes.byref(out_array)
n_p = ctypes.byref(val)
garbage = n_p
res = boring_function(n_p,
in_array_p,
# garbage cannot be observed in any callee arg
ctypes.cast(garbage, ctypes.POINTER(out_array_t)),
out_array_p)
Notice the garbage
parameter. It is so-named because it winds up containing a garbage address. Note that its position is swapped with out_array_p
in the Python call and the Rust declaration.
[src/hello.rs:29] n = 0x00007f56dbce5bc0
[src/hello.rs:30] in_data = 0x00007f56f81e3270
[src/hello.rs:31] out_data = 0x00007f56f81e3230
[src/hello.rs:32] garbage = 0x000000000000000a
in_data
, out_data
, and n
print the correct values in this configuration. The positional swap between garbage
and out_data
makes this possible.
Other examples using more or less arguments reveal similar patterns of intermediate ordered variables containing odd values that resemble addresses earlier in the program or unrelated garbage.
Either I'm missing something in how I set up the calling convention or some special magic in argtypes
must be missing. So far I had no luck with changing the declared calling conventions or explicit argtypes. Are there any other knobs I should try turning?
in_data: *mut [c_ulong],
A slice is not a FFI-safe data type. Namely, Rust's slices use fat pointers, which take up two pointer-sized values.
You need to pass the data pointer and length as two separate arguments.
See also:
The complete example from the Omnibus:
extern crate libc; use libc::{uint32_t, size_t}; use std::slice; #[no_mangle] pub extern fn sum_of_even(n: *const uint32_t, len: size_t) -> uint32_t { let numbers = unsafe { assert!(!n.is_null()); slice::from_raw_parts(n, len as usize) }; let sum = numbers.iter() .filter(|&v| v % 2 == 0) .fold(0, |acc, v| acc + v); sum as uint32_t }
#!/usr/bin/env python3 import sys, ctypes from ctypes import POINTER, c_uint32, c_size_t prefix = {'win32': ''}.get(sys.platform, 'lib') extension = {'darwin': '.dylib', 'win32': '.dll'}.get(sys.platform, '.so') lib = ctypes.cdll.LoadLibrary(prefix + "slice_arguments" + extension) lib.sum_of_even.argtypes = (POINTER(c_uint32), c_size_t) lib.sum_of_even.restype = ctypes.c_uint32 def sum_of_even(numbers): buf_type = c_uint32 * len(numbers) buf = buf_type(*numbers) return lib.sum_of_even(buf, len(numbers)) print(sum_of_even([1,2,3,4,5,6]))
Disclaimer: I am the primary author of the Omnibus