I had another look at this yesterday evening after having an idea and managed to get something working with Dynamic / nullable function parameters (I hope you like templates).
First thing I did was create a closure object which extends the ObjectPtr
object, this is inkeeping with the hxcpp style and allows us to throw exceptions on null pointers. I also overloaded ()
to call the wrapped function.
template<class TReturn, class... TArgs>
class Closure : public ObjectPtr<TypedLocalFunc<TReturn, TArgs...>>
{
public:
Closure(TypedLocalFunc<TReturn, TArgs...>* ptr) : ObjectPtr<TypedLocalFunc<TReturn, TArgs...>>(ptr) {}
TReturn operator()(TArgs... args)
{
if (!mPtr)
{
hx::Throw( HX_NULL_FUNCTION_POINTER );
}
return mPtr->_hx_run(args...);
}
};
Another key part of this Closure object is an implicit user conversion function to other closure types, this is implemented by creating a wrapper function which simply calls the wrapped function. This means that only dynamic / nullable arguments will box primitive types.
template<class TNewReturn, class... TNewArgs>
operator Closure<TNewReturn, TNewArgs...>() const
{
struct HXCPP_EXTERN_CLASS_ATTRIBUTES AdapterFunction : TypedLocalFunc<TNewReturn, TNewArgs...>
{
TypedLocalFunc<TReturn, TArgs...>* wrapping;
AdapterFunction(TypedLocalFunc<TReturn, TArgs...>* _wrapping) : wrapping(_wrapping) { }
TNewReturn _hx_run(TNewArgs... args)
{
return wrapping->_hx_run(args...);
}
// gc guff
inline void DoMarkThis(hx::MarkContext *__inCtx) { HX_MARK_MEMBER(wrapping); }
#ifdef HXCPP_VISIT_ALLOCS
inline void DoVisitThis(hx::VisitContext *__inCtx) { HX_VISIT_MEMBER(wrapping); }
#endif
};
return Closure<TNewReturn, TNewArgs...>(new AdapterFunction(GetPtr()));
}
Looking back at the example in my above post you can now convert from one function type to another function type if some of the parameters are replaced with dynamic.
hx::Closure<float, float, int> v1 = hx::Closure<float, float, int>(new testTyped());
hx::Closure<float, Dynamic, int> v2 = v1;
::haxe::Log_obj::trace(v1(5.7, 7), null());
::haxe::Log_obj::trace(v2(5.7, 7), null());
Both of these variables now hold valid functions and can be successfully invoked. There is an extra virtual function call with v2
(and through repeated casting / replacing parameters to and from dynamic the number of wrappers would increase), but if you’re using dynamic thats probably a price you’re willing to pay.
There is another Dynamic
related edge case and thats a round trip through dynamic where one of the parameters has been swapped with dynamic / is nullable on the other side (e.g. Int->Int
=> Dynamic
=> Dynamic->Int
). The same adapter function style can be used but this time I quickly added an extra constructor to handle it (maybe it would be better handle in dynamic or the logic shared).
Closure(hx::Object* ptr)
{
struct HXCPP_EXTERN_CLASS_ATTRIBUTES AdapterFunction : TypedLocalFunc<TReturn, TArgs...>
{
Dynamic wrapping;
AdapterFunction(Dynamic _wrapping) : wrapping(_wrapping) {}
TReturn _hx_run(TArgs... args)
{
return wrapping(args...);
}
inline void DoMarkThis(hx::MarkContext *__inCtx) { HX_MARK_MEMBER(wrapping); }
#ifdef HXCPP_VISIT_ALLOCS
inline void DoVisitThis(hx::VisitContext *__inCtx) { HX_VISIT_MEMBER(wrapping); }
#endif
};
mPtr = new AdapterFunction(ptr);
}
Once this is done Dynamic round tripping also works with Dynamic swapping on function arguments.
hx::Closure<float, Dynamic, int> v1 = hx::Closure<float, Dynamic, int>(new otherTestTyped());
Dynamic v2 = Dynamic(v2);
hx::Closure<float, float, int> v3 = hx::Closure<float, float, int>(v2.mPtr);
::haxe::Log_obj::trace(v1(5.7, 7), null());
::haxe::Log_obj::trace(v2(5.7, 7), null());
::haxe::Log_obj::trace(v3(5.7, 7), null());
This is all something I threw together in about an hour yesterday evening so not battle tested and there are probably more edge cases I’ve not thought about and ways that the number of warppers could be reduced in some situations. But it could be one way forward for the cpp generator to be updated to output typed function objects while still keeping dynamic support.