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