dl: Add support for dlopen

Change-Id: I3fd109860a1eb0a945ddc3a0376ade248a4b2f75
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/meson_options.txt b/meson_options.txt
index f96c48c..37c428a 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,3 +1,4 @@
+option('dl', type: 'feature', description: 'libdl wrapper support')
 option('fd', type: 'feature', description: 'Managed file descriptor support')
 option('io_uring', type: 'feature', description: 'io_uring wrapper support')
 option('tests', type: 'feature', description: 'Build tests')
diff --git a/src/meson.build b/src/meson.build
index 2a0169a..ec981a5 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -43,6 +43,30 @@
   'stdplus/signal.cpp',
 ]
 
+has_dl = false
+if not get_option('dl').disabled()
+  dl_dep = meson.get_compiler('cpp').find_library('dl', required: false)
+  has_dl = meson.get_compiler('cpp').links('
+    #include <dlfcn.h>
+    int main() { dlopen("", 0); }
+  ', dependencies: dl_dep)
+endif
+if has_dl
+  stdplus_deps += [
+    dl_dep,
+  ]
+
+  stdplus_srcs += [
+    'stdplus/dl.cpp',
+  ]
+
+  install_headers(
+    'stdplus/dl.hpp',
+    subdir: 'stdplus')
+elif get_option('dl').enabled()
+  error('libdl support required')
+endif
+
 has_fd = false
 if not get_option('fd').disabled() and has_span
   has_fd = true
diff --git a/src/stdplus/dl.cpp b/src/stdplus/dl.cpp
new file mode 100644
index 0000000..659389c
--- /dev/null
+++ b/src/stdplus/dl.cpp
@@ -0,0 +1,45 @@
+#include <dlfcn.h>
+#include <fmt/format.h>
+
+#include <stdplus/dl.hpp>
+
+namespace stdplus
+{
+
+Dl::Dl(const char* file, DlOpenFlags flags) :
+    handle(open(file, static_cast<int>(flags)))
+{
+}
+
+struct link_map* Dl::linkMap()
+{
+    struct link_map* ret;
+    info(RTLD_DI_LINKMAP, &ret);
+    return ret;
+}
+
+void Dl::info(int request, void* info)
+{
+    if (::dlinfo(*handle, request, info) != 0)
+    {
+        throw std::runtime_error("dlinfo");
+    };
+}
+
+void* Dl::open(const char* file, int flags)
+{
+    void* ret = ::dlopen(file, flags);
+    if (ret == nullptr)
+    {
+        throw std::runtime_error(fmt::format(
+            "dlopen `{}`: {}", file ? file : "<nullptr>", dlerror()));
+    }
+    return ret;
+}
+
+void Dl::close(void*&& handle)
+{
+    ::dlclose(handle);
+}
+
+} // namespace stdplus
diff --git a/src/stdplus/dl.hpp b/src/stdplus/dl.hpp
new file mode 100644
index 0000000..0526391
--- /dev/null
+++ b/src/stdplus/dl.hpp
@@ -0,0 +1,55 @@
+#pragma once
+#include <dlfcn.h>
+#include <link.h>
+
+#include <stdplus/flags.hpp>
+#include <stdplus/handle/managed.hpp>
+
+namespace stdplus
+{
+
+enum class DlOpenType : int
+{
+    Lazy = RTLD_LAZY,
+    Now = RTLD_NOW,
+};
+
+enum class DlOpenFlag : int
+{
+    Global = RTLD_GLOBAL,
+    Local = RTLD_LOCAL,
+    NoDelete = RTLD_NODELETE,
+    NoLoad = RTLD_NOLOAD,
+    DeepBind = RTLD_DEEPBIND,
+};
+
+class DlOpenFlags : public stdplus::BitFlags<int, DlOpenFlag>
+{
+  public:
+    inline DlOpenFlags(DlOpenType type) :
+        BitFlags<int, DlOpenFlag>(static_cast<int>(type))
+    {
+    }
+
+    inline DlOpenFlags(BitFlags<int, DlOpenFlag> flags) :
+        BitFlags<int, DlOpenFlag>(flags)
+    {
+    }
+};
+
+class Dl
+{
+  public:
+    Dl(const char* filename, DlOpenFlags flags);
+
+    struct link_map* linkMap();
+
+  private:
+    void info(int request, void* info);
+
+    static void* open(const char* filename, int flags);
+    static void close(void*&& handle);
+    stdplus::Managed<void*>::Handle<close> handle;
+};
+
+} // namespace stdplus
diff --git a/test/dl.cpp b/test/dl.cpp
new file mode 100644
index 0000000..cc16293
--- /dev/null
+++ b/test/dl.cpp
@@ -0,0 +1,21 @@
+#include <gtest/gtest.h>
+#include <stdplus/dl.hpp>
+
+namespace stdplus
+{
+
+TEST(Dl, FailedOpen)
+{
+    EXPECT_THROW(Dl("nodl.so", DlOpenFlags(DlOpenType::Now)),
+                 std::runtime_error);
+}
+
+TEST(Dl, GetLinkMap)
+{
+    Dl dl(nullptr, DlOpenFlags(DlOpenType::Now)
+                       .set(DlOpenFlag::Global)
+                       .set(DlOpenFlag::NoLoad));
+    EXPECT_NE(nullptr, dl.linkMap());
+}
+
+} // namespace stdplus
diff --git a/test/meson.build b/test/meson.build
index b303032..7d26297 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -29,6 +29,16 @@
   'util/string',
 ]
 
+if has_dl
+  gtests += [
+    'dl',
+  ]
+elif build_tests.enabled()
+  error('Not testing libdl feature')
+else
+  warning('Not testing libdl feature')
+endif
+
 if has_fd
   gtests += [
     'fd/dupable',