4
\$\begingroup\$

I am writing some C++ code to interact with an external scripting language. I want to be able to register arbitrary C++ callable code as functions to be called from the script.

The scripting library offers an API to register function pointers with signature

std::string callback( std::vector<std::string> const& args);

ie any function called from the script is given an array of strings representing the functions input args and expects a String as a result.

Up to now I would create a wrapper function for each c++ function I want to expose to the script. e.g. for function

int add_two_numbers(int a, int b){ return a + b; }

//I create and register

string add_two_numbers_wrapper( std::vector<std::string> const& args)
{
    auto arg0 = std::stoi( args[0] );
    auto arg1 = std::stoi( args[1] );

    auto result = add_two_numbers( arg0, arg1 );

    return std::to_string(result);
}

Not wanting to write the wrapper by hand each time I've tried to create some templates to manage the boilerplate. I had some success using a number of template wrappers that are written for each set of different argument counts but I want to support 'any' number of args in future without creating a new template. This is my attempt at a one-size-fits-all function wrapper template.

Are there anyways of simplifying further? I'm not too bothered about performance at this juncture and the priority is ease of use.

#include <string_view>
#include <string>
#include <functional>
#include <vector>
#include <tuple>
#include <type_traits>


///Helper Template to convert to and from the script's value types.
//should support functions:
// from_arg   - convert string to expected c++ argument type
// to_result  - convert result back to script type.
template < typename TType > 
struct convert_type;

//specialisation for int
template <> struct convert_type <int >
{
  static int from_arg (std::string const &arg)
  {
    return std::stoi (arg);
  }

  static std::string to_result (int r)
  {
    return std::to_string (r);
  }

};


template <> struct convert_type<void>
{
    static std::string to_result()
    {
        return "";
    }
};


//Wrapper template for adapting c++ function to function that is callable from script
template < typename > class fun_wrapper;

template < typename R, typename ... Args > 
class fun_wrapper < R (Args ...) >
{
public:

  using FunctionType = std::function < R (Args ...) >;
  using ResultType = R;
  using ArgTypes = std::tuple < Args ... >;

  static constexpr int m_arg_count = sizeof ... (Args);

  //constructor
  fun_wrapper (FunctionType f)
  :m_fun {f}
  {
  }

  std::string operator ()( std::vector<std::string> const& inputs)
  {
    
    if constexpr( std::is_void_v<ResultType> )  //wrapped function returns void
    {
        if constexpr ( m_arg_count == 0)        //wrapped function doesn't need args unpacking
        {
            m_fun();
        }
        else
        {
            unpack_args<0>( inputs );
        }
        
        std::string result = "";
        return result;
    }
    else
    {
        ResultType r1;
        if constexpr ( m_arg_count == 0)        //wrapped function doesn't need args unpacking
        {
          r1 = m_fun();
        }
        else
        {
            r1 = make_call<0>(args);
        }
         
        std::string result = convert_type< ResultType >::to_result (r1);
        return result;
    }      
  }

  //Helper template to unpack and convert each arg in inputs into a parameter pack that
  //can then be passed into the function call
  template < int index, typename... TArgs >
  ResultType unpack_args( std::vector<std::string> const& inputs, TArgs... targs)
  {
    using CurrentArgType = std::tuple_element_t< index, ArgTypes >;

    if constexpr (index + 1 == m_arg_count) //all args unpacked - call function
    {
       return m_fun( targs..., convert_type< CurrentArgType >::from_arg( inputs[index]) );  
    }
    else 
    {
       //unpack next argument in list and keep going
       return make_call< index + 1 >(args, targs..., convert_type< CurrentArgType >::from_arg( inputs[index]));
    }
  }

private:
  std::function< R(Args...) > m_fun;

};


//Example usage.

int add_numbers(int in1, int in2)
{
  return in1 + in2;
}


int main ()
{  
  fun_wrapper< int(int, int) > add_numbers_wrapper(add_numbers);

  std::vector<std::string> args = {"18", "24"};
  cout << add_numbers_wrapper( args ) << endl;   //outputs "42"

  return 0;
}
\$\endgroup\$
2
  • 2
    \$\begingroup\$ I did a similar thing using std::index_sequence that might be a bit clearer, but I'm not sure myself. github.com/sdilts/Salmon/blob/master/include/vm/… \$\endgroup\$
    – Soupy
    Commented Nov 4, 2022 at 18:59
  • \$\begingroup\$ @Soupy - Your example looks good - it looks like should be better performing too: requiring a smaller function stack than mine. Thanks for sharing \$\endgroup\$
    – David Woo
    Commented Nov 4, 2022 at 20:42

1 Answer 1

2
\$\begingroup\$

Your code has the right approach, but can indeed be simplified a lot. Let's start with string conversion. Consider that you want to use std::to_string() in most cases, but want to add some overloads for types it does not support (like void). Instead of creating a class template, consider just adding overloads. Since you are not allowed to overload std::to_string() itself, use this trick:

using std::to_string; // pull in all overloads from std:: into the global namespace

std::string to_string(void) // your own private overload
{
    return {};
}

You also want a from_string(), but instead of a class template it can be a function template:

template<typename T>
T from_string(std::string const &);

template<>
int from_string(std::string const &arg)
{
    return std::stoi(arg);
}

Apart from saving some lines of code when adding support for new to/from string conversion, it also makes usage shorter, compare:

convert_type<CurrentArgType>::from_arg(inputs[index])

Versus:

from_string<CurrentArgType>(inputs[index])

A lot of code is spent on unpacking and converting the arguments one by one in a recursive manner. But this can also be improved. As Soupy mentioned, you can make use of std::index_sequence, and combined with simultaneous parameter pack expansion and the simplifications mentioned above, this can result in some very compact code. Consider:

template<std::size_t... I>
std::string invoke_helper(std::vector<std::string> const& inputs,
                          std::index_sequence<I...>)
{
    return to_string(m_fun(from_string<Args>(inputs.at(I))...));
}

std::string operator()(std::vector<std::string> const& inputs)
{
    return invoke_helper(inputs, std::index_sequence_for<Args...>{});
}

You might want to check that inputs.size() == m_arg_count first.

\$\endgroup\$

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.