Function types becoming dynamic after compilation

Hi there, thanks.

Issue with this approach is that it doesn’t work if I am working with anything other than static functions. Also, removes the ability to make platform agnostic code. I’d have to hide this away in an abstraction which I wouldn’t want to do.

If you don’t want abstracts (the most powerful runtime Haxe feature), you can always make a macro (the most powerful compile time feature).

Abstracts are a compile-time feature as well.

1 Like

If inlined, otherwise static functions (especially @:from) are used at runtime. Not to mention what happens if underlying type is dynamic.

The reason function pointers or fancier templates aren’t used is due to dynamic. Currently hxcpp closures generated from haxe anoymous functions all extend the hx::LocalFunc (or hx::LocalThisFunc) class and the HX_BEGIN_LOCAL_FUNC you see in the generated cpp defines a class which extends one of those classes and defines a _hx_run function which contains the users actual code.

We could try and make a fancier closure class using template pack parameters which might look something like this.

 template<class TReturn, class... TArgs>
 struct HXCPP_EXTERN_CLASS_ATTRIBUTES TypedLocalFunc : LocalFunc
 {
    virtual TReturn _hx_run(TArgs... args) = 0;
 };

and as a quick test to ensure things work we can hand write a closure to see what the the cpp generator / hxcpp macros could be changed to potentially output. In this case our closure class inherits from a specialisation of our closure template.

struct HXCPP_EXTERN_CLASS_ATTRIBUTES testTyped : hx::TypedLocalFunc<float, float, int>
{
	float _hx_run(float v1, int v2)
	{
		return v1 * v2;
	}

    // These are needed to ensure compatibility with wrapping the typed closure in Dynamic.
	::Dynamic __Run(const Array< ::Dynamic> &inArgs) { return _hx_run( inArgs[0], inArgs[1] ); return null(); }
	::Dynamic __run(const Dynamic &inArg0, const Dynamic &inArg1) { return _hx_run( inArg0, inArg1 ); return null(); }
};

At first glace this all seems to work rather well. we can then write code like this and it works!

hx::TypedLocalFunc<float, float, int>* func = new testTyped();
Dynamic dyn = Dynamic(func);

::haxe::Log_obj::trace(v1->_hx_run(5.7, 7), null());
::haxe::Log_obj::trace(dyn(5.7, 7), null());

We still retain the old Dynamic calling and if we have the actual closure pointer the only cost we pay is the virtual function call, not a potential GC collection from primitives being boxed.

This approach all falls apart when dynamic is involved. If we take a look at a second very similar closure object where the first argument is replaced with Dynamic from a haxe point of view these two functions should be compatible, but in cpp they’re not.

struct HXCPP_EXTERN_CLASS_ATTRIBUTES otherTestTyped : hx::TypedLocalFunc<float, Dynamic, int>
{
	float _hx_run(Dynamic v1, int v2)
	{
		return v1 + v2;
	}

	::Dynamic __Run(const Array< ::Dynamic> &inArgs) { return _hx_run( inArgs[0], inArgs[1] ); return null(); }
	::Dynamic __run(const Dynamic &inArg0, const Dynamic &inArg1) { return _hx_run( inArg0, inArg1 ); return null(); }
};

The problems start to appear once we try and cast between these two objects, attempting to do so will result in a null pointer because as far as c++ is concerned two different template specialisations are entirely different classes.

hx::TypedLocalFunc<float, float, int>* v1 = new testTyped();
hx::TypedLocalFunc<float, Dynamic, int>* v2 = new otherTestTyped();

v1 = (hx::TypedLocalFunc<float, float, int>*)v2;

// v1 is now null!

while this could probably be solved with some sort of adapter function in true “All problems in computer science can be solved by another level of indirection” style, but its a fair bit more work than using function pointers or templates. While it would be nice to not have primitives be boxes in these situations its probably eaiser to re-work any performance sensitive code to avoid dynamic functions and the current Dynamic functions are probably to just to make the cpp implementation easier.

Hi Aidan, thanks for the reply. Very insightful :smile:

I know other compilers can handle delegates/function types elegantly, yet it seems like the limitation here is because we initially transpile to C++ and then must play by its rules (i.e. how it handles dynamics).

I don’t know if C++ supports function overloading but if it does my naive initial thought would be to generate something like this (in pseudocode):

TypedLocalFunc_FFI<float, float, int> * func1 = new testTyped();
TypedLocalFunc_FDI<float, dynamic, int> * func2 = new otherTestTyped();

TypedLocalFunc(float v1, float v2, int v3) {
// Call func1 
}

TypedLocalFunc(float v1, dynamic v2, int v3) {
// Call func2 
}

I will have a look into this later to see if I can solve my issue, as I feel function types do lead to more elegant code. It makes for simpler eventing, for example. It’s just a shame that the secret boxing and unboxing of primitive types leads to very noticable frame stuttering at runtime.

Interesting hypothesis. My brain just thinks, “programmer’s are intelligent and the output should work. If it doesn’t work, the programmer did something wrong.”

I understand the concept that if Haxe were to generate an output similar to what you provided, you could say that Haxe throws a compile error stating, in this case (just on the C++ target), mixing Dynamic types with real types in function calls is invalid, which actually reinforces type-safety and forces programmers to be more careful with mixing unknown types with known types.

If working with unknown types, they should separated from function arguments whose types are known at compile time. This also reduces ambiguity between function calls. But, this is a decision by the Haxe team of course, I’m just throwing a suggestion in here while we’re on the topic.

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.

1 Like

I am wondering how feasible it would be to fork from the hxcpp source to see if it is possible to get this working, or does further work need to be done inside the Haxe compiler itself?

An additional issue I thought of that might arise from type unboxing would be when using the haxe-concurrency library, which relies on function types for the definition of its tasks.

After I figured out the dynamic swapping I did have a go at changing the cpp generator to support typed functions. I managed to get it working to a state where the following haxe code

function main() {
	final f = () -> {
		trace('hello world!');
	}

	other(f);
	other(another);
}

function other(_f : Void->Void) {
	_f();
}

function another() {
	trace('another!');
}

Outputs the following cpp, as you can see it has a templated closure types instead of dynamic.

void Main_Fields__obj::main()
{
    HX_BEGIN_LOCAL_FUNC_S0(::hx::LocalFunc, _hx_Closure_0)
    HXARGC(0) void _hx_run()
    {
        HX_STACKFRAME(&_hx_pos_195033e4c87195a0_3_main)

        ::haxe::Log_obj::trace(HX_("hello world!", dd, fc, f4, 73), ::hx::SourceInfo(HX_("Main.hx", 05, 5c, 7e, 08), 3, HX_("_Main.Main_Fields_", 76, cc, 48, 1a), HX_("main", 39, 38, 56, 48)));
    }
    HX_END_LOCAL_FUNC0((void))

    HX_STACKFRAME(&_hx_pos_195033e4c87195a0_1_main)

    HX_VARI(::hx::Closure<void HX_COMMA void>, f) = ::hx::Closure<void, void>(new _hx_Closure_0());

    ::_Main::Main_Fields__obj::other(f);
    ::_Main::Main_Fields__obj::other(::_Main::Main_Fields__obj::another_dyn());
}

STATIC_HX_DEFINE_DYNAMIC_FUNC0(Main_Fields__obj, main, (void))

void Main_Fields__obj::other(::hx::Closure<void, void> _f)
{
    HX_STACKFRAME(&_hx_pos_195033e4c87195a0_11_other)
    HX_STACK_ARG(_f, "_f")
    _f();
}

STATIC_HX_DEFINE_DYNAMIC_FUNC1(Main_Fields__obj, other, (void))

void Main_Fields__obj::another()
{
    HX_STACKFRAME(&_hx_pos_195033e4c87195a0_15_another)

    ::haxe::Log_obj::trace(HX_("another!", fe, d3, 79, fb), ::hx::SourceInfo(HX_("Main.hx", 05, 5c, 7e, 08), 15, HX_("_Main.Main_Fields_", 76, cc, 48, 1a), HX_("another", c3, 16, 5a, f3)));
}

STATIC_HX_DEFINE_DYNAMIC_FUNC0(Main_Fields__obj, another, (void))

(I’ve just pushed the change as I forgot about it until now GitHub - Aidan63/haxe at cpp_typed_funcs).

Currently the generated code fails at cpp compilation as I haven’t gone through and updated the hxcpp parts to get this small code sample fully running. In the above code I think the main things to change would be adding the hx:::Closure type which I posted a while back and updating the LOCAL_FUNC and STATIC_HX_DEFINE_DYNAMIC_FUNC macros and the code they call to work with that new type.

I want to get back around to it at some point but I’m not sure when, so if anyone else wants to have a stab at continuing this approach then be my guest.

1 Like

I’m currently working on a macro that might circumvent the value type unboxing, albeit likely with more overhead. My only concern is that this idea might not work with threads.

I’ll post the macro here if I get it working. Otherwise, I guess I’ll end up learning OCaml.

Edit: The proof of concept worked. Even supports threads… I need to clean up the macro and I will post here. There is some additional overhead in the sense that every time you assign to a function type you create a new object.

Hi All,

Went with Ilir-Liburn’s suggestion in the end and built a macro… The library can be found here: haxe-delegates (0.0.0)

I had actually been musing over this idea for a year or so before getting it in, and I’m happy to say it works. For the Hxcpp target, the delegates appear to perform 40% faster than function types, and 200% faster when inlined. It would be nice to see how it performs on other targets.

You can use it by going:

var delegate : Delegate<(Int, Int)->Int> = DelegateBuilder.from(myFunction);

public function myFunction(a : Int, b : Int) : Int {
    return a + b;
}

delegate.call(4, 3);

Essentially, all the macro does is wrap function pointers and calls into an object. It had some serendipitous side effects, such as type-strictness. The compiler will recognise if you’re trying to pass Null<Int> where it expects an Int and it will complain, thereby putting the onus back on the developer.

It also handles inlining quite nicely, as delegate objects are given private access to the class that implements them via a _parent variable, so it has a view onto any variables declared externally to itself.

Obviously as a first release, there’s still limited functionality, but this is a proof of concept that such a thing is possible and possibly even preferred in some cases. I may be able to circumvent excessive object creation using pooling and weak-references, and would like to get full support for local functions in at some point.

Kind regards.

1 Like

Hi All,

Apologies for the third post in a row. I wanted to highlight the speed increases I am getting on JavaScript and C++ and would like opinions as to why this might be the case.

I’ve uploaded the latest version of the code here (which now supports variable capture): https://lib.haxe.org/p/haxe-delegates/0.0.1/

Hxcpp:

This is without debugging:

src/Test.hx:17: *** Running without inlines ***
src/Test.hx:53: Haxe function type: 0.3874378
src/Test.hx:59: Delegate: 0.0279917
src/Test.hx:24: *** Running with inlines ***
src/Test.hx:53: Haxe function type: 0.3732177
src/Test.hx:59: Delegate: 0.0155093000000001
src/Test.hx:31: *** Running with anonymous functions ***
src/Test.hx:53: Haxe function type: 0.3604134
src/Test.hx:59: Delegate: 0.0193384999999999

In other words, the Delegate type executes 24 times faster and 14 times faster with and without inlines respectively.

JavaScript:

app.js:24 src/Test.hx:17: *** Running without inlines ***
app.js:60 src/Test.hx:53: Haxe function type: 0.008699999999254943
app.js:68 src/Test.hx:59: Delegate: 0.007800000000745058
app.js:30 src/Test.hx:24: *** Running with inlines ***
app.js:60 src/Test.hx:53: Haxe function type: 0.09140000000037253
app.js:68 src/Test.hx:59: Delegate: 0.012100000001490113
app.js:37 src/Test.hx:31: *** Running with anonymous functions ***
app.js:60 src/Test.hx:53: Haxe function type: 0.06580000000074504
app.js:68 src/Test.hx:59: Delegate: 0.014500000000000013

Here, the Delegate type executes in roughly the same time as a function type, but 8 times faster with inlines and 4 times faster with anonymous functions.

Some obvious limitations still exist:

  1. Oddly enough, anonymous functions break if we try to reference imported types that expect type parameters, and so need to be preceded by their package in order to work.
  2. Larger output size, and so balancing the cost of runtime execution and output size as expected from generics should be considered.
  3. Delegate types cannot be generated from functions or classes with type parameters.

Hello,

nice, thank you for doing this. I tried to use vector of delegates like this

delegates[0] = DelegateBuilder.from((a, b) -> (return a+b+outer+v));

but fails throwing Uncaught exception Empty module name is not allowed.

I ended up using

	testDelegate = DelegateBuilder.from((a, b) -> (return a+b+outer+v));
	delegates[0] = testDelegate;

Is there a way to overcome this limitation?

Aye, there will definitely be some weird things with the anonymous functions for the time being. I’ll look into this :slight_smile:

Out of curiosity, does it fail if you build with

delegates[0] = DelegateBuilder.from(myFunction);

where myFunction is a class function?

Yes,it does fails. I moved delegates to the constructor, but it fails in any other function (init, setup, etc)

public function new() {
    outer = 5;
    var v = 3;
    delegates = new Vector(3);
    testFunc = (a, b) -> (return a+b+outer+v);
    testDelegate = DelegateBuilder.from((a, b) -> (return a+b+outer+v));
    delegates[0] = testDelegate;
    testFunc = myFunction;
    testDelegate = DelegateBuilder.from(myFunction);
    delegates[1] = testDelegate;
    testFunc = myInlinedFunction;
    testDelegate = DelegateBuilder.from(myInlinedFunction);
    delegates[2] = testDelegate;
}

So I have a solution. I’ll push it to the repo once I tidy things up (which it very much needs)… All delegates will have to have their argument types and return types unified explicitly, like so:

delegates[0] = DelegateBuilder.from((a : Int, b : Int) -> (return a+b+outer : Int));

but I feel like this is a small concession to make as its better than having uncaught exceptions in parts of code that should work

Can you catch exception in your macro? And then generate temporary variable used for assignment. Error is thrown because there is no object present in array/vector field (it’s null by default) I think.

The exception is thrown because the builder expects the lhs of the assignment to be of a specific type, namely a delegate abstract class which it then tries to unify against. It’s not always possible to retrieve the lhs type it seems, which is why your example was failing. It also failed for local assignments too. Not sure if catching an exception would have worked?

Explictly defining the argument types and having a return type check for (Int, Int)->Int lets us just determine if a Delegate_Int_Int_Int was generated and if so we can set it as the superclass to the delegate we generate from the function expression. This then lets us assign delegates locally or to positions in a Vector.

Yes, unification fails because there is no type present (null doesn’t unify), but in that case you can use temporary variable of delegate type for assignment. So that you support both typed and not-typed (dynamic).