2

The problem

I need a checked_cast_call<function> generic wrapper to a function, that would runtime-check any cast involved to call the function, or to get the value.

As an example, calling the following function with an input buffer larger than 2GB will cause some issues (because of the int inl input size argument that would overflow):

int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, const unsigned char *in, int inl);

My imperfect solution

To achieve that, using insightful help from other stackoverflow topics, I ended up with the following solution, which is unfortunately far from being perfect:

  • First, I wrote a small template to runtime-check a cast (it will throw a std::runtime_error if a cast overflows).
#include <stdexcept>
#include <type_traits>

/**
 * Runtime-checked cast to a target type.
 * @throw std::runtime_error If the cast overflowed (or underflowed).
 */
template <class Target, class Source>
Target inline checked_cast(Source v)
{
    if constexpr (std::is_pointer<Source>::value) {
        return v;
    } else if constexpr (std::is_same<Target, Source>::value) {
        return v;
    else {
        const auto r = static_cast<Target>(v);
        if (static_cast<Source>(r) != v) {
            throw std::runtime_error(std::string("cast failed: ") + std::string(__PRETTY_FUNCTION__));
        }
        return r;
    }
}
  • Then, a small template container to hold a type, and allow a runtime-checked cast to possibly another type. This container can be used to hold the return value of a function, but it can also be used to hold every single input argument to a function, relying on operator T () to provide runtime-checked casted values:
/**
 * Container holding a type, and allowing to return a cast runtime-checked casted value.
 * @example
 *     const size_t my_integer = foo();
 *     const checked_cast_call_container c(my_integer);
 *     int a = static_cast<int>(c);
 */
template <typename T>
class checked_cast_call_container {
public:
    inline checked_cast_call_container(T&& result)
        : _result(std::move(result))
    {
    }

    template <typename U>
    inline operator U() const
    {
        return checked_cast<U>(_result);
    }

private:
    const T _result;
};
  • And the final wrapper, taking a decltype of a function pointer, and the function pointer itself, expanding the packed arguments with our container, and putting the result in a container too:
/**
 * Wrapped call to a function, with runtime-checked casted input and output values.
 * @example checked_cast_call<decltype(&my_function), &my_function>(str, 1, size, output)
 */
template <typename Fn, Fn fn, typename... Args>
checked_cast_call_container<typename std::result_of<Fn(Args...)>::type>
checked_cast_call(Args... args)
{
    return checked_cast_call_container(fn(checked_cast_call_container(std::forward<Args>(args))...));
}
  • Sample test:
static char my_write(void* ptr, char size, char nmemb, FILE* stream)
{
    return fwrite(ptr, size, nmemb, stream);
}

int main(int argc, char** argv)
{
    // Input overflow: input argument nmemb is larger than 127
    try {
        char str[256] = "Hello!\n";
        volatile size_t size = sizeof(str);
        const char b = checked_cast_call<decltype(&my_write), &my_write>(str, 1, size, stdout);
        (void)b;
    } catch (const std::runtime_error& e) {
        std::cout << e.what() << "\n";
    }

    return 0;
}

Note on performance impact

On a basic test (equivalent to the sample test in this post), overhead on common (non-erroneous) path is minimal, and is basically one additional cmp+jne for the runtime-checked input argument. (Note: additional code for erroneous path, including throw cold path not shown on disassembled code below)

--- old.S   2019-03-11 11:14:25.847240916 +0100
+++ new.S   2019-03-11 11:14:27.087238775 +0100
@@ -3 +3 @@
-lea    0x10(%rsp),%rbx
+lea    0x10(%rsp),%rdi
@@ -6 +5,0 @@
-mov    %rbx,%rdi
@@ -9 +8,4 @@
-mov    0x8(%rsp),%rax
+mov    0x8(%rsp),%rdx
+movsbq %dl,%rax
+cmp    %rdx,%rax
+jne    0xXXXXXX <_Z5test3v+82>
@@ -11 +13 @@
-movsbq %al,%rdx
+lea    0x10(%rsp),%rdi
@@ -13 +14,0 @@
-mov    %rbx,%rdi

The question

It is possible to improve this wrapper, so that:

  • You only need one template argument (the function) and not the decltype of the function (ie. directly checked_cast_call<&my_write>(...))
  • You would use the inferred function arguments types to runtime-check the casts, and not relying on the container wrapper

There might be some solutions by passing the function as argument to the wrapper, and not as template, but I wanted a pure template solution. Maybe this is simply not feasible, or way too convoluted ?

Dear reader, thank you in advance for any useful hints!

⟾ Partial solution

Solution to question #1 thanks to @kmdreko, by declaring non-type template arguments with auto:

template <auto fn, typename... Args>
auto checked_cast_call(Args... args)
{
    return checked_cast_call_container(fn(checked_cast_call_container(std::forward<Args>(args))...));
}
const char b = checked_cast_call<&my_write>(str, 1, size, stdout);

Allowing a direct sed -e 's/my_write/checked_cast_call<&my_write>/g'

xroche
  • 259
  • 2
  • 7
  • 2
    improvement #1 can be done with [template auto](https://stackoverflow.com/questions/38026884/advantages-of-auto-in-template-parameters-in-c17) – kmdreko Mar 07 '19 at 08:55
  • 1
    @kmdreko Thank you! Modified template is now much nicer: template checked_cast_call_container::type> checked_cast_call(Args... args) ... – xroche Mar 07 '19 at 09:10

1 Answers1

1

A possible improvement should be to stop taking the function as a template argument but taking it as a function parameter, that way C++17 deduction rules will be able to guess the type and you won't need to provide your template parameter.

Here is a quick and dirty version:

template <class F, class... Args>
decltype(auto) checked_cast_call(F&& f, Args... args)
{
      return checked_cast_call_container(std::forward<F>(f)(checked_cast_call_container(std::forward<Args>(args))...));
}

(inspired the "Possible implementation" section from https://en.cppreference.com/w/cpp/utility/functional/invoke )

It seems to have the same behaviour as your code, now I need to check if we have proper inlining (at least as good as yours). There maybe still some details with some cv-qualified types and maybe we should need a std::decay or any other template type stuff ...

Marwan Burelle
  • 1,871
  • 2
  • 11
  • 8
  • Thanks! Produced code seems to be strictly equivalent, that is, only an additional cmp+jne per types needing runtime checks (diffing the disassembled code provides no differences) – xroche Mar 07 '19 at 10:45