ialex32x / unity-jsb

It brings Javascript runtime capability to Unity3D by integrating QuickJS.
MIT License
337 stars 41 forks source link

Code gen for extensions methods #51

Closed FreakTheMighty closed 2 years ago

FreakTheMighty commented 3 years ago

I'm curious if there is anyway to specify that a particular extension method should be used when generating bindings for a particular type? I'm doing this:

bindingManager.AddExportedType(typeof(UniRx.Subject<UniRx.Unit>));

Which eposes a Subscribe method. The method, however, has a number of extension methods. I'd like to specify which method is selected. This would be especially useful, as it would allow me to write some more clean JS <-> C# interactions. For example, in this case I would like to create an extension method that handles JS functions, like so.

public static IDisposable Subscribe<T>(this IObservable<T> source, ScriptFunction onNext)
{
    return source.Subscribe((val) => onNext?.Invoke(val));
}

My current solution was to edit the generated code, but obviously that won't scale very well.

Thoughts?

ialex32x commented 3 years ago

I added a method to manually add an extension method. Is this what you want?

    public static class ExtensionTest
    {
        public static void ResetAll(this Transform transform)
        {
            transform.localPosition = Vector3.zero;
            transform.localRotation = Quaternion.identity;
            transform.localScale = Vector3.one;
        }
    }
bindingManager.AddExtensionMethod<Transform>(ExtensionTest.ResetAll); // expose single extension method

I haven't checked whether it works in static-bind mode, I'll fix further problems later.

FreakTheMighty commented 3 years ago

That looks about right. I’ve only used static bindings, so I should be able to test that soon.

FreakTheMighty commented 3 years ago

🤔 I'm struggling to get this to work just right. Given this extension

    public static class RxBindExtensions
    {
        public static IDisposable Subscribe(this IObservable<Unit> source, ScriptFunction onNext)
        {
            return source.Subscribe((val) => onNext?.Invoke(val));
        }
    }

This binding

        bindingManager.AddExtensionMethod<IObservable<Unit>, ScriptFunction, IDisposable>(RxBindExtensions.Subscribe);

I get this output

        [MonoPInvokeCallbackAttribute(typeof(JSCFunction))]
        public static JSValue Bind_Subscribe(JSContext ctx, JSValue this_obj, int argc, JSValue[] argv)
        {
            try
            {
                if (argc == 1)
                {
                    UniRx.Subject<UniRx.Unit> self;
                    if (!js_get_classvalue(ctx, this_obj, out self))
                    {
                        throw new ThisBoundException();
                    }
                    System.IObserver<UniRx.Unit> arg0;
                    if (!js_get_classvalue(ctx, argv[0], out arg0))
                    {
                        throw new ParameterException(typeof(UniRx.Subject<UniRx.Unit>), "Subscribe", typeof(System.IObserver<UniRx.Unit>), 0);
                    }
                    var ret = self.Subscribe(arg0);
                    return js_push_classvalue(ctx, ret);
                }
                throw new NoSuitableMethodException("Subscribe", argc);
            }
            catch (Exception exception)
            {
                return JSApi.ThrowException(ctx, exception);
            }
        }

I think the templated methods might be complicating this. I believe that arg0 should be of type ScriptFunction.

ialex32x commented 3 years ago

I tried with a similar situation, but can't reproduce this issue. Here is the code I use:

// a generic interface type
public interface GenericExample<T>
{
}
// the extension method for the concrete type of this generic interface, with a ScriptFunction as parameter
public static IDisposable TestWithConcreteGeneric(this GenericExample<GameObject> self, ScriptFunction function)
{
    function?.Invoke();
    return null;
}

It generates the glue code:

        [MonoPInvokeCallbackAttribute(typeof(JSCFunction))]
        public static JSValue Bind_TestWithConcreteGeneric(JSContext ctx, JSValue this_obj, int argc, JSValue[] argv)
        {
            try
            {
                if (argc == 1)
                {
                    Example.GenericExample<UnityEngine.GameObject> self;
                    if (!js_get_classvalue(ctx, this_obj, out self))
                    {
                        throw new ThisBoundException();
                    }
                    QuickJS.ScriptFunction arg0;
                    if (!js_get_classvalue(ctx, argv[0], out arg0))
                    {
                        throw new ParameterException(typeof(Example.ExtensionTest), "TestWithConcreteGeneric", typeof(QuickJS.ScriptFunction), 0);
                    }
                    var ret = Example.ExtensionTest.TestWithConcreteGeneric(self, arg0);
                    return js_push_classvalue(ctx, ret);
                }
                throw new NoSuitableMethodException("TestWithConcreteGeneric", argc);
            }
            catch (Exception exception)
            {
                return JSApi.ThrowException(ctx, exception);
            }
        }

Is there any other condition being missing?