diff --git a/CMakeLists.txt b/CMakeLists.txt index b96bfd0d..6b915999 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -372,20 +372,63 @@ if(BUILD_TESTING) target_link_libraries(test_repl_str ${PROJECT_NAME}) endif() - set(append_library_dirs "$") - - ament_add_gtest(test_shared_library test/test_shared_library.cpp - APPEND_LIBRARY_DIRS "${append_library_dirs}" - ) - - if(TARGET test_shared_library) - add_library(dummy_shared_library test/dummy_shared_library/dummy_shared_library.c) + macro(add_dummy_shared_library target) + add_library(${target} test/dummy_shared_library/dummy_shared_library.c) if(WIN32) # Causes the visibility macros to use dllexport rather than dllimport # which is appropriate when building the dll but not consuming it. - target_compile_definitions(dummy_shared_library PRIVATE "DUMMY_SHARED_LIBRARY_BUILDING_DLL") + target_compile_definitions(${target} PRIVATE "DUMMY_SHARED_LIBRARY_BUILDING_DLL") + endif() + endmacro() + + ament_add_gtest(test_shared_library_in_run_paths test/test_shared_library.cpp) + if(TARGET test_shared_library_in_run_paths) + # Rely on CMake setting build tree RUNPATHs by default on Unix systems. + add_dummy_shared_library(dummy_shared_library_in_run_paths) + target_compile_definitions(test_shared_library_in_run_paths PRIVATE + "SHARED_LIBRARY_UNDER_TEST=dummy_shared_library_in_run_paths") + if(NOT WIN32 AND NOT APPLE) + # NOTE(hidmic): DT_RUNPATH entries are ignored by dlopen in Linux despite the fact + # documentation says otherwise, so here we fallback to DT_RPATH entries. + target_link_libraries(test_shared_library_in_run_paths "-Wl,--disable-new-dtags") + endif() + target_link_libraries(test_shared_library_in_run_paths ${PROJECT_NAME} mimick) + endif() + + set(project_binary_dir "$") + set(test_libraries_dir "$/test_libraries") + ament_add_gtest(test_shared_library_in_load_paths test/test_shared_library.cpp + APPEND_LIBRARY_DIRS ${project_binary_dir} ${test_libraries_dir} + ) + if(TARGET test_shared_library_in_load_paths) + # Make sure CMake adds no RPATHs/RUNPATHs. + set_target_properties(test_shared_library_in_load_paths + PROPERTIES SKIP_BUILD_RPATH TRUE) + add_dummy_shared_library(dummy_shared_library_in_load_paths) + set_target_properties(dummy_shared_library_in_load_paths PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${test_libraries_dir}) + target_compile_definitions(test_shared_library_in_load_paths PRIVATE + "SHARED_LIBRARY_UNDER_TEST=dummy_shared_library_in_load_paths") + target_link_libraries(test_shared_library_in_load_paths ${PROJECT_NAME} mimick) + endif() + + ament_add_gtest(test_shared_library_preloaded test/test_shared_library.cpp) + if(TARGET test_shared_library_in_load_paths) + add_dummy_shared_library(dummy_shared_library_preloaded) + set_target_properties(dummy_shared_library_preloaded PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${test_libraries_dir}) + target_compile_definitions(test_shared_library_preloaded PRIVATE + "SHARED_LIBRARY_UNDER_TEST=dummy_shared_library_preloaded") + if(NOT WIN32) + # Force (apparently) unused libraries to be linked in. + if(CMAKE_CXX_COMPILER_ID MATCHES "(C|c)lang") + target_link_libraries(test_shared_library_preloaded "-Wl,-all_load") + else() + target_link_libraries(test_shared_library_preloaded "-Wl,--no-as-needed") + endif() endif() - target_link_libraries(test_shared_library ${PROJECT_NAME} mimick) + target_link_libraries(test_shared_library_preloaded dummy_shared_library_preloaded) + target_link_libraries(test_shared_library_preloaded ${PROJECT_NAME} mimick) endif() rcutils_custom_add_gtest(test_time diff --git a/src/shared_library.c b/src/shared_library.c index f8ee5cb2..e450c877 100644 --- a/src/shared_library.c +++ b/src/shared_library.c @@ -20,6 +20,11 @@ extern "C" #include #ifndef _WIN32 +#if defined(__APPLE__) +#include +#elif defined (_GNU_SOURCE) +#include +#endif #include #else // When building with MSVC 19.28.29333.0 on Windows 10 (as of 2020-11-11), @@ -35,6 +40,10 @@ extern "C" #include #pragma warning(pop) C_ASSERT(sizeof(void *) == sizeof(HINSTANCE)); +#ifdef UNICODE +#error "rcutils does not support Unicode paths" +#endif +C_ASSERT(sizeof(char) == sizeof(TCHAR)); #endif // _WIN32 #include "rcutils/error_handling.h" @@ -65,33 +74,125 @@ rcutils_load_shared_library( RCUTILS_CHECK_ARGUMENT_FOR_NULL(lib, RCUTILS_RET_INVALID_ARGUMENT); RCUTILS_CHECK_ARGUMENT_FOR_NULL(library_path, RCUTILS_RET_INVALID_ARGUMENT); RCUTILS_CHECK_ALLOCATOR(&allocator, return RCUTILS_RET_INVALID_ARGUMENT); - - if (lib->library_path != NULL) { - lib->allocator.deallocate(lib->library_path, lib->allocator.state); + if (NULL != lib->lib_pointer) { + RCUTILS_SET_ERROR_MSG("lib argument is not zero-initialized"); + return RCUTILS_RET_INVALID_ARGUMENT; } + rcutils_ret_t ret = RCUTILS_RET_OK; lib->allocator = allocator; + // Delegate library path resolution to dynamic linker. + + // Since the given library path might be relative, let the dynamic + // linker search for the binary object in all standard locations and + // current process space (if already loaded) first. + // Then lookup its full path with the handle obtained. + // See POSIX dlopen() API and Windows' LoadLibrary() API documentation + // for further reference. + +#ifndef _WIN32 + lib->lib_pointer = dlopen(library_path, RTLD_LAZY); + if (NULL == lib->lib_pointer) { + RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING("dlopen error: %s", dlerror()); + return RCUTILS_RET_ERROR; + } + +#if defined(__APPLE__) + const char * image_name = NULL; + uint32_t image_count = _dyld_image_count(); + for (uint32_t i = 0; NULL == image_name && i < image_count; ++i) { + // Iterate in reverse as the library is likely near the end of the list. + const char * candidate_name = _dyld_get_image_name(image_count - i - 1); + if (NULL == candidate_name) { + RCUTILS_SET_ERROR_MSG("dyld image index out of range"); + ret = RCUTILS_RET_ERROR; + goto fail; + } + void * handle = dlopen(candidate_name, RTLD_LAZY | RTLD_NOLOAD); + if (handle == lib->lib_pointer) { + image_name = candidate_name; + } + if (dlclose(handle) != 0) { + RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING("dlclose error: %s", dlerror()); + ret = RCUTILS_RET_ERROR; + goto fail; + } + } + if (NULL == image_name) { + RCUTILS_SET_ERROR_MSG("dyld image name could not be found"); + ret = RCUTILS_RET_ERROR; + goto fail; + } + lib->library_path = rcutils_strdup(image_name, lib->allocator); +#elif defined(_GNU_SOURCE) + struct link_map * map = NULL; + if (dlinfo(lib->lib_pointer, RTLD_DI_LINKMAP, &map) != 0) { + RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING("dlinfo error: %s", dlerror()); + ret = RCUTILS_RET_ERROR; + goto fail; + } + lib->library_path = rcutils_strdup(map->l_name, lib->allocator); +#else lib->library_path = rcutils_strdup(library_path, lib->allocator); +#endif if (NULL == lib->library_path) { RCUTILS_SET_ERROR_MSG("unable to allocate memory"); - return RCUTILS_RET_BAD_ALLOC; + ret = RCUTILS_RET_BAD_ALLOC; + goto fail; } -#ifndef _WIN32 - lib->lib_pointer = dlopen(lib->library_path, RTLD_LAZY); - if (!lib->lib_pointer) { - RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING("LoadLibrary error: %s", dlerror()); + return RCUTILS_RET_OK; +fail: + if (dlclose(lib->lib_pointer) != 0) { + RCUTILS_SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING( + "dlclose error: %s\n", dlerror()); + } + lib->lib_pointer = NULL; + return ret; #else - lib->lib_pointer = (void *)(LoadLibrary(lib->library_path)); - if (!lib->lib_pointer) { - RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING("LoadLibrary error: %lu", GetLastError()); -#endif // _WIN32 - lib->allocator.deallocate(lib->library_path, lib->allocator.state); - lib->library_path = NULL; + HMODULE module = LoadLibrary(library_path); + if (!module) { + RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING( + "LoadLibrary error: %lu", GetLastError()); return RCUTILS_RET_ERROR; } + + for (DWORD buffer_capacity = MAX_PATH; ; buffer_capacity *= 2) { + LPSTR buffer = lib->allocator.allocate(buffer_capacity, lib->allocator.state); + if (NULL == buffer) { + RCUTILS_SET_ERROR_MSG("unable to allocate memory"); + ret = RCUTILS_RET_BAD_ALLOC; + goto fail; + } + DWORD buffer_size = GetModuleFileName(module, buffer, buffer_capacity); + if (0 == buffer_size) { + RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING( + "GetModuleFileName error: %lu", GetLastError()); + ret = RCUTILS_RET_ERROR; + goto fail; + } + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + lib->allocator.deallocate(buffer, lib->allocator.state); + continue; + } + lib->library_path = lib->allocator.reallocate( + buffer, buffer_size, lib->allocator.state); + if (NULL == lib->library_path) { + lib->library_path = buffer; + } + break; + } + lib->lib_pointer = (void *)module; + return RCUTILS_RET_OK; +fail: + if (!FreeLibrary(module)) { + RCUTILS_SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING( + "FreeLibrary error: %lu\n", GetLastError()); + } + return ret; +#endif // _WIN32 } void * diff --git a/test/test_shared_library.cpp b/test/test_shared_library.cpp index 26b3eeec..5a32dd0a 100644 --- a/test/test_shared_library.cpp +++ b/test/test_shared_library.cpp @@ -49,15 +49,17 @@ TEST_F(TestSharedLibrary, basic_load) { // Check debug name works first because rcutils_load_shared_library should be called on // non-debug symbol name - ret = rcutils_get_platform_library_name("dummy_shared_library", library_path, 1024, true); + ret = rcutils_get_platform_library_name( + RCUTILS_STRINGIFY(SHARED_LIBRARY_UNDER_TEST), library_path, 1024, true); ASSERT_EQ(RCUTILS_RET_OK, ret); - ret = rcutils_get_platform_library_name("dummy_shared_library", library_path, 1024, false); + ret = rcutils_get_platform_library_name( + RCUTILS_STRINGIFY(SHARED_LIBRARY_UNDER_TEST), library_path, 1024, false); ASSERT_EQ(RCUTILS_RET_OK, ret); // getting shared library ret = rcutils_load_shared_library(&lib, library_path, rcutils_get_default_allocator()); - ASSERT_EQ(RCUTILS_RET_OK, ret); + ASSERT_EQ(RCUTILS_RET_OK, ret) << rcutils_get_error_string().str; EXPECT_TRUE(rcutils_is_shared_library_loaded(&lib)); // unload shared_library @@ -80,7 +82,8 @@ TEST_F(TestSharedLibrary, bad_load) { TEST_F(TestSharedLibrary, fail_allocator) { rcutils_ret_t ret; - ret = rcutils_get_platform_library_name("dummy_shared_library", library_path, 1024, false); + ret = rcutils_get_platform_library_name( + RCUTILS_STRINGIFY(SHARED_LIBRARY_UNDER_TEST), library_path, 1024, false); ASSERT_EQ(RCUTILS_RET_OK, ret); rcutils_allocator_t failing_allocator = get_failing_allocator(); @@ -91,46 +94,63 @@ TEST_F(TestSharedLibrary, fail_allocator) { TEST_F(TestSharedLibrary, load_two_times) { rcutils_ret_t ret; - ret = rcutils_get_platform_library_name("dummy_shared_library", library_path, 1024, false); + ret = rcutils_get_platform_library_name( + RCUTILS_STRINGIFY(SHARED_LIBRARY_UNDER_TEST), library_path, 1024, false); ASSERT_EQ(RCUTILS_RET_OK, ret); // getting shared library ret = rcutils_load_shared_library(&lib, library_path, rcutils_get_default_allocator()); - ASSERT_EQ(RCUTILS_RET_OK, ret); + EXPECT_EQ(RCUTILS_RET_OK, ret); // getting shared library - ret = rcutils_load_shared_library(&lib, library_path, rcutils_get_default_allocator()); - ASSERT_EQ(RCUTILS_RET_OK, ret); + rcutils_shared_library_t clone = rcutils_get_zero_initialized_shared_library(); + ret = rcutils_load_shared_library(&clone, library_path, rcutils_get_default_allocator()); + EXPECT_EQ(RCUTILS_RET_OK, ret); + + // unload shared_library + ret = rcutils_unload_shared_library(&clone); + EXPECT_EQ(RCUTILS_RET_OK, ret); // unload shared_library ret = rcutils_unload_shared_library(&lib); - ASSERT_EQ(RCUTILS_RET_OK, ret); + EXPECT_EQ(RCUTILS_RET_OK, ret); } TEST_F(TestSharedLibrary, error_load) { rcutils_ret_t ret; - rcutils_shared_library_t lib_empty; - ret = rcutils_load_shared_library(&lib_empty, NULL, rcutils_get_zero_initialized_allocator()); - ASSERT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret); - - lib_empty = rcutils_get_zero_initialized_shared_library(); - ret = rcutils_load_shared_library(&lib_empty, NULL, rcutils_get_zero_initialized_allocator()); + ret = rcutils_load_shared_library(&lib, NULL, rcutils_get_default_allocator()); ASSERT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret); + rcutils_reset_error(); - ret = rcutils_get_platform_library_name("dummy_shared_library", library_path, 1024, false); + ret = rcutils_get_platform_library_name( + RCUTILS_STRINGIFY(SHARED_LIBRARY_UNDER_TEST), library_path, 1024, false); ASSERT_EQ(RCUTILS_RET_OK, ret); ret = rcutils_load_shared_library( - &lib_empty, + &lib, library_path, rcutils_get_zero_initialized_allocator()); ASSERT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret); + rcutils_reset_error(); + + ret = rcutils_load_shared_library( + &lib, library_path, rcutils_get_default_allocator()); + ASSERT_EQ(RCUTILS_RET_OK, ret); + + ret = rcutils_load_shared_library( + &lib, library_path, rcutils_get_default_allocator()); + EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret); + rcutils_reset_error(); + + ret = rcutils_unload_shared_library(&lib); + EXPECT_EQ(RCUTILS_RET_OK, ret); } TEST_F(TestSharedLibrary, error_unload) { rcutils_ret_t ret; - ret = rcutils_get_platform_library_name("dummy_shared_library", library_path, 1024, false); + ret = rcutils_get_platform_library_name( + RCUTILS_STRINGIFY(SHARED_LIBRARY_UNDER_TEST), library_path, 1024, false); ASSERT_EQ(RCUTILS_RET_OK, ret); ret = rcutils_load_shared_library(&lib, library_path, rcutils_get_default_allocator()); @@ -153,7 +173,8 @@ TEST_F(TestSharedLibrary, error_symbol) { void * symbol = rcutils_get_symbol(&lib, "print_name"); EXPECT_TRUE(symbol == NULL); - ret = rcutils_get_platform_library_name("dummy_shared_library", library_path, 1024, false); + ret = rcutils_get_platform_library_name( + RCUTILS_STRINGIFY(SHARED_LIBRARY_UNDER_TEST), library_path, 1024, false); ASSERT_EQ(RCUTILS_RET_OK, ret); ret = rcutils_load_shared_library(&lib, library_path, rcutils_get_default_allocator()); ASSERT_EQ(RCUTILS_RET_OK, ret); @@ -177,7 +198,8 @@ TEST_F(TestSharedLibrary, basic_symbol) { ret = rcutils_has_symbol(nullptr, "symbol"); EXPECT_FALSE(ret); - ret = rcutils_get_platform_library_name("dummy_shared_library", library_path, 1024, false); + ret = rcutils_get_platform_library_name( + RCUTILS_STRINGIFY(SHARED_LIBRARY_UNDER_TEST), library_path, 1024, false); ASSERT_EQ(RCUTILS_RET_OK, ret); // getting shared library