Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jimkring opened this issue Jun 21, 2023 · 10 comments
Assignees
Labels
enhancement New feature or request

Comments

@jimkring
Copy link
Contributor

jimkring commented Jun 21, 2023

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

@jimkring jimkring changed the title Read string data using CLFN and also pass out string pointer separately [Investigation] Read string data using CLFN (and pass out string pointer separately) Jun 21, 2023
@jimkring jimkring self-assigned this Jun 21, 2023
@jimkring jimkring added the enhancement New feature or request label Jun 21, 2023
@JamesMc86
Copy link

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!

@jimkring
Copy link
Contributor Author

@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?

@JamesMc86
Copy link

JamesMc86 commented Jun 22, 2023 via email

@jimkring
Copy link
Contributor Author

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

@jimkring
Copy link
Contributor Author

jimkring commented Jun 23, 2023

@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;
}

@JamesMc86
Copy link

JamesMc86 commented Jun 23, 2023 via email

@jimkring
Copy link
Contributor Author

jimkring commented Jun 23, 2023 via email

@jimkring
Copy link
Contributor Author

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")

@jimkring
Copy link
Contributor Author

jimkring commented Jun 23, 2023

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;
        }
    };

@JamesMc86
Copy link

JamesMc86 commented Jul 23, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants