FFI: How to pass a array with structs to a C func that fills the array (out-pointer?), and then how to access the items after in my Rust code?

Ok, been wanting to get into Rust a while now and also work on my NFC projects.
So I'm using nfc1-sys crate with my project right now.
nfc1-sys is just bindings to libnfc C-library if I understand correctly? I'm still learning to work with pointers from C code, but I think I figured a bit out already, until now.

The problem now is I have this libnfc C-function that is supposed to fill a array (targets) of a specific type with info about NFC tags detected.

Here is the func I'm calling:

extern "C" {
    pub fn nfc_initiator_list_passive_targets(
        pnd: *mut nfc_device,
        nm: nfc_modulation,
        ant: *mut nfc_target,
        szTargets: size_t,
    ) -> ::std::os::raw::c_int;
}

And here is my code I have tried using it:

// let mut targets: [MaybeUninit<nfc_target>; 8] = MaybeUninit::uninit_array();
// let mut targets: [MaybeUninit<nfc_target>; 8] = MaybeUninit::uninit().transpose();
let result = unsafe {
    nfc_initiator_list_passive_targets(self.device, modulation, targets, 8);
};

The part I'm having problems both getting to work and understanding is the "targets" argument.
If I understand libnfc and the bindings to Rust correctly I am supposed to get a array of the size 8 out of this function, every item in the array should be a nfc_target struct, filled with info.

I have already worked with libnfc in a C program and there I use the following code that works:
So here I create a array of the nfc_target struct and then pass that as a argument to the libnfc func that seems to accept a array (ant) fine:

nfc_initiator_list_passive_targets(nfc_device *pnd,
                                   const nfc_modulation nm,
                                   nfc_target ant[], const size_t szTargets)
nfc_target targets[8];
if (nfc_initiator_list_passive_targets(reader->device, modulation, targets, 8) > 0) {
    printf("Found a chip (or more) in field, selecting first found DESFire\n");
}

After that I can access info about the first tag by using targets[0]
I want to do something similar in my Rust program.
Preferably get a array of targets (tags) back from the func so I can get info about them and then select the one I need, but I would be happy to just get the first tag in targets array.
Is there a problem with in the C lib its an actually , but in the Rust binding its a pointer?

When looking for tips and trying to figure it out, I also found this issue:

After that I switched to Rust nightly but I still can't figure it out.
It seems this functionality I'm after, or working with is a something that is subject to change so I feel even more lost about how I should figure this out.

I also want to learn if I'm doing and thinking correctly, so I have some questions about the code too:
As I understand it, if I want to use a pointer as a argument to a C func, and that C func changes the data the pointer points to (a out-pointer, is that the right term?) I need to use maybeUninit first on the argument, because Rust don't like uninitialized pointers, right?
The problem seems to be that I actually have to initialize my pointer with a array of maybeUninit and the type of the struct, and that seems to complicate it a bit much for me to figure it out.

This code complains about a unstable feature so I switched to Nightly, but I'm unsure if I'm using it correctly, like this line:
let mut targets: [MaybeUninit<nfc_target>; 8] = MaybeUninit::uninit_array();

But how do I actually then pass the pointer to the libnfc func when the func seems to accept a *mut nfc_target type?

You can cast a *mut MaybeUninit<T> to a *mut T with as (e.g. ptr as *mut Device) or by calling the pointer's cast() method.

The convention in C is that the caller should allocate an array (either on the stack or with malloc) and pass it to the nfc_initiator_list_passive_targets() function so there is somewhere to add the nfc_devices. In C, arrays are just a pointer to the first element so you need to tell the function how big the array is (szTargets) otherwise it could write past the end of your array (a buffer overrun!).

Now, because there's no guarantee there'll be exactly 8 (or however big your buffer is) devices, the C function will tell you how many devices were actually added to the array. This is typically done by returning an int, where a negative number indicates an error occurred, and a positive number indicates the number of devices that were initialized.

Correct!

In general, wherever you'd use an out-pointer to an uninitialized variable in C, Rust would want to use a MaybeUninit variable then pass in my_variable.as_mut_ptr() (a pointer to the uninitialized variable).

If you want to initialize an array on the stack, you can do something like this:

#![feature(maybe_uninit_slice, maybe_uninit_uninit_array)]

let mut devices: [MaybeUninit<Device>; 8] = MaybeUninit::uninit_array();

unsafe {
  let num_initialized = initialize_devices(
    devices.as_mut_ptr().cast(), 
    devices.len() as c_int,
  );
  let initialized: &[Device] = MaybeUninit::slice_assume_init_ref(
    &devices[..num_initialized],
  );
}

A simpler way that doesn't require nightly features is to create a Vec and pass in a pointer to the uninitialized parts of the Vec's buffer.

let mut devices: Vec<Device> = Vec::with_capacity(8);

unsafe {
  // Get a reference to the uninitialized part of our Vec's buffer
  let uninitialized = devices.spare_capacity_mut();

  //
  let num_initialized = initialize_devices(
    uninitialized.as_mut_ptr().cast(),
    uninitialized.len() as c_int,
  );

  // Tell the Vec that we've manually initialized some elements
  devices.set_len(num_initialized as usize);
}

One big benefit this approach has is that you can pass the initialized elements around afterwards (e.g. to return them from a function). With the array version you'd either be stuck with the &[Device] slice we got from MaybeUninit::slice_assume_init_ref(), or we'd need to somehow copy the initialized Devices into a Vec anyway.

You can use this example on the playground if you want to try different things out.

The Vec::set_len() docs have a good example of initializing part of a Vec via FFI.

4 Likes

Your misunderstanding is that [] in a C function signature does not mean an array, it means a pointer (cf. this). This:

void foo(int x[]);

means exactly the same as

void foo(int *x);

and even

void foo(int x[3]);

with an explicit size has the exact same meaning in a signature. (Of course, they are not the same thing in variable or struct field declaration position.)

It's a misleading convention, but you can't pass an array by-value to a function; you can only ever pass a pointer, because the array is said to "decay into a pointer" when passed to a function (and in fact, in most expressions, arrays implicitly convert to pointers similarly; a notable exception is the argument of sizeof). Therefore your function signature was correctly transcribed (by bindgen, I presume) to feature a pointer, rather than an array.

4 Likes