JKISoftware/toml-edit-labview

[Investigation] Read string data using CLFN (and pass out string pointer separately)

jimkring opened this issue · 10 comments

Current Approach - Read String using LabVIEW's MoveBlock functionality (before closing reference)

image

Here is the VI that reads a cstring from a memory location:

image

The MoveBlock function is exported by the LabVIEW executable app:

image

After we're done reading the data, we close the reference:

image

Different Approach - Use CLFN to Read String, but also pass string reference (so we can close reference)

A slightly different approach to reading string data from Rust is to have the CLFN read the string data (instead of just getting the address of the data and reading it with the Memory Manager). This is nice, because we can rely on LabVIEW's built in feature for reading a cstring, however, it means that we must also pass out the string's address, separately, so that we can ask Rust to deallocate/free the string after LabVIEW has read the data.

That looks something like this:

image

image

Notes:

  1. there's an additional parameter string_ptr: *mut c_void, which is how we will obtain the string pointer (in note 2 below).
  2. right before we return, we will write the return string's address to the string_ptr parameter that was passed in --> unsafe { *(string_ptr as *mut *mut c_char) = raw_string; }
// return a toml string from a Document
#[allow(dead_code)]
#[no_mangle]
pub extern "C" fn toml_edit_doc_to_string (
    doc: *mut c_void,
    string_ptr: *mut c_void,
) -> *mut c_char {
    let doc = unsafe { &mut *(doc as *mut Document) };

    let toml_str = match Document::to_string(doc) {
        toml_str => toml_str,
    };

    let raw_string = match CString::new(toml_str).unwrap().into_raw() {
        ptr if ptr.is_null() => {
            println!("Unable to allocate memory for string");
            return CString::new("").unwrap().into_raw();
        },
        ptr => ptr,
    };

    // write raw_string's address as the value stored in string_ptr
    unsafe { *(string_ptr as *mut *mut c_char) = raw_string; }
    return raw_string;
}

When we call this from LabVIEW, we can read the return string using the built in CLFN's C String Pointer format for strings.

And, we will ALSO get the address of the string as an integer that we can pass back to Rust to deallocate/free, using the cstring_free_memory() function shown below:

// exported function that frees the memory allocated for a string
// this *must* be called for every string returned from a function in this library
#[no_mangle]
pub extern "C" fn cstring_free_memory(s: *mut c_char) {
    unsafe {
        if s.is_null() {
            return;
        }
        CString::from_raw(s)
    };
}

image

This is far from ready yet but my goal with the labVIEW interop crate (https://github.com/WiresmithTech/Rust-LabVIEW-Interop) would be that you can pass the string handle into rust and it could handle resizing and perhaps encoding differences.

Right now though I've not started on it so there is some work to be done first!

@JamesMc86 Yes, it would be nice to have an easy way to work with a LabVIEW string handle in Rust. I think you're right that doing the string resizing in Rust would make things nice and simple in LabVIEW. Question: how does one access the LabVIEW memory manager on the Rust side of things? Would Rust call into LabVIEW.exe and access its exported functions similar to how we can do this with a call library function node?

I see. It looks like this is the way that would work...
https://docs.rs/libloading/latest/libloading/

@JamesMc86 I was able to get this far...

image

// exported function that calls into LabVIEW.exe's memory manager
#[allow(dead_code)]
#[no_mangle]
pub extern "C" fn labview_get_string_size(
    labview_path: *const c_char,
    string_to_size: *const c_char,
) -> u32 {
    let labview_path = unsafe { CStr::from_ptr(labview_path).to_string_lossy().into_owned() };

    let labview_lib = match unsafe { libloading::Library::new(labview_path.clone().as_str()) } {
        Ok(lib) => lib,
        Err(_) => {
            println!("Unable to load LabVIEW.exe");
            return 0;
        }
    };
    // the DSGetHandleSize function is exported from LabVIEW.exe and takes a handle as an argument
    let DSGetHandleSize: libloading::Symbol<unsafe extern "C" fn(*const c_char) -> u32> =
        match unsafe { labview_lib.get(b"DSGetHandleSize") } {
            // will get back Result<Symbol<'_, _>, Error>
            Ok(func) => func,
            Err(_) => {
                println!("Unable to find DSGetHandleSize function in LabVIEW.exe");
                return 0;
            }
        };

    let handle_size = unsafe { DSGetHandleSize(string_to_size) };

    return handle_size;
}

I tried just using the name "LabVIEW.exe" instead of the full path and it worked fine:

let labview_lib = unsafe { libloading::Library::new("LabVIEW.exe")

And, here's a little gem (std::env::current_exe()) that gets the main executable's path, which should address the built/stand-alone executable use case:

    use std::env

    let exe_path = match env::current_exe() {
        Ok(exe_path) => exe_path,
        Err(e) => {
            println!("failed to get current exe path");
            return 1;
        },
    };

    let labview_lib = match unsafe { libloading::Library::new(exe_path) } {
        Ok(lib) => lib,
        Err(_) => {
            println!("Unable to load LabVIEW.exe");
            return 42;
        }
    };

When I've gon digging for the interop crate this is the nicest solution I found and seems to be working.

  1. Use dlopen2 crate to create an API container (to handle lifetimes etc for us)
  2. Use ctor to initialise a static with this container when the DLL is loaded.

Initial testing looks good - unsure about error handling in the constructor. Right now it would panic but unclear if there is an expected way to handle this.

Code is in https://github.com/WiresmithTech/Rust-LabVIEW-Interop/blob/main/labview-interop/src/labview.rs

Now I have that worked out string handling is next on my list so hopefully you might be able to just pull from the interop crate soon if you want