For bazel c++ project, how to use "dlopen/sym" to locate my own output library?

3 min read 06-10-2024
For bazel c++ project, how to use "dlopen/sym" to locate my own output library?


Dynamically Linking Your Bazel C++ Output Library: A Guide to dlopen and dlsym

Problem: You've built a C++ project with Bazel, and you need to dynamically load your output library (e.g., a shared library) at runtime. This is common for scenarios like plugin systems or dynamically loading specific functionalities. However, the default Bazel output paths can be tricky to navigate when using dlopen and dlsym.

Rephrased: Imagine you've built a cool tool with Bazel. Now, you want to add plugins that extend its functionality. These plugins are separate libraries, loaded only when needed. The catch: you need to tell your main tool where to find these plugin libraries! This is where dlopen and dlsym come in, but figuring out the library's location within Bazel's structure can be a headache.

Scenario and Code:

Let's say you have a Bazel C++ project with a main library called my_tool and a plugin library called my_plugin. Your Bazel build file might look something like this:

cc_library(
    name = "my_tool",
    srcs = ["main.cc"],
    hdrs = ["my_tool.h"],
)

cc_library(
    name = "my_plugin",
    srcs = ["plugin.cc"],
    hdrs = ["my_plugin.h"],
    deps = ["//my_tool"],
)

Now, in your my_tool code, you want to dynamically load my_plugin using dlopen:

#include <dlfcn.h>
#include <iostream>

int main() {
  // Assuming "my_plugin.so" is located somewhere we know
  void* handle = dlopen("path/to/my_plugin.so", RTLD_NOW);
  if (handle == nullptr) {
    std::cerr << "Error loading plugin: " << dlerror() << std::endl;
    return 1;
  }
  // ... Use dlsym to access functions from the plugin
  return 0;
}

The Challenge: The Bazel output paths for your libraries aren't directly accessible through the build file's name attribute. To dynamically load the my_plugin library, you'll need to figure out where Bazel placed it.

Analysis and Solution:

  1. Bazel's Output Structure: Bazel organizes its output by target name, configuration, and workspace root. For a target like my_plugin, its output might be placed under bazel-bin/your_workspace_root/my_plugin, where your_workspace_root is the directory containing your Bazel project.

  2. Using $(location) in Bazel: Bazel offers the $(location) macro, which can be used to retrieve the path to a target's output. However, $(location) simply gives you the path within Bazel's output directory, not a fully qualified path.

  3. Combining $(location) with $(workspace_root): To get the full path, you need to combine $(location) with the $(workspace_root) macro, which provides the root directory of your Bazel project.

Example:

To dynamically load my_plugin, you can use the following Bazel rule:

cc_binary(
    name = "my_tool",
    srcs = ["main.cc"],
    hdrs = ["my_tool.h"],
    deps = ["//my_plugin"],
    linkopts = [
        "-Wl,-rpath=$(location ..)",
        "-Wl,-rpath=$(workspace_root)/bazel-bin",
    ],
)

Explanation:

  • linkopts = ["-Wl,-rpath=$(location ..)"]: This adds the my_plugin library's directory to the runtime search path for dynamic libraries. It uses $(location ..) to get the parent directory of my_tool, which contains the my_plugin output directory.
  • linkopts = ["-Wl,-rpath=$(workspace_root)/bazel-bin"]: This ensures that Bazel's bazel-bin directory is also in the runtime search path, allowing dlopen to find the library correctly.

Using dlopen with the Correct Path:

Now, you can dynamically load the my_plugin library within your main.cc:

#include <dlfcn.h>
#include <iostream>

int main() {
  // No need to specify the full path anymore
  void* handle = dlopen("libmy_plugin.so", RTLD_NOW);
  if (handle == nullptr) {
    std::cerr << "Error loading plugin: " << dlerror() << std::endl;
    return 1;
  }
  // ... Use dlsym to access functions from the plugin
  return 0;
}

Benefits:

  • Dynamic Loading Flexibility: This approach allows you to dynamically load libraries during runtime, creating flexible and extendable applications.
  • Clear and Concise: Using $(location) and $(workspace_root) ensures the correct path to your output library, simplifying your dlopen calls.
  • Bazel Integration: This solution seamlessly integrates with Bazel's build system, providing a consistent and reliable way to manage your libraries.

Additional Tips:

  • Library Naming Conventions: Make sure your output library names follow the convention lib[library_name].so.
  • Use dlsym: Use dlsym after successfully calling dlopen to access specific functions or data from the loaded library.

References: