I'm wondering what sort of options I have if I want to pass arrays which have size determined at run-time to a DLL function. Currently my function signature in the DLL looks like
#ifdef BUILDING_DLL
#define DBUILDING __declspec(dllexport)
#else
#define DBUILDING __declspec(dllimport)
#endif
void __cdecl DBUILDING myFunc(const double t, const double x[], double *xdot);
So I'm passing plain old data types that are being dynamically allocated inside some class, which then must be deleted manually. I'd prefer to not have to call delete and worry about memory management so I'd like to look at other options. The obvious choice is a vector but I know you're not supposed to pass STL containers to dlls, so I need something else. I require something that handles memory management but that I can also pass to a dll.
My current thought is perhaps to use boost::scoped_array with its get() function to return the raw data in pointer form. Does this seem like the best idea or is there something I'm overlooking?
You are right that it's a bad idea to share C++ objects, especially the classes defined in the Standard, across DLL boundaries. Just some of the reasons are violations of the One Definition Rule and mixing different allocators.
However, there's nothing stopping you from passing the contents of a vector
across the DLL boundary:
vector<double> x;
// ... fill it in, change the size
myFunc(t, &x[0], x.size(), xdot);
Note that you almost always have to pass the size as an extra argument.
[...] I know you're not supposed to pass STL containers to DLLs [...] - that's faulty assumption.
In fact, you can pass STL objects across DLL boundaries. Furthermore, you can do it in cross-platform way. As usual, the clever preprocessor manipulation to rescue. The following configuration header is a must for any cross-platform DLL:
Config.hpp
:
#ifndef MY_DLL_CONFIG_HPP
#define MY_DLL_CONFIG_HPP
#ifdef __cplusplus
#define MY_DLL_CPP
#else
#define MY_DLL_C
#endif
#if \
defined (__CYGWIN__) || \
defined (__CYGWIN32__)
#define MY_DLL_OS
#define MY_DLL_OS_UNIX
#define MY_DLL_OS_WINDOWS
#define MY_DLL_OS_CYGWIN
#elif \
defined (_WIN16) || \
defined (_WIN32) || \
defined (_WIN64) || \
defined (__WIN32__) || \
defined (__TOS_WIN__) || \
defined (__WINDOWS__)
#define MY_DLL_OS
#define MY_DLL_OS_WINDOWS
#elif \
defined (macintosh) || \
defined (Macintosh) || \
defined (__TOS_MACOS__) || \
(defined (__APPLE__) && defined (__MACH__))
#define MY_DLL_OS
#define MY_DLL_OS_UNIX
#define MY_DLL_OS_MAC
#elif \
defined (linux) || \
defined (__linux) || \
defined (__linux__) || \
defined (__TOS_LINUX__)
#define MY_DLL_OS
#define MY_DLL_OS_UNIX
#define MY_DLL_OS_LINUX
#elif \
defined (__FreeBSD__) || \
defined (__OpenBSD__) || \
defined (__NetBSD__) || \
defined (__bsdi__) || \
defined (__DragonFly__)
#define MY_DLL_OS
#define MY_DLL_OS_UNIX
#define MY_DLL_OS_BSD
#elif \
defined (sun) || \
defined (__sun)
#define MY_DLL_OS
#define MY_DLL_OS_UNIX
#define MY_DLL_OS_SOLARIS
#elif \
defined (_AIX) || \
defined (__TOS_AIX__)
#define MY_DLL_OS
#define MY_DLL_OS_UNIX
#define MY_DLL_OS_AIX
#elif \
defined (hpux) || \
defined (_hpux) || \
defined (__hpux)
#define MY_DLL_OS
#define MY_DLL_OS_UNIX
#define MY_DLL_OS_HPUX
#elif \
defined (__QNX__)
#define MY_DLL_OS
#define MY_DLL_OS_UNIX
#define MY_DLL_OS_QNX
#elif \
defined (unix) || \
defined (__unix) || \
defined (__unix__)
#define MY_DLL_OS
#define MY_DLL_OS_UNIX
#endif
#ifndef MY_DLL_OS
#error "Sorry, but current OS is not supported by MyDLL."
#endif
#if \
defined (__MINGW32__) || \
defined (__MINGW64__)
#define MY_DLL_COMPILER
#define MY_DLL_COMPILER_MINGW
#elif \
defined (__GNUC__)
#define MY_DLL_COMPILER
#define MY_DLL_COMPILER_GNU
#define MY_DLL_COMPILER_GNU_VERSION_MAJOR __GNUC__
#define MY_DLL_COMPILER_GNU_VERSION_MINOR __GNUC_MINOR__
#define MY_DLL_COMPILER_GNU_VERSION_PATCH __GNUC_PATCHLEVEL__
#elif \
defined (__clang__)
#define MY_DLL_COMPILER
#define MY_DLL_COMPILER_CLANG
#elif \
defined (_MSC_VER)
#define MY_DLL_COMPILER
#define MY_DLL_COMPILER_MICROSOFT
#elif \
defined (__BORLANDC__)
#define MY_DLL_COMPILER
#define MY_DLL_COMPILER_BORLAND
#elif \
defined (__CODEGEARC__)
#define MY_DLL_COMPILER
#define MY_DLL_COMPILER_CODEGEAR
#elif \
defined (__INTEL_COMPILER) || \
defined (__ICL)
#define MY_DLL_COMPILER
#define MY_DLL_COMPILER_INTEL
#elif \
defined (__xlc__) || \
defined (__xlC__) || \
defined (__IBMC__) || \
defined (__IBMCPP__)
#define MY_DLL_COMPILER
#define MY_DLL_COMPILER_IBM
#elif \
defined (__HP_aCC)
#define MY_DLL_COMPILER
#define MY_DLL_COMPILER_HP
#elif \
defined (__WATCOMC__)
#define MY_DLL_COMPILER
#define MY_DLL_COMPILER_WATCOM
#endif
#ifndef MY_DLL_COMPILER
#error "Sorry, but current compiler is not supported by MyDLL."
#endif
#if \
defined (MY_DLL_OS_WINDOWS) && \
!defined (BOOST_DISABLE_WIN32)
#define MY_DLL_EXPORT_DECLARATION __declspec(dllexport)
#define MY_DLL_IMPORT_DECLARATION __declspec(dllimport)
#elif \
MY_DLL_COMPILER_GNU_VERSION_MAJOR >= 4
#define MY_DLL_EXPORT_DECLARATION __attribute__((visibility("default")))
#define MY_DLL_IMPORT_DECLARATION __attribute__((visibility("default")))
#else
#define MY_DLL_EXPORT_DECLARATION
#define MY_DLL_IMPORT_DECLARATION
#endif
#ifdef MY_DLL_EXPORT
#define MY_DLL_API MY_DLL_EXPORT_DECLARATION
#define MY_DLL_FUNCTION
#define MY_DLL_TYPE
#else
#define MY_DLL_API MY_DLL_IMPORT_DECLARATION
#ifdef MY_DLL_CPP
#define MY_DLL_FUNCTION extern "C"
#else
#define MY_DLL_FUNCTION extern
#endif
#define MY_DLL_TYPE extern
#endif
#endif
NOTE: There are some platform detections which might be considered overkill in the context of OP's question. However, they are more than relevant in terms of creating production DLL, so I didn't feel like stripping them.
MyDLL.hpp
:
#include "Config.hpp"
#include <vector>
// Instantiate class std::vector<int>.
// NOTE: This does not create an object. It only enforces the generation
// of all of the members of class std::vector<int>. It exports them from
// the DLL and imports them into any artifact linked against this DLL.
MY_DLL_TYPE template class MY_DLL_API std::vector<int>;
// Instantiate class std::vector<float>.
// NOTE: This does not create an object. It only enforces the generation
// of all of the members of class std::vector<float>. It exports them from
// the DLL and imports them into any artifact linked against this DLL.
MY_DLL_TYPE template class MY_DLL_API std::vector<float>;
class MY_DLL_API MyClass {
public:
static std::vector<char> floatVector;
void
setIntVector(std::vector<int> intVector);
std::vector<int>&
intVector();
bool operator < (MyClass const& c) const
{ return _intVector < c._intVector; }
bool operator == (MyClass const& c) const
{ return _intVector == c._intVector; }
private:
std::vector<int> _intVector;
};
// Instantiate class std::vector<MyClass>.
// NOTE: This does not create an object. It only enforces the generation
// of all of the members of class std::vector<MyClass>. It exports them from
// the DLL and imports them into any artifact linked against this DLL.
MY_DLL_TYPE template class MY_DLL_API std::vector<MyClass>;
MyDLL.cpp
:
#include "MyDLL.hpp"
std::vector<float> MyClass::floatVector;
void
MyClass::
setIntVector(std::vector<int> intVector)
{ _intVector = intVector; }
std::vector<int>&
MyClass::
intVector()
{ return _intVector; }
NOTE: Build with MY_DLL_EXPORT
defined. The best practice is to supply this define during compilation. For example, on MSVC it would be /DMY_DLL_EXPORT
, while on GCC it would be -DMY_DLL_EXPORT
.
Application.cpp
:
#include "MyDLL.hpp"
#include <iostream>
using std::cout;
using std::endl;
using std::vector;
int main() {
MyClass x;
x.setIntVector({-3, -2, -1});
for (int i = 0; i < 7; ++i)
x.intVector().push_back(i);
for (int i = 0; i < 10; ++i)
x.floatVector.push_back(i);
for (auto value : x.intVector())
cout << value << endl;
cout << endl;
for (auto value : x.floatVector)
cout << value << endl;
cout << endl;
vector<MyClass> v;
for (int i = 0; i < 5; ++i)
v.push_back(MyClass());
}
NOTE: Build without MY_DLL_EXPORT
defined.
As a result, consumers of your DLL will use the binary code exported from the DLL instead of instantiating their own.
There are some pitfalls that you might be not aware of, so I feel like I should mention the important restrictions for the all above to function properly.
You have to be sure that the same version of the same compiler is used to build both your DLL and its consumers.
Build settings that can impact the layout of the STL objects should be exactly the same for both your DLL and its consumers.
Both your DLL and its consumers should be linked against the same compiler runtime.
Beware of this too.
MSVC might generate warning: C4231 "nonstandard extension used : 'extern' before
template explicit instantiation.
, it can be safely ignored.
Some STL classes use other STL classes. These other classes must also be exported. The classes that must be exported are listed in compiler warnings.
Some STL classes contain nested classes. These classes cannot be exported. For
instance, deque
contains a nested class deque::iterator
. If you export deque
, you
will get a warning that you must export deque::iterator
. If you export
deque::iterator
, you get a warning that you must export deque
. I can guarantee
that both vector
and string
can be exported. The other containers all contain
nested classes and cannot be exported. I'm not sure if this is still relevant, you'll
have to experiment.
When you export an STL container parameterized with a user-defined type (UDT), you must
define the operators <
and ==
for your UDT. For example, if you export
vector<MyClass>
, you must define MyClass::operator <
and MyClass operator ==
.
This is because all STL container classes have member comparison operators that
require the existence of the operators <
and ==
for the contained type.
NOTE: Some of these might be already irrelevant, so this has to be checked in practice.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With