编译器不可知的运行时类型信息?

Compiler agnostic runtime type information?

如何生成和比较与编译器无关的 运行 时间类型信息?

特别是函数指针类型如何做到这一点?

例如,以下程序包含类型错误。它无法在编译时检测到。如何在 运行 时间检测到它?一般如何检测此类错误?

main.cpp

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

typedef void (*FooPtrType)();
typedef void (*BarPtrType)();

int main()
{
    const auto dll = dlopen("functions.dylib", RTLD_LAZY);
    if (dll == NULL) {
        std::cerr << dlerror() << std::endl;
        return 1;
    }

    const auto foo_ptr = (FooPtrType)dlsym(dll, "bar");
    if (foo_ptr == NULL) {
        std::cerr << dlerror() << std::endl;
        return 1;
    }

    foo_ptr();
}

functions.cpp,编译为functions.dylib

#include <iostream>

extern "C" void foo() {
    std::cout << "foo()" << std::endl;
}

extern "C" int bar() {
    std::cout << "bar()" << std::endl;
    return 0;
}

您无法在运行时检测到此类类型错误。

dlsym 处理的符号只是与给定 dlopenELF 共享 object 中的某个地址关联的符号。该符号中没有更多类型信息。

(如果 .so 是用 -g 编译的,你可以从 ELF 文件中解析 DWARF 信息——它知道类型。这很痛苦)

阅读 C++ dlopen mini howto, then Drepper's paper: How to write a Shared Library 了解详情。

您可以使用 name mangling 技术(C++ 编译器使用它)。您可能会决定插件中的给定名称具有一些特定的签名(例如,通过声明特定的 dlsym-ed 名称的插件代码提供 header 为 #include-d)。

您可以通过数据提供一些自己的输入。例如,您可以决定 every dlsym-ed 指针具有相同的类型,也许指向公共抽象超类的某个子类的某个实例;或者决定每个 dlsym-ed 指针都指向一些 struct,里面有一些 tagged union。你可以使用例如

 typedef void void0funsig_t (void);
 typedef int int0funsig_t (void);
 typedef void void1ifunsig_t (int);

 enum funtype_en { void0, int0, void1i; };
 struct typefun_st {
    enum funtype_en typ;
    union {
     void0funsig_t* void0funptr; // when typ == void0
     int0funsig_t* int0funptr;   // when typ == int0
     void1ifunsig_t* void1ifunptr; // when typ == void1i
    };
   };

并决定每个 dlsym-ed 指针指向这样一个 struct typefun_st。然后在插件代码中

 extern "C" struct typefun_st foofun ={ void0, foo };

并在程序中执行

 struct typefun_st *foofunp = dlsym(plugin,"foofun");

等..

您可以使用巧妙的 C 宏来简化此类代码(查看 Emacs 代码中的灵感)。

了解 Qt plugins for inspiration. They use the Qt (runtime) type information provided by the moc. You might perhaps use typeid and std::type_info...

使用dlsym时,您总是需要插件约定。在您的情况下,您希望他们在插件加载时 "provide" 一些类型信息。由您来设置所需的基础架构。

多么深刻和发人深省的问题!不错的 OP!

此解决方案背后的基本思想是使用宏生成 class 可以告诉您对象类型名称的模板特化,使用模板编程将这些 class 模板特化组合成函数特化指针,并使用函数重载在专业化中进行搜索。

type_string.h

#ifndef TYPE_STRING_H
#define TYPE_STRING_H

#include <type_traits>
#include <cstdlib>
#include <cstring>
#include <string>

// is_fun_ptr
// Dietmar Kühl
// 

template <typename Fun>
struct is_fun_ptr
    : std::integral_constant<bool, std::is_pointer<Fun>::value
                                  && std::is_function<
                                         typename std::remove_pointer<Fun>::type
                                     >::value>
{
};


template<typename T>
struct TypeString;

// type_string<>() overload for non-function pointer objects
// caller must free
template<typename T>
typename std::enable_if<!is_fun_ptr<T>::value, const std::string>::type
type_string()
{
    const std::string name = TypeString<T>::value();
    char* name_c_str = (char*)malloc(name.length() + 1);
    strcpy(name_c_str, name.c_str());
    return name_c_str;
}

// getting type name into a template
// atzz and Steve Jessop
// 

// There's likely a better way to handle CV qualifiers and pointer/ref
// not all combos covered here
#define ENABLE_TYPE_STRING(TYPE) \
template<> \
struct TypeString<TYPE> { \
    static const std::string value() { return #TYPE; } \
}; \
template<> \
struct TypeString<TYPE&> { \
    static const std::string value() { return #TYPE "&"; } \
}; \
template<> \
struct TypeString<TYPE*> { \
    static const std::string value() { return #TYPE "*"; } \
}; \
template<> \
struct TypeString<const TYPE> { \
    static const std::string value() { return "const " #TYPE; } \
}; \
template<> \
struct TypeString<const TYPE&> { \
    static const std::string value() { return "const " #TYPE "&"; } \
}; \
template<> \
struct TypeString<const TYPE*> { \
    static const std::string value() { return "const " #TYPE "*"; } \
}; \
template<> \
struct TypeString<const TYPE* const> { \
    static const std::string value() { return "const " #TYPE "* const"; } \
};

// some builtin types, add others and user-defined types as desired
ENABLE_TYPE_STRING(char)
ENABLE_TYPE_STRING(short)
ENABLE_TYPE_STRING(int)
ENABLE_TYPE_STRING(long)
ENABLE_TYPE_STRING(long long)

ENABLE_TYPE_STRING(signed char)
ENABLE_TYPE_STRING(unsigned char)
ENABLE_TYPE_STRING(unsigned short)
ENABLE_TYPE_STRING(unsigned int)
ENABLE_TYPE_STRING(unsigned long)
ENABLE_TYPE_STRING(unsigned long long)

ENABLE_TYPE_STRING(float)
ENABLE_TYPE_STRING(double)
ENABLE_TYPE_STRING(long double)

ENABLE_TYPE_STRING(std::string)

// void is a special case, no qualifiers, refs
template<>
struct TypeString<void>
{
    static const std::string value()
    {
        return "void";
    }
};
template<>
struct TypeString<void*>
{
    static const std::string value()
    {
        return "void*";
    }
};


// Function signature to string

// return_type
// angew
// 
template <class F>
struct return_type;

template <class R, class... A>
struct return_type<R (*)(A...)>
{
    typedef R type;
};

// forward declaration so that this overload may be used in CommaSeparatedNames
template<typename T>
typename std::enable_if<is_fun_ptr<T>::value, const std::string>::type
type_string();

// Concatenating argument types with separating commas
template<typename T, typename... Us>
struct CommaSeparatedNames
{
    static const std::string value()
    {
        return std::string{type_string<T>()}
            + ", " + CommaSeparatedNames<Us...>::value();
    }
};

template<typename T>
struct CommaSeparatedNames<T>
{
    static const std::string value()
    {
        return type_string<T>();
    }
};

// Arguments to string
template <class F>
struct ArgNames;

template<class R>
struct ArgNames<R (*)()>
{
    static const std::string value()
    {
        return "";
    }
};

template<class R, typename A, typename... As>
struct ArgNames<R (*)(A, As...)>
{
    static const std::string value()
    {
        return CommaSeparatedNames<A, As...>::value();
    }
};

// overload type_string<>() for function pointers
// caller must free
template<typename T>
typename std::enable_if<is_fun_ptr<T>::value, const std::string>::type
type_string()
{
    const auto name =
        std::string{type_string<typename return_type<T>::type>()}
        + " (*)(" + ArgNames<T>::value() + ")";
    auto name_c_str = (char*)malloc(name.length() + 1);
    strcpy(name_c_str, name.c_str());
    return name_c_str;
}

// overload type_string<>() to deduce type from an object
// caller must free
template<typename T>
const std::string type_string(T) { return type_string<T>(); }

template<typename T>
const char* type_string_c_str()
{
    const auto name = type_string<T>();
    char* name_c_str = (char*)malloc(name.length() + 1);
    strcpy(name_c_str, name.c_str());
    return name_c_str;
}

template<typename T>
const char* type_string_c_str(T) { return type_string_c_str<T>(); }


#endif

用法示例:

#include "type_string.h"

#include <iostream>

void foo() {}
int bar() { return 0; }
float baz(char) { return 0.0f; }

class MyClass;
ENABLE_TYPE_STRING(MyClass)

double quux(const int*, MyClass*) { return 0.0; }

int main()
{
    typedef void (*FooTypePtr)();
    typedef int (*BarTypePtr)();
    typedef float (*BazTypePtr)(char);
    typedef double (*QuuxTypePtr)(const int*, MyClass*);
    FooTypePtr foo_ptr = foo;
    BarTypePtr bar_ptr = bar;
    BazTypePtr baz_ptr = baz;
    QuuxTypePtr quux_ptr = quux;

    QuuxTypePtr (*weird_ptr)(FooTypePtr, BarTypePtr, BazTypePtr) = NULL;

    std::cout << type_string(3) << std::endl;
    std::cout << type_string('a') << std::endl;
    std::cout << type_string(foo_ptr) << std::endl;
    std::cout << type_string(bar_ptr) << std::endl;
    std::cout << type_string(baz_ptr) << std::endl;
    std::cout << type_string(quux_ptr) << std::endl;
    std::cout << type_string(weird_ptr) << std::endl;
}

输出:

int
char
void (*)()
int (*)()
float (*)(char)
double (*)(const int*, MyClass*)
double (*)(const int*, MyClass*) (*)(void (*)(), int (*)(), float (*)(char))

需要手动启用对象类型的类型信息,但如果启用了所有相关类型的信息,则可以推断出函数指针的类型。


动态加载示例(又名使用 X 解决 Y)

这是一种在运行时检测此类类型错误的方法。

safe_dl.h 使用 type_string.h 错误检查动态加载。

#ifndef SAFE_DL_H
#define SAFE_DL_H

#include "type_string.h"

#include <exception>
#include <stdexcept>
#include <sstream>
#include <string>

#include <dlfcn.h>

#define ENABLE_RUNTIME_FNC_PTR_TYPE_INFO(F) \
    extern "C" const char* F ## _fnc_ptr_type_() { \
        return type_string_c_str( F ); }

#define RUNTIME_TYPE_STRING_FNC_NAME(F) \
    F ## _fnc_ptr_type_

namespace {
extern "C" void in_dll_free(void* ptr)
{
    free(ptr);
}
}

class DynamicLibrary
{
public:
    explicit DynamicLibrary(const char* lib_name, const int mode=RTLD_LAZY)
        : filename_(lib_name)
    {
        handle_ = dlopen(lib_name, mode);
        if (handle_ == NULL) {
            throw std::runtime_error(dlerror());
        }
        free_ptr_ = find_in_dll_free();
    }

    DynamicLibrary(const std::string& lib_name)
    : DynamicLibrary(lib_name.c_str()) {}

    ~DynamicLibrary()
    {
        dlclose(handle_);
    }

    template<typename T>
    T safe_dynamic_load(const std::string& func_name) const
    {
        // The type of T* tells us the expected type.  A cooperating library tells
        // us the actual type.
        const auto expected_type = type_string<T>();
        const auto actual_type = symbol_type(func_name);
        if (strcmp(expected_type.c_str(), actual_type))
        {
            std::ostringstream msg;
            msg << "Function pointer type mismatch. Function " << func_name
                << " loaded from file " << filename() << " has expected type "
                << expected_type << " but the actual type is " << actual_type
                << ".";
            free_ptr_((void*)actual_type);
            throw std::runtime_error(msg.str());
        }

        free_ptr_((void*)actual_type);
        return (T)(symbol(func_name));
    }

    const std::string& filename() const { return filename_; }

private:
    // caller is responsible for freeing returned pointer
    const char* symbol_type(const std::string& name) const
    {
        const auto type_func_name = name + "_fnc_ptr_type_";
        typedef const char* (*TypeFuncPtrType)();
        TypeFuncPtrType type_func_ptr =
            (TypeFuncPtrType)dlsym(handle_, type_func_name.c_str());
        if (type_func_ptr == NULL) {
            const auto msg = "Safe dynamic loading not enabled for " + name;
            throw std::runtime_error(msg.c_str());
        }
        return type_func_ptr();
    }

    void* symbol(const std::string& name) const
    {
        void* p = dlsym(handle_, name.c_str());
        if (p == NULL) {
            throw(std::runtime_error{dlerror()});
        }
        return p;
    }

    // free from within the dll
    typedef void (*DllFreePtrType)(void*);
    DllFreePtrType find_in_dll_free() const
    {
        typedef void (*DllFreePtrType)(void*);
        DllFreePtrType free_ptr = (DllFreePtrType)dlsym(handle_, "in_dll_free");
        if (free_ptr == NULL) {
            throw std::runtime_error(dlerror());
        }
        return free_ptr;
    }

private:
    const std::string filename_;
    void* handle_;
    DllFreePtrType free_ptr_;
};

#endif

在原始程序中工作safe_dl.h

main.cpp

#include "safe_dl.h"
#include <iostream>

#if defined(__clang__)
#define COMPILER "clang"
#elif defined(__GNUC__)
#define COMPILER "gcc"
#else
#define COMPILER "other"
#endif

typedef void (*FooPtrType)();
typedef int (*BarPtrType)();

int main()
{
    std::cout << "main()" << std::endl;
    std::cout << "compiler: " COMPILER << std::endl;

    const DynamicLibrary dll{"./functions.so"};
    // Works fine.
    const auto foo_ptr = dll.safe_dynamic_load<FooPtrType>("foo");
    foo_ptr();

    // Throws exception.
    const auto bar_ptr = dll.safe_dynamic_load<FooPtrType>("bar");
    bar_ptr();
}

functions.cpp,编译为functions.dylib

#include "safe_dl.h"
#include <iostream>

#if defined(__clang__)
#define COMPILER "clang"
#elif defined(__GNUC__)
#define COMPILER "gcc"
#else
#define COMPILER "other"
#endif

extern "C" void foo() {
    std::cout << "foo()" << std::endl;
    std::cout << "compiler: " COMPILER << std::endl;
    return;
}
ENABLE_RUNTIME_FNC_PTR_TYPE_INFO(foo)

extern "C" int bar() {
    std::cout << "bar()" << std::endl;
    return 0;
}
ENABLE_RUNTIME_FNC_PTR_TYPE_INFO(bar)

用 clang 编译的 main.cpp 和用 g++ 编译的 functions.cpp 的输出:

main()
compiler: clang
foo()
compiler: gcc
terminate called after throwing an instance of 'std::runtime_error'
  what():  Function pointer type mismatch. Function bar loaded from file
./functions.so has expected type void (*)() but the actual type is int (*)().
Aborted (core dumped)

因为 type_name.h 生成 char * 来编码类型,代码相对编译器不可知,并且 safe_dl.h 将与混合编译器一起工作。 (或者至少该示例在混合 gcc 4.9.2 和 clang 3.5 时有效。)