Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

How do I declare an external MFC function that has LPCTSTR tokens?

I have the following C function in a library I wish to use:

DLL_PUBLIC void output( LPCTSTR format, ... )

Where DLL_PUBLIC is resolved to

__declspec(dllimport) void output( LPCTSTR format, ...)

In Rust, I was able to get things to build with:

use winapi::um::winnt::CHAR;
type LPCTSTR = *const CHAR;

#[link(name="mylib", kind="static")]
extern {
#[link_name = "output"]
    fn output( format:LPCTSTR, ...);
}

I'm not sure this is the best approach, but it seems to get me part way down. Although the module is declared in a DLL module, the symbol decorations in the native DLL are such that it does not have _imp prepended to it in the binary. So I find that "static" seems to look for the correct module, although I still have not been able to get it to link.

The dumpbin output for the module (this target function) in "mylib.dll" is:

 31    ?output@@YAXPEBDZZ (void __cdecl output(char const *,...))
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
138 views
Welcome To Ask or Share your Answers For Others

1 Answer

This is assuming that what you are trying to accomplish here is to link Rust code against a custom DLL implementation. If that is the case, then things are looking good.

First things first, though, you'll need to do some sanity cleanup. LPCTSTR is not a type. It is a preprocessor symbol that either expands to LPCSTR (aka char const*) or LPCWSTR (aka wchar_t const*).

When the library gets built the compiler commits to either one of those, and that decision is made for all eternity. Clients, on the other hand, that #inlcude the header are still free to choose, and you have no control over this. Lucky you, if you're using C++ linkage, and have the linker save you. But we aren't using C++ linkage.

The first order of action is to change the C function signature using an explicit type, so that clients and implementation always agree. I will be using char const* here.


C++ library

Building the library is fairly straight forward. The following is a bare-bones C++ library implementation that simply outputs a formatted string to STDOUT.

dll.cpp:

#include <stdio.h>
#include <stdarg.h>

extern "C" __declspec(dllexport) void output(char const* format, ...)
{
    va_list argptr{};
    va_start(argptr, format);
    vprintf(format, argptr);
    va_end(argptr);
}

The following changes to the original code are required:

  • extern "C": This is requesting C linkage, controlling how symbols are decorated as seen by the linker. It's the only reasonable choice when planning to cross language boundaries.
  • __declspec(dllexport): This is telling the compiler to inform the linker to export the symbol. C and C++ clients will use a declaration with a corresponding __declspec(dllimport) directive.
  • char const*: See above.

This is all that's required to build the library. With MSVC the target architecture is implied by the toolchain used. Open up a Visual Studio command prompt that matches the architecture eventually used by Rust's toolchain, and run the following command:

cl.exe /LD dll.cpp

This produces, among other artifacts, dll.dll and dll.lib. The latter being the import library that needs to be discoverable by Rust. Copying it to the Rust client's crate's root directory is sufficient.


Consuming the library from Rust

Let's start from scratch here and make a new binary crate:

cargo new --bin client

Since we don't need any other dependencies, the default Cargo.toml can remain unchanged. As a sanity check you can cargo run to verify that everything is properly set up.

If that all went down well it's time to import the only public symbol exported by dll.dll. Add the following to src/main.rs:

#[link(name = "dll", kind = "dylib")]
extern "C" {
    pub fn output(format: *const u8, ...);
}

And that's all there is to it. Again, a few details are important here, namely:

  • name = "dll": Specifies the import library. The .lib extension is implied, and must not be appended.
  • kind = "dylib": We're importing from a dynamic link library. This is the default and can be omitted, though I'm keeping it for posterity.
  • extern "C": As in the C++ code this controls name decoration and the calling convention. For variadic functions the C calling convention (__cdecl) is required.
  • *const u8: This is Rust's native type that corresponds to char const* in C and C++. Using type aliases (whether those provided by the winapi crate or otherwise) is not required. It wouldn't hurt either, but let's just keep this simple.

With that everything is set up and we can take this out for a spin. Replace the default generated fn main() with the following code in src/main.rs:

fn main() {
    unsafe { output("Hello, world!".as_ptr()) };
}

and there you have it. cargo running this produces the famous output:

Hello, world!

So, all is fine, right? Well, no, not really. Actually, nothing is fine. You could have just as well written, compiled, and executed the following:

fn main() {
    unsafe { output(b"Make sure this has reasons to crash: %f".as_ptr(), "??") };
}

which produces the following output for me:

Make sure this has reasons to crash: 0.000000≡???

though any other observable behavior is possible, too. After all, the behavior is undefined. There are two bugs: 1 The format specifier doesn't match the argument, and 2 the format string isn't NUL terminated.

Either one can be fixed, trivially even, though you have opted out of Rust's safety guarantees. Rust can't help you detect either issue, and when control reaches the library implementation, it cannot detect this either. It will just do what it was asked to do, subverting each and every one of Rust's safety guarantees.


Remarks

A few words of caution: Getting developers interested in Rust is great, and I will do my best to try whenever I get a chance to. Getting Rust-curious developers excited about Rust is often just a natural progression.

Though I will say that trying to get developers excited about Rust by starting out with unsafe Rust isn't going to be successful. It's eventually going to provoke a response like: "Look, ma, a steep learning curve with absolutely no benefit whatsoever, who could possibly resist?!" (I'm exaggerating, I know).

If your ultimate goal is to establish Rust as a superior alternative to C (and in part C++), don't start by evaluating how not to benefit from Rust. Specifically, trying to import a variadic function (the unsafest language construct in all of C++) and exposing it as an unsafe function to Rust is almost guaranteed to be the beginning of a lost battle.

Now, this may read bad as it is already, but this isn't over yet. In an attempt to make your C++ code accessible from Rust, things have gotten worse! With a C++ compiler and static code analysis tools (assuming the format string is known at compile time, and the tools understand the semantics), the tooling can and frequently will warn about mismatches. That option is now gone, forever, and there's not even a base level of protection.

If you absolutely want to make some sort of logging available to Rust, export a function from the library that takes a single char const*, use Rust's format! macro, and provide a variadic wrapper to C and C++ clients.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...