Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Run-time implementation of std::function

Tags:

c++

To be on the safe side I am already using old style function pointers in my DLL calls like that:

// DLL
typedef int (__stdcall* ty)();
void test(ty t)
{
    if (t)
    {
        int r = t(); 
        ....
    }
}

Whereas I could use this one:

void test(std::function<int()> t)
{
}

The latter however, as known, uses type erasure. std::function cannot be a raw function pointer (for it may be passed a lambda which has captures and thus, cannot be a raw pointer).

So, in Visual studio, using a DLL build in release mode with a function signature that contains std::function crashes, when used from an executable build in debug mode and vice versa. Even if it's both debug or release mode, the behavior is not the same. Sometimes it crashes, some times it works.

Is there a defined run-time behaviour that we can rely on to use std::function? Or this is a compile-only thing that is specialized by the compiler depending on what I pass to it and therefore, run-time behaviour cannot be assumed behorehand?

At the assembly level the function signature on a precompiled unit must be known. As far I as know std::function runtime implementation is not well defined.

So, for example, when I'm compiling, then

void test(std::function<int()> t)

can take any argument like []() -> int, [&]() -> int, [=]() -> int, etc via type erasure. But when this is precompiled and thus runtime only, what can be accepted? How std::function is implemented? With a class pointer? Is there a well defined way?

I'm not looking for a necessarily VS-related solution, but a standard definition of std::function, if any.

like image 374
Michael Chourdakis Avatar asked Dec 20 '20 16:12

Michael Chourdakis


2 Answers

So, in Visual studio, using a DLL build in release mode with a function signature that contains std::function crashes, when used from an executable build in debug mode and vice versa.

This will never work because it changes the ABI.

Even if it's both debug or release mode, the behavior is not the same. Sometimes it crashes, some times it works.

This may work if you are careful about compiler flags, dependent static vs. dynamic libraries, how the C and C++ standard libraries are compiled, exceptions, etc. It is a complex topic and depends on what the compiler vendor guarantees.

Is there a defined run-time behaviour that we can rely on to use std::function?

In general, your best bet (and the most useful one for creating bindings for other languages) is to avoid C++ interfaces and use plain C ones with trivial types.

That is, if you want to pass C++ types, pass them as opaque types instead and only manipulate them from one side.

I'm not looking for a VS-related solution, but a standard definition of std::function, if any.

The C++ standard does not force any particular ABI nor gives implementation details like data members for the vast majority of types.

That is why mixing different STL libraries is also a problem even if you compiled everything in the exact same way.

like image 160
Acorn Avatar answered Oct 23 '22 23:10

Acorn


Old style callbacks should be a function pointer and a void pointer.

With that, you can send a std function (but not store it).

You can wrap up these operations in a standard layout class, and that is 99.99% safe to cross DLL boundaries. The code that is run/injected cannget unloaded early, but that is no different than with C style callbacks.

To safely store callable data, you need call-with-state, and destroy-state operations.

To do this in a totally dll safe manner...

template<class Sig>
struct callback;

template<class R, class...Args>
struct callback<R(Args...)>{
  // standard layout data:
  void* state=0;
  R(*operate)(void*, Args...)=0;
  void(*cleanup)(void*)=0;

  voud clear(){ if(cleanup){cleanup(state);}state=0; operate=0; cleanup=0; }
  callback(callback const&)=delete;
  callback(callback&&o):state(o.state),operate(o.operate),cleanup(o.cleanup){o.cleanup=0;o.clear();}
  callback& operator=(callback&&o){
    if(this==&o) return *this;
    clear();
    state=o.state;
    operate=o.operate;
    cleanup=o.cleanup;
    o.clear();
    return *this;
  }

  ~callback(){ clear(); }
  R operator()(Args...args)const{return operate(state, std::forward<Args>(args)...);}
};

you can convert a std function to the above pretty easily, and even vice versa.

It is standard layout, so should be safe crossing DLL boundaries.

But if insanely paranoid, you can unpack it and send 2 function pointers and a void pointer one by one.

Making a stateful object into above:

template<class T> void(*deleter)(void*)=+[](void*ptr){delete static_cast<T*>(ptr);};
template<class F, class R, class...Args> R(*caller)(void*,Args...)=
  +[](void* ptr, Args...args)->R{
    return (*(F*)(ptr))(std::forward<Args>(args)...);};

 template<class F, class R, class...Args>
 callback<R(Args...)> make_callback(F&& f){
   callback<R(Args...)> retval;
   retval.state=new std::decay_t<F>(std::forward<F>(f));
   retval.operate=caller<std::decay_t<F>, R, Args...>;
   retval.cleanup=deleter<std::decay_t<F>>;
   return retval;
}

which works on lambdas or std functions.

A raw function:

template<class R,class...Args>
callback<R(Args...)> make_callback(R(*pf)(Args...)){
  callback<R(Args...)> retval;
  retval.state=reinterpret_cast<void*>(pf);// note on some obscure platforms this does not work.
  retval.operate=caller<R(Args...), R, Args...>;// ditto
}

no cleanup needed.

This callback is a move-only stripped down std function implementation. Many more improvements can be made while keeping it dll safe.

like image 37
Yakk - Adam Nevraumont Avatar answered Oct 24 '22 00:10

Yakk - Adam Nevraumont