Why does an std::any_cast of a passed std::any inside a dlopen'd function raise an error
Asked Answered
M

1

5

I am toying around with c++17 and plugins, and I have run into an error that I cannot get around. In the following MWE I can call a local function that takes a std::any, and everything works as expected when I try to read the contents. When I load this exact same function through a plugin (dlopen), it correctly sees the type on the any, but it cannot std::any_cast the contents.

Any help would be greatly appreciated in figuring out what is causing this error.

Here is my environment, MWE, and resulting error.

>> g++ --version

g++ (GCC) 7.1.1 20170526 (Red Hat 7.1.1-2)
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

>> scons --version

SCons by Steven Knight et al.:
    script: v2.5.1.rel_2.5.1:3735:9dc6cee5c168[MODIFIED], 2016/11/03 14:02:02, by bdbaddog on mongodog
    engine: v2.5.1.rel_2.5.1:3735:9dc6cee5c168[MODIFIED], 2016/11/03 14:02:02, by bdbaddog on mongodog
    engine path: ['/usr/lib/scons/SCons']
Copyright (c) 2001 - 2016 The SCons Foundation

>> tree

.
├── SConstruct
└── src
    ├── main.cpp
    ├── plugin.cpp
    └── SConscript

1 directory, 4 files

>> cat SConstruct

SConscript('src/SConscript', variant_dir='build', duplicate=0)

>> cat src/SConscript

env = Environment()
env.Append(CXXFLAGS=['-std=c++17'])
plugin = env.SharedLibrary('plugin', 'plugin.cpp')
Install('../lib', plugin)
driver_env = env.Clone()
driver_env.Append(LIBS=['dl', 'stdc++fs'])
driver = driver_env.Program('driver', 'main.cpp')
Install('../bin', driver)

>> cat src/plugin.cpp

#include <any>
#include <iostream>
using namespace std;
extern "C" {
int plugin(any& context) {
    cout << "    Inside Plugin" << endl;
    cout << "    Has Value? " << context.has_value() << endl;
    cout << "    Type Name: " << context.type().name() << endl;
    cout << "    Value: " << any_cast<double>(context) << endl;
}
}

>> cat src/main.cpp

#include <functional>
#include <iostream>
#include <stdexcept>
#include <any>
#include <experimental/filesystem>
#include <dlfcn.h>

using namespace std;
using namespace std::experimental;

function< void(any&) > loadplugin(string filename) {
    function< void(any&) > plugin;
    filesystem::path library_path(filename);
    filesystem::path library_abspath = canonical(library_path);
    void * libraryHandle = dlopen(library_abspath.c_str(), RTLD_NOW);
    if (!libraryHandle) {
        throw runtime_error("ERROR: Could not load the library");
    }
    plugin = (int(*) (any&))dlsym(libraryHandle, "plugin");
    if (!plugin) {
        throw runtime_error("ERROR: Could not load the plugin");
    }
    return plugin;
}

int local(any& context) {
    cout << "    Inside Local" << endl;
    cout << "      Has Value? " << context.has_value() << endl;
    cout << "      Type Name: " << context.type().name() << endl;
    cout << "      Value: " << any_cast<double>(context) << endl;
}

int main(int argc, char* argv[]) {
    cout << "  Resolving Paths..." << endl;
    filesystem::path full_path = filesystem::system_complete( argv[0] ).parent_path();
    filesystem::path plugin_path(full_path/".."/"lib"/"libplugin.so");
    cout << "  Creating Context..." << endl;
    any context(.1);
    cout << "  Loading Plugin..." << endl;
    function<void(any&) > plugin = loadplugin(plugin_path.string());
    cout << "  Calling Local..." << endl;
    local(context);
    cout << "  Calling Plugin..." << endl;
    plugin(context);
}

>> scons

scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
g++ -o build/main.o -c -std=c++17 src/main.cpp
g++ -o build/driver build/main.o -ldl -lstdc++fs
Install file: "build/driver" as "bin/driver"
g++ -o build/plugin.os -c -std=c++17 -fPIC src/plugin.cpp
g++ -o build/libplugin.so -shared build/plugin.os
Install file: "build/libplugin.so" as "lib/libplugin.so"
scons: done building targets.

>> tree

.
├── bin
│   └── driver
├── build
│   ├── driver
│   ├── libplugin.so
│   ├── main.o
│   └── plugin.os
├── lib
│   └── libplugin.so
├── SConstruct
└── src
    ├── main.cpp
    ├── plugin.cpp
    └── SConscript

4 directories, 10 files

>> bin/driver

  Resolving Paths...
  Creating Context...
  Loading Plugin...
  Calling Local...
    Inside Local
      Has Value? 1
      Type Name: d
      Value: 0.1
  Calling Plugin...
    Inside Plugin
    Has Value? 1
    Type Name: d
terminate called after throwing an instance of 'std::bad_any_cast'
  what():  bad any_cast
    Value: Aborted (core dumped)
Matriarchate answered 10/6, 2017 at 2:17 Comment(8)
Can you have it print out the abspath to the shared library it's trying to load?Randolf
Also you don't need to clone the environment to add LIBS, You can do the following "driver = env.Program('driver', 'main.cpp',LIBS=['dl', 'stdc++fs'])"Randolf
Run this in a debugger and obtain a backtrace to the point where the exception has been thrown. As this will be inside the standard library you'll need to make sure you have debugging symbols for it available.Verticillate
@bdbaddog, I can print out the abspath and it is correct, canonical would throw if it did not resolve to an actual file. Thanks for the tip on including slight modifications to the environment through the call to the builder.Matriarchate
@DanielJour, I ran it through the debugger, setting a breakpoint at /usr/include/c++/7/any:482 (where any does the any_cast, just before the throw), what is interesting is that what it is attempting to any_cast back to the double looks good (gdb) print __any $5 = std::any containing double = {[contained value] = 0.10000000000000001}, but after the call to any_cast there is nothing (gdb) print __p $4 = (double *) 0x0 it then throws. I am not sure what to try next as I am no gdb guru...Matriarchate
Do you get any value for libraryHandle (break on the dlopen and print the value after?)Randolf
The type check in libstdc++'s any_cast relies on a function pointer comparison of a pointer to the "manager" function. I suspect that you ended up with two copies of that function, one in the executable and one in the shared library.Cravens
Ah, gcc.gnu.org/faq.html#dsoCravens
C
11

libstdc++'s any relies on the address of the same template instantiation being the same inside a program, and that means you need to take precautions if you are using dlopen:

The compiler has to emit [objects with vague linkage, like template instantiations] in any translation unit that requires their presence, and then rely on the linking and loading process to make sure that only one of them is active in the final executable. With static linking all of these symbols are resolved at link time, but with dynamic linking, further resolution occurs at load time. You have to ensure that objects within a shared library are resolved against objects in the executable and other shared libraries.

  • For a program which is linked against a shared library, no additional precautions are needed.
  • You cannot create a shared library with the -Bsymbolic option, as that prevents the resolution described above.
  • If you use dlopen to explicitly load code from a shared library, you must do several things. First, export global symbols from the executable by linking it with the -E flag (you will have to specify this as -Wl,-E if you are invoking the linker in the usual manner from the compiler driver, g++). You must also make the external symbols in the loaded library available for subsequent libraries by providing the RTLD_GLOBAL flag to dlopen. The symbol resolution can be immediate or lazy.
Cravens answered 11/6, 2017 at 1:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.