This question is a follow-up to this one.
At the linked question, I learned how to create a CMakeLists.txt that added a RUNPATH such that in my C code, I could link the library using only the shared lib name without a path. I.e. dlopen("libfoo.so", RTLD_LAZY)
, and not dlopen("/path/to/libfoo.so", RTLD_LAZY)
.
I wanted to implement what I thought was a trivial change: be able to link libraries using a relative path (dlopen
's man-page says this is possible), but I'm having trouble getting this fully working and interpreting the behavior.
My project's directory structure is:
.
├── CMakeLists.txt
├── build # this directory exists to perform an "out-of-source" build with "cmake .."
└── src
├── CMakeLists.txt
├── main.c
├── module.c
├── module.h
└── modules
├── CMakeLists.txt
├── stage1.c
└── submodules
├── CMakeLists.txt
└── stage2.c
module.c implements function module_load(const char*)
: a thin wrapper over dlopen()
, and function module_run(void*, const char*)
, a thin wrapper over dlsym()
.
I want the callers of module_load()
to specify a shared-library filepath relative to $ORIGIN/modules.
E.g. I'd like module_load("libstage1.so")
to load $ORIGIN/modules/libstage1.so, and I'd like module_load("submodules/libstage2.so")
to load $ORIGIN/modules/submodules/libstage2.so.
I don't know exactly how to qualify the observed result; but my goal seems only partly achieved: module_load("libstage1.so")
has the desired effect of loading $ORIGIN/modules/libstage1, but module_load("submodules/libstage2.so")
fails to load $ORIGIN/modules/submodules/libstage2.so with a "dlopen error: submodules/libstage2.so: cannot open shared object file: No such file or directory" error.
What I have tried:
I added set_property(TARGET module APPEND PROPERTY BUILD_RPATH "$ORIGIN/modules")
in module.c's CMakeLists.txt because I thought it would have the effect of making its dlopen
s relative to the modules/ directory (where $ORIGIN is the directory where the executable is).
Indeed, this seems to work, because main.c can successfully do void* module = module_load("libstage1.so")
, where libstage1.so is in the modules/ directory, and I do not specify "modules/" anywhere in code.
Based on the apparent success of the desired behavior of module_load("libstage1.so")
in main.c, I tried doing void* module = module_load("submodules/libstage2.so")
in main.c -- but this was unsuccessful.
I tried doing void* module = module_load("submodules/libstage2.so")
in stage1.c (i.e. a shared lib loading another shared lib); that failed identically to the attempt to do the same in main.c
Questions:
Can someone please help me understand the observed behavior?
I'm having a hard time reconciling why module_load("libstage1.so")
is working (it is loading a shared library relative to the modules/ directory), while module_load("submodules/libstage2.so")
isn't working -- isn't it also doing the same thing: loading a shared library relative to the modules/ directory?
The code/files:
# ./CMakeLists.txt
cmake_minimum_required(VERSION 3.9)
project(loader)
add_subdirectory(src)
# ./src/CMakeLists.txt
add_library(module SHARED module.c)
target_link_libraries(module PRIVATE ${CMAKE_DL_LIBS})
set_property(TARGET module APPEND PROPERTY BUILD_RPATH "$ORIGIN/modules")
add_executable(runme main.c)
target_link_libraries(runme PRIVATE module)
set_property(TARGET runme APPEND PROPERTY BUILD_RPATH "$ORIGIN")
add_subdirectory(modules)
# ./src/modules/CMakeLists.txt
include_directories("../")
add_library(stage1 SHARED stage1.c)
target_link_libraries(stage1 module)
add_subdirectory(submodules)
# ./src/modules/submodules/CMakeLists.txt
add_library(stage2 SHARED stage2.c)
// ./src/main.c
#include "module.h"
#include <stdlib.h>
int main(int argc, char* argv[]) {
void* module = module_load("libstage1.so");
if (module) {
module_run(module, "func");
module_release(module);
}
{
void* module = module_load("submodules/libstage2.so");
if (module) {
module_run(module, "func");
module_release(module);
}
}
return EXIT_SUCCESS;
}
// ./src/module.h
#ifndef MODULE_H
#define MODULE_H
/**
* Thin-ish dlopen() wrapper.
* @param module [in] The filename argument to dlopen(), relative to
* $ORIGIN/modules/
* @return The dlopen() return value: a handle to the loaded object.
*/
void* module_load(const char* module);
/** Releases resources returned by load_lib(). */
void module_release(void* module);
/**
* Runs "void func()" in the specified module.
* Caller guarantees function with that signature exists in the specified
* module.
*/
void module_run(void* module, const char* func);
#endif // MODULE_H
// ./src/module.c
#include "module.h"
#include <dlfcn.h>
#include <stdio.h>
void* module_load(const char* module) {
void* ret = dlopen(module, RTLD_LAZY);
if (!ret) {
char* err = dlerror();
printf("dlopen error: %s\n", err);
}
return ret;
}
void module_release(void* module) {
if (module) { dlclose(module); }
}
void module_run(void* module, const char* func) {
void (*fptr)() = dlsym(module, func);
if (fptr) { fptr(); }
else { printf("No symbol %s in module\n", func); }
}
// ./src/modules/stage1.c
#include <module.h>
#include <stdio.h>
void func() {
printf("here in stage1!\n");
{
void* module = module_load("submodules/libstage2.so");
if (module) {
module_run(module, "func");
module_release(module);
}
}
}
// ./src/modules/submodules/stage2.c
#include <stdio.h>
void func() {
printf("here in stage2!\n");
}
Build and execution
$ pwd
/home/user/code/scratch/loader
$ cd build/
$ cmake .. && make && ./src/runme
-- The C compiler identification is GNU 9.4.0
-- The CXX compiler identification is GNU 9.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/user/code/scratch/loader/build
Scanning dependencies of target module
[ 12%] Building C object src/CMakeFiles/module.dir/module.c.o
[ 25%] Linking C shared library libmodule.so
[ 25%] Built target module
Scanning dependencies of target runme
[ 37%] Building C object src/CMakeFiles/runme.dir/main.c.o
[ 50%] Linking C executable runme
[ 50%] Built target runme
Scanning dependencies of target stage1
[ 62%] Building C object src/modules/CMakeFiles/stage1.dir/stage1.c.o
[ 75%] Linking C shared library libstage1.so
[ 75%] Built target stage1
Scanning dependencies of target stage2
[ 87%] Building C object src/modules/submodules/CMakeFiles/stage2.dir/stage2.c.o
[100%] Linking C shared library libstage2.so
[100%] Built target stage2
here in stage1!
dlopen error: submodules/libstage2.so: cannot open shared object file: No such file or directory
dlopen error: submodules/libstage2.so: cannot open shared object file: No such file or directory
Update:
I've "solved" this by making module_load()
create an absolute filepath out of its module
argument, to feed dlopen()
.
I guess it works, but I'd prefer any answer that explains how to get dlopen()
to work with relative paths, since the dlopen()
man-page says this should be possible.