0

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 *,...))
Ross Youngblood
  • 502
  • 1
  • 3
  • 16
  • 3
    `LPCTSTR` is not a type. It's a macro that either expands to `LPCSTR` or `LPCWSTR`, that map to `*const u8` and `*const u16`, respectively. You will have to make your C function either accept an `LPCSTR` or `LPCWSTR`, or provide two functions with different names, since Rust doesn't support overloads. I'm not sure how this is related to MFC, though. – IInspectable Jul 07 '21 at 16:11
  • 1
    Make sure to understand that Rust's [`CString`](https://doc.rust-lang.org/std/ffi/struct.CString.html) and MFC's [`CString`](https://learn.microsoft.com/en-us/cpp/atl-mfc-shared/reference/cstringt-class) are vastly different things, completely unrelated. – IInspectable Jul 07 '21 at 16:14
  • 1
    If you need to share this across different languages, have you think to use `std::string` as an another option ? – Flaviu_ Jul 07 '21 at 18:50
  • 2
    @fla Using `std::string` at an interface is a safe way to make absolutely sure that that interface will not **ever** be used from a different language. Or even just the same language compiled with a different compiler. Or the same compiler with different compiler options. – IInspectable Jul 07 '21 at 20:59

1 Answers1

4

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!\0".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.

IInspectable
  • 46,945
  • 8
  • 85
  • 181
  • I had to laugh at your later comments.... exactly why I choose the worst possible function, a varadic output function in a way. I was curious to see if a language focused on doing things "right" would allow me to do things "wrong" much like 'C'. It was actually very easy to do this. So that should set off all kinds of alarm bells. But for me it's a hack, as output() is the crutch for users who can't attach a debugger. And since I'm not wanting to install VS code. I need the crutch until I get a debug VS2019 environment figured out. – Ross Youngblood Jul 12 '21 at 15:30
  • 1
    @ros There is no way in **safe** Rust to link against variadic functions. You've probably glossed over this, but `extern fn`'s are implicitly `unsafe`. Unsafe Rust is no safer than C. Safe Rust, on the other hand, makes a world of a difference. Debugging Rust in Visual Studio is pretty straight forward. rustc produces Windows-compatible PDB's, so all you need is to launch your Rust application and tell the IDE where to look for debug symbols (and source). You can install a [Rust VSIX](https://marketplace.visualstudio.com/items?itemName=dos-cafe.Rust) to get better code navigation. – IInspectable Jul 12 '21 at 16:06
  • The unsafe "output" implementation is a temporary solution as output to the console is not visible in our multi-processing work environment. Our native output() API layer sends data to a UI GUI tab (one tab per embedded system processor). As you suggested creating a "safe" wrapper for I/O would generally be the correct approach. At this time however, I'm simply creating a 'hello world" where our native system calls RUST, and rust sends "hello world" through our native output mechanism. This is working. So it means that we can create RUST DLL's to do multi-threading in our environment. – Ross Youngblood Jul 13 '21 at 16:43
  • I'd also add that the MIT guru who engineered our multi-threaded environment did a good job of managing messaging between threads safely. So the payback for introducing RUST is likely less than it would be if that hadn't been the case. I was just curious how difficult it would be to integrate a Rust DLL into our native environment, and it turns out it took maybe a day and ahalf from zero. – Ross Youngblood Jul 13 '21 at 16:45