C++ DLLs: how to create the corresponding include headers?

Problem

I have a C++ project and create a library A from it. If I now link another project B with this library A, I of course also have to provide an include path for A's headers, so I just use A's source folder. But A's headers contain symbols that aren't exported. I feel like this is not the correct way to do it, but don't know better. A specific thing that makes me feel like this is incorrect is that my IDE suggests the symbols that aren't exported.

I'd guess the solution would be to create an include folder besides the source folder where the same headers are in but only with the exported symbols. So at build-time, every symbol with PROJECTAPI should be automatically copied over to the corresponding headers in the include folder. But if I google, I don't find such a function for e.g. cmake.

So what would be the recommended way here? Is there a functionality to create such an include folder?

Example

example.cpp of project B

#include <A/main.hpp>

int main() {
    ex::World w("Earth");
    w.say_hello();
    //IDE wouldn't see this as error: w.private();
}

main.cpp of project A

#include <iostream>
#include "main.hpp"

namespace ex {
void World::say_hello() {
    std::cout << "Hello, World from " << m_name << std::endl;
}

World::World(std::string name)
    : m_name(name)
{}

void World::hidden() {
    std::cout << "Not exported" << std::endl;
}
}

main.hpp of project A

#include <string>

#ifndef PROJECTAPI
#  ifdef example_EXPORTS
#    define PROJECTAPI __declspec(dllexport)
#  else
#    define PROJECTAPI __declspec(dllimport)
#  endif
#endif

namespace ex {
class World {
private:
    std::string m_name;
public:
    void PROJECTAPI say_hello();
    PROJECTAPI World(std::string name);
    void hidden();
};
}

Edit: private isn't a good method name

2 answers

  • answered 2021-10-12 16:12 Frank

    Normally, a simple way to tackle that would be to create separate public and private headers ahead of time, and only expose the public ones to the user.

    Here's a simple project structure that would accomplish that:

    - lib_a
     - include
       - main.hpp
     - src
       - main_private.hpp
       - main.cpp
    

    Now, obviously, that won't work for the code you posted, since the declarations you want to separate belong to the same class. But that's just a symptom of the fact that what you are trying to do is unfortunately not allowed.

    From the standard basic.def.odr:

    There can be more than one definition of a (13.1) class type ([class]),

    [...]

    in a program provided that each definition appears in a different translation unit and the definitions satisfy the following requirements.

    [...]

    Each such definition shall consist of the same sequence of tokens [...]

    In other words, if you put a class in a public header, it has to be identical to the one that was used when compiling the library.

    As much as it would be convenient, putting "half-a-class" in a public header is just not allowed.

  • answered 2021-10-12 18:55 Alex Reinking

    You are looking for the PIMPL idiom. "PIMPL" is short for "Pointer to IMPLementation". The idea is that, at the cost of a pointer indirection, you hide the implementation data and private methods in an inner class whose definition is opaque to the API consumer.

    This approach is especially effective if you need to provide ABI stability.

    Herb Sutter has a great GOTW on this here: https://herbsutter.com/gotw/_100/


    Here is a full example that's close-ish to your code:

    $ tree
    .
    ├── CMakeLists.txt
    ├── include
    │   └── world.h
    ├── main.cpp
    └── src
        ├── world.cpp
        └── world_priv.h
    

    In ./include/world.h (the public header)

    #ifndef WORLD_H
    #define WORLD_H
    
    #include <memory>
    #include <string>
    
    #include "world_export.h"
    
    namespace ex {
    
    class World {
    public:
      WORLD_EXPORT World(std::string name);
      WORLD_EXPORT ~World() /* = default */;
    
      void WORLD_EXPORT say_hello();
    
    private:
      class Impl;
      std::unique_ptr<Impl> pImpl;
    };
    
    } // namespace ex
    
    #endif
    

    In ./src/world_priv.h:

    #ifndef WORLD_PRIV_H
    #define WORLD_PRIV_H
    
    #include "world.h"
    
    namespace ex {
    
    class World::Impl {
    public:
      Impl(std::string name) : name(std::move(name)) {}
    
      void say_hello();
      void hidden();
    
    private:
      std::string name;
    };
    
    } // namespace ex
    
    #endif
    

    In ./src/world.cpp:

    #include <iostream>
    
    #include "world_priv.h"
    
    namespace ex {
    
    World::World(std::string name)
        : pImpl(std::make_unique<Impl>(std::move(name))) {}
    World::~World() = default;
    
    void World::say_hello() { pImpl->say_hello(); }
    
    void World::Impl::say_hello() {
      std::cout << "Hello, World from " << name << "\n";
    }
    
    void World::Impl::hidden() { std::cout << "Not exported" << std::endl; }
    
    } // namespace ex
    

    In main.cpp:

    #include <world.h>
    
    int main() {
      ex::World w("Earth");
      w.say_hello();
    }
    

    Finally, here's the build:

    cmake_minimum_required(VERSION 3.21)
    project(pimpl_example)
    
    option(BUILD_SHARED_LIBS "Build world as shared rather than static" ON)
    
    # Library
    
    include(GenerateExportHeader)
    
    set(CMAKE_CXX_VISIBILITY_PRESET hidden)
    set(CMAKE_VISIBILITY_INLINES_HIDDEN 1)
    
    add_library(world src/world.cpp src/world_priv.h include/world.h)
    add_library(world::world ALIAS world)
    
    target_include_directories(
      world PRIVATE "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>"
            PUBLIC  "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
    )
    
    generate_export_header(world EXPORT_FILE_NAME include/world_export.h)
    target_compile_definitions(
        world PUBLIC "$<$<NOT:$<BOOL:${BUILD_SHARED_LIBS}>>:WORLD_STATIC_DEFINE>")
    target_include_directories(
        world PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>")
    
    # Application
    
    add_executable(app main.cpp)
    target_link_libraries(app PRIVATE world::world)
    

    This doesn't include install rules or anything, but it's ready for those to be written.


    Building it:

    $ cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
    ...
    $ cmake --build build
    $ ./build/app
    Hello, World from Earth
    $ $ nm ./build/libworld.so | c++filt | grep ' T ' | uniq
    0000000000001460 T ex::World::say_hello()
    00000000000012e0 T ex::World::World(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
    00000000000013c0 T ex::World::~World()
    

    You can see that only the API of ex::World is exported by the library. All the private details are hidden both in the code and in the library itself.

    On Windows:

    >dumpbin /EXPORTS build\world.dll
    Microsoft (R) COFF/PE Dumper Version 14.28.29915.0
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    
    Dump of file build\world.dll
    
    File Type: DLL
    
      Section contains the following exports for world.dll
    
        00000000 characteristics
        FFFFFFFF time date stamp
            0.00 version
               1 ordinal base
               3 number of functions
               3 number of names
    
        ordinal hint RVA      name
    
              1    0 00001390 ??0World@ex@@QEAA@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z
              2    1 00001550 ??1World@ex@@QEAA@XZ
              3    2 000015D0 ?say_hello@World@ex@@QEAAXXZ
    
      Summary
    
            1000 .data
            1000 .pdata
            2000 .rdata
            1000 .reloc
            1000 .rsrc
            2000 .text
    

How many English words
do you know?
Test your English vocabulary size, and measure
how many words do you know
Online Test
Powered by Examplum