x64 DLL export function names
Asked Answered
S

4

13

I am trying to port a 32-bit dll (and application) to 64-bit and I have managed to build it without errors. When trying to load it with my 64-bit application I noticed that the exported function names differ. This is how I export the functions:

#ifdef __cplusplus
extern "C" {
#endif

__declspec(dllexport) long __stdcall Connect(char * name, long size);

#ifdef __cplusplus 
}
#endif

In Dependency Walker the exported functions have the following format:

32-bit: _Connect@8

64-bit: Connect

In the application using the dll I explicitly load the dll (LoadLibrary is successful) but GetProcAddress fails for 64-bit because it cannot find a function with the provided name.

In our application I keep the function names as follows:

#define ConnectName "_Connect@8"
...
GetProcAddress(Dll, ConnectName);

So I was wondering if it is possible to export the same function names for both 32-bit and 64-bit dlls or is this a bad idea? Or do I need to do the following in my applications:

#if _WIN64
#define ConnectName "Connect"
#else
#define ConnectName "_Connect@8"
#endif

I appreciate any help.

Sollows answered 21/1, 2015 at 8:16 Comment(0)
C
11

An option you have to export function names without any decoration (independently from the particular calling convention you used in x86, __stdcall, __cdecl, or other) and with the same undecorated name in both x86 and x64 builds, is to export your DLL functions using DEF files.

E.g. you could add a .DEF file like this to your project:

LIBRARY YOURDLL
EXPORTS
   Connect          @1
   AnotherFunction  @2
   ... etc. ...   

Repro Follows

Create an empty solution in Visual Studio (I used VS2013), and inside that create an empty Win32 console project (the test client) and an empty Win32 DLL project (the test DLL).

Add this NativeDll.def .DEF file in the DLL project:

LIBRARY NATIVEDLL
EXPORTS
    SayHello @1

Add this NativeDll.cpp C++ source code in the DLL project:

///////////////////////////////////////////////////////////////////////////////
// 
// NativeDll.cpp    -- DLL Implementation Code
//
///////////////////////////////////////////////////////////////////////////////


#include <Windows.h>
#include <atldef.h>
#include <atlstr.h>


//
// Test function exported from the DLL
// 
extern "C" HRESULT WINAPI SayHello(PCWSTR name)
{
    //
    // Check for null input string pointer
    //
    if (name == nullptr)
    {
        return E_POINTER;
    }

    try
    {
        //
        // Build a greeting message and show it in a message box
        //
        CString message;
        message.Format(L"Hello %s from the native DLL!", name);        
        MessageBox(nullptr, message, L"Native DLL Test", MB_OK);

        // All right
        return S_OK;
    }
    //
    // Catch exceptions and convert them to HRESULT codes
    //
    catch (const CAtlException& ex)
    {
        return static_cast<HRESULT>(ex);
    }
    catch (...)
    {
        return E_FAIL;
    }
}

Add this NativeClient.cpp C++ source code in the client test project:

///////////////////////////////////////////////////////////////////////////////
//
// NativeClient.cpp     -- EXE Test Client Code
//
///////////////////////////////////////////////////////////////////////////////


#include <Windows.h>


//
// Prototype of the function to be loaded from the DLL
//
typedef HRESULT (WINAPI *SayHelloFuncPtr)(PCWSTR /* name */);


//
// Simple RAII wrapper on LoadLibrary()/FreeLibrary().
//
class ScopedDll
{
public:

    //
    // Load the DLL
    //
    ScopedDll(PCWSTR dllFilename) throw()
        : m_hDll(LoadLibrary(dllFilename))
    {
    }


    //
    // Unload the DLL
    //
    ~ScopedDll() throw()
    {
        if (m_hDll)
        {
            FreeLibrary(m_hDll);
        }
    }


    //
    // Was the DLL loaded successfully?
    //
    explicit operator bool() const throw()
    {
        return (m_hDll != nullptr);
    }


    //
    // Get the DLL handle
    //
    HINSTANCE Get() const throw()
    {
        return m_hDll;
    }


    //
    // *** IMPLEMENTATION ***
    //
private:

    //
    // The wrapped raw DLL handle
    //
    HINSTANCE m_hDll;


    //
    // Ban copy
    //
private:
    ScopedDll(const ScopedDll&) = delete;
    ScopedDll& operator=(const ScopedDll&) = delete;
};


//
// Display an error message box
//
inline void ErrorMessage(PCWSTR errorMessage) throw()
{
    MessageBox(nullptr, errorMessage, L"*** ERROR ***", MB_OK | MB_ICONERROR);
}


//
// Test code calling the DLL function via LoadLibrary()/GetProcAddress()
//
int main()
{
    //
    // Return codes
    //
    static const int kExitOk = 0;
    static const int kExitError = 1;


    //
    // Load the DLL with LoadLibrary().
    // 
    // NOTE: FreeLibrary() automatically called thanks to RAII!
    //
    ScopedDll dll(L"NativeDll.dll");
    if (!dll)
    {
        ErrorMessage(L"Can't load the DLL.");
        return kExitError;
    }


    //
    // Use GetProcAddress() to access the DLL test function.
    // Note the *undecorated* "SayHello" function name!!
    //
    SayHelloFuncPtr pSayHello 
        = reinterpret_cast<SayHelloFuncPtr>(GetProcAddress(dll.Get(), 
                                                           "SayHello"));
    if (pSayHello == nullptr)
    {
        ErrorMessage(L"GetProcAddress() failed.");
        return kExitError;
    }


    //
    // Call the DLL test function
    //
    HRESULT hr = pSayHello(L"Connie");
    if (FAILED(hr))
    {
        ErrorMessage(L"DLL function call returned failure HRESULT.");
        return kExitError;
    }


    //
    // All right
    //
    return kExitOk;
}

Build the whole solution (both the .EXE and the .DLL) and run the native .EXE client.
This is what I get on my computer:

The DLL Function Call in Action

It works without modifications and with the undecorated function name (just SayHello) on both x86 and x64 builds.

Crockery answered 21/1, 2015 at 9:17 Comment(10)
So if I understand correctly I would only have to add this to the dll-projects and then my applications and C# PInvokes using the dlls would work without changes? If so are there any downsides to this solution compared to the other proposed solutions?Sollows
@dbostream: To export functions with pure C interfaces from native C++ DLL, I find .DEF files convenient to get undecorated function names.Crockery
@Mr.C64: I stand corrected. Adding an ordinal does indeed make link undecorate the symbol name. As I said, the last time I had to deal with DEF files was a long time ago (remember FAR PASCAL declarations?). Deleting previous comment, but I maintain my position that DEF files are generally a huge PITA (especially as most of my development is cross-platform). Oh, and the MS documentation is apparently quite wrong (no surprise there).Genseric
Ok so if one uses DEF files one still has to change the PInvoke to __cdecl or specifiy the undecorated name using the EntryPoint field of DllImport? Unless I have missed something it still feels like less work to just change to __cdecl than create a DEF-file for every dll I have especially since I have never used DEF-files before.Sollows
@frasnian: OK, no problem, I deleted my comment in reply to your deleted comment, too :)Crockery
@Mr.C64, I have a question after seeing your example. To PInvoke your native dll one would still use CallingConvention.StdCall right and then set EntryPoint="SayHello"?Sollows
@dbostream: I think the default calling convention for the CLR marshaller is __stdcall (i.e. the calling convention used by Win32 APIs), so there should be no need to explicitly specify CallingConvention.StdCall, and since the exported name is not mangled/decorated, I think explicitly specifying the EntryPoint="SayHello" would be useless. Anyway, just try on the C# side of things, and see what happens.Crockery
@Crockery It worked just like you said. I will play around with the proposed solutions I have been given to see which one I prefer.Thanks for the help.Sollows
@Crockery I have run into a problem using this solution. Some of the dll's we have implicitly load dll's using .lib-files and I have noticed that for my 32-bit dll's the associated lib-file has the exported functions prepended by an underscore even if I am not using underscores in the def-file. So now the exports in the lib-file have underscore but the exports in the dll don't.Sollows
BTW, if you add a .def file to your Visual Studio project. One more link option will be silently added to your linker command line /DEF:"DllFunctions.def" in the All Options window. And if you change your .def file name later for whatever reason, this link option seems not change accordingly. So be careful.Rhizobium
G
3

As you can tell, in 64-bit Windows names are not decorated.

In 32-bit __cdecl and __stdcall symbols, the symbol name is prepended by an underscore. The trailing '@8' in the exported name for the 32-bit version of your example function is the number of bytes in the parameter list. It is there because you specified __stdcall. If you use the __cdecl calling convention (the default for C/C++ code), you won't get that. If you use __cdecl, it makes it much easier to wrap GetProcAddress() with something like:

#if _WIN64
#define DecorateSymbolName(s)   s
#else
#define DecorateSymbolName(s)   "_" ## s
#endif

then just call with

pfnConnect   = GetProcAddress(hDLL, DecorateSymbolName("Connect"));
pfnOtherFunc = GetProcAddress(hDLL, DecorateSymbolName("OtherFunc"));

or something similar (error checking omitted in example). To do this, remember to declare your exported functions as:

__declspec(dllexport) long __cdecl Connect(char * name, long size);
__declspec(dllexport) long __cdecl OtherFunc(int someValue);

In addition to being easier to maintain, if during development the signature of an exported function changes, you don't have to screw around with your #define wrappers.

Downside: if during development the number of bytes in a given function's parameter list changes, it will not be caught by the application importing the function because the changing the signature will not change the name. Personally, I don't think this is an issue because the 64-bit build would blow up under the same circumstances anyway as the names are not decorated. You just have to make sure your application is using the right version of the DLL.

If the user of the DLL is using C++, you can wrap things in a better way using C++ capabilities (wrap the entire explicitly-loaded library in a wrapper class, e.g.):

class MyDLLWrapper {
public:
  MyDLLWrapper(const std::string& moduleName);  // load library here
  ~MyDLLWrapper();                              // free library here

  FARPROC WINAPI getProcAddress(const std::string& symbolName) const {
    return ::GetProcAddress(m_hModule, decorateSymbolName(symbolName));
  }
  // etc., etc.
private:
  HMODULE m_hModule;
  // etc.
  // ...
};

There's actually a lot more you can do with a wrapper class like this, it's just an example.

On edit: since OP mentioned using PInvoke in the comments - if anyone decides to do this, do not forget to add CallingConvention = CallingConvention.Cdecl in the [DllImport] declaration when using PInvoke. __cdecl might be the default for unmanaged C/C++, but is not the default for managed code.

Genseric answered 21/1, 2015 at 8:27 Comment(4)
Thanks I like this idea, one question though. Could the change to __cdecl have any side effects on the software using the dlls? We have several dlls and applications in our tool suite that would have to be changed because we use stdcall everywhere. Also we have C# dlls that PInvoke the unmanaged dlls (currently using stdcall) is it just a matter of changing Calling Convention to cdecl or will there be other problems now that the exported names will differ when using 32-bit and 64-bit.Sollows
If you change the header files containing the new (different calling convention) declarations for the exported functions and rebuild the DLL, you just have to rebuild everything that uses those exported functions so they also use the new calling convention. If everybody is on the same page, calling-convention-wise, you should be fine.Genseric
I changed the Connect function to __cdecl and using Dependency Walker now show the same name for both 32-bit and 64-bit dlls, namely Connect. If I understand link correctly I get no prefixed underscore because I use extern "C"; thus I don't need DecorateSymbolName. Does this seem reasonable or did I do something wrong?Sollows
No, that should be expected. DependencyWalker understands name decoration (haven't checked, but it probably uses UnDecorateSymbolName() - msdn.microsoft.com/en-us/library/windows/desktop/…)Genseric
A
3

__stdcall is not supported (and is ignored) on x64. Quoting MSDN:

On ARM and x64 processors, __stdcall is accepted and ignored by the compiler; on ARM and x64 architectures, by convention, arguments are passed in registers when possible, and subsequent arguments are passed on the stack.

The calling convention on x64 is pretty much __fastcall.

Since the calling conventions and name decoration rules on x86 and x64 differ, you have to abstract this somehow. So your idea with #if _WIN64 goes in the right direction.

You can examine x86 calling conventions and your needs and perhaps devise a macro which could automate the name selection process.

Aubrette answered 21/1, 2015 at 8:31 Comment(0)
R
0

For Win32 build:

If you use __stdcall, you will get something like this (dumped with dumpbin /exports):

__declspec(dllexport) int __stdcall

->

   ordinal hint RVA      name

          1    0 00001240 _F1@0 = _F1@0
          2    1 0000124D _F2@0 = _F2@0

And you have to use GetProcAddress("_F1@0") to locate the function pointer.

If you use __cdecl, you will get something like this:

__declspec(dllexport) int __cdecl

->

   ordinal hint RVA      name

          1    0 00001240 F1 = _F1
          2    1 0000124D F2 = _F2

And you can use GetProcAddress("F1") to locate the function pointer.

BTW, if you add a XXX.def file to your Visual Studio project. One more link option will be silently added to your linker command line /DEF:"XXX.def" in the All Options window. And if you change your .def file name later for whatever reason, this link option doesn't change accordingly. You need to manually change the def file name in the project properties window.

Rhizobium answered 17/9, 2020 at 3:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.