0

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 dlopens 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.

StoneThrow
  • 5,314
  • 4
  • 44
  • 86
  • 1
    Off: the Unix way would be installing every plugin into a fixed directory, which is determined build-time such as /usr/local/libexec/myapp or ~/myapp/plugins – Lorinczy Zsigmond Aug 10 '22 at 08:45
  • You could of course determine the location of your shared lib and resolve the path based on this info: https://stackoverflow.com/a/51993539/2991525 – fabian Aug 10 '22 at 16:30

0 Answers0