carb/extras/Library.h

File members: carb/extras/Library.h

// Copyright (c) 2019-2023, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//
#pragma once

#include "../Defines.h"
#include "Path.h"
#include "StringSafe.h"
#include "../../omni/extras/ScratchBuffer.h"

#if CARB_POSIX
#    if CARB_PLATFORM_LINUX
#        include <link.h>
#    elif CARB_PLATFORM_MACOS
#        include <mach-o/dyld.h>
#    endif
#    include <dlfcn.h>
#elif CARB_PLATFORM_WINDOWS
#    include "../CarbWindows.h"
#    include "Errors.h"
#    include "WindowsPath.h"
#else
CARB_UNSUPPORTED_PLATFORM();
#endif

namespace carb
{
namespace extras
{

#if CARB_POSIX || defined(DOXYGEN_BUILD)
using LibraryHandle = void*;
#elif CARB_PLATFORM_WINDOWS
using LibraryHandle = HMODULE;
#else
CARB_UNSUPPORTED_PLATFORM();
#endif

constexpr LibraryHandle kInvalidLibraryHandle = {};

using LibraryFlags = uint32_t;

constexpr LibraryFlags fLibFlagMakeFullLibName = 0x00000001;

constexpr LibraryFlags fLibFlagNow = 0x00000002;

constexpr LibraryFlags fLibFlagDeepBind = 0x00000004;

constexpr LibraryFlags fLibFlagLoadExisting = 0x00000008;

constexpr LibraryFlags fLibFlagPin = 0x00000010;

#if CARB_PLATFORM_WINDOWS || defined(DOXYGEN_BUILD)
#    define CARB_LIBRARY_EXTENSION ".dll"
#    define CARB_EXECUTABLE_EXTENSION ".exe"
#elif CARB_PLATFORM_LINUX
#    define CARB_LIBRARY_EXTENSION ".so"
#    define CARB_EXECUTABLE_EXTENSION ""
#elif CARB_PLATFORM_MACOS
#    define CARB_LIBRARY_EXTENSION ".dylib"
#    define CARB_EXECUTABLE_EXTENSION ""
#else
CARB_UNSUPPORTED_PLATFORM();
#endif

constexpr const char* getDefaultLibraryExtension()
{
    return CARB_LIBRARY_EXTENSION;
}

#if CARB_PLATFORM_WINDOWS
#    define CARB_LIBRARY_PREFIX ""
#elif CARB_PLATFORM_LINUX || CARB_PLATFORM_MACOS
#    define CARB_LIBRARY_PREFIX "lib"
#else
CARB_UNSUPPORTED_PLATFORM();
#endif

constexpr const char* getDefaultLibraryPrefix()
{
    return CARB_LIBRARY_PREFIX;
}

#define CARB_LIBRARY_GET_LITERAL_NAME(name) CARB_LIBRARY_PREFIX name CARB_LIBRARY_EXTENSION

inline std::string createLibraryNameForModule(const char* baseName)
{
    const char* prefix;
    const char* ext;
    char* buffer;
    size_t len = 0;
    size_t pathLen = 0;
    const char* sep[2] = {};
    const char* name = baseName;

    if (baseName == nullptr || baseName[0] == 0)
        return {};

    sep[0] = strrchr(baseName, '/');
#if CARB_PLATFORM_WINDOWS
    // also handle mixed path separators on Windows.
    sep[1] = strrchr(baseName, '\\');

    if (sep[1] > sep[0])
        sep[0] = sep[1];
#endif

    if (sep[0] != nullptr)
    {
        pathLen = (sep[0] - baseName) + 1;
        name = sep[0] + 1;
        len += pathLen;
    }

    prefix = getDefaultLibraryPrefix();
    ext = getDefaultLibraryExtension();
    len += strlen(prefix) + strlen(ext);

    len += strlen(name) + 1;
    buffer = CARB_STACK_ALLOC(char, len);
    carb::extras::formatString(buffer, len, "%.*s%s%s%s", (int)pathLen, baseName, prefix, name, ext);
    return buffer;
}

template <typename T>
T getLibrarySymbol(LibraryHandle libHandle, const char* name)
{
#if CARB_PLATFORM_WINDOWS
    return reinterpret_cast<T>(::GetProcAddress(libHandle, name));
#elif CARB_POSIX
    if (libHandle == nullptr || name == nullptr)
        return nullptr;

    return reinterpret_cast<T>(::dlsym(libHandle, name));
#else
    CARB_UNSUPPORTED_PLATFORM();
#endif
}

std::string getLibraryFilenameByHandle(LibraryHandle handle); // forward declare

#ifndef DOXYGEN_BUILD
namespace detail
{
struct FreeString
{
    void operator()(char* p) noexcept
    {
        free(p);
    }
};

using UniqueCharPtr = std::unique_ptr<char, FreeString>;

#    if CARB_POSIX
struct FreePosixLib
{
    void operator()(void* p) noexcept
    {
        dlclose(p);
    }
};

using UniquePosixLib = std::unique_ptr<void, FreePosixLib>;
#    endif
} // namespace detail
#endif

// clang-format off
// clang-format on
inline LibraryHandle loadLibrary(const char* libraryName, LibraryFlags flags = 0)
{
    std::string fullLibName;
    LibraryHandle handle;

    // asked to construct a full library name => create the name and adjust the path as needed.
    if (libraryName != nullptr && libraryName[0] != '\0' && (flags & fLibFlagMakeFullLibName) != 0)
    {
        fullLibName = createLibraryNameForModule(libraryName);
        libraryName = fullLibName.c_str();
    }

#if CARB_PLATFORM_WINDOWS
    // retrieve the main executable module's handle.
    if (libraryName == nullptr)
        return ::GetModuleHandleW(nullptr);

    // retrieve the handle of a specific module.
    std::wstring widecharName = carb::extras::convertCarboniteToWindowsPath(libraryName);

    // asked to only retrieve a library handle if it is already loaded.  Note that this will
    // still increment the library's ref count on return (unless it is pinned).  It is always
    // safe to call unloadLibrary() on the returned (non-nullptr) handle in this case.
    if ((flags & fLibFlagLoadExisting) != 0)
    {
        DWORD gmhFlags = !!(flags & fLibFlagPin) ? CARBWIN_GET_MODULE_HANDLE_EX_FLAG_PIN : 0;
        return ::GetModuleHandleExW(gmhFlags, widecharName.c_str(), &handle) ? handle : nullptr;
    }

    handle = ::LoadLibraryExW(widecharName.c_str(), nullptr,
                              CARBWIN_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | CARBWIN_LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);

    // Although convertCarboniteToWindowsPath will ensure that a path over MAX_PATH has the long-path prefix,
    // LoadLibraryExW complains about strings slightly smaller than that. If we get that specific error then try
    // again with the long-path prefix.
    if (!handle && ::GetLastError() == CARBWIN_ERROR_FILENAME_EXCED_RANGE)
    {
        handle = ::LoadLibraryExW((L"\\\\?\\" + widecharName).c_str(), nullptr,
                                  CARBWIN_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | CARBWIN_LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
    }

    // failed to load the loading the module from the 'default search dirs' => attempt to load
    //   it with the default system search path.  Oddly enough, this is different from the search
    //   path provided by the flags used above - it includes the current working directory and
    //   the paths in $PATH.  Another possible reason for the above failing is that the library
    //   name was a relative path.  The CARBWIN_LOAD_LIBRARY_SEARCH_DEFAULT_DIRS flag used above
    //   requires that an absolute path be used.  To keep the behavior of this function on par
    //   with Linux's behavior, we'll attempt another load from the default paths instead.
    if (handle == nullptr)
    {
        handle = ::LoadLibraryExW(widecharName.c_str(), nullptr, 0);

        // As above, try again with the long-path prefix if we get a specific error response from LoadLibrary.
        if (!handle && ::GetLastError() == CARBWIN_ERROR_FILENAME_EXCED_RANGE)
        {
            handle = ::LoadLibraryExW((L"\\\\?\\" + widecharName).c_str(), nullptr, 0);
        }
    }

    if (handle && !!(flags & fLibFlagPin))
    {
        HMODULE h = nullptr;
        BOOL b = GetModuleHandleExW(CARBWIN_GET_MODULE_HANDLE_EX_FLAG_PIN, widecharName.c_str(), &h);
        CARB_UNUSED(b, h);
        CARB_ASSERT(b != 0);
        CARB_ASSERT(h == handle);
    }
#elif CARB_POSIX
    int openFlags = RTLD_LAZY;

    if ((flags & fLibFlagNow) != 0)
        openFlags |= RTLD_NOW;

    if ((flags & fLibFlagLoadExisting) != 0)
        openFlags |= RTLD_NOLOAD;

    if ((flags & fLibFlagPin) != 0)
        openFlags |= RTLD_NODELETE;

#    if CARB_PLATFORM_LINUX
    if ((flags & fLibFlagDeepBind) != 0)
        openFlags |= RTLD_DEEPBIND;
#    endif

    handle = dlopen(libraryName, openFlags);

    // failed to get a module handle or load the module => check if this was a request to load the
    //   handle for the main executable module by its path name.
    if (handle == nullptr && libraryName != nullptr && libraryName[0] != 0)
    {
        detail::UniqueCharPtr path(realpath(libraryName, nullptr));
        if (path == nullptr)
        {
            // probably trying to load a library that doesn't exist
            CARB_LOG_INFO("realpath(%s) failed (errno = %d)", libraryName, errno);
            return nullptr;
        }

        std::string raw = getLibraryFilenameByHandle(nullptr);
        CARB_FATAL_UNLESS(!raw.empty(), "getLibraryFilenameByHandle(nullptr) failed");

        // use realpath() to ensure the paths can be compared
        detail::UniqueCharPtr path2(realpath(raw.c_str(), nullptr));
        CARB_FATAL_UNLESS(path2 != nullptr, "realpath(%s) failed (errno = %d)", raw.c_str(), errno);

        // the two names match => retrieve the main executable module's handle for return.
        if (strcmp(path.get(), path2.get()) == 0)
        {
            return dlopen(nullptr, openFlags);
        }
    }

#    if CARB_PLATFORM_LINUX
    if (handle != nullptr)
    {
        // Linux's dlopen() has a strange issue where it's possible to have the call succeed
        // even though one or more of the library's dependencies fail to load.  The dlopen()
        // call succeeds because there are still references on the handle despite the module's
        // link map having been destroyed (visible from the 'LD_DEBUG=all' output).
        // Unfortunately, if the link map is destroyed, any attempt to retrieve a symbol from
        // the library with dlsym() will fail.  This causes some very confusing and misleading
        // error messages or crashes (depending on usage) instead of just having the module load
        // fail.
        void* linkMap = nullptr;
        const char* errorMsg = dlerror();

        if (dlinfo(handle, RTLD_DI_LINKMAP, &linkMap) == -1 || linkMap == nullptr)
        {
            CARB_LOG_WARN("Library '%s' loaded with errors '%s' and no link map.  The likely cause of this is that ",
                          libraryName, errorMsg);
            CARB_LOG_WARN("a dependent library or symbol in the dependency chain is missing.  Use the environment ");
            CARB_LOG_WARN("variable 'LD_DEBUG=all' to diagnose.");

            // close the bad library handle.  Note that this may not actually unload the bad
            // library since it may still have multiple references on it (part of the failure
            // reason).  However, we can only safely clean up one reference here.
            dlclose(handle);
            return nullptr;
        }
    }
#    endif
#else
    CARB_UNSUPPORTED_PLATFORM();
#endif
    return handle;
}

inline std::string getLastLoadLibraryError()
{
#if CARB_PLATFORM_WINDOWS
    return carb::extras::getLastWinApiErrorMessage();
#else
    return dlerror();
#endif
}

inline void unloadLibrary(LibraryHandle libraryHandle)
{
    if (libraryHandle)
    {
#if CARB_PLATFORM_WINDOWS
        if (!::FreeLibrary(libraryHandle))
        {
            DWORD err = ::GetLastError();
            CARB_LOG_WARN("FreeLibrary for handle %p failed with error: %d/%s", libraryHandle, err,
                          convertWinApiErrorCodeToMessage(err).c_str());
        }
#elif CARB_POSIX
        if (::dlclose(libraryHandle) != 0)
        {
            CARB_LOG_WARN("Closing library handle %p failed with error: %s", libraryHandle, dlerror());
        }
#else
        CARB_UNSUPPORTED_PLATFORM();
#endif
    }
}

inline LibraryHandle getLibraryHandleByFilename(const char* libraryName, LibraryFlags flags = 0)
{
    std::string fullLibName;

    if (libraryName != nullptr && libraryName[0] != '\0' && (flags & fLibFlagMakeFullLibName) != 0)
    {
        fullLibName = createLibraryNameForModule(libraryName);
        libraryName = fullLibName.c_str();
    }

#if CARB_PLATFORM_WINDOWS
    if (libraryName == nullptr)
        return ::GetModuleHandleW(nullptr);

    std::wstring wideCharName = carb::extras::convertCarboniteToWindowsPath(libraryName);
    return GetModuleHandleW(wideCharName.c_str());
#else
    if (libraryName != nullptr && libraryName[0] == 0)
        return nullptr;

    // A successful dlopen() with RTLD_NOLOAD increments the reference count, so we dlclose() it to make sure that
    // the reference count stays the same. This function is inherently racy as another thread could be unloading the
    // library while we're trying to load it.
    // Note that we can't use UniquePosixLib here because it causes clang to think that we're returning a freed
    // pointer.
    void* handle = ::dlopen(libraryName, RTLD_LAZY | RTLD_NOLOAD);
    if (handle != nullptr) // dlclose(nullptr) crashes
    {
        dlclose(handle);
    }
    return handle;
#endif
}

inline std::string getLibraryFilenameByHandle(LibraryHandle handle)
{
#if CARB_PLATFORM_WINDOWS
    omni::extras::ScratchBuffer<wchar_t, CARBWIN_MAX_PATH> path;
    // There's no way to verify the correct length, so we'll just double the buffer
    // size every attempt until it fits.
    for (;;)
    {
        DWORD res = GetModuleFileNameW(handle, path.data(), DWORD(path.size()));
        if (res == 0)
        {
            // CARB_LOG_ERROR("GetModuleFileNameW(%p) failed (%d)", handle, GetLastError());
            return "";
        }
        if (res < path.size())
        {
            break;
        }

        bool suc = path.resize(path.size() * 2);
        OMNI_FATAL_UNLESS(suc, "failed to allocate %zu bytes", path.size() * 2);
    }

    return carb::extras::convertWindowsToCarbonitePath(path.data());
#elif CARB_PLATFORM_LINUX
    struct link_map* map;

    // requested the filename for the main executable module => dlinfo() will succeed on this case
    //   but will give an empty string for the path.  To work around this, we'll simply read the
    //   path to the process's executable symlink.
    if (handle == nullptr)
    {
        detail::UniqueCharPtr path(realpath("/proc/self/exe", nullptr));
        CARB_FATAL_UNLESS(path != nullptr, "calling realpath(\"/proc/self/exe\") failed (%d)", errno);
        return path.get();
    }

    int res = dlinfo(handle, RTLD_DI_LINKMAP, &map);
    if (res != 0)
    {
        // CARB_LOG_ERROR("failed to retrieve the link map from library handle %p (%d)", handle, errno);
        return "";
    }

    // for some reason, the link map doesn't provide a filename for the main executable module.
    // This simply gets returned as an empty string.  If we get that case, we'll try getting
    // the main module's filename instead.
    if (!map->l_name || map->l_name[0] == '\0')
    {
        // first make sure the handle passed in is for our main executable module.
        auto binaryHandle = loadLibrary(nullptr);
        if (binaryHandle)
        {
            unloadLibrary(binaryHandle);
        }

        if (binaryHandle != handle)
        {
            // CARB_LOG_ERROR("library had no filename in the link map but was not the main module");
            return {};
        }

        // recursively call to get the main module's name
        return getLibraryFilenameByHandle(nullptr);
    }

    return map->l_name;
#elif CARB_PLATFORM_MACOS
    // dlopen(nullptr) gives a different (non-null) result than dlopen(path_to_exe), so
    // we need to test against it as well.
    if (handle == nullptr || detail::UniquePosixLib{ dlopen(nullptr, RTLD_LAZY | RTLD_NOLOAD) }.get() == handle)
    {
        omni::extras::ScratchBuffer<char, 4096> buffer;
        uint32_t len = buffer.size();
        int res = _NSGetExecutablePath(buffer.data(), &len);
        if (res != 0)
        {
            bool succ = buffer.resize(len);
            CARB_FATAL_UNLESS(succ, "failed to allocate %" PRIu32 " bytes", len);

            res = _NSGetExecutablePath(buffer.data(), &len);
            CARB_FATAL_UNLESS(res != 0, "_NSGetExecutablePath() failed");
        }

        detail::UniqueCharPtr path(realpath(buffer.data(), nullptr));
        CARB_FATAL_UNLESS(path != nullptr, "realpath(%s) failed (errno = %d)", buffer.data(), errno);
        return path.get();
    }

    // Look through all the currently loaded libraries for the our handle.
    for (uint32_t i = 0;; i++)
    {
        const char* name = _dyld_get_image_name(i);
        if (name == nullptr)
        {
            break;
        }

        // RTLD_NOLOAD is passed to avoid unnecessarily loading a library if it happened to be unloaded concurrently
        // with this call. UniquePosixLib is used to release the reference that dlopen adds if successful.
        if (detail::UniquePosixLib{ dlopen(name, RTLD_LAZY | RTLD_NOLOAD) }.get() == handle)
        {
            return name;
        }
    }

    return {};
#else
    CARB_UNSUPPORTED_PLATFORM();
#endif
}

inline std::string getLibraryFilename(const void* symbolAddress)
{
#if CARB_PLATFORM_WINDOWS
    HMODULE hm = NULL;

    if (0 == GetModuleHandleExW(
                 CARBWIN_GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | CARBWIN_GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                 (LPCWSTR)symbolAddress, &hm))
    {
        return {};
    }

    return getLibraryFilenameByHandle(hm);
#elif CARB_PLATFORM_LINUX
    Dl_info info;
    struct link_map* lm;
    if (dladdr1(symbolAddress, &info, reinterpret_cast<void**>(&lm), RTLD_DL_LINKMAP))
    {
        if (info.dli_fname != nullptr && info.dli_fname[0] == '/')
            return info.dli_fname;

        else if (lm->l_name != nullptr && lm->l_name[0] == '/')
            return lm->l_name;

        else
        {
            // the main executable doesn't have a path set for it => retrieve it directly.  This
            //   seems to be the expected behavior for the link map for the main module.
            if (lm->l_name == nullptr || lm->l_name[0] == 0)
                return getLibraryFilenameByHandle(nullptr);

            // no info to retrieve the name from => fail.
            if (info.dli_fname == nullptr || info.dli_fname[0] == 0)
                return {};

            // if this process was launched using a relative path, the returned name from dladdr()
            // will also be a relative path => convert it to a fully qualified path before return.
            //   Note that for this to work properly, the working directory should not have
            //   changed since the process launched.  This is not necessarily a valid assumption,
            //   but since we have no control over that behavior here, it is the best we can do.
            //   Note that we took all possible efforts above to minimize the cases where this
            //   step will be needed however.
            detail::UniqueCharPtr path(realpath(info.dli_fname, nullptr));
            if (path == nullptr)
                return {};

            return path.get();
        }
    }

    return {};
#elif CARB_PLATFORM_MACOS
    Dl_info info;
    if (dladdr(symbolAddress, &info))
    {
        if (info.dli_fname == nullptr)
        {
            return getLibraryFilenameByHandle(nullptr);
        }
        else if (info.dli_fname[0] == '/')
        {
            // path is already absolute, just return it
            return info.dli_fname;
        }
        else
        {
            detail::UniqueCharPtr path(realpath(info.dli_fname, nullptr));
            CARB_FATAL_UNLESS(path != nullptr, "realpath(%s) failed (errno = %d)", info.dli_fname, errno);
            return path.get();
        }
    }

    return {};
#else
    CARB_UNSUPPORTED_PLATFORM();
#endif
}

inline LibraryHandle getLibraryHandle(const void* symbolAddress)
{
#if CARB_PLATFORM_WINDOWS
    HMODULE hm = NULL;

    if (0 == GetModuleHandleExW(
                 CARBWIN_GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | CARBWIN_GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                 (LPCWSTR)symbolAddress, &hm))
    {
        return kInvalidLibraryHandle;
    }

    return hm;
#elif CARB_POSIX
    std::string module = getLibraryFilename(symbolAddress);
    if (!module.empty())
    {
        // loadLibrary increments the reference count, so decrement it immediately after.
        auto handle = loadLibrary(module.c_str());
        if (handle != kInvalidLibraryHandle)
        {
            unloadLibrary(handle);
        }
        return handle;
    }

    return kInvalidLibraryHandle;
#else
    CARB_UNSUPPORTED_PLATFORM();
#endif
}

inline std::string getLibraryDirectoryByHandle(LibraryHandle handle)
{
    return carb::extras::getPathParent(getLibraryFilenameByHandle(handle));
}

inline std::string getLibraryDirectory(void* symbolAddress)
{
    return carb::extras::getPathParent(getLibraryFilename(symbolAddress));
}

} // namespace extras
} // namespace carb