From 85aec12b13a7d6acd1c566e3714eef5140850e22 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Tue, 22 Dec 2015 11:48:11 -0500 Subject: [PATCH] Fixes #5, fixes #8 Adds support for IPromise arguments in the last argument of native module methods. Additionally, adds checks during module initialization to ensure native module methods abide by the expected contract (i.e., void or Task return type, positioning of ICallback and IPromise arguments, etc.). --- .../Bridge/NativeModuleBaseTests.cs | 281 +++++++++++++++++- .../Bridge/CompiledReactDelegateFactory.cs | 138 ++++++--- ReactWindows/ReactNative/Bridge/IPromise.cs | 34 +++ .../Bridge/IReactDelegateFactory.cs | 13 + .../ReactNative/Bridge/NativeModuleBase.cs | 15 +- .../ReactNative/Bridge/Queue/Callback.cs | 23 -- .../Bridge/ReactDelegateFactoryBase.cs | 168 +++++++++++ .../Bridge/ReflectionReactDelegateFactory.cs | 154 +++++++--- ReactWindows/ReactNative/ReactNative.csproj | 3 +- 9 files changed, 713 insertions(+), 116 deletions(-) create mode 100644 ReactWindows/ReactNative/Bridge/IPromise.cs delete mode 100644 ReactWindows/ReactNative/Bridge/Queue/Callback.cs create mode 100644 ReactWindows/ReactNative/Bridge/ReactDelegateFactoryBase.cs diff --git a/ReactWindows/ReactNative.Tests/Bridge/NativeModuleBaseTests.cs b/ReactWindows/ReactNative.Tests/Bridge/NativeModuleBaseTests.cs index dfb11c15f66..54c07a90195 100644 --- a/ReactWindows/ReactNative.Tests/Bridge/NativeModuleBaseTests.cs +++ b/ReactWindows/ReactNative.Tests/Bridge/NativeModuleBaseTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace ReactNative.Tests.Bridge { @@ -11,9 +12,29 @@ namespace ReactNative.Tests.Bridge public class NativeModuleBaseTests { [TestMethod] - public void NativeModuleBase_MethodOverload_ThrowsNotSupported() + public void NativeModuleBase_ReactMethod_ThrowsNotSupported() { - AssertEx.Throws(() => new MethodOverloadNativeModule()); + var actions = new Action[] + { + () => new MethodOverloadNotSupportedNativeModule(), + () => new ReturnTypeNotSupportedNativeModule(), + () => new CallbackNotSupportedNativeModule(), + () => new CallbackNotSupportedNativeModule2(), + () => new PromiseNotSupportedNativeModule(), + () => new AsyncCallbackNotSupportedNativeModule(), + () => new AsyncPromiseNotSupportedNativeModule(), + }; + + foreach (var action in actions) + { + AssertEx.Throws(action); + } + } + + [TestMethod] + public void NativeModuleBase_ReactMethod_Async_ThrowsNotImplemented() + { + AssertEx.Throws(() => new AsyncNotImplementedNativeModule()); } [TestMethod] @@ -140,6 +161,26 @@ public void NativeModuleBase_Invocation_Callbacks_NullCallback() Assert.AreEqual(0, args.Count); } + [TestMethod] + public void NativeModuleBase_Invocation_Promises_Resolve() + { + var module = new PromiseNativeModule(() => 17); + module.Initialize(); + + var id = default(int); + var args = default(List); + + var catalystInstance = new MockCatalystInstance((i, a) => + { + id = i; + args = a.ToObject>(); + }); + + module.Methods[nameof(PromiseNativeModule.Foo)].Invoke(catalystInstance, JArray.FromObject(new[] { 42, 43 })); + Assert.AreEqual(42, id); + Assert.IsTrue(args.SequenceEqual(new[] { 17 })); + } + [TestMethod] public void NativeModuleBase_CompiledDelegateFactory_Perf() { @@ -156,6 +197,97 @@ public void NativeModuleBase_CompiledDelegateFactory_Perf() } } + [TestMethod] + public void NativeModuleBase_Invocation_Promises_InvalidArgumentThrows() + { + var module = new PromiseNativeModule(() => 17); + module.Initialize(); + + var id = default(int); + var args = default(List); + + var catalystInstance = new MockCatalystInstance((i, a) => + { + id = i; + args = a.ToObject>(); + }); + + AssertEx.Throws( + () => module.Methods[nameof(PromiseNativeModule.Foo)].Invoke(catalystInstance, JArray.FromObject(new[] { default(object), 43 })), + ex => Assert.AreEqual("jsArguments", ex.ParamName)); + + AssertEx.Throws( + () => module.Methods[nameof(PromiseNativeModule.Foo)].Invoke(catalystInstance, JArray.FromObject(new[] { 42, default(object) })), + ex => Assert.AreEqual("jsArguments", ex.ParamName)); + } + + [TestMethod] + public void NativeModuleBase_Invocation_Promises_IncorrectArgumentCount() + { + var module = new PromiseNativeModule(() => null); + module.Initialize(); + + var id = default(int); + var args = default(List); + + var catalystInstance = new MockCatalystInstance((i, a) => + { + id = i; + args = a.ToObject>(); + }); + + AssertEx.Throws( + () => module.Methods[nameof(PromiseNativeModule.Foo)].Invoke(catalystInstance, JArray.FromObject(new[] { 42 })), + ex => Assert.AreEqual("jsArguments", ex.ParamName)); + } + + [TestMethod] + public void NativeModuleBase_Invocation_Promises_Reject() + { + var expectedMessage = "Foo bar baz"; + var exception = new Exception(expectedMessage); + var module = new PromiseNativeModule(() => { throw exception; }); + module.Initialize(); + + var id = default(int); + var args = default(Dictionary[]); + + var catalystInstance = new MockCatalystInstance((i, a) => + { + id = i; + args = a.ToObject[]>(); + }); + + module.Methods[nameof(CallbackNativeModule.Foo)].Invoke(catalystInstance, JArray.FromObject(new[] { 42, 43 })); + Assert.AreEqual(43, id); + Assert.AreEqual(1, args.Length); + var d = args[0]; + Assert.AreEqual(1, d.Count); + var actualMessage = default(string); + Assert.IsTrue(d.TryGetValue("message", out actualMessage)); + Assert.AreEqual(expectedMessage, actualMessage); + } + + [TestMethod] + public void NativeModuleBase_Invocation_Promises_NullCallback() + { + var module = new PromiseNativeModule(() => null); + module.Initialize(); + + var id = default(int); + var args = default(List); + + var catalystInstance = new MockCatalystInstance((i, a) => + { + id = i; + args = a.ToObject>(); + }); + + module.Methods[nameof(PromiseNativeModule.Foo)].Invoke(catalystInstance, JArray.FromObject(new[] { 42, 43 })); + Assert.AreEqual(1, args.Count); + Assert.IsNull(args[0]); + } + [TestMethod] public void NativeModuleBase_ReflectionDelegateFactory_Perf() { @@ -172,7 +304,7 @@ public void NativeModuleBase_ReflectionDelegateFactory_Perf() } } - class MethodOverloadNativeModule : NativeModuleBase + class MethodOverloadNotSupportedNativeModule : NativeModuleBase { public override string Name { @@ -193,6 +325,113 @@ public void Foo(int x) } } + class ReturnTypeNotSupportedNativeModule : NativeModuleBase + { + public override string Name + { + get + { + return "Test"; + } + } + + [ReactMethod] + public int Foo() { return 0; } + } + + class CallbackNotSupportedNativeModule : NativeModuleBase + { + public override string Name + { + get + { + return "Test"; + } + } + + [ReactMethod] + public void Foo(ICallback foo, int bar, string qux) { } + } + + class CallbackNotSupportedNativeModule2 : NativeModuleBase + { + public override string Name + { + get + { + return "Test"; + } + } + + [ReactMethod] + public void Foo(ICallback bar, int foo) { } + } + + class PromiseNotSupportedNativeModule : NativeModuleBase + { + public override string Name + { + get + { + return "Test"; + } + } + + [ReactMethod] + public void Foo(IPromise promise, int foo) { } + } + + class AsyncCallbackNotSupportedNativeModule : NativeModuleBase + { + public override string Name + { + get + { + return "Test"; + } + } + + [ReactMethod] + public Task Foo(ICallback callback) + { + return Task.FromResult(true); + } + } + + class AsyncPromiseNotSupportedNativeModule : NativeModuleBase + { + public override string Name + { + get + { + return "Test"; + } + } + + [ReactMethod] + public Task Foo(IPromise promise) + { + return Task.FromResult(true); + } + } + + class AsyncNotImplementedNativeModule : NativeModuleBase + { + public override string Name + { + get + { + return "Test"; + } + } + + [ReactMethod] + public Task Foo() + { + return Task.FromResult(true); + } + } + class TestNativeModule : NativeModuleBase { private readonly Action _onFoo; @@ -259,6 +498,42 @@ public void Foo(ICallback callback) } } + class PromiseNativeModule : NativeModuleBase + { + private readonly Func _resolveFactory; + + public PromiseNativeModule() + : this(() => null) + { + } + + public PromiseNativeModule(Func resolveFactory) + { + _resolveFactory = resolveFactory; + } + + public override string Name + { + get + { + return "Test"; + } + } + + [ReactMethod] + public void Foo(IPromise promise) + { + try + { + promise.Resolve(_resolveFactory()); + } + catch (Exception ex) + { + promise.Reject(ex); + } + } + } + class PerfNativeModule : NativeModuleBase { public PerfNativeModule(IReactDelegateFactory delegateFactory) diff --git a/ReactWindows/ReactNative/Bridge/CompiledReactDelegateFactory.cs b/ReactWindows/ReactNative/Bridge/CompiledReactDelegateFactory.cs index 8c725a5b6f6..be4fceececc 100644 --- a/ReactWindows/ReactNative/Bridge/CompiledReactDelegateFactory.cs +++ b/ReactWindows/ReactNative/Bridge/CompiledReactDelegateFactory.cs @@ -2,6 +2,7 @@ using ReactNative.Reflection; using System; using System.Globalization; +using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -10,13 +11,14 @@ namespace ReactNative.Bridge /// /// A delegate factory that will compile a delegate to call the native method. /// - public sealed class CompiledReactDelegateFactory : IReactDelegateFactory + public sealed class CompiledReactDelegateFactory : ReactDelegateFactoryBase { private static readonly ConstructorInfo s_newArgumentNullException = (ConstructorInfo)ReflectionHelpers.InfoOf(() => new ArgumentNullException(default(string))); private static readonly ConstructorInfo s_newArgumentException = (ConstructorInfo)ReflectionHelpers.InfoOf(() => new ArgumentException(default(string), default(string))); private static readonly ConstructorInfo s_newNativeArgumentParseException = (ConstructorInfo)ReflectionHelpers.InfoOf(() => new NativeArgumentsParseException(default(string), default(string))); private static readonly ConstructorInfo s_newNativeArgumentParseExceptionInner = (ConstructorInfo)ReflectionHelpers.InfoOf(() => new NativeArgumentsParseException(default(string), default(string), default(Exception))); - private static readonly ConstructorInfo s_newCallback = (ConstructorInfo)ReflectionHelpers.InfoOf(() => new Callback(default(int), default(ICatalystInstance))); + private static readonly MethodInfo s_createCallback = ((MethodInfo)ReflectionHelpers.InfoOf(() => CreateCallback(default(JToken), default(ICatalystInstance)))); + private static readonly MethodInfo s_createPromise = ((MethodInfo)ReflectionHelpers.InfoOf(() => CreatePromise(default(JToken), default(JToken), default(ICatalystInstance)))); private static readonly MethodInfo s_toObject = ((MethodInfo)ReflectionHelpers.InfoOf((JToken token) => token.ToObject(typeof(Type)))); private static readonly MethodInfo s_stringFormat = (MethodInfo)ReflectionHelpers.InfoOf(() => string.Format(default(IFormatProvider), default(string), default(object))); private static readonly MethodInfo s_getIndex = (MethodInfo)ReflectionHelpers.InfoOf((JArray arr) => arr[0]); @@ -35,7 +37,7 @@ private CompiledReactDelegateFactory() { } /// /// The method. /// The invocation delegate. - public Action Create(INativeModule module, MethodInfo method) + public override Action Create(INativeModule module, MethodInfo method) { return GenerateExpression(module, method).Compile(); } @@ -43,7 +45,9 @@ public Action Create(INativeModule mod private static Expression> GenerateExpression(INativeModule module, MethodInfo method) { var parameterInfos = method.GetParameters(); + var n = parameterInfos.Length; + var argc = n > 0 && parameterInfos.Last().ParameterType == typeof(IPromise) ? n + 1 : n; var parameterExpressions = new ParameterExpression[n]; var extractExpressions = new Expression[n]; @@ -61,7 +65,7 @@ private static Expression> Gene extractExpressions[i] = GenerateExtractExpression( parameterInfo.ParameterType, parameterExpression, - Expression.Call(jsArgumentsParameter, s_getIndex, Expression.Constant(i)), + jsArgumentsParameter, catalystInstanceParameter, jsArgumentsParameter.Name, module.Name, @@ -69,7 +73,7 @@ private static Expression> Gene i); } - var blockStatements = new Expression[parameterInfos.Length + 5]; + var blockStatements = new Expression[n + 5]; // // if (moduleInstance == null) @@ -90,7 +94,7 @@ private static Expression> Gene blockStatements[2] = CreateNullCheckExpression(jsArgumentsParameter); // - // if (jsArguments.Count != valueOf(parameterInfos.Count)) + // if (jsArguments.Count != argc) // throw new NativeArgumentsParseException( // string.Format( // CultureInfo.InvariantCulture, @@ -100,7 +104,7 @@ private static Expression> Gene blockStatements[3] = Expression.IfThen( Expression.NotEqual( Expression.MakeMemberAccess(jsArgumentsParameter, s_countProperty), - Expression.Constant(parameterInfos.Length) + Expression.Constant(argc) ), Expression.Throw( Expression.New( @@ -114,7 +118,7 @@ private static Expression> Gene "Module '{0}' method '{1}' got '{{0}}' arguments, expected '{2}'.", module.Name, method.Name, - parameterInfos.Length) + argc) ), Expression.Convert( Expression.MakeMemberAccess(jsArgumentsParameter, s_countProperty), @@ -132,7 +136,7 @@ private static Expression> Gene // ... // pn = Extract(jsArguments[n]); // - Array.Copy(extractExpressions, 0, blockStatements, 4, parameterInfos.Length); + Array.Copy(extractExpressions, 0, blockStatements, 4, n); blockStatements[blockStatements.Length - 1] = Expression.Call( Expression.Convert(moduleInstanceParameter, method.DeclaringType), @@ -150,7 +154,7 @@ private static Expression> Gene private static Expression GenerateExtractExpression( Type type, Expression leftExpression, - Expression tokenExpression, + Expression argumentsExpression, Expression catalystInstanceExpression, string parameterName, string moduleName, @@ -165,10 +169,7 @@ private static Expression GenerateExtractExpression( // catch (Exception ex) // { // throw new NativeArgumentParseException( - // string.Format( - // CultureInfo.InvariantCulture, - // "Error extracting argument for module 'moduleName' method 'methodName' at index '{0}'.", - // argumentIndex), + // "Error extracting argument for module 'moduleName' method 'methodName' at index 'argumentIndex'."), // paramName, // ex); // } @@ -179,17 +180,13 @@ private static Expression GenerateExtractExpression( Expression.Throw( Expression.New( s_newNativeArgumentParseExceptionInner, - Expression.Call( - s_stringFormat, - Expression.Constant(CultureInfo.InvariantCulture), - Expression.Constant( - string.Format( - CultureInfo.InvariantCulture, - "Error extracting argument for module '{0}' method '{1}' at index '{{0}}'.", - moduleName, - methodName) - ), - Expression.Constant(argumentIndex, typeof(object)) + Expression.Constant( + string.Format( + CultureInfo.InvariantCulture, + "Error extracting argument for module '{0}' method '{1}' at index '{2}'.", + moduleName, + methodName, + argumentIndex) ), Expression.Constant(parameterName), ex @@ -201,29 +198,80 @@ private static Expression GenerateExtractExpression( var valueExpression = default(Expression); if (type == typeof(ICallback)) { - valueExpression = Expression.Parameter(typeof(int), "id").Let(id => - Expression.Block( - new[] { id }, - Expression.Assign( - id, - Expression.Convert( - Expression.Call( - tokenExpression, - s_toObject, - Expression.Constant(typeof(int)) - ), - typeof(int) - ) + // + // CreateCallback(jsArguments[i], catalystInstance); + // + valueExpression = Expression.Call( + s_createCallback, + Expression.Call( + argumentsExpression, + s_getIndex, + Expression.Constant(argumentIndex) + ), + catalystInstanceExpression); + } + else if (type == typeof(IPromise)) + { + // + // if (i > jsArguments.Count - 2) + // throw new NativeArgumentsParseException(...); + // + // CreatePromise(jsArguments[i], jsArguments[i + 1], catalystInstance); + // + valueExpression = Expression.Condition( + Expression.Equal( + Expression.Constant(argumentIndex), + Expression.Subtract( + Expression.Property( + argumentsExpression, + s_countProperty + ), + Expression.Constant(2) + ) + ), + Expression.Call( + s_createPromise, + Expression.Call( + argumentsExpression, + s_getIndex, + Expression.Constant(argumentIndex) + ), + Expression.Call( + argumentsExpression, + s_getIndex, + Expression.Constant(argumentIndex + 1) ), - Expression.New(s_newCallback, id, catalystInstanceExpression) + catalystInstanceExpression + ), + Expression.Throw( + Expression.New( + s_newNativeArgumentParseException, + Expression.Constant( + string.Format( + CultureInfo.InvariantCulture, + "Error extracting argument for module '{0}' method '{1}' at index '{2}'.", + moduleName, + methodName, + argumentIndex + " and " + (argumentIndex + 1)) + ), + Expression.Constant(parameterName) + ), + type ) ); } else { + // + // (T)jsArguments[i].ToObject(typeof(T)); + // valueExpression = Expression.Convert( Expression.Call( - tokenExpression, + Expression.Call( + argumentsExpression, + s_getIndex, + Expression.Constant(argumentIndex) + ), s_toObject, Expression.Constant(type) ), @@ -231,6 +279,16 @@ private static Expression GenerateExtractExpression( ); } + // + // try + // { + // arg = ... + // } + // catch (Exception ex) + // { + // ... + // } + // return Expression.TryCatch( Expression.Block( typeof(void), diff --git a/ReactWindows/ReactNative/Bridge/IPromise.cs b/ReactWindows/ReactNative/Bridge/IPromise.cs new file mode 100644 index 00000000000..24ae0e67f64 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/IPromise.cs @@ -0,0 +1,34 @@ +using System; + +namespace ReactNative.Bridge +{ + /// + /// Interface that represents a JavaScript Promise which can be passed to + /// the native module as a method parameter. + /// + /// + /// Methods annotated with that use + /// as type of the last parameter will be marked as + /// "remoteAsync" and will return a promise when invoked from JavaScript. + /// + public interface IPromise + { + /// + /// Resolve the promise with the given value. + /// + /// The value. + void Resolve(object value); + + /// + /// Reject the promise with the given exception. + /// + /// The exception. + void Reject(Exception exception); + + /// + /// Reject the promise with the given reason. + /// + /// The reason. + void Reject(string reason); + } +} diff --git a/ReactWindows/ReactNative/Bridge/IReactDelegateFactory.cs b/ReactWindows/ReactNative/Bridge/IReactDelegateFactory.cs index 01fe1d402b4..5389b1c3611 100644 --- a/ReactWindows/ReactNative/Bridge/IReactDelegateFactory.cs +++ b/ReactWindows/ReactNative/Bridge/IReactDelegateFactory.cs @@ -9,11 +9,24 @@ namespace ReactNative.Bridge /// public interface IReactDelegateFactory { + /// + /// Extracts the native method type from the method. + /// + /// The method. + /// The native method type. + string GetMethodType(MethodInfo method); + /// /// Create an invocation delegate from the given method. /// /// The method. /// The invocation delegate. Action Create(INativeModule module, MethodInfo method); + + /// + /// Check that the method is valid for . + /// + /// The method. + void Validate(MethodInfo method); } } diff --git a/ReactWindows/ReactNative/Bridge/NativeModuleBase.cs b/ReactWindows/ReactNative/Bridge/NativeModuleBase.cs index bdd76d91d32..a74126990a9 100644 --- a/ReactWindows/ReactNative/Bridge/NativeModuleBase.cs +++ b/ReactWindows/ReactNative/Bridge/NativeModuleBase.cs @@ -179,24 +179,17 @@ private IReadOnlyDictionary InitializeMethods() class NativeMethod : INativeMethod { - const string METHOD_TYPE_REMOTE = "remote"; - const string METHOD_TYPE_REMOTE_ASYNC = "remoteAsync"; - private readonly NativeModuleBase _instance; - private readonly Lazy> _invokeDelegate; public NativeMethod(NativeModuleBase instance, MethodInfo method) { _instance = instance; - _invokeDelegate = new Lazy>(() => instance._delegateFactory.Create(instance, method)); - - if (method.IsAsync()) - { - throw new NotImplementedException("Async methods not yet supported."); - } - Type = METHOD_TYPE_REMOTE; + var delegateFactory = instance._delegateFactory; + delegateFactory.Validate(method); + _invokeDelegate = new Lazy>(() => delegateFactory.Create(instance, method)); + Type = delegateFactory.GetMethodType(method); } public string Type diff --git a/ReactWindows/ReactNative/Bridge/Queue/Callback.cs b/ReactWindows/ReactNative/Bridge/Queue/Callback.cs deleted file mode 100644 index 79f927ae666..00000000000 --- a/ReactWindows/ReactNative/Bridge/Queue/Callback.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace ReactNative.Bridge -{ - class Callback : ICallback - { - private static readonly object[] s_empty = new object[0]; - - private readonly int _id; - private readonly ICatalystInstance _instance; - - public Callback(int id, ICatalystInstance instance) - { - _id = id; - _instance = instance; - } - - public void Invoke(params object[] arguments) - { - _instance.InvokeCallback(_id, JArray.FromObject(arguments ?? s_empty)); - } - } -} diff --git a/ReactWindows/ReactNative/Bridge/ReactDelegateFactoryBase.cs b/ReactWindows/ReactNative/Bridge/ReactDelegateFactoryBase.cs new file mode 100644 index 00000000000..29896e8663a --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/ReactDelegateFactoryBase.cs @@ -0,0 +1,168 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace ReactNative +{ + /// + /// Base implementation for . + /// + public abstract class ReactDelegateFactoryBase : IReactDelegateFactory + { + const string METHOD_TYPE_REMOTE = "remote"; + const string METHOD_TYPE_REMOTE_ASYNC = "remoteAsync"; + + /// + /// Instantiates a . + /// + protected ReactDelegateFactoryBase() { } + + /// + /// Create an invocation delegate from the given method. + /// + /// The method. + /// The invocation delegate. + public abstract Action Create(INativeModule module, MethodInfo method); + + /// + /// Extracts the native method type from the method. + /// + /// The method. + /// The native method type. + public string GetMethodType(MethodInfo method) + { + if (method.ReturnType == typeof(Task)) + { + throw new NotImplementedException("Async methods are not yet supported."); + } + + var parameters = method.GetParameters(); + if (parameters.Length > 0 && parameters.Last().ParameterType == typeof(IPromise)) + { + return METHOD_TYPE_REMOTE_ASYNC; + } + + return METHOD_TYPE_REMOTE; + } + + /// + /// Check that the method is valid for . + /// + /// The method. + public void Validate(MethodInfo method) + { + var returnType = method.ReturnType; + if (returnType != typeof(Task) && returnType != typeof(void)) + { + throw new NotSupportedException("Native module methods must either return void or Task."); + } + + var parameters = method.GetParameters(); + var n = parameters.Length; + for (var i = 0; i < n; ++i) + { + var parameterType = parameters[i].ParameterType; + if (parameterType == typeof(IPromise) && i != (n - 1)) + { + throw new NotSupportedException("Promises are only supported as the last parameter of a native module method."); + } + else if (parameterType == typeof(ICallback) && i != (n - 1)) + { + if (i != (n - 2) || parameters[n - 1].ParameterType != typeof(ICallback)) + { + throw new NotSupportedException("Callbacks are only supported in the last two positions of a native module method."); + } + } + else if (returnType == typeof(Task) && (parameterType == typeof(ICallback) || parameterType == typeof(IPromise))) + { + throw new NotSupportedException("Callbacks and promises are not supported in async native module methods."); + } + } + } + + /// + /// Create a callback. + /// + /// The callback ID token. + /// The catalyst instance. + /// The callback. + protected static ICallback CreateCallback(JToken callbackToken, ICatalystInstance catalystInstance) + { + var id = callbackToken.Value(); + return new Callback(id, catalystInstance); + } + + /// + /// Create a promise. + /// + /// The resolve callback ID token. + /// The reject callback ID token. + /// The catalyst instance. + /// The promise. + protected static IPromise CreatePromise(JToken resolveToken, JToken rejectToken, ICatalystInstance catalystInstance) + { + var resolveCallback = CreateCallback(resolveToken, catalystInstance); + var rejectCallback = CreateCallback(rejectToken, catalystInstance); + return new Promise(resolveCallback, rejectCallback); + } + + class Callback : ICallback + { + private static readonly object[] s_empty = new object[0]; + + private readonly int _id; + private readonly ICatalystInstance _instance; + + public Callback(int id, ICatalystInstance instance) + { + _id = id; + _instance = instance; + } + + public void Invoke(params object[] arguments) + { + _instance.InvokeCallback(_id, JArray.FromObject(arguments ?? s_empty)); + } + } + + class Promise : IPromise + { + private readonly ICallback _resolve; + private readonly ICallback _reject; + + public Promise(ICallback resolve, ICallback reject) + { + _resolve = resolve; + _reject = reject; + } + + public void Reject(string reason) + { + if (_reject != null) + { + _reject.Invoke(new Dictionary + { + { "message", reason }, + }); + } + } + + public void Reject(Exception exception) + { + Reject(exception.Message); + } + + public void Resolve(object value) + { + if (_resolve != null) + { + _resolve.Invoke(value); + } + } + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/ReflectionReactDelegateFactory.cs b/ReactWindows/ReactNative/Bridge/ReflectionReactDelegateFactory.cs index f89f258ad0c..74c464aa26e 100644 --- a/ReactWindows/ReactNative/Bridge/ReflectionReactDelegateFactory.cs +++ b/ReactWindows/ReactNative/Bridge/ReflectionReactDelegateFactory.cs @@ -1,16 +1,17 @@ using Newtonsoft.Json.Linq; -using ReactNative.Reflection; using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Reflection; +using System.Threading.Tasks; namespace ReactNative.Bridge { /// /// A delegate factory that uses reflection to create the native method. /// - public sealed class ReflectionReactDelegateFactory : IReactDelegateFactory + public sealed class ReflectionReactDelegateFactory : ReactDelegateFactoryBase { private ReflectionReactDelegateFactory() { } @@ -25,16 +26,26 @@ private ReflectionReactDelegateFactory() { } /// /// The method. /// The invocation delegate. - public Action Create(INativeModule module, MethodInfo method) + public override Action Create(INativeModule module, MethodInfo method) { var extractors = CreateExtractors(module, method); - return (moduleInstance, catalystInstance, arguments) => Invoke(method, extractors, moduleInstance, catalystInstance, arguments); + var expectedArguments = extractors.Sum(e => e.ExpectedArguments); + var extractFunctions = extractors.Select(e => e.ExtractFunction).ToList(); + + return (moduleInstance, catalystInstance, arguments) => + Invoke( + method, + expectedArguments, + extractFunctions, + moduleInstance, + catalystInstance, + arguments); } - private IList> CreateExtractors(INativeModule module, MethodInfo method) + private IList CreateExtractors(INativeModule module, MethodInfo method) { var parameters = method.GetParameters(); - var extractors = new List>(parameters.Length); + var extractors = new List(parameters.Length); foreach (var parameter in parameters) { extractors.Add(CreateExtractor(parameter.ParameterType, module.Name, method.Name)); @@ -43,7 +54,7 @@ private IList> CreateExtractors(INa return extractors; } - private Func CreateExtractor(Type type, string moduleName, string methodName) + private Extractor CreateExtractor(Type type, string moduleName, string methodName) { var exceptionFormat = string.Format( CultureInfo.InvariantCulture, @@ -51,46 +62,84 @@ private Func CreateExtractor(Type type, moduleName, methodName); + if (type == typeof(ICallback)) { - return (catalystInstance, token, index) => - { - try + return new Extractor( + 1, + (catalystInstance, arguments, index) => { - var id = token.Value(); - return new Callback(id, catalystInstance); - } - catch (Exception ex) + try + { + return new Result( + index + 1, + CreateCallback(arguments[index], catalystInstance)); + } + catch (Exception ex) + { + throw new NativeArgumentsParseException( + string.Format(exceptionFormat, index), + "jsArguments", + ex); + } + }); + } + else if (type == typeof(IPromise)) + { + return new Extractor( + 2, + (catalystInstance, arguments, index) => { - throw new NativeArgumentsParseException( - string.Format(exceptionFormat, index), - "jsArguments", - ex); - } - }; + var nextIndex = index + 1; + if (nextIndex >= arguments.Count) + { + throw new NativeArgumentsParseException( + string.Format(exceptionFormat, index + " and " + (index + 1)), + "jsArguments"); + } + + try + { + return new Result( + nextIndex + 1, + CreatePromise(arguments[index], arguments[nextIndex], catalystInstance)); + } + catch (Exception ex) + { + throw new NativeArgumentsParseException( + string.Format(exceptionFormat, index + " and " + nextIndex), + "jsArguments", + ex); + } + }); } else { - return (catalystInstance, token, index) => - { - try - { - return token.ToObject(type); - } - catch (Exception ex) + return new Extractor( + 1, + (catalystInstance, arguments, index) => { - throw new NativeArgumentsParseException( - string.Format(exceptionFormat, index), - "jsArguments", - ex.InnerException); - } - }; + try + { + return new Result( + index + 1, + arguments[index].ToObject(type)); + } + catch (Exception ex) + { + throw new NativeArgumentsParseException( + string.Format(exceptionFormat, index), + "jsArguments", + ex.InnerException); + } + }); } } private static void Invoke( MethodInfo method, - IList> extractors, + int expectedArguments, + IList> extractors, INativeModule moduleInstance, ICatalystInstance catalystInstance, JArray jsArguments) @@ -102,7 +151,8 @@ private static void Invoke( if (jsArguments == null) throw new ArgumentNullException(nameof(jsArguments)); - var n = extractors.Count; + var n = expectedArguments; + var c = extractors.Count; if (jsArguments.Count != n) { throw new NativeArgumentsParseException( @@ -115,13 +165,41 @@ private static void Invoke( nameof(jsArguments)); } - var args = new object[n]; - for (var i = 0; i < n; ++i) + var idx = 0; + var args = new object[extractors.Count]; + for (var j = 0; j < c; ++j) { - args[i] = extractors[i](catalystInstance, jsArguments[i], i); + var result = extractors[j](catalystInstance, jsArguments, idx); + args[j] = result.Value; + idx = result.NextIndex; } method.Invoke(moduleInstance, args); } + + private struct Result + { + public Result(int nextIndex, object value) + { + NextIndex = nextIndex; + Value = value; + } + + public int NextIndex { get; } + + public object Value { get; } + } + + private struct Extractor + { + public Extractor(int expectedArguments, Func extractFunction) + { + ExpectedArguments = expectedArguments; + ExtractFunction = extractFunction; + } + + public int ExpectedArguments { get; } + public Func ExtractFunction { get; } + } } } diff --git a/ReactWindows/ReactNative/ReactNative.csproj b/ReactWindows/ReactNative/ReactNative.csproj index ad5f1a11182..867a8e49e68 100644 --- a/ReactWindows/ReactNative/ReactNative.csproj +++ b/ReactWindows/ReactNative/ReactNative.csproj @@ -107,7 +107,7 @@ - + @@ -168,6 +168,7 @@ +