#How to use Rust with FFI; in my case LabVIEW

161 messages · Page 1 of 1 (latest)

soft grotto
#

I want to write a small library in another language (LabVIEW, LV) to work with HID USB devices because I can't find an existing LV library for it. I think rather than working with the low level Windows API function calls in LV I want to write a nice API in rust and create a DLL from it, then call the DLL functions in LV.

The functions I'll need are:

  1. List serial numbers of connected devices with a passed in VID (vendor ID), PID (product ID), and Manufacture string
  2. Connect by VID, PID, and serial number string
  3. Write
  4. Read
  5. Close

I plan to use the hidapi crate.

I've started following the Nomicon from https://doc.rust-lang.org/nomicon/ffi.html#calling-rust-code-from-c

I'm probably going to have several questions.

My first question is how do I take a string as input to rust function? I think I understand how strings in C end with a \0 NUL character and how arrays of other types are pointers with a separate length value. But what is the right way to pass in a string to a DLL and the syntax for rust? Note: I vaguly remember trying this before and LV may have not worked nicely with the C string way of using NUL.

soft grotto
#

Second question How do I output the HIDDevice (which is basically a handle for the device? When I just return the HIDDevice type out of the DLL function I get this waring.

#
warning: `extern` fn uses type `HidDevice`, which is not FFI-safe
  --> src\lib.rs:21:29
   |
21 | pub extern "C" fn open() -> hidapi::HidDevice {
   |                             ^^^^^^^^^^^^^^^^^ not FFI-safe
   |
   = help: consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct
   = note: this struct has unspecified layout
rustic badger
soft grotto
#

When I used CString in the function signature I got this warning

warning: `extern` fn uses type `CString`, which is not FFI-safe
 --> src\lib.rs:9:53
  |
9 | pub extern "C" fn open1(vid: u16, pid: u16, serial: CString) -> bool {
  |                                                     ^^^^^^^ not FFI-safe
  |
  = help: consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct
  = note: this struct has unspecified layout
  = note: `#[warn(improper_ctypes_definitions)]` on by default
soft grotto
rustic badger
soft grotto
#

This is the code I have now. With the error above.

use std::ffi::CString;

#[no_mangle]
pub extern "C" fn open1(vid: u16, pid: u16, serial: CString) -> bool {
    let serial = serial.into_string().unwrap();
    // .......other code......
    false
}
rustic badger
#

Then you can properly extract it as Rust types

#

So you might write

#[no_mangle]
pub extern "C" fn open1(vid: u16, pid: u16, serial: *const c_char) -> bool {
    let serial = CStr::from_ptr(serial).to_string_lossy().into_owned();
    // .......other code......
    false
}
soft grotto
#

I just had to put the from_ptr function in an unsafe block. Would it be best practice to check if the pointer is null or anything?

soft grotto
# rustic badger So you might write ```rs #[no_mangle] pub extern "C" fn open1(vid: u16, pid: u16...

I haven't completely figured out all the safety things but do you think this is ok? In LV I've configured it to have an argument type of String with a String format of "C String Pointer" and Minimum size of <None>. At the bottom it shows a Function prototype (I guess that's like a function signature) with the type being CStr.

On the rust side I'm using the type *const c_char and converting it to a Rust String like in our last couple messages.

#

It does at least appear to work.

rustic badger
#

Sorry, I have no idea what that UI is

soft grotto
#

Yeah, gotcha. Does the function prototype near the bottom appear right as though that were C code?

rustic badger
#

Yes, that looks like a C signature

#

But uh, not sure why CStr would be in C

#

C would write that as char *serial

soft grotto
#

Does CStr have a meaning in C code?

#

I'll follow up on the with LV support on that.

#
warning: `extern` fn uses type `HidDevice`, which is not FFI-safe
  --> src\lib.rs:21:29
   |
21 | pub extern "C" fn open() -> hidapi::HidDevice {
   |                             ^^^^^^^^^^^^^^^^^ not FFI-safe
   |
   = help: consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct
   = note: this struct has unspecified layout
tall osprey
#

No, cstr is a rust type

soft grotto
#

How do I output HidDevice so I can pass it around in LV? I don't need to modify the structure in the LV side, I would only pass it back into the same DLL so maybe I can keep the data owned by the DLL and only pass an integer ID out. That might be a bad idea though for memory safety, idk.

tall osprey
#

Do you own hiddevice?

#

Or is it from another crate

soft grotto
#

It's the hidapi crate. I don't own it.

tall osprey
#

I think you are not meant to do that

soft grotto
#

Ok. I guess you mean the author of hidapi didn't intend me to pass it out of a DLL (at least easily) but how would I accomplish the goal of writing a DLL with rust that makes working with HID devices easier for another DLL compatible language?

tall osprey
#

You could return a pointer to it

#

You know, like how C libs like handing out opaque pointers

soft grotto
soft grotto
#

Or does it even care?

tall osprey
#

They dont, only you know the layout

#

Theyd have to call your functions and give it back to do anything

soft grotto
#

Ok, that's making since. So in other words only rust knows the size and structure of the HidDevice object and when another rust function recieves the pointer, it will be able to reinterpret the object. Right?

tall osprey
#

Yes, but only the rust functions from the same dll

#

The trait object in it has an unspecified layout, only code from the same compilation can safely manipulate it

#

There is also #dark-arts for these kinds of questions btw

soft grotto
#

Should I output the pointer in the return type or use an out parameter?

#

I'll need to do some error handling too (e.g. in case the device is not connected).

tall osprey
#

You could return null in that case

soft grotto
#

What if I wanted to differentiate between multiple types of errors?

tall osprey
#

The C way of handling that is to store that information in global variables

#

Then when you find a function returned null, you grab the error from the global

#

I guess an out variable works too

soft grotto
#

Hmm, ok. I have no idea how to interact with C global variables in LV so I'll put that off until later and ask someone on the LV side.

#

What's the syntax for getting a pointer to an object and returning it in as the return type?

unkempt turret
#

you can just cast a reference into a pointer

#

you just have to be careful not to return pointers to locals

somber violet
somber violet
soft grotto
#
#[no_mangle]
pub extern "C" fn daq_open(vid: u16, pid: u16, serial: *const c_char) -> i32 {
    let serial = unsafe{CStr::from_ptr(serial)}.to_string_lossy().into_owned();
    println!("From rust, serial: {}", serial);
    let device_handle = Device::open(vid, pid, &serial).unwrap();
    0
}
#

I want to return a pointer to device_handle

#

The Device struct is in another module that works with the hidapi crate.

soft grotto
# somber violet can you be specific about what code you want to make work?

Is this how you would do it?

#[no_mangle]
pub extern "C" fn daq_open(vid: u16, pid: u16, serial: *const c_char) -> *mut Device {
    let serial = unsafe{CStr::from_ptr(serial)}.to_string_lossy().into_owned();
    println!("From rust, serial: {}", serial);
    let device_handle = Device::open(vid, pid, &serial).unwrap();
    Box::into_raw(Box::new(device_handle))
}
somber violet
#

oh hello.

somber violet
somber violet
#

what crate is this?

soft grotto
#

The Device struct is just a bit of my own functionality around the hidapi crate.

somber violet
#

ah

soft grotto
#

Thanks. I think this is working for now but I'll probably have more questions next week.

somber violet
#

The main thing you have to be wary of is that all of the Rust types in that crate are allocated.

#

and none are repr(C)

soft grotto
#

So using the Box type solves and getting the pointer to the box solves this?

#

Why is the return type *mut Device and not just *Device? Is it saying this function no longer bears responsibility for dropping the memory?

somber violet
#

Yes, Box is basically "malloc this".

#

and then using into_raw pulls out the pointer.

#

it is just a pointer, though in Rust, a pointer can be an address + metadata and thus not FFI compatible. (not a concern here, just a "general information" thing)

#

And yes, because you've leaked the pointer, that function no longer bears responsibility for dropping the memory.

soft grotto
#

👍

untold dawn
somber violet
#

doh

#

yes.

soft grotto
steady lichen
#

It's just a pointer to const

soft grotto
#

And would I be incorrect in saying all the values (whether pointed to or not) in the function signature of the above example are const because of the absence of them being declared mut?

tall osprey
#

but, the difference between *const and *mut are mostly just a lint

soft grotto
#

With this code, clippy is complaining.

mod device;

use device::Device;
use std::ffi::{CStr, c_char};

#[no_mangle]
pub extern "C" fn daq_open(vid: u16, pid: u16, serial: *const c_char) -> *mut Device {
    if serial.is_null() {
        return std::ptr::null_mut();
    }
    let serial = unsafe{CStr::from_ptr(serial)}.to_string_lossy().into_owned();
    let device_handle = Device::open(vid, pid, &serial).unwrap();
    Box::into_raw(Box::new(device_handle))
}
#
error: this public function might dereference a raw pointer but is not marked `unsafe`
  --> src\lib.rs:11:40
   |
11 |     let serial = unsafe{CStr::from_ptr(serial)}.to_string_lossy().into_owned();
   |                                        ^^^^^^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#not_unsafe_ptr_arg_deref
   = note: `#[deny(clippy::not_unsafe_ptr_arg_deref)]` on by default
tall osprey
#

well the link explains it

soft grotto
#

Before the unsafe conversion from pointer to CStr I checked that serial is not null, so what am I missing?

tall osprey
#

but it doesnt really matter

#

if i call this function with a bogus raw pointer and your program blows up, whose fault is it?

soft grotto
#

So the function should be marked as unsafe AND extern? I thought I read somewhere extern functions were implicitly unsafe so I'm confused why this lint is complaining.

tall osprey
#

They are unsafe to call if they're declarations in an extern C block

soft grotto
#

ok

#

In this code is it the sensible thing to do to use Box::from_raw() and then Box::into_raw() or is there a single function that would do the same thing?

#
#[no_mangle]
pub extern "C" fn daq_write(device_handle: *mut Device, command: u8, data: u16) -> i32 {
    if device_handle.is_null() {
        return 1;
    }
    let device_handle = unsafe {Box::from_raw(device_handle)};
    device_handle.write(command, data);
    Box::into_raw(Box::new(device_handle));
    0
}
tall osprey
#

no need to make a box

#

just make a &mut

#

or &

#

so rust #[no_mangle] pub extern "C" fn daq_write(device_handle: *mut Device, command: u8, data: u16) -> i32 { if device_handle.is_null() { return 1; } let device_handle = unsafe { &*device_handle }; device_handle.write(command, data); 0 }

#

Or rust let device_handle = unsafe { &mut *device_handle };

soft grotto
#

Ok. In the first case with unsafe { &*device_handle }; would my intuition be write the *mut Device in the function signature could appropriately be changed to *const Device?

tall osprey
#

just keep it *mut

soft grotto
#

k

#

Thanks for all the help so far! A few more questions yet.

#

I've noticed that the rust code panicking makes LV freeze and I think I've heard that panicking in a DLL produced undefined behavior. Is there a best practice way to make sure the rust code either never panics in a DLL context or when it does, exits gracefully by returning in some defined way?

#

Theoretically, I could manually vet my code and remove all .unwraps() and .expects() but this doesn't scale well when I'm using creates maintained by others and something as simple as indexing a Vec with square brackets in the wrong way could cause a panic.

steady lichen
#

If you'd like something less final, we have catch_unwind, but note panics are meant to represent bugs, not recoverable conditions, so you probably don't want to do too much with this recovery

soft grotto
# steady lichen As of a while ago, unless you're explicitly using an ABI (the string after `exte...

an uncaught panic simply becomes an abort

This appears to contradict that.

https://doc.rust-lang.org/std/panic/fn.catch_unwind.html

It is currently undefined behavior to unwind from Rust code into foreign code, so this function is particularly useful when Rust is called from another language (normally C). This can run arbitrary Rust code, capturing a panic and allowing a graceful handling of the error.

tall osprey
#

It depends on your rust version

soft grotto
#

I'm on the latest stable rust version.

tall osprey
#

you can compile with panic=abort so that panics always abort, which is the easiest way to solve this

soft grotto
soft grotto
# steady lichen If you'd like something less final, we have `catch_unwind`, but note panics are ...

Does this look right?

#[no_mangle]
pub unsafe extern "C" fn daq_write(device_handle: *mut Device, command: u8, data: u16) -> i32 {
    if device_handle.is_null() {
        return 2;
    }
    let device_handle = unsafe {&*device_handle};
    let device_handle = AssertUnwindSafe(device_handle);
    catch_unwind(|| {
        device_handle.write(command, data);
        0
    }).unwrap_or(1)
}

I tried doing like this

#[no_mangle]
pub unsafe extern "C" fn daq_write(device_handle: *mut Device, command: u8, data: u16) -> i32 {
    let device_handle = AssertUnwindSafe(device_handle);
    catch_unwind(|| {
        if device_handle.is_null() {
            return 2;
        }
        let device_handle = unsafe {&*device_handle};
        device_handle.write(command, data);
        0
    }).unwrap_or(1)
}

but that wrong. It's thinking the output of unsafe {&*device_handle} is &*mut Device where it should be AssertUnwindSafe<&Device> I think. How to I convert AssertUnwindSafe<*mut Device> to AssertUnwindSafe<&Device>?

#

Is what I'm doing with AssertUnwindSafe() even ok. I don't understand what could go wrong if it's used improperly.

tall osprey
#

try rust unsafe {&*(device_handle.0)};

#

it's in a tuple struct now

soft grotto
tall osprey
#

i'd recommend making a wrapper for doing this

soft grotto
#

Will do.

#

Is the AssertUnwindSafe thing obviously ok or not?

#

I read the documentation on it but if you could paraphase the docs in simpler form, that might help my understanding.

tall osprey
#

for example we use this in pyo3 to catch and resume panics ```rust
/// Implementation of trampoline functions, which sets up a GILPool and calls F.
///
/// Panics during execution are trapped so that they don't propagate through any
/// outer FFI boundary.
#[inline]
pub(crate) fn trampoline<F, R>(body: F) -> R
where
F: for<'py> FnOnce(Python<'py>) -> PyResult<R> + UnwindSafe,
R: PyCallbackOutput,
{
let trap = PanicTrap::new("uncaught panic at ffi boundary");
let pool = unsafe { GILPool::new() };
let py = pool.python();
let out = panic_result_into_callback_output(
py,
panic::catch_unwind(move || -> PyResult<_> { body(py) }),
);
trap.disarm();
out
}

/// Converts the output of std::panic::catch_unwind into a Python function output, either by raising a Python
/// exception or by unwrapping the contained success output.
#[inline]
fn panic_result_into_callback_output<R>(
py: Python<'_>,
panic_result: Result<PyResult<R>, Box<dyn Any + Send + 'static>>,
) -> R
where
R: PyCallbackOutput,
{
let py_err = match panic_result {
Ok(Ok(value)) => return value,
Ok(Err(py_err)) => py_err,
Err(payload) => PanicException::from_panic_payload(payload),
};
py_err.restore(py);
R::ERR_VALUE
}```

#

what unwindsafe means is whether your object (say, a data structure) is still coherent after it's been unwound out of

soft grotto
#

What does unwinding do the the object?

tall osprey
tall osprey
somber violet
soft grotto
#

Is AssertUnwindSafe() in other words a promise I make that either

  1. I know the type is implemented in such a way that it will be in a consistent state after an unwind OR
  2. In the event of an unwind I promise to be careful about the way I look at or modify the type.
#

In this case, since I am not the one that wrote the hidapi crate, I don't know that point 1 above is true, therefore in the event I catch a panic, I have to be careful how I look at the type and how I modify it.

#

In the event of an unwind, can we at least assume that we should deallocate (i.e. close) it?

#

If yes, how would it typically be handled? For us to immediately drop the object or for the calling code outside the DLL to be notified and subsequently call the DLL's close function?

tall osprey
#

i wouldnt worry about AssertUnwind, it's more meant for safe code

#

since you are writing unsafe code, you must always be ready for an unwind

#

but this is only really relevant if you are executing user code (like if you are cloning some T that the user gives you

tall osprey
soft grotto
soft grotto
#

windows in this case.

soft grotto
#

I got some help from @silent torrent about using catch_unwind() and AssertUnwindSafe() who also suggested I use Option<&mut Option<Device>> in the function signature of daq_write() and daq_close(). I figured out how to change it for daq_open(). Is my use of lifetime parameters correct? The outer option is a nice way of checking if the pointer passed in is null or not. The inner option represents if there has been an unwind in a previous call. If there is an unwind, I should avoid doing anything with the Device object even dropping it. Leaking memory is an undesirable bug but not a memory safety issue.

Perhaps, if there is an unwind I should error out all the way in the top level LabVIEW application, however I don't have an acceptable way to guarantee that will happen. Also, in the future maybe I want to do a high availability thing where even if there is a bug that gets triggered by one http client, I want to recover and keep the program running for other http clients. I think I've seen some web server crate catch unwinds using tasks or threads for this purpose.

Does this sound right?

This is what I have now:

#
#![deny(unsafe_op_in_unsafe_fn)]

mod device;

use device::Device;
use std::ffi::{CStr, c_char};
use std::panic::{catch_unwind, AssertUnwindSafe, UnwindSafe};

#[no_mangle]
pub unsafe extern "C" fn daq_open<'a>(vid: u16, pid: u16, serial: *const c_char) -> Option<&'a mut Option<Device>> {
    catch_unwind(|| {
        if serial.is_null() {
            return None;
        }
        let serial = unsafe{CStr::from_ptr(serial)}.to_string_lossy().into_owned();
        let device = Device::open(vid, pid, &serial).unwrap();
        let device_ptr = Box::into_raw(Box::new(Some(device)));
        unsafe{ device_ptr.as_mut() }
    }).unwrap_or(None)
}

#[no_mangle]
pub unsafe extern "C" fn daq_write(device_handle: Option<&mut Option<Device>>, command: u8, data: u16) -> i32 {
    operate_on_device(device_handle, |device| {
        match device.write(command, data) {
            Ok(()) => 0,  // Success
            Err(_) => 1,  // Error writing data
        }
    })
}

#[no_mangle]
pub unsafe extern "C" fn daq_close(device_handle: Option<&mut Option<Device>>) -> i32 {
    operate_on_device(device_handle, |device| {
        drop(unsafe {Box::from_raw(device)});
        0
    })
}

fn operate_on_device<F>(device_handle: Option<&mut Option<Device>>, f: F) -> i32
where
    F: FnOnce(&mut Device) -> i32 + UnwindSafe
{
    catch_unwind(AssertUnwindSafe(|| {
        match device_handle {
            None => {-1},  // Null pointer passed in
            Some(None) => {-3},  // Unwound in a previous DLL call
            Some(Some(device)) => f(device)  // Have a valid device
        }
    })).unwrap_or_else(|_| {  // Unwound
        if let Some(device_handle_option) = device_handle {
            device_handle_option.take();
        }
        -2
    })
}
silent torrent
#

just make sure to only drop it once using drop(option.take())

soft grotto
#

Ok. I guess operate_on_device() would be the place for that.

silent torrent
#

yep! any time you catch an unwind

soft grotto
#

That's not a memory safety thing though right?

silent torrent
#

nope, it just makes sure you won't leak memory
infact none of the whole "unwind safety" thing is memory safety. undefined behavior can't be caused by catching an unwind. it's just for controlling non-safety invariants

soft grotto
#

Ok.

soft grotto
# silent torrent just make sure to only drop it once using `drop(option.take())`

Actually we're already doing that near the end of operate_on_device() right?

And worrying about dropping in daq_open() seems unreasonable because once the pointer is created, I can see nothing is going to cause it to panic and my implementation of explicitly dropping add a lot of complexity.

use std::ptr;

#[no_mangle]
pub unsafe extern "C" fn daq_open<'a>(vid: u16, pid: u16, serial: *const c_char) -> Option<&'a mut Option<Device>> {
    let mut device_ptr = ptr::null_mut();
    catch_unwind(AssertUnwindSafe(|| {
        if serial.is_null() {
            return None;
        }
        let serial = unsafe{CStr::from_ptr(serial)}.to_string_lossy().into_owned();
        let device = Device::open(vid, pid, &serial).unwrap();
        device_ptr = Box::into_raw(Box::new(Some(device)));
        unsafe{ device_ptr.as_mut() }
    })).unwrap_or_else(|_| {
        if !device_ptr.is_null() {
            drop(unsafe { Box::from_raw(device_ptr) });
        }
        None
    })
}
soft grotto
#

With how I'm now using Option<&mut Option<Device>> with the outer Option representing if a pointer is null or non-null and the inner Option representing if the DLL has unwond in a previous call I did some work to change the inner Option to a RwLock.

The RwLock lock get's me a few things:

  1. Ensures myself that if I call a DLL function that needs a &mut Device in different threads, one thread will wait for the other. External code still has to ensure daq_close() is my last (and exclusive) use of the pointer.
  2. The RwLock will poison itself automatically if locked, having access to &mut Device (I don't have to implement that logic myself with the inner Option).
  3. Allows me to have access to either &Device or &mut Device as needed.
#
mod device;

use device::Device;
use std::ffi::{c_char, CStr};
use std::panic::{catch_unwind, AssertUnwindSafe, UnwindSafe};
use std::sync::RwLock;

const SUCCESS: i32 = 0;
const NULL_PTR_PASSED_IN: i32 = -1;
const UNWOUND: i32 = -2;
const UNWOUND_IN_A_PREVIOUS_CALL: i32 = -3;
#
#[no_mangle]
pub unsafe extern "C" fn daq_open<'a>(
    vid: u16,
    pid: u16,
    serial: *const c_char,
) -> Option<&'a mut RwLock<Device>> {
    catch_unwind(|| match serial.is_null() {
        true => None,
        false => {
            let serial = unsafe { CStr::from_ptr(serial) }
                .to_string_lossy()
                .into_owned();
            match Device::open(vid, pid, &serial) {
                Err(_) => None,
                Ok(device) => {
                    let device_ptr = Box::into_raw(Box::new(RwLock::new(device)));
                    unsafe { device_ptr.as_mut() }
                }
            }
        }
    })
    .unwrap_or(None)
}

#[no_mangle]
pub unsafe extern "C" fn daq_write(
    device_handle: Option<&RwLock<Device>>,
    command: u8,
    data: u16,
) -> i32 {
    operate_on(device_handle, |device| {
        const ERROR_WRITING_DATA: i32 = 1;

        match device.write(command, data) {
            Ok(()) => SUCCESS,
            Err(_) => ERROR_WRITING_DATA,
        }
    })
}

#[no_mangle]
pub unsafe extern "C" fn daq_close(device_handle: Option<&mut RwLock<Device>>) -> i32 {
    catch_unwind(AssertUnwindSafe(|| match device_handle {
        None => NULL_PTR_PASSED_IN,
        Some(device_rw_lock) => {
            drop(unsafe { Box::from_raw(device_rw_lock) });
            0
        }
    }))
    .unwrap_or(UNWOUND)
}

fn operate_on<T, F>(object_handle: Option<&RwLock<T>>, f: F) -> i32
where
    F: FnOnce(&T) -> i32 + UnwindSafe,
{
    catch_unwind(|| {
        match object_handle {
            None => NULL_PTR_PASSED_IN,
            Some(object_rw_lock) => {
                match object_rw_lock.read() {
                    Err(_) => UNWOUND_IN_A_PREVIOUS_CALL,
                    Ok(object) => f(&object), // Have a valid device
                }
            }
        }
    })
    .unwrap_or(UNWOUND)
}
soft grotto
# tall osprey just keep it *mut

I'm unclear why you said NOT to use *const Device instead of *mut Device for the type of device_handle in the function signature of daq_write. The hidapi::device::Device::write() function only needs a shared reference.