12

I separated a code fragment into a DLL because it will be frequently updated and in this way it should be easier to deploy.

But I have questions about what I can do and what I cannot do with a DLL.

  1. Can I pass a std:string or a CString to a DLL?
  2. Can I pass a pointer to a struct with std::string members and fill it in a DLL?
  3. Can a DLL return a pointer to a struct allocated there? Will it be valid? Can I delete it after?
  4. What should better to pass, a std::String or a Cstring?

Thanks !

Vitality
  • 20,705
  • 4
  • 108
  • 146
bratao
  • 1,980
  • 3
  • 21
  • 38

2 Answers2

31

You have a choice to make:

  • Tightly coupled DLL: The DLL is built with the exact same compiler version, packing and calling convention settings, library options as the application, and both dynamically link to the runtime library (/MD compiler option). This lets you pass objects back and forth including STL containers, allocate DLL objects from inside the application, derive from base classes in the other module, do just about everything you could without using DLLs. The disadvantage is that you can no longer deploy the DLL independently of the main application. Both must be built together. The DLL is just to improve your process startup time and working set, because the application can start running before loading the DLL (using the /delayload linker option). Build times are also faster than a single module, especially when whole program optimization is used. But optimization doesn't take place across the application-DLL boundary. And any non-trivial change will still require rebuilding both.

  • Loosely coupled: The application doesn't depend on the class layout of objects defined by the DLL. You use only highly compatible data types: primitive types, pointers, function pointers, and user-defined types made up of these elements. Classes inherit from a base class which defines interface and has no data members and no non-virtual functions (this means no constructors and no sharing of standard library objects such as std::string or CString). All allocation and object creation must be done through a factory function. Memory must be deallocated from the module which allocated it. Code and data are separated. The header file explicitly states the calling convention of each exported function and packing of each structure allowed to cross module boundaries. The advantage is that the DLL and application can be updated completely independently. You can rebuild one with a new runtime library, new compiler version, or even in a completely new language, and don't have to even touch the other.

I always advise using the loosely coupled approach.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • Wow, nice explanation.I will choose the Loosely coupled. But what is "All allocation and object creation must be done through a factory function" ? I can use std inside the dll or Atl functions ? I cannot delete a memory allocated in dll from the main application ? – bratao Mar 18 '11 at 02:42
  • 2
    @bratao: You cannot delete memory allocated in the DLL from the main application. And the DLL can use `std::string`, but it is different from `std::string` in the application. You cannot pass `std::string` between application and DLL, you instead pass `char*` as Mark suggested. – Ben Voigt Mar 18 '11 at 03:12
  • @BenVoigt true, I found this the hard way trying to use Unreal Engine 4.7 + Boost + librabbitmq + SimpleAmqpClient together, and I can't even connect because the `std::string` URI is corrupted. Still trying to find out what I should do now... – Hendy Irawan Apr 13 '15 at 11:39
  • I came across this very issue. But passing `std::string` by reference seems to work. Will passing `std::string` by reference always work?Like my dll can expose interface receiving `std::string &something` and i should be fine? – Gaurav Sehgal Jun 18 '18 at 14:10
  • @GauravSehgal: It will not "always" work. It is almost guaranteed to fail if length-affecting changes are made to the `std::string` object, but when the compiler and library versions don't match between DLL and caller, even read-only operations can fail horribly. – Ben Voigt Jun 18 '18 at 14:35
  • @BenVoigt When you say "pointers" in your answer, does that include `std::unique_ptr` & `std::shared_ptr`? Are they safe to pass across boundaries? Or not, because they're standard library types? – Ela782 Oct 04 '18 at 14:31
  • 1
    @Ela782: Scoped resource management classes are NOT safe to pass across boundaries. You can use them, client-side, to manage the raw pointers passed across the boundary (be sure to set up a custom deleter that calls the correct deallocation function, the default deleters won't work in this case), but the smart pointer objects can't cross. – Ben Voigt Oct 04 '18 at 15:19
  • @BenVoigt Thanks a lot for the clarification! That's unfortunately really sad. – Ela782 Oct 04 '18 at 20:15
  • @BenVoigt Another thing I don't quite understand: Microsoft provides an ABI compatibility guarantee between all versions of Visual Studio 2015 and 2017. To me that means it would be safe to use `std` types across these versions? And similarly with gcc - its standard library ABI is kept with a stable ABI too, "inofficially" - except for some rare exceptions like the `string` CoW change which caused an ABI change. Of course, this is all not guaranteed by the C++ standard and there is no official ABI. But it's a de-facto guarantee by the individual vendors. How does this apply to your answer? – Ela782 Oct 04 '18 at 20:56
  • 2
    @Ela782: You'd have to look at details of the guarantee, but I don't believe it makes the situation significantly better. ABI compatibility means that if your aggregates of primitive types don't change definition, you can treat those particular compiler releases as meeting the "exact same compiler version" requirement. But definitions of classes in `namespace std` are still subject to change (they must, because the C++ standards committee changes the requirements), so those still cannot be used across module boundaries. – Ben Voigt Oct 04 '18 at 23:49
  • 1
    It sounds like you are determined to push your luck instead of adopting an approach which is robust. Even if you had total ABI compatibility and library invariance between certain versions from the same compiler vendor, I still would consider that tightly coupled, because you take away the choice of compiler from consumers of your DLL. – Ben Voigt Oct 04 '18 at 23:50
  • 1
    Hm.. How come OpenCV takes `std::string&` as an argument in some of it's public functions ([imwrite](https://docs.opencv.org/4.1.0/d4/da8/group__imgcodecs.html#gabbc7ef1aa2edfaa87772f1202d67e0ce), [imread](https://docs.opencv.org/4.1.0/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56), `cv::String` is a typedef to `std::string`)? They couldn't just miss it – Andrii Zymohliad Jun 10 '19 at 07:29
  • 1
    @AndriiZymohliad: OpenCV is Open Source, so tight coupling doesn't take away the choice of compiler... even if you do distribute prebuilt binaries for a few platforms, a developer working with a different one can just recompile the source code for OpenCV. My answer addresses libraries which are distributed in binary (DLL or .so) format. – Ben Voigt Aug 10 '20 at 20:58
4

There is a danger when passing anything into and out of a DLL if it's based on a template. Compiler options can affect the layout of the object, and a template class can't be confined to a single compilation unit; some of it will be distributed to the calling module.

In the case of a string, I would pass a const char * (or const wchar_t * or const TCHAR *) and do the conversion to std::string or CString on the other side of the interface, within the DLL.

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622