How can I export symbols? (Exporting class-methods)

I still consider myself a fairly novice C++ programmer and with CMake I have even less experience.
In an Eclipse CMake project I have written a shared library for accessing an XML database (BaseX). In the IDE the code compiles without errors to a shared object. In another project I wrote a test program that links to the library. This program compiles with the public API and works as expected.

According to this article ‘Modern CMake tutorial for C++ library developers’, it should be able to package a library. This article provides a template that should make packaging easy (GitHub - pananton/cpp-lib-template: Template for C++ library built with CMake).

  • In this template I first replaced all occurrences of Mylib and to MYLIB to BasexCpp respectively BASEXCPP.
  • The commands cmake -S . -B Build/Debug --fresh --preset=dev-linux -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -DCMAKE_BUILD_TYPE=Debug -G Ninja and ninja (after cd to Build/Debug) result in a shared library with only 1 function (Add) and an executable (libTest). This program uses the Add function that is provided by the library.

I then added the dependency with Curl in CMakeLists.txt and copied the source code of my library to the directory tree. The new CMakeLists.txt, the public API BasexCpp.h and the testfile main.cpp are added below.
Even after adding my sources, the test program works. However, when I uncomment the code in main.cpp, ninja returns this error message:

[11/11] Linking CXX executable examples/add/libTest
FAILED: examples/add/libTest 
: && /usr/lib64/ccache/c++ -std=c++20 -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-conversion -Wcast-align -Wcast-qual -Wnull-dereference -Woverloaded-virtual -Wformat=2 -Werror -g  examples/add/CMakeFiles/libTest.dir/main.cpp.o -o examples/add/libTest  -Wl,-rpath,/home/bengbers/Thuis/CPP_LIB4/Build/Debug  libBasexCpp.so.1.0.0  -Llib64  -lcurlpp  -L/usr/lib64  -lcurl && :
/usr/bin/ld: examples/add/CMakeFiles/libTest.dir/main.cpp.o: in function `main':
/home/bengbers/Thuis/CPP_LIB4/examples/add/main.cpp:19:(.text+0xf6): undefined reference to `BasexCpp::BasexClient::BasexClient(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, int const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
/usr/bin/ld: /home/bengbers/Thuis/CPP_LIB4/examples/add/main.cpp:21:(.text+0x154): undefined reference to `BasexCpp::BasexClient::Create(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
/usr/bin/ld: /home/bengbers/Thuis/CPP_LIB4/examples/add/main.cpp:24:(.text+0x185): undefined reference to `BasexCpp::BasexClient::~BasexClient()'
/usr/bin/ld: /home/bengbers/Thuis/CPP_LIB4/examples/add/main.cpp:24:(.text+0x1f8): undefined reference to `BasexCpp::BasexClient::~BasexClient()'
collect2: fout: ld gaf exit-status 1 terug
ninja: build stopped: subcommand failed.

I don’t know how to interpret the results from the command readelf -s Build/Debug/libBasexCpp.so > ReadElf.out but after some googling I learned that readelf can be used to inspect the contents from the library.
This is part of ReadElf.out

    9: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS Add.cpp
    10: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS Base.cpp
    11: 00000000000451c7     1 OBJECT  LOCAL  DEFAULT   14 _ZNSt8__detail30[...]
    12: 00000000000451c8     1 OBJECT  LOCAL  DEFAULT   14 _ZNSt8__detail30[...]
    13: 00000000000451c9     1 OBJECT  LOCAL  DEFAULT   14 _ZNSt8__detail30[...]
    14: 00000000000451ca     1 OBJECT  LOCAL  DEFAULT   14 _ZNSt8__detail14[...]
    15: 00000000000451cb     1 OBJECT  LOCAL  DEFAULT   14 _ZNSt8__detail19[...]
    16: 00000000000451cc     1 OBJECT  LOCAL  DEFAULT   14 _ZNSt8__detail14[...]
    17: 00000000000451cd     1 OBJECT  LOCAL  DEFAULT   14 _ZNSt8__detail19[...]

I should expect that the lines 11-17 would export the Base-constructor.

Cane someone help me and tell me what I am doing wrong?

Ben

CmakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(BasexCpp
    VERSION 1.0.0
    DESCRIPTION "Template for C++ library built with CMake"
    LANGUAGES CXX)

#----------------------------------------------------------------------------------------------------------------------
# general settings and options
#----------------------------------------------------------------------------------------------------------------------

include(cmake/utils.cmake)
include(GNUInstallDirs)

string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}" is_top_level)

# BASEXCPP_SHARED_LIBS option (undefined by default) can be used to force shared/static build
set(BASEXCPP_SHARED_LIBS ON)
option(BASEXCPP_BUILD_TESTS "Build BasexCpp tests" OFF)
# option(BASEXCPP_BUILD_EXAMPLES "Build BasexCpp examples" ON)
option(BASEXCPP_BUILD_DOCS "Build BasexCpp documentation" OFF)
option(BASEXCPP_INSTALL "Generate target for installing BasexCpp" ${is_top_level})
set_if_undefined(BASEXCPP_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/BasexCpp" CACHE STRING
    "Install path for BasexCpp package-related CMake files")

if(DEFINED BASEXCPP_SHARED_LIBS)
    set(BUILD_SHARED_LIBS ${BASEXCPP_SHARED_LIBS})
endif()

if(NOT DEFINED CMAKE_BUILD_TYPE AND NOT DEFINED CMAKE_CONFIGURATION_TYPES)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()

set_if_undefined(CMAKE_CXX_VISIBILITY_PRESET hidden)
set_if_undefined(CMAKE_VISIBILITY_INLINES_HIDDEN ON)

add_library(BasexCpp) # initialized below
add_library(BasexCpp::BasexCpp ALIAS BasexCpp)

#----------------------------------------------------------------------------------------------------------------------
# BasexCpp dependencies
#----------------------------------------------------------------------------------------------------------------------

# Search for your dependencies here
include(FindPkgConfig)
pkg_check_modules(CURLPP REQUIRED curlpp)
include(FindCURL)
find_package(CURL REQUIRED)
if(NOT CURL_FOUND)
  message(FATAL_ERROR "Could not find CURL")
else()
  target_link_libraries(${PROJECT_NAME} ${CURLPP_LDFLAGS})
endif()

#----------------------------------------------------------------------------------------------------------------------
# BasexCpp sources
#----------------------------------------------------------------------------------------------------------------------

include(GenerateExportHeader)
set(export_file_name "export_shared.h")

if(NOT BUILD_SHARED_LIBS)
    set(export_file_name "export_static.h")
endif()

generate_export_header(BasexCpp EXPORT_FILE_NAME include/BasexCpp/${export_file_name})

target_include_directories(BasexCpp PRIVATE src)

set(public_headers
    include/BasexCpp/export.h
#    include/BasexCpp/Add.h
    include/BasexCpp/BasexCpp.h
)

set(sources
    ${public_headers}
    src/Add.cpp

    src/Base.cpp
    src/BasexClient.cpp
    src/BasexSocket.cpp
    src/ClientUtils.cpp
    src/QueryObject.cpp
    src/ResponseObj.cpp
    src/BasexClient.h
    src/BasexSocket.h
    src/ClientUtils.h
    src/QueryObject.h
    src/ResponseObj.h
    )
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${sources})

#----------------------------------------------------------------------------------------------------------------------
# BasexCpp target
#----------------------------------------------------------------------------------------------------------------------

include(CMakePackageConfigHelpers)

target_sources(BasexCpp PRIVATE ${sources})
target_compile_definitions(BasexCpp PUBLIC "$<$<NOT:$<BOOL:${BUILD_SHARED_LIBS}>>:BASEXCPP_STATIC_DEFINE>")

target_include_directories(BasexCpp
    PUBLIC
        "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
        "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>")

set_target_properties(BasexCpp PROPERTIES
    SOVERSION ${PROJECT_VERSION_MAJOR}
    VERSION ${PROJECT_VERSION})

if(BASEXCPP_INSTALL AND NOT CMAKE_SKIP_INSTALL_RULES)
    configure_package_config_file(cmake/BasexCpp-config.cmake.in BasexCpp-config.cmake
        INSTALL_DESTINATION "${BASEXCPP_INSTALL_CMAKEDIR}")

    write_basic_package_version_file(BasexCpp-config-version.cmake
        COMPATIBILITY SameMajorVersion)

    install(TARGETS BasexCpp EXPORT BasexCpp_export
        RUNTIME COMPONENT BasexCpp
        LIBRARY COMPONENT BasexCpp NAMELINK_COMPONENT BasexCpp-dev
        ARCHIVE COMPONENT BasexCpp-dev
        INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
    install(DIRECTORY include/
        TYPE INCLUDE
        COMPONENT BasexCpp-dev)
    install(FILES "${CMAKE_CURRENT_BINARY_DIR}/include/BasexCpp/${export_file_name}"
        COMPONENT BasexCpp-dev
        DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/BasexCpp")

    set(targets_file "BasexCpp-shared-targets.cmake")

    if(NOT BUILD_SHARED_LIBS)
        set(targets_file "BasexCpp-static-targets.cmake")
    endif()

    install(EXPORT BasexCpp_export
        COMPONENT BasexCpp-dev
        FILE "${targets_file}"
        DESTINATION "${BASEXCPP_INSTALL_CMAKEDIR}"
        NAMESPACE BasexCpp::)

    install(FILES
        "${CMAKE_CURRENT_BINARY_DIR}/BasexCpp-config.cmake"
        "${CMAKE_CURRENT_BINARY_DIR}/BasexCpp-config-version.cmake"
        COMPONENT BasexCpp-dev
        DESTINATION "${BASEXCPP_INSTALL_CMAKEDIR}")

    if(MSVC)
        set(pdb_file "")
        set(pdb_file_destination "")

        if(BUILD_SHARED_LIBS)
            set(pdb_file "$<TARGET_PDB_FILE:BasexCpp>")
            set(pdb_file_destination "${CMAKE_INSTALL_BINDIR}")
        else()
            # TARGET_PDB_FILE does not work for pdb file generated for static library build, determining it manually
            set(pdb_file "$<TARGET_FILE_DIR:BasexCpp>/$<TARGET_FILE_PREFIX:BasexCpp>$<TARGET_FILE_BASE_NAME:BasexCpp>.pdb")
            set(pdb_file_destination "${CMAKE_INSTALL_LIBDIR}")
        endif()

        install(FILES "${pdb_file}"
            COMPONENT BasexCpp-dev
            CONFIGURATIONS Debug RelWithDebInfo
            DESTINATION "${pdb_file_destination}"
            OPTIONAL)
    endif()
endif()

#----------------------------------------------------------------------------------------------------------------------
# other targets
#----------------------------------------------------------------------------------------------------------------------

if(BASEXCPP_BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

if(BASEXCPP_BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

if(BASEXCPP_BUILD_DOCS)
    find_package(Doxygen REQUIRED)
    doxygen_add_docs(docs include)
endif()

BasexCpp.h

Edit

I have added BASEXCPP_EXPORT to all the class methods that should be exported.
Also I added another testfunction (minus'). minusandadd` will be removed in the final library.

#pragma once

#ifndef BASEXCPP_H_
#define BASEXCPP_H_

#include <string>
#include <vector>
#include <iterator>
#include <algorithm>
#include <cstddef>
#include <iomanip>
#include <wordexp.h>
#include <iostream>
#include <list>
#include <sys/types.h>
#include <stdlib.h>

typedef std::vector < std::byte > ByteVect;
typedef std::vector < ByteVect> VectOfByteVect;

#include <memory>
class BasexSocket;
typedef std::shared_ptr < BasexSocket > BasexSock_Sptr;
#include <utility>
#include <cassert>
#include <stdexcept>
#include <fstream>
#include <sstream>
#include <array>
#include <regex>
#include <curlpp/cURLpp.hpp>
#include <curlpp/Easy.hpp>
#include <curlpp/Options.hpp>
#include <curlpp/Exception.hpp>
#include <cstdio>


#ifdef __WIN32__
#include <winsock2.h>
#include <Windows.h>
#else
#include <sys/socket.h>
#include <unistd.h>
#endif

#include <BasexCpp/export.h>

using namespace std;

namespace BasexCpp {

BASEXCPP_EXPORT  int add(int a, int b);
BASEXCPP_EXPORT  int minus(int a, int b);

class QueryObject;
typedef std::unique_ptr < QueryObject > QueryObj_Uptr;

// 1 ### #include "ClientUtils.h"
BASEXCPP_EXPORT  ByteVect getBytes (string const &s);
BASEXCPP_EXPORT  string getChar (ByteVect const &b);
BASEXCPP_EXPORT  string pathExpander (const string & s, int flags = 0);
BASEXCPP_EXPORT  std::ostream & debug_dump (const ByteVect & str, std::ostream & stm = std::cout);
BASEXCPP_EXPORT  std::ostream & debug_dump (const string & str, std::ostream & stm = std::cout);

// 2 ### #include "Base.h"
class BASEXCPP_EXPORT Base {
public:
BASEXCPP_EXPORT 	explicit Base (const string &, const int &, const string &, const string &);
BASEXCPP_EXPORT 	explicit Base (const string &, const string &, const string &, const string &);
BASEXCPP_EXPORT 	explicit Base (BasexSock_Sptr socket);
protected:
	BasexSock_Sptr Socket;
};

// 3 ### #include "ResponseObj.h"
class BASEXCPP_EXPORT ResponseObj {
public:
	ResponseObj ();
BASEXCPP_EXPORT 	explicit ResponseObj (const string &, ByteVect &);
};

// 4 ### #include "QueryObject.h"
class BASEXCPP_EXPORT QueryObject: virtual public Base {
public:
BASEXCPP_EXPORT 	void Close ();
BASEXCPP_EXPORT 	void Bind (const string & name, const string & value, const string & type = "");
BASEXCPP_EXPORT 	void Bind (const string & name, const std::list < string > &valueList, const string & type = "");
BASEXCPP_EXPORT 	void Bind (const string & name, const std::list < string > &valueList, const std::list < string > &typeList);
BASEXCPP_EXPORT 	ByteVect Next ();
BASEXCPP_EXPORT 	bool More ();
BASEXCPP_EXPORT 	void Exec ();
BASEXCPP_EXPORT 	void Info ();
BASEXCPP_EXPORT 	void Options ();
BASEXCPP_EXPORT 	void Context (const string & value, const string & type = "");
BASEXCPP_EXPORT 	void Updating ();
BASEXCPP_EXPORT 	void Full ();

BASEXCPP_EXPORT 	string getQueryString ();
BASEXCPP_EXPORT 	string getStatus ();
BASEXCPP_EXPORT 	ByteVect getResultBytes ();
BASEXCPP_EXPORT 	ByteVect getResult ();
BASEXCPP_EXPORT 	string getResultString ();
BASEXCPP_EXPORT 	string asString (const ByteVect & vect);

	ResponseObj Response;
};

// 5 ### #include "BasexClient.h"
class BASEXCPP_EXPORT BasexClient: virtual public Base {
public:
BASEXCPP_EXPORT 	explicit BasexClient (const string &, const int &, const string &, const string &);
BASEXCPP_EXPORT 	explicit BasexClient (const string &, const string &, const string &, const string &);
BASEXCPP_EXPORT 	virtual ~ BasexClient ();

BASEXCPP_EXPORT 	void Execute (const string & command);
BASEXCPP_EXPORT 	void Create (const string & dbName, const string & content = "");
BASEXCPP_EXPORT 	void Add (const string & path, const string & input);
BASEXCPP_EXPORT 	void Put (const string & path, const string & input);
BASEXCPP_EXPORT 	void PutBinary (const string & path, const ByteVect & input);
BASEXCPP_EXPORT 	QueryObj_Uptr Query (const string & query);
BASEXCPP_EXPORT 	string getCaller ();
BASEXCPP_EXPORT 	string getInfo ();
BASEXCPP_EXPORT 	string getStatus ();
BASEXCPP_EXPORT 	ByteVect getResultBytes ();
BASEXCPP_EXPORT 	ByteVect getResult ();
};

}

#endif // BASEXCPP_H_

Main.cpp

#include <BasexCpp/BasexCpp.h>

#include <iostream>

using namespace std;

using namespace BasexCpp;
int main(int, char*[])
{
    // auto sum = BasexCpp::add(3, 1);
    auto sum = add(3, 1);
    std::cout << sum << std::endl;
/*
	string DBHOST("localhost");
	int DBPORT(1984);
	string DBUSERNAME("Test");
	string DBPASSWORD("testBaseX");

	BasexClient Session (DBHOST, DBPORT, DBUSERNAME, DBPASSWORD);

	Session.Create("TestCpp");
*/
    return 0;
}

I don’t see you linking to that library (should be where you do add_executable() with that main.cpp source, which you haven’t shared), so this might be the reason.

The testprogram (main.cpp) resides in a subproject. The function add is one of the functions in the library which is build in the main project.

That does not tell me much, I still don’t see how you linked to the library (and if you did it at all).

In root/CMakePresets.json I have added the line "BASEXCPP_BUILD_EXAMPLES":"ON".

**Target in root/CMakeLists.txt**
if(BASEXCPP_BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

**examples/CMakeLists.txt**
add_subdirectory(add)

**examples/add/CMakeLists.txt**
cmake_minimum_required(VERSION 3.14)
project(libTest LANGUAGES CXX)

include("../../cmake/utils.cmake")
string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}" is_top_level)

if(is_top_level)
    find_package(BasexCpp REQUIRED)
endif()

set(sources main.cpp)
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${sources})

add_executable(libTest)
target_sources(libTest PRIVATE ${sources})
target_link_libraries(libTest PRIVATE BasexCpp::BasexCpp)

if(NOT is_top_level)
    win_copy_deps_to_target_dir(libTest BasexCpp::BasexCpp)
endif()

The function add that is used in libTest is defined in he public header BasexCpp.h and implemented in the library libBasexCpp.so so I am certain that this library is linked.

target_link_libraries(libTest PRIVATE BasexCpp::BasexCpp)`

That looks correct (aside from the executable target name), but does BasexCpp::BasexCpp exist at that point? And is the library’s binary present in the ninja.build file among the linking commands?

  • Why is naming the executable libTest not correct?
build examples/add/libTest: CXX_EXECUTABLE_LINKER__libTest_Debug examples/add/CMakeFiles/libTest.dir/main.cpp.o | libBasexCpp.so.1.0.0 || libBasexCpp.so libBasexCpp.so
  FLAGS = -std=c++20 -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-conversion -Wcast-align -Wcast-qual -Wnull-dereference -Woverloaded-virtual -Wformat=2 -Werror -g
  LINK_LIBRARIES = -Wl,-rpath,/home/bengbers/Thuis/CPP_LIB4/Build/Debug  libBasexCpp.so.1.0.0  -Llib64  -lcurlpp  -L/usr/lib64  -lcurl
  OBJECT_DIR = examples/add/CMakeFiles/libTest.dir
  POST_BUILD = :
  PRE_LINK = :
  TARGET_COMPILE_PDB = examples/add/CMakeFiles/libTest.dir/
  TARGET_FILE = examples/add/libTest
  TARGET_PDB = examples/add/libTest.pdb

In build.ninja this part is used for linking. As you can see libBasexCpp.so is present.
readelf -s Build/Debug/libBasexCpp.so tells me that all class-files have been touched but as I mentioned earlier, I don’t know how to interpret the output from readelf.

  • The library has to be present because otherwise the function add would not be found.

Why is naming the executable libTest not correct?

Because executable is not a library, is it, so having lib in its target name (and then its binaries) isn’t quite right, although of course no one can stop you from naming it like that. And in general it isn’t recommended to have lib anywhere in the target name of either executable or library.

For the rest, I really can’t tell what is wrong there. Perhaps you could share a minimal project for trying to reproduce the problem.

  • I’ll give the testfile another name in the release files (whenever I come that far :smirk:)
  • I don’t see how I could make a minimal project. Testing it requires that Basex is installed and that the complete sources are uploaded.

I have been struggling with this particular problem for over a month now. In the meantime, I have solved several small, non-essential problems but the main problem remains the failure to export the symbols.
On the internet I have seen examples showing that the name of class-methods should also appear in the output of readelf but I cannot find documentation anywhere on how to avoid using only mangled names.

Can this be prevented by using a setting in the gcc compiler or linker?

Ben
PS. It is unfortunate that I cannot contact either the author of that article or Craig Scott for questions. I assume they could have helped me ages ago. :face_exhaling:

It is unfortunate that I cannot contact […] Craig Scott

You can definitely contact and pay Craig Scott to consult you, he is providing these exact services :slight_smile:
Here is also here on the forum, so he might stumble upon this thread eventually.

I don’t see how I could make a minimal project

In the original post you said that you “have written a shared library”, so you can write another one the same way, just without this “Basex” thing. And then also create a basic executable, which would try to call a method from that library. And then anyone else will be able to try to build that example project on their machines and spot what you might have done wrong in exporting symbols.

What is the content of the file

include/BasexCpp/export.h

in your source tree? Does it correctly define BASEXCPP_EXPORT?

perhaps you may try my version Modernize the cmake files a bit by ClausKlein · Pull Request #3 · pananton/cpp-lib-template · GitHub