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

Found here: A little C with your Rust - The Embedded Rust Book

As C++ does not have a stable ABI for the Rust compiler to target, it is recommended to use the C ABI when combining Rust with C or C++.

I guess that means it should work with extern "C"?

No, if the C function is defined with the stdcall abi like in this case you have to use extern "stdcall". The abi specified by the caller and callee must always match.

1 Like

According to this comment the stdcall is ignored for 64bit?

I don't really have any helpful advice, other than that the symptoms you're seeing - strange behavior that goes away and comes back as you try to inspect it - is classic Undefined Behavior, and is very familiar to the C++ programmers in the audience...

1 Like

OK, so is your function written in C++, after all? If so, then you probably have to write a C wrapper around it. Try this:

  • Write and compile a C++ file that calls your C++ function in a wrapper. Make sure the wrapper is defined as extern "C" so it is effectively C, giving it the stable C ABI.
  • Call the C wrapper from Rust (and keep its extern "C" signature in the Rust declaration).
  • Link the C wrapper together with everything else at the end of the build; you might find the cc crate helpful if you are not using it already.

If you by "my" function mean the MPI_GetVersion(), then I actually don't know. It's a third party library, so I can just guess. It looks like C++ and they have an example program written in C++ (and one in C# and one in VB).

I think the approach sound promising. Unfortunately I cannot get it to work. I have made a C++ DLL project that defines the following function:

__declspec(dllexport) void __cdecl HiwinWrapper_GetVersion(char* pszVer)

and with the header

extern "C" __declspec(dllexport) void __cdecl HiwinWrapper_GetVersion(char* pszVer);

When linking it to another C++ program it works as expected, but when linking it from rust I still get scrap!
I tried printing to the console from within the wrapper and when calling it from another C++ program it shows the expected version, but when calling it from rust it sure is scrap! So when Rust is calling my wrapper DLL, the wrapper DLL calls the MPI_GetVersion the same way as Rust did!?!

What is the problem with my C++ DLL project? Is there a setting somewhere or does the DLL actually inherit the caller ABI passing it on to the other function?

I'm perplexed!

Does the library not come with header files? If all you have is a DLL without any headers in which the authors declare the signature of functions, then it's likely that you simply guessed the signature of the function wrong.

Is the header included when you compile the C++ file? The compiler has to see that you are intending to define the function with a given ABI in order to emit the correct code.

No, there's no inheriting going on. (The ABI is not something that can possibly be manipulated at runtime – as it is the set of compile-time conventions the compiler follows when emitting the code for the function.)

But if the ABI with which the top-level function is being called is wrong, then of course everything else down the call stack will receive garbage.

Does the library not come with header files? If all you have is a DLL without any headers in which the authors declare the signature of functions, then it's likely that you simply guessed the signature of the function wrong.

The library comes with header files. The definition of the function in the header is:

void __stdcall MPI_GetVersion( char *pszVer );

So the signature is known.

Is the header included when you compile the C++ file? The compiler has to see that you are intending to define the function with a given ABI in order to emit the correct code.

The third party library header is included in my wrapper library project when compiling my wrapper. My expectation was that the wrapper would expose a C-compatible function signature but use the stdcall signature to call the third party library. But it doesn't seem to work?
If I use my wrapper library from a C++ console application then everything works as expected, so the library itself works!

For the sake of testing I added a test-modifier like this:

__declspec(dllexport) void __cdecl HiwinWrapper_GetVersion(char* pszVer)
    pszVer[9] = 'A'; // <-- Modify the result after fetching the string

The result in Rust is a scrap string with A on position 9. So the library is working, but it fails to call the third party function correctly. If I call the function from the C++ test application then I get a correct version number string with an A on position 9, as one would have expected.

Which MPI implementation are you using? I just downloaded the "Microsoft MPI v10.1.2" sdk and it's mpi.h calls it MPI_Get_version, not MPI_GetVersion. Additionally it has two arguments: int *version and int *subversion. There is also a function called MPI_Get_library_version which takes char versionandint resultlen`.

1 Like

Hi Bjorn,
I'm afraid this is something completely different. I don't know what MPI stands for, but the version requested is for the Hiwin Servo motor driver API DLL file.

Have you contacted the vendor for help?

Is their SDK something you can share?

Is this what you're using?

So from what it looks like, my wrapper DLL works differently if called from another C++ project or when called from Rust. The wrapper DLL itself works both from C++ and from Rust, data produced by the wrapper DLL is identical in both cases (see the injected 'A' three posts up). But data produced by the third party library differs when the wrapper DLL is called from C++ and from Rust.
How can that be possible?? :tired_face:

One possibility is the calling convention.

Can you elaborate on that?

I see. I thought you were talking about the MPI (message passing interface) that is commonly used for HPC applications that run on supercomputers.

The calling convention defines the method for passing arguments (as well as other details). For example, a calling convention might require all arguments to be pushed onto the stack. Or, the first N arguments might be passed using registers. Let's say the latter is true; that the argument pszVer is passed using a register. Let's also say this register is named X5. Let's also say the Rust side is using the wrong calling convention; that Rust expects the arguments to be placed on the stack.

So, Rust faithfully loads the pointer to array, pushes that value on the stack, then calls MPI_GetVersion. In this scenario, we don't know what's stored in X5. The compiler very likely used the register at some point. Given the way your program behaves X5 very likely has the address of writable memory. But, it does not have the address of array. MPI_GetVersion "works" in the sense that your program does not crash. But, it stored the data in the wrong location.

Having said all that, the symptoms don't quite fit. They're close. But there are some discrepancies. Those discrepancies could be explained by things you've been doing like testing with a release build, posting an update, testing with a debug build, posting an update.

In short, really weird things can happen when the wrong calling convention is used and the minutia is important when troubleshooting calling convention trouble.

The function from bindgen (?MPI_GetVersion@@YAXPEAD@Z) demangles as void __cdecl MPI_GetVersion(char *);, so ABI is likely not the issue here. Also, the issue persists even when the function is called through an explicit void __cdecl wrapper.

@HenrikK Where did you get this API from? HIWIN has a publically available mpi.dll file and associated mpint.h header for its D-series and E1-series servo drives, but they only contain a char *ver(int print); function for returning the version and optionally printing it in a message box.

I suspect that the C++ code might be implicitly creating an environment where the library works properly, in some way that the Rust code isn't.

The API is received from my distributor. It is the API for E1 series driver with MegaUlink protocol. The mpi.dll and mpint.h are part of the API, but it also has an Axis.h containing this function.

Hi Coding-Badly,
Yes, that is what I have been elaborating with. I'm not experienced with this type of problems, so I'm quite lost. What I do know is that the pointer gets to my wrapper function in both cases. I print the address from within the wrapper function and it corresponds with the caller application. That makes me believe that at least my two programs (the Rust one and the C++ one) both are able to call my wrapper function. The strange thing is that the wrapper function then seems to call the Hiwin function different depending on who the caller is. Or that's not the problem at all?
Also, every once in a blue moon I suddenly get the correct value from rust as well! But the next time I try it gets corrupted again.

Unfortunate. From your experiment with writing to the string at index 9, it seems like this is a problem on the library's end. 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.