3
\$\begingroup\$
#include <functional>

template<typename return_type, typename... argument_types>
struct stateful_function_pointer
{
  using function_type         = std::function<return_type(argument_types...)>;
  using function_pointer_type = return_type (*) (argument_types..., void*);

  function_pointer_type callback ()
  {
    return [ ] (argument_types... arguments, void* user_data)
    {
      return (*static_cast<function_type*>(user_data))(arguments...);
    };
  }
  void*                 user_data()
  {
    return static_cast<void*>(&function);
  }

  function_type function;
};

The C style callback must be providing a void* user data as its final parameter (which is a de-facto standard). It may be possible to pass additional user data other than the function state, but I do not see the point as the lambda can already capture user state.

An eccentricity: It is instantiated as e.g. stateful_function_pointer<void, int, char*> instead of stateful_function_pointer<void(int, char*)>.

Example usage as requested (wrapping a HDF5 C function in C++):

class link_access_property_list : public property_list
{
public:
  using function_pointer_type = stateful_function_pointer<herr_t, const char*, const char*, const char*, const char*, unsigned*, hid_t>;
  using callback_type = function_pointer_type::function_type;

  bool set_external_link_callback(const callback_type& callback)
  {
    function_pointer_.function = callback;
    return H5Pset_elink_cb(id_, function_pointer_.callback(), function_pointer_.user_data()) >= 0;
  }

protected:
  function_pointer_type function_pointer_;
};

Then I could:

lapl.set_external_link_callback(
  [CAPTURE_WHATEVER_YOU_WANT] 
  (const char*, const char*, const char*, const char*, unsigned*, hid_t) 
  { 
    USE_WHATEVER_YOU_WANT 
    return herr_t(0);
  });
\$\endgroup\$
4
  • 1
    \$\begingroup\$ On the other end how do you intend to cast the void* back to its original type if you don't know the type information? \$\endgroup\$ Aug 20, 2020 at 21:00
  • 2
    \$\begingroup\$ Please show us the C function you are using to convert this back. Also an example of how you think the callback is being used would be helpful. \$\endgroup\$ Aug 21, 2020 at 0:08
  • \$\begingroup\$ This allows you to capture arbitrary state into the C callback via lambda closures. You don't cast anything. You capture what you need. \$\endgroup\$
    – demiralp
    Sep 2, 2020 at 3:32
  • 1
    \$\begingroup\$ But the callback is a C function. The only thing it knows is void*. So you have to convert it to a void* at some point and then you need to convert it back to the original type. You can't pass a lambda to be registered as a C function. \$\endgroup\$ Sep 2, 2020 at 17:38

1 Answer 1

3
\$\begingroup\$

When you have a C callback you usually pass two things

  • A pointer to a C function
  • A void* pointer that is passed to your C function.

The C function must know the type of the original object so it can cast the pointer back to its original type. only after it has been cast back can it be used. And you must cast it back to the exact type it was before you converted to void* or you are in undefined behavior.

So this means the C function must understand the original type.

Since your original type is a templated type (i.e. you don't know the type in general) you can't cast it back (unless you templitize the function so that the function you pass to C has a understanding of the templitized types).

The easy way to do this is to implement a virtual interface. Then you can get a pointer to the base class convert this to a void* then in the C function you can convert back to the base class pointer and call the appropriate virtual interface on this base pointer. Virtual dispatch will then take care of calling the correct implementation.


OK found the reference you were eluding to:

Next time just add it to the question.

From: https://support.hdfgroup.org/HDF5/doc1.8/RM/RM_H5P.html#Property-SetELinkCb

herr_t elink_callback(const char* parent_file_name,
                      const char* parent_group_name,
                      const char* child_file_name,
                      const char* child_object_name,
                      unsigned*   acc_flags,
                      hid_t       fapl_id,
                      void*       op_data)
{
    puts(child_file_name);
    return 0;
}

int main(void) {
    hid_t lapl_id = H5Pcreate(H5P_LINK_ACCESS);
    H5Pset_elink_cb(lapl_id, elink_callback, NULL);
    ...
}

Notice at the top of the document it talks about C functions: https://support.hdfgroup.org/HDF5/doc1.8/RM/RM_H5P.html

This library is robably built by a C compiler so the whole ABI is a C ABI. Thus you can not pass any functions of C++ linkage as a parameter to a callback (The C code does not know how to correctly set up the stack frame to make a correct C++ call). You can only pass functions with C linkage.

Now some compilers use a very similar ABI for C and C++ and you may get lucky passing a function that uses C++ linkage, but that just means you are getting lucky.

Things like exceptions (that's just the first thing I can think of) will not be properly tracked across the stack frame boundries when going into C built stack frame.

As you are getting lucky it is likely that sometime in the future it will just suddenly stop working with no way to diagnose the problem.

\$\endgroup\$
11
  • 1
    \$\begingroup\$ @demiralp Obviously I am not understanding your usage. Can you provide an exact example of it being used in some code. Personally I am still doubtful this will work. But I could be wrong and it will depend on the interfaces it is being used in. \$\endgroup\$ Sep 2, 2020 at 17:41
  • 1
    \$\begingroup\$ What is this: lapl.set_external_link_callback() This does not look like a C callback interface. \$\endgroup\$ Sep 2, 2020 at 17:42
  • 1
    \$\begingroup\$ @demiralp Found the docs for HP5 stuff: support.hdfgroup.org/HDF5/doc1.8/RM/… OK. It becomes clearer now. I pretty sure that is broken code. Note: There is a difference between working and working by accident. The H5Pset_elink_cb() call takes a C-function as an argument. Unfortunately you are not giving it a C-function (as you are passing a variadic template function). There is no gurantee provided by the C++ standard that a C++ function has the same ABI as a C function. \$\endgroup\$ Sep 2, 2020 at 22:51
  • 1
    \$\begingroup\$ @demiralp To make this valid the function passed to H5Pset_elink_cb() MUST be declared as extern "C" herr_t <funcName>(const char*,const char*,const char*,const char*, unsigned*, hid_t,void*). Sure you may be getting lucky but that does not make it legal or valid. Currently it is simply undefined behavior. \$\endgroup\$ Sep 2, 2020 at 22:53
  • 1
    \$\begingroup\$ @demiralp: The compilers must be creating multiple definitions of lambdas, one with and one without extern "C"? definitely not. The code is simply undefined behavior. It works by accident. \$\endgroup\$ Sep 3, 2020 at 18:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.