Like your previous question you continue to ignore how compilers and linkers work. The two concepts you need to wrap your head around are these:
LPCTSTR
is not a type. It is a preprocessor macro that expands to char const*
, wchar_t const*
, or __wchar_t const*
if you are particularly unlucky. Either way, once the compiler is done, LPCTSTR
is gone. Forever. It will not ever show up as a type even when using C++ name decoration.
It is not a type, don't use it in places where only types are allowed.
Compilers support different types of language linkage for external symbols. While you insist to have a C DLL, you are in fact using C++ linkage. This is evidenced by the symbol assigned to the exported function. While C++ linkage is great in that it allows type information to be encoded in the decorated names, the name decoration scheme isn't standardized in any way, and varies widely across compilers and platforms. As such, it is useless when the goal is cross language interoperability (or any interoperability).
As explained in my previous answer, you will need to get rid of the LPCTSTR
in your C (or C++) interface. That's non-negotiable. It must go, and unwittingly you have done that already. Since DUMPBIN understands MSVC's C++ name decoration scheme, it was able to turn this symbol
?output@@YAXPEBDZZ
into this code
void __cdecl output(char const *,...)
All type information is encoded in the decorated name, including the calling convention used. Take special note that the first formal parameter is of type char const *
. That's fixed, set in stone, compiled into the DLL. There is no going back and changing your mind, so make sure your clients can't either.
You MUST change the signature of your C or C++ function. Pick either char const*
or wchar_t const*
. When it comes to strings in Rust on Windows there is no good option. Picking either one is the best you have.
The other issue you are going up against is insisting on having Rust come to terms with C++' language linkage. That isn't going to be an option until Standard C++ has formally standardized C++ language linkage. In statistics, this is called the "Impossible Event", so don't sink any more time into something that's not going to get you anywhere.
Instead, instruct your C or C++ library to export symbols using C language linkage by prepending an extern "C"
specifier. While not formally specified either, most tools agree on a sufficiently large set of rules to be usable. Whether you like it or not, extern "C"
is the only option we have when making compiled C or C++ code available to other languages (or C and C++, for that matter).
If for whatever reason you cannot use C language linkage (and frankly, since you are compiling C code I don't see a valid reason for that being the case) you could export from a DLL using a DEF file, giving you control over the names of the exported symbols. I don't see much benefit in using C++ language linkage, then throwing out all the benefits and pretend to the linker that this were C language linkage. I mean, why not just have the compiler do all that work instead?
Of course, if you are this desperately trying to avoid the solution, you could also follow the approach from your proposed answer, so long as you understand, why it works, when it stops working, and which new error mode you've introduced.
It works, in part by tricking the compiler, and in part by coincidence. The link_name = "?output@@YAXPEBDZZ"
attribute tells the compiler to stop massaging the import symbol and instead use the provided name when requesting the linker to resolve symbols. This works by coincidence because Rust defaults to __cdecl
which happens to be the calling convention for all variadic functions in C. Most functions in the Windows API use __stdcall
, though. Now ironically, had you used C linkage instead, you would have lost all type information, but retained the calling convention in the name decoration. A mismatch between calling conventions would have thus been caught during linking. Another opportunity missed, oh well.
It stops working when you recompile your C DLL and define UNICODE
or _UNICODE
, because now the symbol has a different name, due to different types. It will also stop working when Microsoft ever decide to change their (undocumented) name decoration scheme. And it will certainly stop working when using a different C++ compiler.
The Rust implementation introduced a new error mode. Presumably, LPCTSTR
is a type alias, gated by some sort of configuration. This allows clients to select, whether they want an output
that accepts a *const u8
or *const u16
. The library, though, is compiled to accept char const*
only. Another mismatch opportunity introduced needlessly. There is no place for generic-text mappings in Windows code, and hasn't been for decades.
As always, a few words of caution: Trying to introduce Rust into a business that's squarely footed on C and C++ requires careful consideration. Someone doing that will need to be intimately familiar with C++ compilers, linkers, and Rust. I feel that you are struggling with all three of those, and fear that you are ultimately going to provide a disservice.
Consider whether you should be bringing someone in that is sufficiently experienced. You can either thank me later for the advice, or pay me to fill in that role.