Converting a LPSTR Array to Rust (C FFI)

Hi all. I am currently working on a project that involves communicating with a DLL via Rust. To start this process, I utilized bindgen to generate some bindings based on my C header file.

The C header defined the function as such,

BOOL Initialize(_In_ DWORD argc, _In_ LPSTR argv[]);

After running my bindgen code, the generated function looks like this:

pub fn Initialize(argc: DWORD, argv: *mut LPSTR) -> BOOL;

This is where I am stuck and cannot figure out the translation between *mut LPSTR and the LPSTR argv[] array in C. More specifically, I am not sure on how I should be passing the *mut LPSTR as the LPSTR argv[] parameter.

I would truly appreciate any help and/or feedback on this as I have been going back and forth on this for some time now. Thank you very much.

1 Like

argv is the same in both the C and Rust code. In C, LPSTR foo[] and LPSTR* foo are equivalent - they both declare a pointer to one or more pointers. The declaration of argv in the Rust code is the same: it's a pointer to one or more pointers. So you can just use casts, or possible std::mem::transmute(), to convert argv to the required type.

Note: I assume that argc and argv are the command-line arguments of your program, which means they are zero-terminated ASCII strings. These strings can be passed directly to your DLL without modification. This is not true of Rust strings. If you want to pass actual Rust strings to your DLL, that's a bit more complicated; you'll need the CString type from std::ffi.

2 Likes

Thank you very much for the explanation and guidance! I will consider this all and give it another attempt. Thank you once again, really appreciate it!

Once again, thanks for the help thus far. I've gone ahead and tried working on my code some more and have come up with the following attempt:

// Creating a Vector containing 4 CStrings. These are meant to be passed into the C function
let input = vec![CString::new("foo").unwrap(), CString::new("bar").unwrap(), CString::new("-foo").unwrap(), CString::new("-bar").unwrap()];
let mut pointers = vec![];

for i in input {
    // Transmute the *const i8 that comes from CString.as_ptr() into an LPSTR
    pointers.push(std::mem::transmute::<*const i8, LPSTR>(i.as_ptr()));
}
let ptr = pointers.as_mut_ptr();

// Finally, call the C function via Rust
let result = initialize(4, ptr);    // argc = 4, arv = the LPSTR collection

// If all goes well, we expect "True". If the invocation fails, we get "False"
println!("Result: {}", result);

As of now, the Rust code does seem to be invoking our C function, but the result that we are getting is False, indicating that the initialization did not succeed (most likely due to invalid arguments in the parameter).

While much more debugging needs to go on, based on this code snippet, do you see any areas in particular that I may have the wrong approach to? Any further advice or feedback would be greatly appreciated!

Thanks again.

Hi Matt,

The code looks fine to me except for one thing: in C, argv must be terminated with one final null pointer. It's kind of superfluous, since argc tells you how many strings there must be in argv, but a lot of C code expects that final null pointer. So add this line:

    pointers.push(std::ptr::null_mut()); /// NEW LINE
    let ptr = pointers.as_mut_ptr();

One additional thing to point out: make sure your C code doesn't store those pointers and access them after Initialize() has returned. In C, this would be OK, since the argv pointers remain valid for the life of the program. But in your case, those pointers will become invalid as soon as initialize() returns, and the input Vec is destroyed.

Cheers,
Ross

1 Like

To be exact, this is only true in function prototypes. (So it's true in this case, but in other code it's not, as [] declares an array, not a pointer.)

Please do not use or suggest using transmute unless there's no other possible solution. Transmuting is very unsafe, and transmuting pointers and pointer-containing structures is, if possible, even more unsafe.

For converting between pointers to different types, you should use cast, like this:

i.as_ptr().cast::<LPSTR>()

Or, if you need to change the mutability of the pointee, use a primitive as cast, like this:

i.as_ptr() as *const i8
1 Like

This creates an array of dangling pointers :warning:

Indeed:

let input = vec![CString::new("foo").unwrap(), CString::new("bar").unwrap(), CString::new("-foo").unwrap(), CString::new("-bar").unwrap()];
let mut pointers = vec![];

for i in input {
    // Transmute the *const i8 that comes from CString.as_ptr() into an LPSTR
    pointers.push(std::mem::transmute::<*const i8, LPSTR>(i.as_ptr()));
} // <- `i : CString` dropped here, after each iteration of the loop
  // the `LPSTR` derived from `i.as_ptr()` thus dangles.

To avoid such footguns, I recommend you use a lifetime infected (slim-)pointer-sized type in the declaration of Initialize:

use ::safer_ffi::prelude::*;

fn initialize (args: &'_ [char_p::Ref<'_>])
  -> bool
{
    type LPSTR<'_> = Option<char_p::Ref<'_>>;

    extern "…" {
        fn Initialize (argc: DWORD, argv: *const LPSTR<'_>)
          -> BOOL
        ;
    }

    let argc: DWORD = args.len().try_into().expect("Integer overflow");
    let ref args =
        args.iter().copied()
            .map(Some).chain(iter::once(None)) // Append the final `NULL`
            .collect::<Vec<_>>()
    ;
    let c_bool = unsafe { Initialize(argc, args.as_ptr()) };
    c_bool != 0
}

Usage:

initialize(&[
    c!("foo"),
    c!("bar"),
    c!("-foo"),
    c!("-bar"),
]);
2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.