Reputation: 24039
How can compiler agnostic run-time type information be generated and compared?
In particular, how can this be done for function pointer types?
For example, the following program contains a type error. It cannot be detected at compile-time. How can it be detected at run-time? How can such errors be detected in general?
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, compiled to functions.dylib
#include <iostream>
extern "C" void foo() {
std::cout << "foo()" << std::endl;
}
extern "C" int bar() {
std::cout << "bar()" << std::endl;
return 0;
}
Upvotes: 0
Views: 551
Reputation: 24039
What a profound and thought provoking question! Nice one OP!
The basic idea behind this solution is using macros to generate class template specializations that can tell you the name of object types, using template programming to compose those class template specializations into specializations for function pointers, and using function overloading to search among the specializations.
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
// http://stackoverflow.com/a/18667268/1128289
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
// http://stackoverflow.com/a/4485051/1128289
// 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
// http://stackoverflow.com/a/18695701/1128289
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
Example usage:
#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;
}
Output:
int
char
void (*)()
int (*)()
float (*)(char)
double (*)(const int*, MyClass*)
double (*)(const int*, MyClass*) (*)(void (*)(), int (*)(), float (*)(char))
Type information for object types needs to be manually enabled but types for function pointers can be deduced if info for all the involved types was enabled.
Dynamic loading example (aka using X to solve Y)
Here's a way to detect such type errors at runtime.
safe_dl.h
uses type_string.h
to error check dynamic loading.
#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
Work safe_dl.h
into the original program:
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
, compiled to 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)
Output for main.cpp
compiled with clang and functions.cpp
compiled with g++:
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)
Because type_name.h
generates char *
to encode types the code is relatively compiler agnostic and safe_dl.h
will work with mixed compilers. (Or at least the example works when mixing gcc 4.9.2 and clang 3.5.)
Upvotes: 2
Reputation: 1
You cannot detect such type errors at runtime.
A symbol processed by dlsym
is just a symbol associated to some address in the ELF shared object given to dlopen
. There is no more type information in that symbol.
(if the .so
was compiled with -g
you could parse DWARF information -which knows the type- from the ELF file. It is painful)
Read the C++ dlopen mini howto, then Drepper's paper: How to write a Shared Library for details.
You could use name mangling techniques (and C++ compilers use that). You might decide that a given name in the plugin has some specific signature (e.g. by providing a header to be #include
-d by plugin code which declares the particular dlsym
-ed names).
You could provide some typing yourself thru data. For example you could decide that every dlsym
-ed pointer has the same type, perhaps to some instance of some subclass of a common abstract superclass; or decide that every dlsym
-ed pointer points to some struct
with some tagged union inside. You could use for example
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
};
};
and decide that every dlsym
-ed pointer points to such a struct typefun_st
. Then in the plugin code
extern "C" struct typefun_st foofun ={ void0, foo };
and in the program do
struct typefun_st *foofunp = dlsym(plugin,"foofun");
etc..
You could use clever C macros to make such code simpler (look inside Emacs code for inspiration).
Read about Qt plugins for inspiration. They use the Qt (runtime) type information provided by the moc. You might perhaps use typeid
and std::type_info...
When using dlsym
you always need plugins conventions. In your case you want them to "provide" some type information at plugin load time. It is up to you to set up the required infrastructure.
Upvotes: 2