Returning Strings from DLL Functions

2019-02-09 02:39发布

问题:

For some reason, returning a string from a DLL function crashes my program on runtime with the error Unhandled exception at 0x775dfbae in Cranberry Library Tester.exe: Microsoft C++ exception: std::out_of_range at memory location 0x001ef604...

I have verified it's not a problem with the function itself by compiling the DLL code as an .exe and doing a few simple tests in the main function.

Functions with other return types (int, double, etc.) work perfectly.

  • Why does this happen?
  • Is there a way to work around this behavior?

Source code for DLL:

// Library.h
#include <string>

std::string GetGreeting();

.

// Library.cpp
#include "Library.h"

std::string GetGreeting()
{
    return "Hello, world!";
}

Source code for tester:

// Tester.cpp
#include <iostream>
#include <Library.h>

int main()
{
    std::cout << GetGreeting()
}

EDIT: I'm using VS2010.


Conclusion

A workaround is to make sure the library and source are compiled using the same compiler with the same options, etc.

回答1:

Since your error message indicates you're using Microsoft C++ I'll offer an MS specific answer.

As long as you compile both the EXE and the DLL with the SAME compiler, and both link the the SAME version of the runtime DYNAMICALLY then you'll be just fine. For example, using "Multi-threaded DLL" for both.

If you link against the runtime statically, or link against different versions of the runtime then you're SOL for the reasons @Billy ONeal points out (memory will be allocated in one heap and freed in another).



回答2:

This occurs because you're allocating memory in one DLL (using std::string's constructor), and freeing it in another DLL. You can't do that because each DLL typically sets up it's own heap.



回答3:

I had such problem that has been solved by:

  • using runtime library as DLL for both the application and the DLL (so that allocation is handle in a unique place)

  • making sure all project settings (compiler, linker) are the same on both project



回答4:

Update:

I have compiled your sample in both VS2008 and VS2010 and I was able to successfully compile and execute without a problem. I compiled the library both as a static and a dynamic library.

Original:

The following relates to my discussion with bdk and imaginaryboy. I did not delete it as it might be of some interest to someone.

Ok this question really bothers me because it looks like its being passed by value not by reference. There does not appear to be any objects created in the heap it looks to be entirely stack based.

I did a quick test to check how objects are passed in Visual Studios (compiled in release mode without link time optimization and optimization disabled).

The Code:

class Foo {
    int i, j;
public:
    Foo() {}
    Foo(int i, int j) : i(i), j(j) { }
};

Foo builder(int i, int j)
{
    Foo built(i, j);
    return built;
}


int main()
{
    int i = sizeof(Foo);
    int j = sizeof(int);
    Foo buildMe;
    buildMe = builder(i, j);
    //std::string test = GetGreeting();
    //std::cout << test;
    return 0;
}

The Disassembly:

int main()
{
00AD1030  push        ebp  
00AD1031  mov         ebp,esp 
00AD1033  sub         esp,18h 
    int i = sizeof(Foo);
00AD1036  mov         dword ptr [i],8 
    int j = sizeof(int);
00AD103D  mov         dword ptr [j],4 
    Foo buildMe;
    buildMe = builder(i, j);
00AD1044  mov         eax,dword ptr [j]               ;param j
00AD1047  push        eax  
00AD1048  mov         ecx,dword ptr [i]               ;param i
00AD104B  push        ecx  
00AD104C  lea         edx,[ebp-18h]                   ;buildMe
00AD104F  push        edx  
00AD1050  call        builder (0AD1000h) 
00AD1055  add         esp,0Ch 
00AD1058  mov         ecx,dword ptr [eax]             ;builder i
00AD105A  mov         edx,dword ptr [eax+4]           ;builder j
00AD105D  mov         dword ptr [buildMe],ecx 
00AD1060  mov         dword ptr [ebp-8],edx 
    return 0;
00AD1063  xor         eax,eax 
}
00AD1065  mov         esp,ebp 
00AD1067  pop         ebp  
00AD1068  ret  


Foo builder(int i, int j)
{
01041000  push        ebp  
01041001  mov         ebp,esp 
01041003  sub         esp,8 
    Foo built(i, j);
01041006  mov         eax,dword ptr [i]     
01041009  mov         dword ptr [built],eax ;ebp-8 built i
0104100C  mov         ecx,dword ptr [j] 
0104100F  mov         dword ptr [ebp-4],ecx ;ebp-4 built j
    return built;
01041012  mov         edx,dword ptr [ebp+8] ;buildMe
01041015  mov         eax,dword ptr [built] 
01041018  mov         dword ptr [edx],eax   ;buildMe (i)
0104101A  mov         ecx,dword ptr [ebp-4] 
0104101D  mov         dword ptr [edx+4],ecx ;buildMe (j)
01041020  mov         eax,dword ptr [ebp+8] 
}

The Stack:

0x003DF964  08 00 00 00  ....
0x003DF968  04 00 00 00  ....
0x003DF96C  98 f9 3d 00  ˜ù=.
0x003DF970  55 10 ad 00  U.­.
0x003DF974  80 f9 3d 00  €ù=.
0x003DF978  08 00 00 00  ....   ;builder i param
0x003DF97C  04 00 00 00  ....   ;builder j param
0x003DF980  08 00 00 00  ....   ;builder return j 
0x003DF984  04 00 00 00  ....   ;builder return i 
0x003DF988  04 00 00 00  ....   ;j
0x003DF98C  08 00 00 00  ....   ;buildMe i param
0x003DF990  04 00 00 00  ....   ;buildMe j param
0x003DF994  08 00 00 00  ....   ;i
0x003DF998  dc f9 3d 00  Üù=.   ;esp

Why it applies:

Even if the code is in a seperate DLL the string that is returned is copied by value into the callers stack. There is a hidden param that passes the object to GetGreetings(). I do not see any heap getting created. I do not see the heap having anything to do with the problem.

int main()
{
01021020  push        ebp  
01021021  mov         ebp,esp 
01021023  push        0FFFFFFFFh 
01021025  push        offset __ehhandler$_main (10218A9h) 
0102102A  mov         eax,dword ptr fs:[00000000h] 
01021030  push        eax  
01021031  sub         esp,24h 
01021034  mov         eax,dword ptr [___security_cookie (1023004h)] 
01021039  xor         eax,ebp 
0102103B  mov         dword ptr [ebp-10h],eax 
0102103E  push        eax  
0102103F  lea         eax,[ebp-0Ch] 
01021042  mov         dword ptr fs:[00000000h],eax 
    std::string test = GetGreeting();
01021048  lea         eax,[ebp-2Ch]   ;mov test string to eax
0102104B  push        eax   
0102104C  call        GetGreeting (1021000h) 
01021051  add         esp,4 
01021054  mov         dword ptr [ebp-4],0 
    std::cout << test;
0102105B  lea         ecx,[ebp-2Ch]   
0102105E  push        ecx  
0102105F  mov         edx,dword ptr [__imp_std::cout (1022038h)] 
01021065  push        edx  
01021066  call        dword ptr [__imp_std::operator<<<char,std::char_traits<char>,std::allocator<char> > (102203Ch)] 
0102106C  add         esp,8 
    return 0;
0102106F  mov         dword ptr [ebp-30h],0 
01021076  mov         dword ptr [ebp-4],0FFFFFFFFh 
0102107D  lea         ecx,[ebp-2Ch] 
01021080  call        dword ptr [__imp_std::basic_string<char,std::char_traits<char>,std::allocator<char> >::~basic_string<char,std::char_traits<char>,std::allocator<char> > (1022044h)] 
01021086  mov         eax,dword ptr [ebp-30h] 
}

std::string GetGreeting()
{
01021000  push        ecx  ;ret + 4
01021001  push        esi  ;ret + 4 + 4 = 0C
01021002  mov         esi,dword ptr [esp+0Ch] ; this is test string 
    std::string greet("Hello, world!");
01021006  push        offset string "Hello, world!" (1022124h) 
0102100B  mov         ecx,esi 
0102100D  mov         dword ptr [esp+8],0 
01021015  call        dword ptr [__imp_std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (1022040h)] 
    return greet;
0102101B  mov         eax,esi ;put back test
0102101D  pop         esi  
    //return "Hello, world!";
}


回答5:

Just like snmacdonald I was unable to repro your crash, under normal circumstances. Others already mentioned: Configuration Properties -> Code Generation -> Runtime Library must be exactly the same

If I change one of them I get your crash. I have both the DLL and EXE set to Multi-Threaded DLL.



回答6:

You may return a std::string from a class as long as you inline the function that creates the string. This is done, for example, by the Qt toolkit in their QString::toStdString method:

inline std::string QString::toStdString() const
{ const QByteArray asc = toAscii(); return std::string(asc.constData(), asc.length()); }

The inline function is compiled with the program that uses the dll, not when the dll is compiled. This avoids any problems encountered by incompatible runtimes or compiler switches.

So in fact, you should only return standard types from your dll (such as const char * and int above) and add inline functions to convert them to std::string().

Passing in a string & parameter may also be unsafe because they are often implemented as copy on write.



回答7:

Just like user295190 and Adam said that it would work fine if with the exactly same compilation settings.

For example, in Qt QString::toStdString() would return a std::string and you could use it in your EXE from QtCore.dll.

It would crash if DLL and EXE had different compiling settings and linking settings. For example, DLL linked to MD and EXE linked to MT CRT library.