DRTVWR-543: Add ClassicCallback utility class with tests
parent
c40b8310b0
commit
2b96d1bbe0
|
|
@ -127,6 +127,7 @@ set(llcommon_SOURCE_FILES
|
|||
set(llcommon_HEADER_FILES
|
||||
CMakeLists.txt
|
||||
|
||||
classic_callback.h
|
||||
ctype_workaround.h
|
||||
fix_macros.h
|
||||
indra_constants.h
|
||||
|
|
@ -328,16 +329,17 @@ if (LL_TESTS)
|
|||
${BOOST_CONTEXT_LIBRARY}
|
||||
${BOOST_THREAD_LIBRARY}
|
||||
${BOOST_SYSTEM_LIBRARY})
|
||||
LL_ADD_INTEGRATION_TEST(commonmisc "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(bitpack "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(classic_callback "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(commonmisc "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(llbase64 "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(llcond "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(lldate "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(lldeadmantimer "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(lldependencies "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(llerror "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(lleventdispatcher "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(lleventcoro "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(lleventdispatcher "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(lleventfilter "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(llframetimer "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(llheteromap "" "${test_libs}")
|
||||
|
|
@ -355,8 +357,8 @@ if (LL_TESTS)
|
|||
LL_ADD_INTEGRATION_TEST(llstring "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(lltrace "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(lltreeiterators "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(lluri "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(llunits "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(lluri "" "${test_libs}")
|
||||
LL_ADD_INTEGRATION_TEST(stringize "" "${test_libs}")
|
||||
|
||||
## llexception_test.cpp isn't a regression test, and doesn't need to be run
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* @file classic_callback.cpp
|
||||
* @author Nat Goodspeed
|
||||
* @date 2021-09-23
|
||||
* @brief Implementation for classic_callback.
|
||||
*
|
||||
* $LicenseInfo:firstyear=2021&license=viewerlgpl$
|
||||
* Copyright (c) 2021, Linden Research, Inc.
|
||||
* $/LicenseInfo$
|
||||
*/
|
||||
|
||||
namespace {
|
||||
|
||||
const char dummy[] = "cpp file required to build test program";
|
||||
|
||||
} // anonymous namespace
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
/**
|
||||
* @file classic_callback.h
|
||||
* @author Nat Goodspeed
|
||||
* @date 2016-06-21
|
||||
* @brief ClassicCallback and HeapClassicCallback
|
||||
*
|
||||
* This header file addresses the problem of passing a method on a C++ object
|
||||
* to an API that requires a classic-C function pointer. Typically such a
|
||||
* callback API accepts a void* pointer along with the function pointer, and
|
||||
* the function pointer signature accepts a void* parameter. The API passes
|
||||
* the caller's pointer value into the callback function so it can find its
|
||||
* data. In C++, there are a few ways to deal with this case:
|
||||
*
|
||||
* - Use a static method with correct signature. If you don't need access to a
|
||||
* specific instance, that works fine.
|
||||
* - Store the object statically (or store a static pointer to a non-static
|
||||
* instance). As long as you only care about one instance, that works, but
|
||||
* starts to get a little icky. As soon as there's more than one pertinent
|
||||
* instance, fight valiantly against the temptation to stuff the instance
|
||||
* pointer into a static pointer variable "just for a moment."
|
||||
* - Code a static trampoline callback function that accepts the void* user
|
||||
* data pointer, casts it to the appropriate class type and calls the actual
|
||||
* method on that class.
|
||||
*
|
||||
* ClassicCallback encapsulates the last. You need only construct a
|
||||
* ClassicCallback instance somewhere that will survive until the callback is
|
||||
* called, binding the target C++ callable. You then call its get_callback()
|
||||
* and get_userdata() methods to pass an appropriate classic-C function
|
||||
* pointer and void* user data pointer, respectively, to the old-style
|
||||
* callback API. get_callback() synthesizes a static trampoline function
|
||||
* that casts the user data pointer and calls the bound C++ callable.
|
||||
*
|
||||
* $LicenseInfo:firstyear=2016&license=viewerlgpl$
|
||||
* Copyright (c) 2016, Linden Research, Inc.
|
||||
* $/LicenseInfo$
|
||||
*/
|
||||
|
||||
#if ! defined(LL_CLASSIC_CALLBACK_H)
|
||||
#define LL_CLASSIC_CALLBACK_H
|
||||
|
||||
#include <tuple>
|
||||
#include <type_traits> // std::is_same
|
||||
|
||||
/*****************************************************************************
|
||||
* Helpers
|
||||
*****************************************************************************/
|
||||
|
||||
// find a type in a parameter pack: http://stackoverflow.com/q/17844867/5533635
|
||||
// usage: index_of<0, sought_t, PackName...>::value
|
||||
template <int idx, typename sought, typename candidate, typename ...rest>
|
||||
struct index_of
|
||||
{
|
||||
static constexpr int const value =
|
||||
std::is_same<sought, candidate>::value ?
|
||||
idx : index_of<idx + 1, sought, rest...>::value;
|
||||
};
|
||||
|
||||
// recursion tail
|
||||
template <int idx, typename sought, typename candidate>
|
||||
struct index_of<idx, sought, candidate>
|
||||
{
|
||||
static constexpr int const value =
|
||||
std::is_same<sought, candidate>::value ? idx : -1;
|
||||
};
|
||||
|
||||
/*****************************************************************************
|
||||
* ClassicCallback
|
||||
*****************************************************************************/
|
||||
/**
|
||||
* Instantiate ClassicCallback in whatever storage will persist long enough
|
||||
* for the callback to be called. It holds a modern C++ callable, providing a
|
||||
* static function pointer and a USERDATA (default void*) capable of being
|
||||
* passed through a classic-C callback API. When the static function is called
|
||||
* with that USERDATA pointer, ClassicCallback forwards the call to the bound
|
||||
* C++ callable.
|
||||
*
|
||||
* Usage:
|
||||
* @code
|
||||
* // callback signature required by the API of interest
|
||||
* typedef void (*callback_t)(int, const char*, void*, double);
|
||||
* // old-style API that accepts a classic-C callback function pointer
|
||||
* void oldAPI(callback_t callback, void* userdata);
|
||||
* // but I want to pass a lambda that references data local to my function!
|
||||
* // (We don't need to name the void* parameter in the C++ callable;
|
||||
* // ClassicCallback already used it to locate the lambda instance.)
|
||||
* auto ccb{
|
||||
* makeClassicCallback<callback_t>(
|
||||
* [=](int n, const char* s, void*, double f){ ... }) };
|
||||
* oldAPI(ccb.get_callback(), ccb.get_userdata());
|
||||
* // If the passed callback is called before oldAPI() returns, we can now
|
||||
* // safely destroy ccb. If the callback might be called later, consider
|
||||
* // HeapClassicCallback instead.
|
||||
* @endcode
|
||||
*
|
||||
* If you have a callable object in hand, and you want to pass that to
|
||||
* ClassicCallback, you may either consume it by passing std::move(object), or
|
||||
* explicitly specify a reference to that object type as the CALLABLE template
|
||||
* parameter:
|
||||
* @code
|
||||
* CallableObject obj;
|
||||
* ClassicCallback<callback_t, void*, CallableObject&> ccb{obj};
|
||||
* @endcode
|
||||
*/
|
||||
// CALLABLE should either be deduced, e.g. by makeClassicCallback(), or
|
||||
// specified explicitly. Its default type is meaningless, coded only so we can
|
||||
// provide a useful default for USERDATA.
|
||||
template <typename SIGNATURE, typename USERDATA=void*, typename CALLABLE=void(*)()>
|
||||
class ClassicCallback
|
||||
{
|
||||
typedef ClassicCallback<SIGNATURE, USERDATA, CALLABLE> self_t;
|
||||
|
||||
public:
|
||||
/// ClassicCallback binds any modern C++ callable.
|
||||
ClassicCallback(CALLABLE&& callable):
|
||||
mCallable(std::forward<CALLABLE>(callable))
|
||||
{}
|
||||
|
||||
/**
|
||||
* ClassicCallback must not itself be copied or moved! Once you've passed
|
||||
* get_userdata() to some API, this object MUST remain at that address.
|
||||
*/
|
||||
// However, we can't yet count on C++17 Class Template Argument Deduction,
|
||||
// which means makeClassicCallback() is still useful, which means we MUST
|
||||
// be able to return one to construct into caller's instance (move ctor).
|
||||
// Possible defense: bool 'referenced' data member set by get_userdata(),
|
||||
// with an llassert_always(! referenced) check in the move constructor.
|
||||
ClassicCallback(ClassicCallback const&) = delete;
|
||||
ClassicCallback(ClassicCallback&&) = default; // delete;
|
||||
ClassicCallback& operator=(ClassicCallback const&) = delete;
|
||||
ClassicCallback& operator=(ClassicCallback&&) = delete;
|
||||
|
||||
/// Call get_callback() to get the necessary function pointer.
|
||||
SIGNATURE get_callback() const
|
||||
{
|
||||
// This declaration is where the compiler instantiates the correct
|
||||
// signature for the call() function template.
|
||||
SIGNATURE callback = call;
|
||||
return callback;
|
||||
}
|
||||
|
||||
/// Call get_userdata() to get the opaque USERDATA pointer to pass
|
||||
/// through the classic-C callback API.
|
||||
USERDATA get_userdata() const
|
||||
{
|
||||
// The USERDATA userdata is of course a pointer to this object.
|
||||
return static_cast<USERDATA>(const_cast<self_t*>(this));
|
||||
}
|
||||
|
||||
protected:
|
||||
/**
|
||||
* This call() method accepts one or more callback arguments. It assumes
|
||||
* the first USERDATA parameter is the userdata.
|
||||
*/
|
||||
// Note that we're not literally using C++ perfect forwarding here -- it
|
||||
// doesn't work to specify (Args&&... args). But that's okay because we're
|
||||
// dealing with a classic-C callback! It's not going to pass any move-only
|
||||
// types.
|
||||
template <typename... Args>
|
||||
static auto call(Args... args)
|
||||
{
|
||||
auto userdata = extract_userdata(std::forward<Args>(args)...);
|
||||
// cast the userdata param to 'this' and call mCallable
|
||||
return static_cast<self_t*>(userdata)->
|
||||
mCallable(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
static USERDATA extract_userdata(Args... args)
|
||||
{
|
||||
// Search for the first void* parameter type, then extract that pointer.
|
||||
// extract value from parameter pack: http://stackoverflow.com/a/24710433/5533635
|
||||
return std::get<index_of<0, void*, Args...>::value>(std::forward_as_tuple(args...));
|
||||
}
|
||||
|
||||
CALLABLE mCallable;
|
||||
};
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* @code
|
||||
* auto ccb{ makeClassicCallback<classic_callback_signature>(actual_callback) };
|
||||
* @endcode
|
||||
*/
|
||||
template <typename SIGNATURE, typename USERDATA=void*, typename CALLABLE=void(*)()>
|
||||
auto makeClassicCallback(CALLABLE&& callable)
|
||||
{
|
||||
return std::move(ClassicCallback<SIGNATURE, USERDATA, CALLABLE>
|
||||
(std::forward<CALLABLE>(callable)));
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* HeapClassicCallback
|
||||
*****************************************************************************/
|
||||
/**
|
||||
* HeapClassicCallback is like ClassicCallback, with this exception: it MUST
|
||||
* be allocated on the heap because, once the callback has been called, it
|
||||
* deletes itself. This addresses the problem of a callback whose lifespan
|
||||
* must persist beyond the scope in which the callback API is engaged -- but
|
||||
* naturally this callback must be called exactly ONCE.
|
||||
*
|
||||
* Usage:
|
||||
* @code
|
||||
* // callback signature required by the API of interest
|
||||
* typedef void (*callback_t)(int, const char*, void*, double);
|
||||
* // here's the old-style API
|
||||
* void oldAPI(callback_t callback, void* userdata);
|
||||
* // want to call someObjPtr->method() when oldAPI() fires the callback,
|
||||
* // sometime in the future after the enclosing function has returned
|
||||
* auto ccb{
|
||||
* makeHeapClassicCallback<callback_t>(
|
||||
* [someObjPtr](int n, const char* s, void*, double f)
|
||||
* { someObjPtr->method(); }) };
|
||||
* oldAPI(ccb.get_callback(), ccb.get_userdata());
|
||||
* // We don't need a smart pointer for ccb, because it will be deleted once
|
||||
* // oldAPI() calls the bound lambda. HeapClassicCallback is for when the
|
||||
* // callback will be called exactly once. If the classic API might call the
|
||||
* // passed callback more than once -- or might never call it at all --
|
||||
* // manually construct a ClassicCallback on the heap and manage its lifespan
|
||||
* // explicitly.
|
||||
* @endcode
|
||||
*/
|
||||
template <typename SIGNATURE, typename USERDATA=void*, typename CALLABLE=void(*)()>
|
||||
class HeapClassicCallback: public ClassicCallback<SIGNATURE, USERDATA, CALLABLE>
|
||||
{
|
||||
typedef ClassicCallback<SIGNATURE, USERDATA, CALLABLE> super;
|
||||
typedef HeapClassicCallback<SIGNATURE, USERDATA, CALLABLE> self_t;
|
||||
|
||||
// This destructor is intentionally private to prevent allocation anywhere
|
||||
// but the heap. (The Design and Evolution of C++, section 11.4.2: Control
|
||||
// of Allocation)
|
||||
~HeapClassicCallback() {}
|
||||
|
||||
public:
|
||||
HeapClassicCallback(CALLABLE&& callable):
|
||||
super(std::forward<CALLABLE>(callable))
|
||||
{}
|
||||
|
||||
// makeHeapClassicCallback() only needs to return a pointer -- not an
|
||||
// instance -- so we can lock down our move constructor too.
|
||||
HeapClassicCallback(HeapClassicCallback&&) = delete;
|
||||
|
||||
/// Replicate get_callback() from the base class because we must
|
||||
/// instantiate OUR call() function template.
|
||||
SIGNATURE get_callback() const
|
||||
{
|
||||
// This declaration is where the compiler instantiates the correct
|
||||
// signature for the call() function template.
|
||||
SIGNATURE callback = call;
|
||||
return callback;
|
||||
}
|
||||
|
||||
/// Replicate get_userdata() from the base class because our call()
|
||||
/// method must be able to reconstitute a pointer to this subclass.
|
||||
USERDATA get_userdata() const
|
||||
{
|
||||
// The USERDATA userdata is of course a pointer to this object.
|
||||
return static_cast<const USERDATA>(const_cast<self_t*>(this));
|
||||
}
|
||||
|
||||
private:
|
||||
// call() uses a helper class to delete the HeapClassicCallback when done,
|
||||
// for two reasons. Most importantly, this deletes even if the callback
|
||||
// throws an exception. But also, call() must directly return the callback
|
||||
// result for return-type deduction.
|
||||
struct Destroyer
|
||||
{
|
||||
Destroyer(self_t* p): mPtr(p) {}
|
||||
~Destroyer() { delete mPtr; }
|
||||
|
||||
self_t* mPtr;
|
||||
};
|
||||
|
||||
template <typename... Args>
|
||||
static auto call(Args... args)
|
||||
{
|
||||
// extract userdata at this level too
|
||||
USERDATA userdata = super::extract_userdata(std::forward<Args>(args)...);
|
||||
// arrange to delete it when we leave by whatever means
|
||||
Destroyer destroy(static_cast<self_t*>(userdata));
|
||||
|
||||
return super::call(std::forward<Args>(args)...);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename SIGNATURE, typename USERDATA=void*, typename CALLABLE=void(*)()>
|
||||
auto makeHeapClassicCallback(CALLABLE&& callable)
|
||||
{
|
||||
return new HeapClassicCallback<SIGNATURE, USERDATA, CALLABLE>
|
||||
(std::forward<CALLABLE>(callable));
|
||||
}
|
||||
|
||||
#endif /* ! defined(LL_CLASSIC_CALLBACK_H) */
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* @file classic_callback_test.cpp
|
||||
* @author Nat Goodspeed
|
||||
* @date 2021-09-22
|
||||
* @brief Test ClassicCallback and HeapClassicCallback.
|
||||
*
|
||||
* $LicenseInfo:firstyear=2021&license=viewerlgpl$
|
||||
* Copyright (c) 2021, Linden Research, Inc.
|
||||
* $/LicenseInfo$
|
||||
*/
|
||||
|
||||
// Precompiled header
|
||||
#include "linden_common.h"
|
||||
// associated header
|
||||
#include "classic_callback.h"
|
||||
// STL headers
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
// std headers
|
||||
// external library headers
|
||||
// other Linden headers
|
||||
#include "../test/lltut.h"
|
||||
|
||||
/*****************************************************************************
|
||||
* example callback accepting only (void* userdata)
|
||||
*****************************************************************************/
|
||||
// callback_t is part of the specification of someAPI()
|
||||
typedef void (*callback_t)(const char*, void*);
|
||||
void someAPI(callback_t callback, void* userdata)
|
||||
{
|
||||
callback("called", userdata);
|
||||
}
|
||||
|
||||
// C++ callable I want as the actual callback
|
||||
struct MyCallback
|
||||
{
|
||||
void operator()(const char* msg, void*)
|
||||
{
|
||||
mMsg = msg;
|
||||
}
|
||||
|
||||
void callback_with_extra(const std::string& extra, const char* msg)
|
||||
{
|
||||
mMsg = extra + ' ' + msg;
|
||||
}
|
||||
|
||||
std::string mMsg;
|
||||
};
|
||||
|
||||
// a function for which I want to bind other data
|
||||
void callback_with_extra(const std::string& extra, void*)
|
||||
{
|
||||
std::cout << "callback_with_extra('" << extra << "', *)\n";
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* example callback accepting several params, and void* userdata isn't first
|
||||
*****************************************************************************/
|
||||
typedef std::string (*complex_callback)(int, const char*, void*, double);
|
||||
std::string otherAPI(complex_callback callback, void* userdata)
|
||||
{
|
||||
return callback(17, "hello world", userdata, 3.0);
|
||||
}
|
||||
|
||||
// struct into which we can capture complex_callback params
|
||||
static struct Data
|
||||
{
|
||||
void set(int i, const char* s, double f)
|
||||
{
|
||||
mi = i;
|
||||
ms = s;
|
||||
mf = f;
|
||||
}
|
||||
|
||||
void clear() { set(0, "", 0.0); }
|
||||
|
||||
int mi;
|
||||
std::string ms;
|
||||
double mf;
|
||||
} sData;
|
||||
|
||||
// C++ callable I want to pass
|
||||
struct OtherCallback
|
||||
{
|
||||
std::string operator()(int num, const char* str, void*, double approx)
|
||||
{
|
||||
sData.set(num, str, approx);
|
||||
return "hello back!";
|
||||
}
|
||||
};
|
||||
|
||||
/*****************************************************************************
|
||||
* TUT
|
||||
*****************************************************************************/
|
||||
namespace tut
|
||||
{
|
||||
struct classic_callback_data
|
||||
{
|
||||
};
|
||||
typedef test_group<classic_callback_data> classic_callback_group;
|
||||
typedef classic_callback_group::object object;
|
||||
classic_callback_group classic_callbackgrp("classic_callback");
|
||||
|
||||
template<> template<>
|
||||
void object::test<1>()
|
||||
{
|
||||
set_test_name("ClassicCallback");
|
||||
// engage someAPI(MyCallback())
|
||||
auto ccb{ makeClassicCallback<callback_t>(MyCallback()) };
|
||||
someAPI(ccb.get_callback(), ccb.get_userdata());
|
||||
// Unfortunately, with the side effect confined to the bound
|
||||
// MyCallback instance, that call was invisible. Bind a reference to a
|
||||
// named instance by specifying a ref type.
|
||||
MyCallback mcb;
|
||||
ClassicCallback<callback_t, void*, MyCallback&> ccb2(mcb);
|
||||
someAPI(ccb2.get_callback(), ccb2.get_userdata());
|
||||
ensure_equals("failed to call through ClassicCallback", mcb.mMsg, "called");
|
||||
|
||||
// try with HeapClassicCallback
|
||||
mcb.mMsg.clear();
|
||||
auto hcbp{ makeHeapClassicCallback<callback_t>(mcb) };
|
||||
someAPI(hcbp->get_callback(), hcbp->get_userdata());
|
||||
ensure_equals("failed to call through HeapClassicCallback", mcb.mMsg, "called");
|
||||
|
||||
// lambda
|
||||
// The tricky thing here is that a lambda is an unspecified type, so
|
||||
// you can't declare a ClassicCallback<signature, void*, that type>.
|
||||
mcb.mMsg.clear();
|
||||
auto xcb(
|
||||
makeClassicCallback<callback_t>(
|
||||
[&mcb](const char* msg, void*)
|
||||
{ mcb.callback_with_extra("extra", msg); }));
|
||||
someAPI(xcb.get_callback(), xcb.get_userdata());
|
||||
ensure_equals("failed to call lambda", mcb.mMsg, "extra called");
|
||||
|
||||
// engage otherAPI(OtherCallback())
|
||||
OtherCallback ocb;
|
||||
// Instead of specifying a reference type for the bound CALLBACK, as
|
||||
// with ccb2 above, you can alternatively move the callable object
|
||||
// into the ClassicCallback (of course AFTER any other reference).
|
||||
// That's why OtherCallback uses external data for its observable side
|
||||
// effect.
|
||||
auto occb{ makeClassicCallback<complex_callback>(std::move(ocb)) };
|
||||
std::string result{ otherAPI(occb.get_callback(), occb.get_userdata()) };
|
||||
ensure_equals("failed to return callback result", result, "hello back!");
|
||||
ensure_equals("failed to set int", sData.mi, 17);
|
||||
ensure_equals("failed to set string", sData.ms, "hello world");
|
||||
ensure_equals("failed to set double", sData.mf, 3.0);
|
||||
}
|
||||
} // namespace tut
|
||||
Loading…
Reference in New Issue