Passing string to c++-library to be filled with information, then getting it back

So all I can really recommend is to see whether the C++ program is doing something else with the library before calling MPI_GetVersion that the Rust program isn't.

It's a bit confusing by now, I know, but the flow is:

C++ test program -> wrapper C++ DLL -> Hiwin DLL
and
Rust program -> wrapper C++ DLL -> Hiwin DLL

The C++ test program does nothing but create a char[10] array and then call the wrapper C++ DLL with a pointer to the array as an argument. The Rust program does the same thing, still the result differs...

I'm going to go out in a limb and guess there's something else in that header file that's doing static initialization, which rust doesn't support, or that the dll assumes that the c++ runtime (separate from the c runtime) is initialized, which it might not be in Rust: that gets weird quickly (static vs dynamic runtimes, different vendors and os do things differently, etc.).

The short version is you can't use C++ from Rust directly without a bunch of trouble. You might want to look into https://cxx.rs/ which solves this by generating the c++ side in it's bindings too, so the actual FFI interface is well defined C (I haven't used it, but it gets mentioned)

Thank you for the tip. I will look into Cxx. Although, shouldn't my own C++ dll work as a middle-man? My own C++ dll does nothing funny but exporting the wrapper function to Rust in a C-manner. How can the caller of the C++ dll define how the C++ dll calls other libraries? That goes beyond my head!?

Now we're really making progress! I just realised that my wrapper dll actually works, when called from Rust, if not run in debug mode! If I start a Rust program using the debugger in VS Code the call to my wrapper returns scrap. If I just do cargo run then the returned value is valid!

I took my program down to the absolute basic (skipping bindgen and everything else non-related). The wrapper-version looks like this:

use std::ffi::CString;

#[allow(non_snake_case)]
extern "C" {
    pub fn HiwinWrapper_GetVersion(pszVer: *mut ::std::os::raw::c_char);
}

fn main() {
    println!("Hello, world!");
    let mut array_u8: [u8;10] = [0;10];
    unsafe {
        HiwinWrapper_GetVersion(array_u8.as_mut_ptr().cast());
        let valid = array_u8.split(|&b| b == 0).next().unwrap(); // handle error!
        let string = CString::new(valid).unwrap(); // handle error!
        if valid.is_ascii() {
            let ascii = string.into_string().unwrap();
            println!("{}",ascii);
        } else {
            println!("Returned string is not valid ASCII");
        }
    }
    println!("\nGoodbye, world!");
}

If I swap my wrapper-dll for the real target Hiwin dll, giving the code below, then the call fails no matter if I run it with cargo run or from the debugger. (The link_name comes from the bindgen generated code, no clue what it's basing the name on?)

use std::ffi::CString;

 #[allow(non_snake_case)]
 extern "C" {
     #[link_name = "\u{1}?MPI_GetVersion@@YAXPEAD@Z"]
     pub fn MPI_GetVersion(pszVer: *mut ::std::os::raw::c_char);
 }

fn main() {
    println!("Hello, world!");
    let mut array_u8: [u8;10] = [0;10];
    unsafe {
        MPI_GetVersion(array_u8.as_mut_ptr().cast());
        let valid = array_u8.split(|&b| b == 0).next().unwrap(); // handle error!
        let string = CString::new(valid).unwrap(); // handle error!
        if valid.is_ascii() {
            let ascii = string.into_string().unwrap();
            println!("{}",ascii);
        } else {
            println!("Returned string is not valid ASCII");
        }
    }
    println!("\nGoodbye, world!");
}

For now, my conclusion is that

  1. Rust cannot talk directly to stdcall-libraries
  2. Rust can talk to export "C" libraries
  3. The export "C" library works as expected in run-mode, but not in debug mode.

I can live with the fact that Rust cannot talk directly to stdcall-libraries. That is mentioned in other places and it's outside of my control. I think that this will be resolved sooner or later, but for now I can do with a workaround.
But the fact that my wrapper is bugging out when called from rust in debug mode, that feels like something that should be possible to mitigate? Is it something with my debug-settings in VS Code?
My VS Code settings are:

{
    "version": "0.2.0",
    "configurations": [

        {
            "name": "(Windows) Launch",
            "type": "cppvsdbg",
            "request": "launch",
            "program": "${workspaceRoot}/target/debug/RustHiwinTest.exe",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceRoot}",
            "environment": [],
            "console": "externalTerminal"
        }
    ]
}

/Henrik

Given that you are on Windows, I wouldn't be surprised if whatever debugger you are using simply misinterpreted Rust debug symbols. (Toolchains and debuggers on Windows tend to have the opinion of not wanting to support anything but the most tightly Microsoft-integrated languages, i.e., their own outdated and non-standard dialects of C and C++, and not more.) The fact that you don't even get the correct variables in the debugger (is that what you wrote earlier?) is also suggestive of this. Can you perhaps check the same library and the same wrapper/Rust code on another platform, e.g. Linux?

2 Likes

The library I'm trying to call is a dll and I don't have any Linux versions of it, so testing on other platform is hard...

Stdcall is absolutely supported by Rust (spelled extern "system") - nearly every Windows API is stdcall, afterall. But as mentioned, on x64 the C and stdcall conventions are the same, so it doesn't matter.

I will note that the link name #[link_name = "\u{1}?MPI_GetVersion@@YAXPEAD@Z"] is the mangled name (essentially, including the namespace, argument and return types) of whatever compiler built that dll (doesn't look like the msvc mangling from memory?)

This means it's a c++ function and doesn't have a stable documented ABI. You would need to wrap the c++ part in a C function for Rust (or any other language) to call it. This is what cxx does for you.

2 Likes

Ok, I got confused by the x64 part.

But the link_name was generated by bindgen, where did bindgen get the name from if not by folowing the same naming scheme as the c++ compiler?

But apparently even that didn't work for OP in debug mode, IIUC.

1 Like

That is the MSVC mangling, referring to void __cdecl MPI_GetVersion(char*);. (Not quite sure what the '\u{1}' is for.) Bindgen can generate these mangled names directly from the header file using libclang. But I concur that it's likely not a good idea to depend on the calling convention of a C++ function.

x64 is x86_64 is AMD64, as in pretty much any modern PC 64 bit computer other than Mac M1. The history behind all the names is confusing and stupid.

The important part is that mangling is only part of the ABI for calling a function, so it's just a hint as to whether the function is using the C ABI. To be clear, you can normally get away fine with this: most of the time the C++ and C ABIs line up and the only difference is the mangling (and that you're not allowed to throw exceptions through C, but you'd notice that!). But since you're hitting this weird behavior when calling the same function from Rust, I'm suspicious the dll is doing something triggering one of those edge cases: possibly due to some implicit in C++ runtime init or the like.

It tells LLVM to use the symbol name verbatim rather than doing platform specific mangling to it like adding an _ prefix for Mach-O or adding an @<argumentssize> postfix for one of the Windows calling conventions (forgot which) with COFF/PE.

2 Likes

[EDIT] To reiterate the last comment from @simonbuchan which I just saw now...

most of the time the C++ and C ABIs line up [...] I'm suspicious the dll is doing something triggering one of those edge cases: possibly due to some implicit in C++ runtime init or the like.

I haven't read this whole thread but, from my long ago experience with win api, anything you can do with C++/MFC style coding you can do with straight C. I recommend that approach, and avoid CString altogether. That way, you at least know exactly what's happening on that side of the FFI.

Seems like it's the C stdcall (and fastcall with a leading @ too) for MSVC, at least for 32-bit. That was the one I was expecting above (due to all those windows apis), but makes sense that C++ symbols don't need that.

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.