Is it safe to throw an exception from a DLL?

Started by
4 comments, last by jpetrie 15 years, 9 months ago
Like the title says, is it safe to throw an exception in a DLL? I'm working with boost::asio and I'm creating a class in a DLL that does all the boost::asio stuff. Since boost::asio might throw an exception (and since all boost::asio related things are in the DLL, the exception would be thrown from the DLL), is it safe to try to catch that exception in the .exe? Or is there a problem with throwing an exception in a DLL and catching it in a exe?
[size=2][ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]
Advertisement
Yes allowing an exception to propagate across a dll boundary is bad ™
You can do it yet you must ensure that the dll and the exe are compiled using the same runtime and compiler. Why not try (haha) and catch the exception in the dll?
As long as you can verify that your compile settings will be consistent and will always be used with the same compiler, you're fine. The minute you have inconsistent compile settings, it's potentially a problem. But, then, if you have different compile settings, having C++ objects between DLL boundaries becomes potentially unsafe, as well.

If you are in the situation where you can't guarantee the compile settings and compiler will be the same, then you should probably wrap it in a C interface, and serialize the exception on the throwing side. On the calling side, wrap your C interface in a proxy C++ class, and then you can then check for an exception in the method, if so, deserialize it, and throw again.
@dmail: Catching the exception in the DLL wouldn't be a problem, but I can't recover from it (it would be a fatal exception), so I'd like the .exe to know that there was a fatal exception, which means I need a way for the .exe to know if an exception was thrown.

I could use error codes and have the functions return an error code, but that's a last resort for me. I don't want to deal with error codes.

@Rydinare: What exactly do you mean? Is it not safe to do:

class __declspec(dllexport) MyClass
{
};

in some header for the dll and then in the exe do:

class __declspec(dllimport) MyClass;

?? What about Ogre3D or FMOD? They have a dll and an object oriented interface (meaning C++).
[size=2][ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]
Quote:Original post by MikeTacular
@dmail: Catching the exception in the DLL wouldn't be a problem, but I can't recover from it (it would be a fatal exception), so I'd like the .exe to know that there was a fatal exception, which means I need a way for the .exe to know if an exception was thrown.

I could use error codes and have the functions return an error code, but that's a last resort for me. I don't want to deal with error codes.

@Rydinare: What exactly do you mean? Is it not safe to do:

class __declspec(dllexport) MyClass
{
};

in some header for the dll and then in the exe do:

class __declspec(dllimport) MyClass;

?? What about Ogre3D or FMOD? They have a dll and an object oriented interface (meaning C++).


It's not necessarily unsafe, just potentially unsafe if you're not careful. I was simply urging caution. As a quick example, take a look at STL containers in VS2005. As a new addition to VS2005 with their checked iterators, containers are not the same size in debug vs. release mode. As such, you if you mix a debug exe with release DLL (or vice versa) and STL is used through the DLL interface, you are likely to get a pretty nasty crash.

Likewise, certain optimizations or lack of optimizations, can cause potential incompatibilities. It's something to be aware of, that if you provide DLLs, that you may need to provide versions for every compiler you support, and multiple versions for different compiler settings that might be invoked.
Quote:
Is it not safe to do:

class __declspec(dllexport) MyClass
{
};

in some header for the dll and then in the exe do:

class __declspec(dllimport) MyClass;

?? What about Ogre3D or FMOD? They have a dll and an object oriented interface (meaning C++).

What you have described above, with regards to the __declspec() stuff and class definition, is not passing an object across the DLL boundary. That's simply the definition of a class that programs on either side of the boundary require to be compiled. Passing objects across the boundary occurs at runtime.

Consider this simplified scenario on a theoretically simple compiler. You are on a 32-bit machine. You have foo.exe and bar.dll, both making use of a common header describing some class C. foo.exe was compiled with alignment set to 4. bar.exe was compiled with alignment set to 8. C is a simple struct with the appropriate DLL export cruft and a pair of integer members, a and b. It has a constructor that zero-initializes both members.

The DLL contains one exported function, BarDllCreateC(), which basically does "return new C();"

foo.exe looks like this:
#include <iostream>#include "C.h"#include "Bar.h"int main() {  C *local = new C();  C *remote = BarDllCreateC();  std::cout << local->b << "\n";  std::cout << remote->b << "\n";}


When the compiler is generating code for main, it knows about C, and it knows that C's members are contiguous (since alignment is 4 bytes, and integers are 4 bytes, no extra space is inserted). So you will likely end up with machine code that basically finds the value of local->b by doing "fetch the value at (the address of local plus four bytes)". Likewise for remote->b "fetch the value at (the address of remote plus four bytes)." Because that's all the compiler does, after all -- it knows the definition of the class, and how it was configured to lay that class out, so it can make compile-time decisions like that.

Except that remote was allocated by a function in the DLL, which was compiled with a padding of 8. This means that when BarDllCreateC() ran, it calls the constructor of C as compiled in the DLL. In the DLL, C has four bytes of padding between each member to make each member align on eight byte boundaries. So when the instance is initialized by the constructor, 0 is written to "address of this plus 0" (a) and "address of this plus eight" (b). "Address of this plus four" is left uninitialized.

Then we give the pointer to that object to main() by returning it from BarDllCreateC(), and main tries to print what is at "address of remote plus four." The result is garbage.

The takeaway is to understanding that compiler settings can dictate the "shape" of an object in memory, as well how the code being compiled interprets those objects. If give a square to code that expects to see a triangle, it's not going to function exactly the same.

This topic is closed to new replies.

Advertisement