Search code examples
pythonrustctypesfficalling-convention

Why do I get a calling convention mismatch when passing a pointer to a slice with a cdecl Rust function and Python's Ctypes?


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?


Solution

  • 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