diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dcde239..1a4ea77 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,6 +8,7 @@ on: - include/* - tests/* - src/* + - example/* pull_request: paths: @@ -16,6 +17,7 @@ on: - include/* - tests/* - src/* + - example/* defaults: run: @@ -38,6 +40,37 @@ jobs: - name: Check formatting run: find src include tests -name '*.cpp' -o -name '*.hpp' | xargs clang-format --dry-run --Werror + example: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + name: example / ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + # FETCHCONTENT_SOURCE_DIR_DYLIB makes FetchContent use the checked out + # sources instead of the released tag, so that the example is built + # against the code of the current branch. + - name: Generate project files + run: cmake example -B example/build -DFETCHCONTENT_SOURCE_DIR_DYLIB="${{ github.workspace }}" + + - name: Build example + run: cmake --build example/build --config Release + + # Multi config generators place the binaries in a per config subdirectory, + # and the example loads its dynamic library from the working directory. + - name: Run example + working-directory: example/build + run: | + if [ -d Release ]; then + cd Release + fi + ./dylib_example + windows_msvc: strategy: fail-fast: false diff --git a/CMakeLists.txt b/CMakeLists.txt index c785428..b95a7fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,18 @@ cmake_minimum_required(VERSION 3.11...3.31) -project(dylib VERSION 3.0.1 LANGUAGES CXX) +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/include/dylib.hpp" DYLIB_HEADER) + +foreach(part MAJOR MINOR PATCH) + if(NOT DYLIB_HEADER MATCHES "#define DYLIB_VERSION_${part} ([0-9]+)") + message(FATAL_ERROR "Could not parse DYLIB_VERSION_${part} from include/dylib.hpp") + endif() + set(DYLIB_VERSION_${part} ${CMAKE_MATCH_1}) +endforeach() + +project(dylib + VERSION ${DYLIB_VERSION_MAJOR}.${DYLIB_VERSION_MINOR}.${DYLIB_VERSION_PATCH} + LANGUAGES CXX +) include(GNUInstallDirs) diff --git a/README.md b/README.md index b20df12..a6539f3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # dylib -[![version](https://img.shields.io/badge/Version-3.0.1-blue.svg)](https://github.com/martin-olivier/dylib/releases/tag/v3.0.1) +[![version](https://img.shields.io/badge/Version-3.1.0-blue.svg)](https://github.com/martin-olivier/dylib/releases/tag/v3.1.0) [![license](https://img.shields.io/badge/License-MIT-orange.svg)](https://github.com/martin-olivier/dylib/blob/main/LICENSE) [![cpp](https://img.shields.io/badge/Compatibility-C++11-darkgreen.svg)](https://isocpp.org) [![ci](https://github.com/martin-olivier/dylib/actions/workflows/CI.yml/badge.svg)](https://github.com/martin-olivier/dylib/actions/workflows/CI.yml) @@ -28,7 +28,7 @@ vcpkg install dylib ``` ```sh -conan install --requires=dylib/3.0.1 +conan install --requires=dylib/3.1.0 ``` ### Using CMake Fetch @@ -41,7 +41,7 @@ include(FetchContent) FetchContent_Declare( dylib GIT_REPOSITORY "https://github.com/martin-olivier/dylib" - GIT_TAG "v3.0.1" + GIT_TAG "v3.1.0" ) FetchContent_MakeAvailable(dylib) @@ -219,6 +219,17 @@ try { } ``` +### Version + +`dylib.hpp` exposes its version as macros, which lets you detect the availability of a feature at compile time: + +```c++ +#if DYLIB_VERSION_MAJOR > 3 || (DYLIB_VERSION_MAJOR == 3 && DYLIB_VERSION_MINOR >= 1) + for (auto §ion : lib.sections()) + std::cout << section << std::endl; +#endif +``` + ## Example A full example about the usage of the `dylib` library is available [HERE](example) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 0b9dbeb..d249b3c 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -15,7 +15,7 @@ include(FetchContent) FetchContent_Declare( dylib GIT_REPOSITORY "https://github.com/martin-olivier/dylib" - GIT_TAG "v3.0.1" + GIT_TAG "v3.1.0" ) FetchContent_MakeAvailable(dylib) diff --git a/example/README.md b/example/README.md index e2a8edd..980b273 100644 --- a/example/README.md +++ b/example/README.md @@ -34,7 +34,7 @@ You will have the following result: ```sh Hello World! -dylib - v3.0.1 +dylib - v3.1.0 pi value: 3.14159 magic value: cafebabe 10 + 10 = 20 diff --git a/example/lib.cpp b/example/lib.cpp index 758a53e..4b68aa3 100644 --- a/example/lib.cpp +++ b/example/lib.cpp @@ -26,7 +26,7 @@ namespace example { namespace dylib { LIB_EXPORT std::string info() { - return "dylib - v3.0.1"; + return "dylib - v3.1.0"; } } diff --git a/include/dylib.hpp b/include/dylib.hpp index 42542a5..a4750a1 100644 --- a/include/dylib.hpp +++ b/include/dylib.hpp @@ -1,6 +1,6 @@ /** * @file dylib.hpp - * @version 3.0.1 + * @version 3.1.0 * @brief C++ cross-platform wrapper around dynamic loading of shared libraries * @link https://github.com/martin-olivier/dylib * @@ -12,6 +12,21 @@ #pragma once +#define DYLIB_VERSION_MAJOR 3 +#define DYLIB_VERSION_MINOR 1 +#define DYLIB_VERSION_PATCH 0 + +#ifdef _WIN32 +#define DYLIB_WIN_MAC_OTHER(win_def, mac_def, other_def) win_def +#define DYLIB_WIN_OTHER(win_def, other_def) win_def +#elif defined(__APPLE__) +#define DYLIB_WIN_MAC_OTHER(win_def, mac_def, other_def) mac_def +#define DYLIB_WIN_OTHER(win_def, other_def) other_def +#else +#define DYLIB_WIN_MAC_OTHER(win_def, mac_def, other_def) other_def +#define DYLIB_WIN_OTHER(win_def, other_def) other_def +#endif + #include #include #include @@ -42,17 +57,6 @@ #endif #endif -#ifdef _WIN32 -#define DYLIB_WIN_MAC_OTHER(win_def, mac_def, other_def) win_def -#define DYLIB_WIN_OTHER(win_def, other_def) win_def -#elif defined(__APPLE__) -#define DYLIB_WIN_MAC_OTHER(win_def, mac_def, other_def) mac_def -#define DYLIB_WIN_OTHER(win_def, other_def) other_def -#else -#define DYLIB_WIN_MAC_OTHER(win_def, mac_def, other_def) other_def -#define DYLIB_WIN_OTHER(win_def, other_def) other_def -#endif - namespace dylib { using native_handle_type = DYLIB_WIN_OTHER(HINSTANCE, void *); @@ -316,12 +320,12 @@ class library { /** * @return the dynamic library handle */ - native_handle_type native_handle() noexcept; + native_handle_type native_handle() const noexcept; protected: native_handle_type m_handle{nullptr}; #ifndef _WIN32 - int m_fd{-1}; + std::string m_path; #endif }; diff --git a/src/dylib.cpp b/src/dylib.cpp index 8c60f0a..bc25fd1 100644 --- a/src/dylib.cpp +++ b/src/dylib.cpp @@ -42,7 +42,6 @@ struct internal_symbol_info { std::vector get_symbols(native_handle_type handle, int fd); std::vector get_sections(native_handle_type handle, int fd); -std::string demangle_symbol(const char *symbol); static native_handle_type open_lib(const char *path) noexcept { #ifdef _WIN32 @@ -90,10 +89,34 @@ static std::string get_error_description() noexcept { #endif } +#ifndef _WIN32 +class scoped_fd { +public: + explicit scoped_fd(const std::string &path) : m_fd(open(path.c_str(), O_RDONLY)) { + if (m_fd < 0) + throw std::runtime_error("Could not open file '" + path + "': " + strerror(errno)); + } + + scoped_fd(const scoped_fd &) = delete; + scoped_fd &operator=(const scoped_fd &) = delete; + + ~scoped_fd() { + close(m_fd); + } + + int get() const noexcept { + return m_fd; + } + +private: + int m_fd; +}; +#endif + library::library(library &&other) noexcept { std::swap(m_handle, other.m_handle); #ifndef _WIN32 - std::swap(m_fd, other.m_fd); + std::swap(m_path, other.m_path); #endif } @@ -101,7 +124,7 @@ library &library::operator=(library &&other) noexcept { if (this != &other) { std::swap(m_handle, other.m_handle); #ifndef _WIN32 - std::swap(m_fd, other.m_fd); + std::swap(m_path, other.m_path); #endif } return *this; @@ -141,9 +164,7 @@ library::library(const char *lib_path, dylib::decorations decorations) { throw load_error("Could not load library '" + lib + "':\n" + get_error_description()); #ifndef _WIN32 - m_fd = open(lib.c_str(), O_RDONLY); - if (m_fd < 0) - throw load_error("Could not open file '" + lib + "':\n" + strerror(errno)); + m_path = lib; #endif } @@ -158,10 +179,6 @@ library::library(const std::filesystem::path &lib_path, decorations decorations) library::~library() { if (m_handle) close_lib(m_handle); -#ifndef _WIN32 - if (m_fd > -1) - close(m_fd); -#endif } native_symbol_type library::get_symbol(const char *symbol_name) const { @@ -186,10 +203,14 @@ native_symbol_type library::get_symbol(const char *symbol_name) const { initial_error = get_error_description(); for (const auto &sym : symbols()) { - if (!sym.loadable) + /* + * Only C++ symbols are considered here: a C symbol is not mangled, so it would + * already have been resolved by the locate_symbol call above. + */ + if (!sym.loadable || sym.type != symbol_type::CPP) continue; - std::string demangled = demangle_symbol(sym.name.c_str()); + const std::string &demangled = sym.demangled_name; if (demangled.find(symbol_name) == 0 && (demangled.size() == symbol_name_len || demangled[symbol_name_len] == '(')) @@ -215,7 +236,7 @@ native_symbol_type library::get_symbol(const std::string &symbol_name) const { return get_symbol(symbol_name.c_str()); } -native_handle_type library::native_handle() noexcept { +native_handle_type library::native_handle() const noexcept { return m_handle; } @@ -227,7 +248,13 @@ std::vector library::symbols() const { throw std::logic_error("Attempted to use a moved library object"); try { - internal_symbols = get_symbols(m_handle, DYLIB_WIN_MAC_OTHER(-1, m_fd, -1)); +#ifdef __APPLE__ + scoped_fd fd(m_path); + + internal_symbols = get_symbols(m_handle, fd.get()); +#else + internal_symbols = get_symbols(m_handle, -1); +#endif symbols.reserve(internal_symbols.size()); @@ -251,7 +278,13 @@ std::vector library::sections() const { throw std::logic_error("Attempted to use a moved library object"); try { - return get_sections(m_handle, DYLIB_WIN_MAC_OTHER(-1, m_fd, m_fd)); +#ifdef _WIN32 + return get_sections(m_handle, -1); +#else + scoped_fd fd(m_path); + + return get_sections(m_handle, fd.get()); +#endif } catch (const std::runtime_error &e) { throw section_collection_error(e.what()); } diff --git a/src/symbols.cpp b/src/symbols.cpp index bc22ef6..6144f2f 100644 --- a/src/symbols.cpp +++ b/src/symbols.cpp @@ -385,13 +385,24 @@ std::vector get_symbols(void *handle, int fd) { if (!symtab || !strtab || symentries == 0) return symbols_list; - size = strtab - (char *)symtab; + /* + * The dynamic section does not record the size of the symbol table, so it is + * deduced from the fact that the string table usually directly follows it. + * Bail out instead of underflowing if a linker lays them out the other way. + */ + if (strtab <= (const char *)symtab) + return symbols_list; + + size = (unsigned long)(strtab - (char *)symtab); - for (int i = 0; i < size / symentries; ++i) { - ElfSym *sym = &symtab[i]; + for (unsigned long i = 0; i < size / symentries; ++i) { + unsigned char type = DYLIB_ELF_ST_TYPE(symtab[i].st_info); - if (DYLIB_ELF_ST_TYPE(symtab[i].st_info) == STT_FUNC) { - const char *name = &strtab[sym->st_name]; + /* + * Collect functions (STT_FUNC) and global variables (STT_OBJECT) + */ + if (type == STT_FUNC || type == STT_OBJECT) { + const char *name = &strtab[symtab[i].st_name]; add_symbol(symbols_list, name, !!dlsym(handle, name)); } diff --git a/tests/lib.cpp b/tests/lib.cpp index f6f9f05..241cbe6 100644 --- a/tests/lib.cpp +++ b/tests/lib.cpp @@ -44,6 +44,8 @@ LIB_EXPORT void list_add_string(std::vector &cont, std::string elem } namespace tools { +LIB_EXPORT double pi_value = 3.14159; + LIB_EXPORT double adder() { return 0; } diff --git a/tests/tests.cpp b/tests/tests.cpp index f4e6a1a..50d33a5 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -164,6 +164,13 @@ TEST(cpp_symbols, variables) { EXPECT_EQ(strcmp(secret, "12345"), 0); } +TEST(cpp_symbols, variables_namespace) { + dylib::library lib("./dynamic_lib", dylib::decorations::os_default()); + + auto pi = lib.get_variable("tools::pi_value"); + EXPECT_EQ(pi, 3.14159); +} + TEST(cpp_symbols, functions) { dylib::library lib("./dynamic_lib", dylib::decorations::os_default()); @@ -291,8 +298,6 @@ TEST(cpp_symbols, demangle) { } } -#include - TEST(sections, lookup) { dylib::library lib("./dynamic_lib", dylib::decorations::os_default()); std::vector sections;