Search code examples
c++visual-c++dlllinker

Encapsulating static libraries in dynamic-link libraries (DLL)


I'm trying to increase my understanding of basic library linking, dependencies, etc. I created a Visual Studio solution with three projects

  1. Static lib using /MTd with a single class (Foo), one method int GetNum() { return 5; }

  2. Shared dll using /MDd with a single class (Bar), one method int GetNum() { Foo f; return f.GetNum(); }

  3. Win32 console app. That calls Bar b; std::cout << b.GetNum() << std::endl

When I tried to build this, it complained it couldn't find my dll's associated lib. Did a little research, saw that I needed to add __declspec(dllexport) to my GetNum() method and I'd get a .lib. Cool.

Next hurtle was the console app said it couldn't find the static lib for Foo. I added it to my references and it all build and ran fine.

My question is - why does my exe need to know anything about Foo? I wanted to effectively "bake" in all my dependencies into the dll so I could just share that, link into it, and be good to go.

Is this just not how the language works or a setting / pattern I'm missing? My end goal is to be able to build a dll that encapsulates the usage of third party .lib's and not have the client app need to worry about adding references to all of them.

Update

Here is most of the code.

    // ---------------------- Lib (e.g. Foo)
    #pragma once
    class MathLib
    {
    public:
        MathLib(void);
        ~MathLib(void);
        int GetNum() { return 83; }
    };

    // ---------------------- DLL (e.g. Bar)
    #pragma once

    #ifdef CONSOLETEST_EXPORT
        #define CONSOLETEST_API __declspec(dllexport)
    #else
        #define CONSOLETEST_API __declspec(dllimport)
    #endif

    #include "MathLib.h"

    class MathDll
    {
    public:
        __declspec(dllexport) MathDll(void);
        __declspec(dllexport) ~MathDll(void);
        __declspec(dllexport) int GetNumFromDyn() 
        {
            MathLib m;
            return m.GetNum();
        }

    };


    // ---------------------- exe
    int _tmain(int argc, _TCHAR* argv[])
    {
        MathDll m;
        std::cout << "num is " << m.GetNumFromDyn() << std::endl;
        return 0;
    }

Solution

  • With C/C++, it's very important to structure your code properly across headers (e.g. h, hpp, hxx, h++, etc.) and translation units (usually called sources, e.g. c, cpp, cxx, c++, etc.). When you design a library, you should be constantly thinking what belongs to its interface (i.e. supposed to be seen by consumers) and what belongs to its implementation (i.e. not supposed to be seen by consumers).

    Remember the rule of thumb - all symbols that are present in any header will be seen by consumers (if included), and, as a result, required by consumers to be resolved during linking stage at some point in time later!

    This is essentially what happened to you in your toy example. So let's fix it by using a simple rule, which you should remember by heart: Put as much as possible into translation units, i.e. keep headers minimal. Now let's use your example to show how it works:

    MathLib.hpp:

    #pragma once
    
    class MathLib {
    public:
      MathLib();
      ~MathLib();
      int GetNum();
    };
    

    MathLib.cpp:

    #include "MathLib.hpp"
    
    MathLib::MathLib() {}
    
    MathLib::~MathLib() {}
    
    int MathLib::GetNum() { return 83; }
    

    Now build MathLib.cpp as static library.

    MathDll.hpp:

    #pragma once
    
    #ifdef CONSOLETEST_EXPORT
    #  define CONSOLETEST_API __declspec(dllexport)
    #else
    #  define CONSOLETEST_API __declspec(dllimport)
    #endif
    
    class CONSOLETEST_API MathDll {
    public:
      MathDll();
      ~MathDll();
      int GetNumFromDyn();
    };
    

    MathDll.cpp:

    #include "MathDll.hpp"
    #include "MathLib.hpp"
    
    MathDll::MathDll() {}
    
    MathDll::~MathDll() {}
    
    int MathDll::GetNumFromDyn() { 
      MathLib m;
      return m.GetNum();
    }
    

    Now build MathDll.cpp as dynamic-link library (DLL) and don't forget to add definition CONSOLETEST_EXPORT during its build, so that CONSOLETEST_API is __declspec(dllexport), and, as a result, an import library with exported symbols (i.e. the MathDll class and its methods) is generated for the DLL. On MSVC you can achieve this by adding /DCONSOLETEST_API to the invocation of compiler. Finally, when building this DLL, certainly link it with previously built static library, MathLib.lib.

    NOTE: It's better to export the whole class like I did above with class CONSOLETEST_API MathDll, rather than export all methods individually.

    main.cpp:

    #include "MathDll.hpp"
    
    #include <iostream>
    
    int _tmain(int argc, _TCHAR* argv[]) {
      MathDll m;
      std::cout << "num is " << m.GetNumFromDyn() << std::endl;
      return 0;
    }
    

    Now build main.cpp as console application and only link it with previously built import library for DLL, MathDll.lib.

    Notice how the problem is gone because I've got rid of transitive dependency to MathLib (through MathDll.hpp) from main.cpp, since now the #include "MathLib.hpp" inclusion is done in the translation unit MathDll.cpp (because it's actually only needed there according to above rule), and is therefore built into binary artifact (DLL in this case) and not present in its interface.

    Understanding all of this is really important for proper native software development with C/C++, so it's really good that you ask this question beforehand. I meet people who don't know/understand this quite often, what results in complete nightmare for them (amateurs), and us, when we have to deal with that crappy software they write...