diff --git a/ReactWindows/.gitignore b/ReactWindows/.gitignore new file mode 100644 index 00000000000..996016dc621 --- /dev/null +++ b/ReactWindows/.gitignore @@ -0,0 +1,76 @@ +*AppPackages* + +#OS junk files +[Tt]humbs.db +*.DS_Store + +#Visual Studio files +*.[Oo]bj +*.user +*.aps +*.pch +*.vspscc +*.vssscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.[Cc]ache +*.ilk +*.log +*.lib +*.sbr +*.sdf +*.opensdf +*.opendb +*.unsuccessfulbuild +ipch/ +[Oo]bj/ +[Bb]in +[Dd]ebug*/ +[Rr]elease*/ +Ankh.NoLoad + +#MonoDevelop +*.pidb +*.userprefs + +#Tooling +_ReSharper*/ +*.resharper +[Tt]est[Rr]esult* +*.sass-cache + +#Project files +[Bb]uild/ + +#Subversion files +.svn + +# Office Temp Files +~$* + +# vim Temp Files +*~ + +#NuGet +packages/ +*.nupkg + +#ncrunch +*ncrunch* +*crunch*.local.xml + +# visual studio database projects +*.dbmdl + +#Test files +*.testsettings + +#Other files +*.DotSettings +.vs/ +*project.lock.json \ No newline at end of file diff --git a/ReactWindows/Playground/App.xaml b/ReactWindows/Playground/App.xaml new file mode 100644 index 00000000000..33929dda240 --- /dev/null +++ b/ReactWindows/Playground/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/ReactWindows/Playground/App.xaml.cs b/ReactWindows/Playground/App.xaml.cs new file mode 100644 index 00000000000..5cf14d70460 --- /dev/null +++ b/ReactWindows/Playground/App.xaml.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +namespace Playground +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + sealed partial class App : Application + { + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + Microsoft.ApplicationInsights.WindowsAppInitializer.InitializeAsync( + Microsoft.ApplicationInsights.WindowsCollectors.Metadata | + Microsoft.ApplicationInsights.WindowsCollectors.Session); + this.InitializeComponent(); + this.Suspending += OnSuspending; + } + + /// + /// Invoked when the application is launched normally by the end user. Other entry points + /// will be used such as when the application is launched to open a specific file. + /// + /// Details about the launch request and process. + protected override void OnLaunched(LaunchActivatedEventArgs e) + { + +#if DEBUG + if (System.Diagnostics.Debugger.IsAttached) + { + this.DebugSettings.EnableFrameRateCounter = true; + } +#endif + + Frame rootFrame = Window.Current.Content as Frame; + + // Do not repeat app initialization when the Window already has content, + // just ensure that the window is active + if (rootFrame == null) + { + // Create a Frame to act as the navigation context and navigate to the first page + rootFrame = new Frame(); + + rootFrame.NavigationFailed += OnNavigationFailed; + + if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) + { + //TODO: Load state from previously suspended application + } + + // Place the frame in the current Window + Window.Current.Content = rootFrame; + } + + if (rootFrame.Content == null) + { + // When the navigation stack isn't restored navigate to the first page, + // configuring the new page by passing required information as a navigation + // parameter + rootFrame.Navigate(typeof(MainPage), e.Arguments); + } + // Ensure the current window is active + Window.Current.Activate(); + } + + /// + /// Invoked when Navigation to a certain page fails + /// + /// The Frame which failed navigation + /// Details about the navigation failure + void OnNavigationFailed(object sender, NavigationFailedEventArgs e) + { + throw new Exception("Failed to load Page " + e.SourcePageType.FullName); + } + + /// + /// Invoked when application execution is being suspended. Application state is saved + /// without knowing whether the application will be terminated or resumed with the contents + /// of memory still intact. + /// + /// The source of the suspend request. + /// Details about the suspend request. + private void OnSuspending(object sender, SuspendingEventArgs e) + { + var deferral = e.SuspendingOperation.GetDeferral(); + //TODO: Save application state and stop any background activity + deferral.Complete(); + } + } +} diff --git a/ReactWindows/Playground/ApplicationInsights.config b/ReactWindows/Playground/ApplicationInsights.config new file mode 100644 index 00000000000..cb2a232da35 --- /dev/null +++ b/ReactWindows/Playground/ApplicationInsights.config @@ -0,0 +1,3 @@ + + + diff --git a/ReactWindows/Playground/Assets/LockScreenLogo.scale-200.png b/ReactWindows/Playground/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 00000000000..735f57adb5d Binary files /dev/null and b/ReactWindows/Playground/Assets/LockScreenLogo.scale-200.png differ diff --git a/ReactWindows/Playground/Assets/SplashScreen.scale-200.png b/ReactWindows/Playground/Assets/SplashScreen.scale-200.png new file mode 100644 index 00000000000..023e7f1feda Binary files /dev/null and b/ReactWindows/Playground/Assets/SplashScreen.scale-200.png differ diff --git a/ReactWindows/Playground/Assets/Square150x150Logo.scale-200.png b/ReactWindows/Playground/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 00000000000..af49fec1a54 Binary files /dev/null and b/ReactWindows/Playground/Assets/Square150x150Logo.scale-200.png differ diff --git a/ReactWindows/Playground/Assets/Square44x44Logo.scale-200.png b/ReactWindows/Playground/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 00000000000..ce342a2ec8a Binary files /dev/null and b/ReactWindows/Playground/Assets/Square44x44Logo.scale-200.png differ diff --git a/ReactWindows/Playground/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/ReactWindows/Playground/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000000..f6c02ce97e0 Binary files /dev/null and b/ReactWindows/Playground/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/ReactWindows/Playground/Assets/StoreLogo.png b/ReactWindows/Playground/Assets/StoreLogo.png new file mode 100644 index 00000000000..7385b56c0e4 Binary files /dev/null and b/ReactWindows/Playground/Assets/StoreLogo.png differ diff --git a/ReactWindows/Playground/Assets/Wide310x150Logo.scale-200.png b/ReactWindows/Playground/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000000..288995b397f Binary files /dev/null and b/ReactWindows/Playground/Assets/Wide310x150Logo.scale-200.png differ diff --git a/ReactWindows/Playground/MainPage.xaml b/ReactWindows/Playground/MainPage.xaml new file mode 100644 index 00000000000..f09931990a1 --- /dev/null +++ b/ReactWindows/Playground/MainPage.xaml @@ -0,0 +1,13 @@ + + + + + + diff --git a/ReactWindows/Playground/MainPage.xaml.cs b/ReactWindows/Playground/MainPage.xaml.cs new file mode 100644 index 00000000000..1189daa7f1b --- /dev/null +++ b/ReactWindows/Playground/MainPage.xaml.cs @@ -0,0 +1,46 @@ +using ReactNative.Hosting; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409 + +namespace Playground +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class MainPage : Page + { + public MainPage() + { + this.InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + try + { + var runtime = new JavaScriptRuntime(); + var context = runtime.CreateContext(); + } + catch (Exception ex) + { + + } + } + } +} diff --git a/ReactWindows/Playground/Package.appxmanifest b/ReactWindows/Playground/Package.appxmanifest new file mode 100644 index 00000000000..12652801043 --- /dev/null +++ b/ReactWindows/Playground/Package.appxmanifest @@ -0,0 +1,49 @@ + + + + + + + + + + Playground + ericroz + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ReactWindows/Playground/Playground.csproj b/ReactWindows/Playground/Playground.csproj new file mode 100644 index 00000000000..981e75778dd --- /dev/null +++ b/ReactWindows/Playground/Playground.csproj @@ -0,0 +1,149 @@ + + + + + Debug + x86 + {D52267B5-396F-424A-BB26-C9E750032846} + AppContainerExe + Properties + Playground + Playground + en-US + UAP + 10.0.10586.0 + 10.0.10240.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Playground_TemporaryKey.pfx + + + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + true + + + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + true + true + + + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM + false + prompt + true + + + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM + false + prompt + true + true + + + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + true + + + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + true + true + + + + + PreserveNewest + + + + + + App.xaml + + + MainPage.xaml + + + + + + Designer + + + + + + + + + + + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + + + {c7673ad5-e3aa-468c-a5fd-fa38154e205c} + ReactNative + + + + 14.0 + + + + \ No newline at end of file diff --git a/ReactWindows/Playground/Playground_TemporaryKey.pfx b/ReactWindows/Playground/Playground_TemporaryKey.pfx new file mode 100644 index 00000000000..65693d19327 Binary files /dev/null and b/ReactWindows/Playground/Playground_TemporaryKey.pfx differ diff --git a/ReactWindows/Playground/Properties/AssemblyInfo.cs b/ReactWindows/Playground/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..0793c0ed293 --- /dev/null +++ b/ReactWindows/Playground/Properties/AssemblyInfo.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Playground")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Playground")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/ReactWindows/Playground/Properties/Default.rd.xml b/ReactWindows/Playground/Properties/Default.rd.xml new file mode 100644 index 00000000000..80a960ce32f --- /dev/null +++ b/ReactWindows/Playground/Properties/Default.rd.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/ReactWindows/Playground/project.json b/ReactWindows/Playground/project.json new file mode 100644 index 00000000000..e3b2dba25fa --- /dev/null +++ b/ReactWindows/Playground/project.json @@ -0,0 +1,19 @@ +{ + "dependencies": { + "Microsoft.ApplicationInsights": "1.0.0", + "Microsoft.ApplicationInsights.PersistenceChannel": "1.0.0", + "Microsoft.ApplicationInsights.WindowsApps": "1.0.0", + "Microsoft.NETCore.UniversalWindowsPlatform": "5.0.0" + }, + "frameworks": { + "uap10.0": {} + }, + "runtimes": { + "win10-arm": {}, + "win10-arm-aot": {}, + "win10-x86": {}, + "win10-x86-aot": {}, + "win10-x64": {}, + "win10-x64-aot": {} + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative.Tests/Assets/LockScreenLogo.scale-200.png b/ReactWindows/ReactNative.Tests/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 00000000000..735f57adb5d Binary files /dev/null and b/ReactWindows/ReactNative.Tests/Assets/LockScreenLogo.scale-200.png differ diff --git a/ReactWindows/ReactNative.Tests/Assets/SplashScreen.scale-200.png b/ReactWindows/ReactNative.Tests/Assets/SplashScreen.scale-200.png new file mode 100644 index 00000000000..023e7f1feda Binary files /dev/null and b/ReactWindows/ReactNative.Tests/Assets/SplashScreen.scale-200.png differ diff --git a/ReactWindows/ReactNative.Tests/Assets/Square150x150Logo.scale-200.png b/ReactWindows/ReactNative.Tests/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 00000000000..af49fec1a54 Binary files /dev/null and b/ReactWindows/ReactNative.Tests/Assets/Square150x150Logo.scale-200.png differ diff --git a/ReactWindows/ReactNative.Tests/Assets/Square44x44Logo.scale-200.png b/ReactWindows/ReactNative.Tests/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 00000000000..ce342a2ec8a Binary files /dev/null and b/ReactWindows/ReactNative.Tests/Assets/Square44x44Logo.scale-200.png differ diff --git a/ReactWindows/ReactNative.Tests/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/ReactWindows/ReactNative.Tests/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000000..f6c02ce97e0 Binary files /dev/null and b/ReactWindows/ReactNative.Tests/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/ReactWindows/ReactNative.Tests/Assets/StoreLogo.png b/ReactWindows/ReactNative.Tests/Assets/StoreLogo.png new file mode 100644 index 00000000000..7385b56c0e4 Binary files /dev/null and b/ReactWindows/ReactNative.Tests/Assets/StoreLogo.png differ diff --git a/ReactWindows/ReactNative.Tests/Assets/Wide310x150Logo.scale-200.png b/ReactWindows/ReactNative.Tests/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000000..288995b397f Binary files /dev/null and b/ReactWindows/ReactNative.Tests/Assets/Wide310x150Logo.scale-200.png differ diff --git a/ReactWindows/ReactNative.Tests/Bridge/JavaScriptModuleRegistryTests.cs b/ReactWindows/ReactNative.Tests/Bridge/JavaScriptModuleRegistryTests.cs new file mode 100644 index 00000000000..8ab7f6545d9 --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Bridge/JavaScriptModuleRegistryTests.cs @@ -0,0 +1,97 @@ +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using ReactNative.Modules.Core; +using ReactNative.UIManager; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace ReactNative.Tests.Bridge +{ + [TestClass] + public class JavaScriptModuleRegistryTests + { + [TestMethod] + public void JavaScriptModuleRegistry_ArgumentChecks() + { + var catalystInstance = new MockCatalystInstance(); + var config = new JavaScriptModulesConfig.Builder().Build(); + + AssertEx.Throws( + () => new JavaScriptModuleRegistry(null, config), + ex => Assert.AreEqual("catalystInstance", ex.ParamName)); + + AssertEx.Throws( + () => new JavaScriptModuleRegistry(catalystInstance, null), + ex => Assert.AreEqual("config", ex.ParamName)); + } + + [TestMethod] + public void JavaScriptModuleRegistry_Invoke() + { + var config = new JavaScriptModulesConfig.Builder() + .Add() + .Add() + .Add() + .Build(); + + var are = new AutoResetEvent(false); + var moduleIds = new List(); + var methodIds = new List(); + var argsList = new List(); + var catalystInstance = new MockCatalystInstance((moduleId, methodId, args, tracingName) => + { + moduleIds.Add(moduleId); + methodIds.Add(methodId); + argsList.Add(args); + are.Set(); + }); + + var registry = new JavaScriptModuleRegistry(catalystInstance, config); + var module = registry.GetJavaScriptModule(); + + module.Foo(42); + + are.WaitOne(); + + Assert.AreEqual(1, moduleIds.Count); + Assert.AreEqual(1, methodIds.Count); + Assert.AreEqual(1, moduleIds.Count); + + Assert.AreEqual(2, moduleIds[0]); + Assert.AreEqual(2, methodIds[0]); + Assert.AreEqual( + JArray.FromObject(new[] { 42 }).ToString(Formatting.None), + argsList[0].ToString(Formatting.None)); + } + + [TestMethod] + public void JavaScriptModuleRegistry_InvalidModule_Throws() + { + var config = new JavaScriptModulesConfig.Builder().Build(); + var catalystInstance = new MockCatalystInstance(); + var registry = new JavaScriptModuleRegistry(catalystInstance, config); + AssertEx.Throws(() => registry.GetJavaScriptModule()); + } + + class TestJavaScriptModule : JavaScriptModuleBase + { + public void Bar() + { + Invoke(nameof(Bar)); + } + + public void Baz() + { + Invoke(nameof(Baz)); + } + + public void Foo(int x) + { + Invoke(nameof(Foo), x); + } + } + } +} diff --git a/ReactWindows/ReactNative.Tests/Bridge/JavaScriptModulesConfigTests.cs b/ReactWindows/ReactNative.Tests/Bridge/JavaScriptModulesConfigTests.cs new file mode 100644 index 00000000000..f3ba394a53c --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Bridge/JavaScriptModulesConfigTests.cs @@ -0,0 +1,102 @@ +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using System; +using System.Collections.Generic; +using System.IO; + +namespace ReactNative.Tests.Bridge +{ + [TestClass] + public class JavaScriptModulesConfigTests + { + [TestMethod] + public void JavaScriptModulesConfig_MethodOverrides_ThrowsNotSupported() + { + var builder = new JavaScriptModulesConfig.Builder(); + AssertEx.Throws(() => builder.Add()); + } + + [TestMethod] + public void JavaScriptModulesConfig_WriteModuleDefinitions() + { + var builder = new JavaScriptModulesConfig.Builder(); + builder.Add(); + var config = builder.Build(); + + using (var stringWriter = new StringWriter()) + { + using (var writer = new JsonTextWriter(stringWriter)) + { + config.WriteModuleDescriptions(writer); + } + + var actual = stringWriter.ToString(); + var expected = JObject.FromObject( + new Map + { + { + "TestJavaScriptModule", + new Map + { + { "moduleID", 0 }, + { + "methods", + new Map + { + { + "Bar", + new Map + { + { "methodID", 0 }, + } + }, + { + "Foo", + new Map + { + { "methodID", 1 }, + } + } + } + } + } + } + } + ).ToString(Formatting.None); + + Assert.AreEqual(expected, actual); + } + } + } + + public class Map : Dictionary { } + + public class OverridesJavaScriptModule : JavaScriptModuleBase + { + public void Foo() + { + Invoke(nameof(Foo)); + } + + + public void Foo(int x) + { + Invoke(nameof(Foo), x); + } + } + + public class TestJavaScriptModule : JavaScriptModuleBase + { + public void Bar() + { + Invoke(nameof(Bar)); + } + + public void Foo() + { + Invoke(nameof(Foo)); + } + } +} diff --git a/ReactWindows/ReactNative.Tests/Bridge/NativeModuleBaseTests.cs b/ReactWindows/ReactNative.Tests/Bridge/NativeModuleBaseTests.cs new file mode 100644 index 00000000000..edd4ab8987a --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Bridge/NativeModuleBaseTests.cs @@ -0,0 +1,230 @@ +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ReactNative.Tests.Bridge +{ + [TestClass] + public class NativeModuleBaseTests + { + [TestMethod] + public void NativeModuleBase_MethodOverload_ThrowsNotSupported() + { + AssertEx.Throws(() => new MethodOverloadNativeModule()); + } + + [TestMethod] + public void NativeModuleBase_Invocation_ArgumentNull() + { + var testModule = new TestNativeModule(); + + testModule.Initialize(); + + var catalystInstance = new MockCatalystInstance(); + AssertEx.Throws( + () => testModule.Methods[nameof(TestNativeModule.Foo)].Invoke(null, new JArray()), + ex => Assert.AreEqual("catalystInstance", ex.ParamName)); + AssertEx.Throws( + () => testModule.Methods[nameof(TestNativeModule.Foo)].Invoke(catalystInstance, null), + ex => Assert.AreEqual("jsArguments", ex.ParamName)); + } + + [TestMethod] + public void NativeModuleBase_Invocation_ArgumentInvalidCount() + { + var testModule = new TestNativeModule(); + + testModule.Initialize(); + + var catalystInstance = new MockCatalystInstance(); + AssertEx.Throws( + () => testModule.Methods[nameof(TestNativeModule.Bar)].Invoke(catalystInstance, new JArray()), + ex => Assert.AreEqual("jsArguments", ex.ParamName)); + } + + [TestMethod] + public void NativeModuleBase_Invocation_ArgumentConversionException() + { + var testModule = new TestNativeModule(); + + testModule.Initialize(); + + var catalystInstance = new MockCatalystInstance(); + AssertEx.Throws( + () => testModule.Methods[nameof(TestNativeModule.Bar)].Invoke(catalystInstance, JArray.FromObject(new[] { default(object) })), + ex => Assert.AreEqual("jsArguments", ex.ParamName)); + } + + [TestMethod] + public void NativeModuleBase_Invocation() + { + var fooCount = 0; + var barSum = 0; + var testModule = new TestNativeModule(() => fooCount++, x => barSum += x); + + testModule.Initialize(); + + Assert.AreEqual(2, testModule.Methods.Count); + + var catalystInstance = new MockCatalystInstance(); + testModule.Methods[nameof(TestNativeModule.Foo)].Invoke(catalystInstance, new JArray()); + testModule.Methods[nameof(TestNativeModule.Foo)].Invoke(catalystInstance, new JArray()); + Assert.AreEqual(2, fooCount); + + testModule.Methods[nameof(TestNativeModule.Bar)].Invoke(catalystInstance, JArray.FromObject(new[] { 42 })); + testModule.Methods[nameof(TestNativeModule.Bar)].Invoke(catalystInstance, JArray.FromObject(new[] { 17 })); + Assert.AreEqual(59, barSum); + } + + [TestMethod] + public void NativeModuleBase_Invocation_Callbacks() + { + var callbackArgs = new object[] { 1, 2, 3 }; + var module = new CallbackNativeModule(callbackArgs); + module.Initialize(); + + var id = default(int); + var args = default(List); + + var catalystInstance = new MockCatalystInstance((i, a) => + { + id = i; + args = a.ToObject>(); + }); + + module.Methods[nameof(CallbackNativeModule.Foo)].Invoke(catalystInstance, JArray.FromObject(new[] { 42 })); + Assert.AreEqual(42, id); + Assert.IsTrue(args.Cast().SequenceEqual(callbackArgs)); + } + + [TestMethod] + public void NativeModuleBase_Invocation_Callbacks_InvalidArgumentThrows() + { + var callbackArgs = new object[] { 1, 2, 3 }; + var module = new CallbackNativeModule(callbackArgs); + 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(CallbackNativeModule.Foo)].Invoke(catalystInstance, JArray.FromObject(new[] { default(object) })), + ex => Assert.AreEqual("jsArguments", ex.ParamName)); + } + + [TestMethod] + public void NativeModuleBase_Invocation_Callbacks_NullCallback() + { + var module = new CallbackNativeModule(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(CallbackNativeModule.Foo)].Invoke(catalystInstance, JArray.FromObject(new[] { 42 })); + Assert.AreEqual(0, args.Count); + } + + class MethodOverloadNativeModule : NativeModuleBase + { + public override string Name + { + get + { + return "Test"; + } + } + + [ReactMethod] + public void Foo() + { + } + + [ReactMethod] + public void Foo(int x) + { + } + } + + class TestNativeModule : NativeModuleBase + { + private readonly Action _onFoo; + private readonly Action _onBar; + + public TestNativeModule() + : this(() => { }, _ => { }) + { + } + + public TestNativeModule(Action onFoo, Action onBar) + { + _onFoo = onFoo; + _onBar = onBar; + } + + public override string Name + { + get + { + return "Foo"; + } + } + + [ReactMethod] + public void Foo() + { + _onFoo(); + } + + [ReactMethod] + public void Bar(int x) + { + _onBar(x); + } + } + + class CallbackNativeModule : NativeModuleBase + { + private readonly object[] _callbackArgs; + + public CallbackNativeModule() + : this(null) + { + } + + public CallbackNativeModule(object[] callbackArgs) + { + _callbackArgs = callbackArgs; + } + + public override string Name + { + get + { + return "Test"; + } + } + + [ReactMethod] + public void Foo(ICallback callback) + { + callback.Invoke(_callbackArgs); + } + } + } +} diff --git a/ReactWindows/ReactNative.Tests/Bridge/NativeModuleRegistryTests.cs b/ReactWindows/ReactNative.Tests/Bridge/NativeModuleRegistryTests.cs new file mode 100644 index 00000000000..be0b4a43da0 --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Bridge/NativeModuleRegistryTests.cs @@ -0,0 +1,150 @@ +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using System; +using System.IO; +using System.Linq; + +namespace ReactNative.Tests.Bridge +{ + [TestClass] + public class NativeModuleRegistryTests + { + [TestMethod] + public void NativeModuleRegistry_ArgumentChecks() + { + var builder = new NativeModuleRegistry.Builder(); + AssertEx.Throws( + () => builder.Add(null), + ex => Assert.AreEqual("module", ex.ParamName)); + } + + [TestMethod] + public void NativeModuleRegistry_Override_Disallowed() + { + var builder = new NativeModuleRegistry.Builder(); + builder.Add(new OverrideDisallowedModule()); + AssertEx.Throws(() => builder.Add(new OverrideDisallowedModule())); + } + + [TestMethod] + public void NativeModuleRegistry_Override_Allowed() + { + var registry = new NativeModuleRegistry.Builder() + .Add(new OverrideAllowedModule()) + .Add(new OverrideAllowedModule()) + .Build(); + + Assert.AreEqual(1, registry.Modules.Count()); + } + + [TestMethod] + public void NativeModuleRegistry_ModuleWithNullName_Throws() + { + var builder = new NativeModuleRegistry.Builder(); + AssertEx.Throws( + () => builder.Add(new NullNameModule()), + ex => Assert.AreEqual("module", ex.ParamName)); + } + + [TestMethod] + public void NativeModuleRegistry_WriteModuleDefinitions() + { + var registry = new NativeModuleRegistry.Builder() + .Add(new TestNativeModule()) + .Build(); + + using (var stringWriter = new StringWriter()) + { + using (var writer = new JsonTextWriter(stringWriter)) + { + registry.WriteModuleDescriptions(writer); + } + + var actual = JObject.Parse(stringWriter.ToString()); + Assert.AreEqual(1, actual.Properties().Count()); + + var moduleDef = actual.GetValue("Test") as JObject; + Assert.IsNotNull(moduleDef); + + var moduleId = moduleDef.GetValue("moduleID"); + Assert.IsNotNull(moduleId); + Assert.AreEqual("0", moduleId.ToString()); + + var methods = moduleDef.GetValue("methods") as JObject; + Assert.IsNotNull(methods); + + var fooMethod = methods.GetValue("Foo") as JObject; + Assert.IsNotNull(fooMethod); + + var barMethod = methods.GetValue("Bar") as JObject; + Assert.IsNotNull(barMethod); + + var fooMethodId = fooMethod.GetValue("methodID"); + var barMethodId = barMethod.GetValue("methodID"); + Assert.AreNotEqual(fooMethodId.ToString(), barMethodId.ToString()); + Assert.IsTrue(fooMethodId.ToString() == "0" || fooMethodId.ToString() == "1"); + Assert.IsTrue(barMethodId.ToString() == "0" || barMethodId.ToString() == "1"); + } + } + + class OverrideDisallowedModule : NativeModuleBase + { + public override string Name + { + get + { + return "Foo"; + } + } + } + + class OverrideAllowedModule : NativeModuleBase + { + public override string Name + { + get + { + return "Foo"; + } + } + + public override bool CanOverrideExistingModule + { + get + { + return true; + } + } + } + + class NullNameModule : NativeModuleBase + { + public override string Name + { + get + { + return null; + } + } + } + + class TestNativeModule : NativeModuleBase + { + public override string Name + { + get + { + return "Test"; + } + } + + [ReactMethod] + public void Foo(int x) { } + + [ReactMethod] + public void Bar(string x) { } + } + } +} diff --git a/ReactWindows/ReactNative.Tests/Bridge/Queue/MessageQueueThreadTests.cs b/ReactWindows/ReactNative.Tests/Bridge/Queue/MessageQueueThreadTests.cs new file mode 100644 index 00000000000..4bd3e2b6c9e --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Bridge/Queue/MessageQueueThreadTests.cs @@ -0,0 +1,127 @@ +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using ReactNative.Bridge.Queue; +using System; +using System.Threading; +using System.Threading.Tasks; +using static ReactNative.Tests.DispatcherHelpers; + +namespace ReactNative.Tests.Bridge.Queue +{ + [TestClass] + public class MessageQueueThreadTests + { + [TestMethod] + public void MessageQueueThread_ArgumentChecks() + { + AssertEx.Throws( + () => MessageQueueThread.Create(null, ex => { }), + ex => Assert.AreEqual("spec", ex.ParamName)); + + AssertEx.Throws( + () => MessageQueueThread.Create(MessageQueueThreadSpec.DispatcherThreadSpec, null), + ex => Assert.AreEqual("handler", ex.ParamName)); + } + + [TestMethod] + public void MessageQueueThread_CreateUiThread_ThrowsNotSupported() + { + AssertEx.Throws(() => MessageQueueThreadSpec.Create("ui", MessageQueueThreadKind.DispatcherThread)); + } + + [TestMethod] + public async Task MessageQueueThread_IsOnThread() + { + var thrown = 0; + var uiThread = default(IMessageQueueThread); + await RunOnDispatcherAsync(() => uiThread = MessageQueueThread.Create(MessageQueueThreadSpec.DispatcherThreadSpec, ex => thrown++)); + var backgroundThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("background", MessageQueueThreadKind.BackgroundSingleThread), ex => thrown++); + var taskPoolThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("any", MessageQueueThreadKind.BackgroundAnyThread), ex => thrown++); + + var queueThreads = new[] + { + uiThread, + backgroundThread, + taskPoolThread + }; + + var countdown = new CountdownEvent(queueThreads.Length); + foreach (var queueThread in queueThreads) + { + queueThread.RunOnQueue(() => + { + Assert.IsTrue(queueThread.IsOnThread()); + countdown.Signal(); + }); + } + + Assert.IsTrue(countdown.Wait(5000)); + Assert.AreEqual(0, thrown); + } + + [TestMethod] + public async Task MessageQueueThread_HandlesException() + { + var exception = new Exception(); + var countdown = new CountdownEvent(3); + var handler = new Action(ex => + { + Assert.AreSame(exception, ex); + countdown.Signal(); + }); + + var uiThread = default(IMessageQueueThread); + await RunOnDispatcherAsync(() => uiThread = MessageQueueThread.Create(MessageQueueThreadSpec.DispatcherThreadSpec, handler)); + var backgroundThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("background", MessageQueueThreadKind.BackgroundSingleThread), handler); + var taskPoolThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("any", MessageQueueThreadKind.BackgroundAnyThread), handler); + + var queueThreads = new[] + { + uiThread, + backgroundThread, + taskPoolThread + }; + + foreach (var queueThread in queueThreads) + { + queueThread.RunOnQueue(() => { throw exception; }); + } + + Assert.IsTrue(countdown.Wait(5000)); + } + + [TestMethod] + public async Task MessageQueueThread_OneAtATime() + { + var uiThread = default(IMessageQueueThread); + await RunOnDispatcherAsync(() => uiThread = MessageQueueThread.Create(MessageQueueThreadSpec.DispatcherThreadSpec, ex => { Assert.Fail(); })); + var backgroundThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("background", MessageQueueThreadKind.BackgroundSingleThread), ex => { Assert.Fail(); }); + var taskPoolThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("any", MessageQueueThreadKind.BackgroundAnyThread), ex => { Assert.Fail(); }); + + var enter = new AutoResetEvent(false); + var exit = new AutoResetEvent(false); + + var queueThreads = new[] + { + uiThread, + backgroundThread, + taskPoolThread + }; + + foreach (var queueThread in queueThreads) + { + var count = 10; + for (var i = 0; i < count; ++i) + { + queueThread.RunOnQueue(() => { enter.Set(); exit.WaitOne(); }); + } + + for (var i = 0; i < count; ++i) + { + Assert.IsTrue(enter.WaitOne()); + Assert.IsFalse(enter.WaitOne(100)); + exit.Set(); + } + } + } + } +} diff --git a/ReactWindows/ReactNative.Tests/Internal/AssertEx.cs b/ReactWindows/ReactNative.Tests/Internal/AssertEx.cs new file mode 100644 index 00000000000..b95609260ed --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Internal/AssertEx.cs @@ -0,0 +1,30 @@ +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using System; + +namespace ReactNative.Tests +{ + static class AssertEx + { + public static void Throws(Action action) + where T : Exception + { + Throws(action, _ => { }); + } + + public static void Throws(Action action, Action assert) + where T : Exception + { + try + { + action(); + } + catch (T ex) + { + assert(ex); + return; + } + + Assert.Fail("Excepted exception of type '{0}'.", typeof(T)); + } + } +} diff --git a/ReactWindows/ReactNative.Tests/Internal/DispatcherHelpers.cs b/ReactWindows/ReactNative.Tests/Internal/DispatcherHelpers.cs new file mode 100644 index 00000000000..4f4250d72e0 --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Internal/DispatcherHelpers.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading.Tasks; +using Windows.UI.Core; + +namespace ReactNative.Tests +{ + static class DispatcherHelpers + { + public static async Task RunOnDispatcherAsync(Action action) + { + await App.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(action)); + } + } +} diff --git a/ReactWindows/ReactNative.Tests/Internal/MockCatalystInstance.cs b/ReactWindows/ReactNative.Tests/Internal/MockCatalystInstance.cs new file mode 100644 index 00000000000..00c27a6563e --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Internal/MockCatalystInstance.cs @@ -0,0 +1,63 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ReactNative.Tests +{ + class MockCatalystInstance : ICatalystInstance + { + private readonly Action _callback; + private readonly Action _function; + + public MockCatalystInstance() + : this((_, __) => { }, (p0, p1, p2, p3) => { }) + { + } + + public MockCatalystInstance(Action callback) + : this(callback, (p0, p1, p2, p3) => { }) + { + } + + public MockCatalystInstance(Action function) + : this((_, __) => { }, function) + { + } + + public MockCatalystInstance(Action callback, Action function) + { + _callback = callback; + _function = function; + } + + public IEnumerable NativeModules + { + get + { + throw new NotImplementedException(); + } + } + + public T GetNativeModule() where T : INativeModule + { + throw new NotImplementedException(); + } + + public Task InitializeAsync() + { + return Task.FromResult(true); + } + + public void InvokeCallback(int callbackId, JArray arguments) + { + _callback(callbackId, arguments); + } + + public void InvokeFunction(int moduleId, int methodId, JArray arguments, string tracingName) + { + _function(moduleId, methodId, arguments, tracingName); + } + } +} diff --git a/ReactWindows/ReactNative.Tests/Package.appxmanifest b/ReactWindows/ReactNative.Tests/Package.appxmanifest new file mode 100644 index 00000000000..c4a026da1df --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Package.appxmanifest @@ -0,0 +1,45 @@ + + + + + + + + + ReactNative.Tests + ericroz + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ReactWindows/ReactNative.Tests/Properties/AssemblyInfo.cs b/ReactWindows/ReactNative.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..b903241f6f6 --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ReactNative.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ReactNative.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyMetadata("TargetPlatform","UAP")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/ReactWindows/ReactNative.Tests/Properties/UnitTestApp.rd.xml b/ReactWindows/ReactNative.Tests/Properties/UnitTestApp.rd.xml new file mode 100644 index 00000000000..efee59d2788 --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Properties/UnitTestApp.rd.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ReactWindows/ReactNative.Tests/ReactNative.Tests.csproj b/ReactWindows/ReactNative.Tests/ReactNative.Tests.csproj new file mode 100644 index 00000000000..78aa48be995 --- /dev/null +++ b/ReactWindows/ReactNative.Tests/ReactNative.Tests.csproj @@ -0,0 +1,153 @@ + + + + + Debug + x86 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691} + AppContainerExe + Properties + ReactNative.Tests + ReactNative.Tests + en-US + UAP + 10.0.10586.0 + 10.0.10240.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + ReactNative.Tests_TemporaryKey.pfx + 14.0 + + + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + true + + + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + true + true + + + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM + false + prompt + true + + + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM + false + prompt + true + true + + + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + true + + + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + true + true + + + + + + + + + + + + + + + + + + + UnitTestApp.xaml + + + + + MSBuild:Compile + Designer + + + + + Designer + + + + + + + + + + + + + + + + + + + {c7673ad5-e3aa-468c-a5fd-fa38154e205c} + ReactNative + + + + 14.0 + + + + \ No newline at end of file diff --git a/ReactWindows/ReactNative.Tests/ReactNative.Tests_TemporaryKey.pfx b/ReactWindows/ReactNative.Tests/ReactNative.Tests_TemporaryKey.pfx new file mode 100644 index 00000000000..b6e16b4b7c6 Binary files /dev/null and b/ReactWindows/ReactNative.Tests/ReactNative.Tests_TemporaryKey.pfx differ diff --git a/ReactWindows/ReactNative.Tests/UnitTestApp.xaml b/ReactWindows/ReactNative.Tests/UnitTestApp.xaml new file mode 100644 index 00000000000..997faae36ff --- /dev/null +++ b/ReactWindows/ReactNative.Tests/UnitTestApp.xaml @@ -0,0 +1,8 @@ + + + diff --git a/ReactWindows/ReactNative.Tests/UnitTestApp.xaml.cs b/ReactWindows/ReactNative.Tests/UnitTestApp.xaml.cs new file mode 100644 index 00000000000..d13aa490bcf --- /dev/null +++ b/ReactWindows/ReactNative.Tests/UnitTestApp.xaml.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Core; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +namespace ReactNative.Tests +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + sealed partial class App : Application + { + public static CoreDispatcher Dispatcher { get; private set; } + + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + this.Suspending += OnSuspending; + } + + /// + /// Invoked when the application is launched normally by the end user. Other entry points + /// will be used such as when the application is launched to open a specific file. + /// + /// Details about the launch request and process. + protected override void OnLaunched(LaunchActivatedEventArgs e) + { + +#if DEBUG + if (System.Diagnostics.Debugger.IsAttached) + { + this.DebugSettings.EnableFrameRateCounter = true; + } +#endif + + Frame rootFrame = Window.Current.Content as Frame; + + // Do not repeat app initialization when the Window already has content, + // just ensure that the window is active + if (rootFrame == null) + { + // Create a Frame to act as the navigation context and navigate to the first page + rootFrame = new Frame(); + + rootFrame.NavigationFailed += OnNavigationFailed; + + if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) + { + //TODO: Load state from previously suspended application + } + + // Place the frame in the current Window + Window.Current.Content = rootFrame; + } + + Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.CreateDefaultUI(); + + // Ensure the current window is active + Window.Current.Activate(); + + Dispatcher = CoreWindow.GetForCurrentThread().Dispatcher; + + Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.Run(e.Arguments); + } + + /// + /// Invoked when Navigation to a certain page fails + /// + /// The Frame which failed navigation + /// Details about the navigation failure + void OnNavigationFailed(object sender, NavigationFailedEventArgs e) + { + throw new Exception("Failed to load Page " + e.SourcePageType.FullName); + } + + /// + /// Invoked when application execution is being suspended. Application state is saved + /// without knowing whether the application will be terminated or resumed with the contents + /// of memory still intact. + /// + /// The source of the suspend request. + /// Details about the suspend request. + private void OnSuspending(object sender, SuspendingEventArgs e) + { + var deferral = e.SuspendingOperation.GetDeferral(); + //TODO: Save application state and stop any background activity + deferral.Complete(); + } + } +} diff --git a/ReactWindows/ReactNative.Tests/project.json b/ReactWindows/ReactNative.Tests/project.json new file mode 100644 index 00000000000..c5949392705 --- /dev/null +++ b/ReactWindows/ReactNative.Tests/project.json @@ -0,0 +1,16 @@ +{ + "dependencies": { + "Microsoft.NETCore.UniversalWindowsPlatform": "5.0.0" + }, + "frameworks": { + "uap10.0": {} + }, + "runtimes": { + "win10-arm": {}, + "win10-arm-aot": {}, + "win10-x86": {}, + "win10-x86-aot": {}, + "win10-x64": {}, + "win10-x64-aot": {} + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative.sln b/ReactWindows/ReactNative.sln new file mode 100644 index 00000000000..b27578ddf15 --- /dev/null +++ b/ReactWindows/ReactNative.sln @@ -0,0 +1,84 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.24720.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactNative", "ReactNative\ReactNative.csproj", "{C7673AD5-E3AA-468C-A5FD-FA38154E205C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playground", "Playground\Playground.csproj", "{D52267B5-396F-424A-BB26-C9E750032846}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactNative.Tests", "ReactNative.Tests\ReactNative.Tests.csproj", "{5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|ARM = Debug|ARM + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|ARM = Release|ARM + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|ARM.ActiveCfg = Debug|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|ARM.Build.0 = Debug|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x64.ActiveCfg = Debug|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x64.Build.0 = Debug|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x86.ActiveCfg = Debug|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x86.Build.0 = Debug|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|Any CPU.Build.0 = Release|Any CPU + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|ARM.ActiveCfg = Release|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|ARM.Build.0 = Release|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x64.ActiveCfg = Release|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x64.Build.0 = Release|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x86.ActiveCfg = Release|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x86.Build.0 = Release|x86 + {D52267B5-396F-424A-BB26-C9E750032846}.Debug|Any CPU.ActiveCfg = Debug|x86 + {D52267B5-396F-424A-BB26-C9E750032846}.Debug|ARM.ActiveCfg = Debug|ARM + {D52267B5-396F-424A-BB26-C9E750032846}.Debug|ARM.Build.0 = Debug|ARM + {D52267B5-396F-424A-BB26-C9E750032846}.Debug|ARM.Deploy.0 = Debug|ARM + {D52267B5-396F-424A-BB26-C9E750032846}.Debug|x64.ActiveCfg = Debug|x64 + {D52267B5-396F-424A-BB26-C9E750032846}.Debug|x64.Build.0 = Debug|x64 + {D52267B5-396F-424A-BB26-C9E750032846}.Debug|x64.Deploy.0 = Debug|x64 + {D52267B5-396F-424A-BB26-C9E750032846}.Debug|x86.ActiveCfg = Debug|x86 + {D52267B5-396F-424A-BB26-C9E750032846}.Debug|x86.Build.0 = Debug|x86 + {D52267B5-396F-424A-BB26-C9E750032846}.Debug|x86.Deploy.0 = Debug|x86 + {D52267B5-396F-424A-BB26-C9E750032846}.Release|Any CPU.ActiveCfg = Release|x86 + {D52267B5-396F-424A-BB26-C9E750032846}.Release|ARM.ActiveCfg = Release|ARM + {D52267B5-396F-424A-BB26-C9E750032846}.Release|ARM.Build.0 = Release|ARM + {D52267B5-396F-424A-BB26-C9E750032846}.Release|ARM.Deploy.0 = Release|ARM + {D52267B5-396F-424A-BB26-C9E750032846}.Release|x64.ActiveCfg = Release|x64 + {D52267B5-396F-424A-BB26-C9E750032846}.Release|x64.Build.0 = Release|x64 + {D52267B5-396F-424A-BB26-C9E750032846}.Release|x64.Deploy.0 = Release|x64 + {D52267B5-396F-424A-BB26-C9E750032846}.Release|x86.ActiveCfg = Release|x86 + {D52267B5-396F-424A-BB26-C9E750032846}.Release|x86.Build.0 = Release|x86 + {D52267B5-396F-424A-BB26-C9E750032846}.Release|x86.Deploy.0 = Release|x86 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Debug|Any CPU.ActiveCfg = Debug|x86 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Debug|ARM.ActiveCfg = Debug|ARM + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Debug|ARM.Build.0 = Debug|ARM + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Debug|ARM.Deploy.0 = Debug|ARM + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Debug|x64.ActiveCfg = Debug|x64 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Debug|x64.Build.0 = Debug|x64 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Debug|x64.Deploy.0 = Debug|x64 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Debug|x86.ActiveCfg = Debug|x86 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Debug|x86.Build.0 = Debug|x86 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Debug|x86.Deploy.0 = Debug|x86 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Release|Any CPU.ActiveCfg = Release|x86 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Release|ARM.ActiveCfg = Release|ARM + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Release|ARM.Build.0 = Release|ARM + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Release|ARM.Deploy.0 = Release|ARM + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Release|x64.ActiveCfg = Release|x64 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Release|x64.Build.0 = Release|x64 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Release|x64.Deploy.0 = Release|x64 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Release|x86.ActiveCfg = Release|x86 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Release|x86.Build.0 = Release|x86 + {5AC8108B-1C39-4C4A-8653-DBBE4ECCE691}.Release|x86.Deploy.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/ReactWindows/ReactNative/Bridge/CatalystInstance.cs b/ReactWindows/ReactNative/Bridge/CatalystInstance.cs new file mode 100644 index 00000000000..b59d45d633c --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/CatalystInstance.cs @@ -0,0 +1,202 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using ReactNative.Bridge.Queue; +using ReactNative.Common; +using ReactNative.Hosting.Bridge; +using ReactNative.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace ReactNative.Bridge +{ + class CatalystInstance : ICatalystInstance, IDisposable + { + private readonly NativeModuleRegistry _registry; + private readonly CatalystQueueConfiguration _catalystQueueConfiguration; + private readonly IJavaScriptExecutor _jsExecutor; + + private readonly JavaScriptModulesConfig _jsModulesConfig; + private IReactBridge _bridge; + + private bool _initialized; + private bool _disposed; + + public CatalystInstance( + CatalystQueueConfigurationSpec catalystQueueConfigurationSpec, + IJavaScriptExecutor jsExecutor, + NativeModuleRegistry registry, + JavaScriptModulesConfig jsModulesConfig) + { + _registry = registry; + _jsExecutor = jsExecutor; + _jsModulesConfig = jsModulesConfig; + + _catalystQueueConfiguration = CatalystQueueConfiguration.Create( + catalystQueueConfigurationSpec, + HandleException); + } + + public IEnumerable NativeModules + { + get + { + return _registry.Modules; + } + } + + public T GetNativeModule() where T : INativeModule + { + return _registry.GetModule(); + } + + public async Task InitializeAsync() + { + DispatcherHelpers.AssertOnDispatcher(); + if (_initialized) + { + throw new InvalidOperationException("This catalyst instance has already been initialized."); + } + + await InitializeBridgeAsync(); + + _initialized = true; + _registry.NotifyCatalystInstanceInitialize(); + } + + public void InvokeCallback(int callbackId, JArray arguments) + { + if (_disposed) + { + Tracer.Write(ReactConstants.Tag, "Invoking JS callback after bridge has been destroyed."); + return; + } + + _catalystQueueConfiguration.JSQueueThread.RunOnQueue(() => + { + _catalystQueueConfiguration.JSQueueThread.AssertIsOnThread(); + if (_disposed) + { + return; + } + + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "")) + { + _bridge.InvokeCallback(callbackId, arguments); + } + }); + } + + public /* TODO: internal? */ void InvokeFunction(int moduleId, int methodId, JArray arguments, string tracingName) + { + _catalystQueueConfiguration.JSQueueThread.RunOnQueue(() => + { + _catalystQueueConfiguration.JSQueueThread.AssertIsOnThread(); + + if (_disposed) + { + return; + } + + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, tracingName)) + { + if (_bridge == null) + { + throw new InvalidOperationException("Bridge has not been initialized."); + } + + _bridge.CallFunction(moduleId, methodId, arguments); + } + }); + } + + public void Dispose() + { + DispatcherHelpers.AssertOnDispatcher(); + + if (_disposed) + { + return; + } + + _disposed = true; + _registry.NotifyCatalystInstanceDispose(); + _catalystQueueConfiguration.Dispose(); + // TODO: notify bridge idle listeners + } + + private Task InitializeBridgeAsync() + { + return _catalystQueueConfiguration.JSQueueThread.CallOnQueue(() => + { + _catalystQueueConfiguration.JSQueueThread.AssertIsOnThread(); + + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "ReactBridgeCtor")) + { + _bridge = new ReactBridge( + _jsExecutor, + new NativeModulesReactCallback(this), + _catalystQueueConfiguration.NativeModulesQueueThread); + } + + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "setBatchedBridgeConfig")) + { + _bridge.SetGlobalVariable("__fbBatchedBridgeConfig", BuildModulesConfig()); + } + + return _bridge; + }); + } + + private string BuildModulesConfig() + { + using (var stringWriter = new StringWriter()) + { + using (var writer = new JsonTextWriter(stringWriter)) + { + writer.WriteStartObject(); + writer.WritePropertyName("remoteModuleConfig"); + _registry.WriteModuleDescriptions(writer); + writer.WritePropertyName("localModulesConfig"); + _jsModulesConfig.WriteModuleDescriptions(writer); + writer.WriteEndObject(); + } + + return stringWriter.ToString(); + } + } + + private void HandleException(Exception ex) + { + // TODO + } + + class NativeModulesReactCallback : IReactCallback + { + private readonly CatalystInstance _parent; + + public NativeModulesReactCallback(CatalystInstance parent) + { + _parent = parent; + } + + public void Invoke(int moduleId, int methodId, JArray parameters) + { + _parent._catalystQueueConfiguration.NativeModulesQueueThread.AssertIsOnThread(); + + if (_parent._disposed) + { + return; + } + + _parent._registry.Invoke(_parent, moduleId, methodId, parameters); + } + + public void OnBatchComplete() + { + _parent._catalystQueueConfiguration.NativeModulesQueueThread.AssertIsOnThread(); + } + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/DispatcherHelpers.cs b/ReactWindows/ReactNative/Bridge/DispatcherHelpers.cs new file mode 100644 index 00000000000..76720a13005 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/DispatcherHelpers.cs @@ -0,0 +1,16 @@ +using System; +using Windows.UI.Core; + +namespace ReactNative.Bridge +{ + static class DispatcherHelpers + { + public static void AssertOnDispatcher() + { + if (CoreWindow.GetForCurrentThread().Dispatcher != null) + { + throw new InvalidOperationException("Thread does not have dispatcher access."); + } + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/ICallback.cs b/ReactWindows/ReactNative/Bridge/ICallback.cs new file mode 100644 index 00000000000..e04550933f6 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/ICallback.cs @@ -0,0 +1,15 @@ +namespace ReactNative.Bridge +{ + /// + /// Interface that represents a JavaScript callback function that can be + /// passed to a native module as a method parameter. + /// + public interface ICallback + { + /// + /// Invokes the callback. + /// + /// The callback arguments. + void Invoke(params object[] arguments); + } +} diff --git a/ReactWindows/ReactNative/Bridge/ICatalystInstance.cs b/ReactWindows/ReactNative/Bridge/ICatalystInstance.cs new file mode 100644 index 00000000000..8c0bf99ffcb --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/ICatalystInstance.cs @@ -0,0 +1,49 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ReactNative.Bridge +{ + /// + /// An abstraction for the asynchronous JavaScript bridge. This provides an + /// environment allowing the invocation of JavaScript methods and lets a + /// set of native APIs be invokable from JavaScript as well. + /// + public interface ICatalystInstance + { + /// + /// Enumerates the available native modules. + /// + IEnumerable NativeModules { get; } + + /// + /// Initializes the instance. + /// + /// A task to await initialization. + Task InitializeAsync(); + + /// + /// Invokes a JavaScript function. + /// + /// The module ID. + /// The method ID. + /// The arguments. + /// The tracing name. + void InvokeFunction(int moduleId, int methodId, JArray arguments, string tracingName); + + /// + /// Invokes a JavaScript callback. + /// + /// The callback ID. + /// The arguments. + void InvokeCallback(int callbackId, JArray arguments); + + /// + /// Gets a native module instance. + /// + /// Type of native module. + /// The native module instance. + T GetNativeModule() where T : INativeModule; + } +} diff --git a/ReactWindows/ReactNative/Bridge/IInvocationHandler.cs b/ReactWindows/ReactNative/Bridge/IInvocationHandler.cs new file mode 100644 index 00000000000..3f21db92775 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/IInvocationHandler.cs @@ -0,0 +1,15 @@ +namespace ReactNative.Bridge +{ + /// + /// An interface for invoking methods specified by name. + /// + public interface IInvocationHandler + { + /// + /// Invoke the specified method. + /// + /// The name of the method. + /// The arguments for the method. + void Invoke(string name, object[] args); + } +} diff --git a/ReactWindows/ReactNative/Bridge/IJavaScriptExecutor.cs b/ReactWindows/ReactNative/Bridge/IJavaScriptExecutor.cs new file mode 100644 index 00000000000..1749d74fe68 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/IJavaScriptExecutor.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json.Linq; +using System; + +namespace ReactNative.Bridge +{ + public interface IJavaScriptExecutor : IDisposable + { + JToken Call(string moduleName, string methodName, JArray arguments); + + void SetGlobalVariable(string propertyName, JToken value); + } +} diff --git a/ReactWindows/ReactNative/Bridge/IJavaScriptModule.cs b/ReactWindows/ReactNative/Bridge/IJavaScriptModule.cs new file mode 100644 index 00000000000..2ac62b82d0c --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/IJavaScriptModule.cs @@ -0,0 +1,13 @@ +namespace ReactNative.Bridge +{ + /// + /// An interface for JavaScript modules. + /// + public interface IJavaScriptModule + { + /// + /// The invocation handler. + /// + IInvocationHandler InvocationHandler { set; } + } +} diff --git a/ReactWindows/ReactNative/Bridge/INativeMethod.cs b/ReactWindows/ReactNative/Bridge/INativeMethod.cs new file mode 100644 index 00000000000..d42cb57a8ef --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/INativeMethod.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json.Linq; + +namespace ReactNative.Bridge +{ + /// + /// An interface representing native methods. + /// + public interface INativeMethod + { + /// + /// The type of method. + /// + string Type { get; } + + /// + /// Invoke the native method. + /// + /// The catalyst instance. + /// The arguments. + void Invoke(ICatalystInstance catalystInstance, JArray jsArguments); + } +} diff --git a/ReactWindows/ReactNative/Bridge/INativeModule.cs b/ReactWindows/ReactNative/Bridge/INativeModule.cs new file mode 100644 index 00000000000..524675d9a70 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/INativeModule.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; + +namespace ReactNative.Bridge +{ + /// + /// A native module whose API can be provided to JavaScript catalyst + /// instances. + /// + /// + /// s whose implementation is written in C# + /// should extend or + /// . + /// + public interface INativeModule + { + /// + /// Return true if you intend to override some other native module that + /// was registered, e.g., as part of a different package (such as the + /// core one). Trying to override without returning true from this + /// method is considered an error and will throw an exception during + /// initialization. By default, all modules return false. + /// + bool CanOverrideExistingModule { get; } + + /// + /// Get the constants exported by this module. + /// + IReadOnlyDictionary Constants { get; } + + /// + /// Get the methods callabke from JavaScript on this module. + /// + IReadOnlyDictionary Methods { get; } + + /// + /// Get the name of the module. + /// + /// + /// This will be the name used to require() this module + /// from JavaScript. + /// + string Name { get; } + + /// + /// Called after the creation of a , in + /// order to initialize native modules that require the catalyst or + /// JavaScript modules. + /// + void Initialize(); + + /// + /// Called before a is disposed. + /// + void OnCatalystInstanceDispose(); + } +} diff --git a/ReactWindows/ReactNative/Bridge/IOnBatchCompleteListener.cs b/ReactWindows/ReactNative/Bridge/IOnBatchCompleteListener.cs new file mode 100644 index 00000000000..12d94899d62 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/IOnBatchCompleteListener.cs @@ -0,0 +1,14 @@ +namespace ReactNative.Bridge +{ + /// + /// Interface that will be notified when a batch of JavaScript to native + /// calls has finished. + /// + public interface IOnBatchCompleteListener + { + /// + /// Invoked when a batch of JavaScript to native calls has finished. + /// + void OnBatchComplete(); + } +} diff --git a/ReactWindows/ReactNative/Bridge/IReactBridge.cs b/ReactWindows/ReactNative/Bridge/IReactBridge.cs new file mode 100644 index 00000000000..cab49c9087a --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/IReactBridge.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json.Linq; +using System; + +namespace ReactNative.Bridge +{ + /// + /// Interface to the JavaScript execution environment and means of + /// transport for messages between JavaScript and the native environment. + /// + public interface IReactBridge + { + /// + /// Calls a JavaScript function. + /// + /// The module ID. + /// The method ID. + /// The arguments. + void CallFunction(int moduleId, int methodId, JArray arguments); + + /// + /// Invokes a JavaScript callback. + /// + /// The callback ID. + /// The arguments. + void InvokeCallback(int callbackID, JArray arguments); + + /// + /// Sets a global JavaScript variable. + /// + /// The property name. + /// The JSON-encoded value. + void SetGlobalVariable(string propertyName, string jsonEncodedArgument); + } +} diff --git a/ReactWindows/ReactNative/Bridge/IReactCallback.cs b/ReactWindows/ReactNative/Bridge/IReactCallback.cs new file mode 100644 index 00000000000..55d38113add --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/IReactCallback.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json.Linq; + +namespace ReactNative.Bridge +{ + /// + /// Interface that represents a native callback that can be invoked from + /// JavaScript. + /// + public interface IReactCallback + { + /// + /// Invoke the native callback. + /// + /// The module ID. + /// The method ID. + /// The parameters. + void Invoke(int moduleId, int methodId, JArray parameters); + + /// + /// Signals that a batch of operations is complete. + /// + void OnBatchComplete(); + } +} diff --git a/ReactWindows/ReactNative/Bridge/JavaScriptModuleBase.cs b/ReactWindows/ReactNative/Bridge/JavaScriptModuleBase.cs new file mode 100644 index 00000000000..19b3a3d459f --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/JavaScriptModuleBase.cs @@ -0,0 +1,49 @@ +using System; + +namespace ReactNative.Bridge +{ + /// + /// Base class for s. + /// + public abstract class JavaScriptModuleBase : IJavaScriptModule + { + private IInvocationHandler _invokeHandler; + + /// + /// The invocation handler. + /// + public IInvocationHandler InvocationHandler + { + set + { + if (_invokeHandler != null) + { + throw new InvalidOperationException("InvokeHandler set more than once."); + } + + _invokeHandler = value; + } + } + + /// + /// Invoke a method by name. + /// + /// The name of the method. + /// The arguments. + /// + /// The expectation is that s will use + /// this method to notify the framework of a JavaScript call to be + /// executed. This is to overcome the absense of a performant "proxy" + /// implementation in the .NET framework. + /// + protected void Invoke(string name, params object[] args) + { + if (_invokeHandler == null) + { + throw new InvalidOperationException("InvokeHandler has not been set."); + } + + _invokeHandler.Invoke(name, args); + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/JavaScriptModuleRegistration.cs b/ReactWindows/ReactNative/Bridge/JavaScriptModuleRegistration.cs new file mode 100644 index 00000000000..1a1a5e1b60f --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/JavaScriptModuleRegistration.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; + +namespace ReactNative.Bridge +{ + /// + /// Registration information for a . Creates + /// a mapping of methods to IDs. + /// + internal class JavaScriptModuleRegistration + { + private readonly IDictionary _methodsToIds; + private readonly IDictionary _methodsToTracingStrings; + + /// + /// Instantiates the . + /// + /// The module ID. + /// The module type. + public JavaScriptModuleRegistration(int moduleId, Type moduleInterface) + { + ModuleId = moduleId; + ModuleInterface = moduleInterface; + + var methods = moduleInterface.GetTypeInfo().DeclaredMethods; + var methodNames = new List(); + foreach (var method in methods) + { + methodNames.Add(method.Name); + } + + methodNames.Sort((s1, s2) => s1.CompareTo(s2)); + + _methodsToIds = new Dictionary(methodNames.Count); + _methodsToTracingStrings = new Dictionary(methodNames.Count); + + InitializeMethodTables(methodNames); + } + + /// + /// The module ID. + /// + public int ModuleId { get; } + + /// + /// The module type. + /// + public Type ModuleInterface { get; } + + /// + /// The module name. + /// + public string Name + { + get + { + return ModuleInterface.Name; + } + } + + /// + /// The set of methods available in the module. + /// + public IEnumerable Methods + { + get + { + return _methodsToIds.Keys; + } + } + + /// + /// Get the ID for a particular module method by name. + /// + /// The method name. + /// The method ID. + public int GetMethodId(string method) + { + var idx = default(int); + if (!_methodsToIds.TryGetValue(method, out idx)) + { + throw new InvalidOperationException("Unknown method: " + method); + } + + return idx; + } + + /// + /// Get the tracing name for a particular module method by name.s + /// + /// The method name. + /// The tracing name. + public string GetTracingName(string method) + { + var name = default(string); + if (!_methodsToTracingStrings.TryGetValue(method, out name)) + { + throw new InvalidOperationException("Unknown method: " + method); + } + + return name; + } + + private void InitializeMethodTables(IList methods) + { + var lastMethod = default(string); + for (var i = 0; i < methods.Count; ++i) + { + var method = methods[i]; + if (method == lastMethod) + { + throw new NotSupportedException( + string.Format( + CultureInfo.InvariantCulture, + "Method overloading is not supported: {0}.{1}", + ModuleInterface.Name, + method)); + } + + lastMethod = method; + _methodsToIds.Add(method, i); + _methodsToTracingStrings.Add(method, "JSCall__" + Name + "_" + method); + } + } + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative/Bridge/JavaScriptModuleRegistry.cs b/ReactWindows/ReactNative/Bridge/JavaScriptModuleRegistry.cs new file mode 100644 index 00000000000..f4f51e1c908 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/JavaScriptModuleRegistry.cs @@ -0,0 +1,88 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace ReactNative.Bridge +{ + /// + /// Class responsible for holding all s + /// registered to a . Requires that + /// JavaScript modules use the base + /// class, and implement each of it's methods to dispatch through the + /// method. + /// + public class JavaScriptModuleRegistry + { + private readonly IDictionary _moduleInstances; + + /// + /// Instantiates the . + /// + /// The catalyst instance. + /// The module configuration. + public JavaScriptModuleRegistry( + ICatalystInstance catalystInstance, + JavaScriptModulesConfig config) + { + if (catalystInstance == null) + throw new ArgumentNullException(nameof(catalystInstance)); + if (config == null) + throw new ArgumentNullException(nameof(config)); + + _moduleInstances = new Dictionary(config.ModuleDefinitions.Count); + foreach (var registration in config.ModuleDefinitions) + { + var type = registration.ModuleInterface; + var moduleInstance = (IJavaScriptModule)Activator.CreateInstance(type); + var invokeHandler = new JavaScriptModuleInvocationHandler(catalystInstance, registration); + moduleInstance.InvocationHandler = invokeHandler; + _moduleInstances.Add(type, moduleInstance); + } + } + + /// + /// Gets an instance of a . + /// + /// Type of JavaScript module. + /// The JavaScript module instance. + public T GetJavaScriptModule() where T : IJavaScriptModule + { + var instance = default(IJavaScriptModule); + if (!_moduleInstances.TryGetValue(typeof(T), out instance)) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "JS module '{0}' hasn't been registered.", + typeof(T))); + } + + return (T)instance; + } + + class JavaScriptModuleInvocationHandler : IInvocationHandler + { + private readonly ICatalystInstance _catalystInstance; + private readonly JavaScriptModuleRegistration _moduleRegistration; + + public JavaScriptModuleInvocationHandler( + ICatalystInstance catalystInstance, + JavaScriptModuleRegistration moduleRegistration) + { + _catalystInstance = catalystInstance; + _moduleRegistration = moduleRegistration; + } + + public void Invoke(string name, object[] args) + { + var tracingName = _moduleRegistration.GetTracingName(name); + _catalystInstance.InvokeFunction( + _moduleRegistration.ModuleId, + _moduleRegistration.GetMethodId(name), + JArray.FromObject(args), + tracingName); + } + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/JavaScriptModulesConfig.cs b/ReactWindows/ReactNative/Bridge/JavaScriptModulesConfig.cs new file mode 100644 index 00000000000..7750838fed3 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/JavaScriptModulesConfig.cs @@ -0,0 +1,84 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace ReactNative.Bridge +{ + /// + /// Class to store the configuration of JavaScript modules that can be used + /// across the bridge. + /// + public class JavaScriptModulesConfig + { + private JavaScriptModulesConfig(IReadOnlyList modules) + { + ModuleDefinitions = modules; + } + + /// + /// The module definitions. + /// + internal IReadOnlyList ModuleDefinitions + { + get; + } + + /// + /// Writes the description of the modules to a . + /// + /// The JSON writer. + public void WriteModuleDescriptions(JsonWriter writer) + { + writer.WriteStartObject(); + foreach (var module in ModuleDefinitions) + { + writer.WritePropertyName(module.Name); + writer.WriteStartObject(); + writer.WritePropertyName("moduleID"); + writer.WriteValue(module.ModuleId); + writer.WritePropertyName("methods"); + writer.WriteStartObject(); + foreach (var method in module.Methods) + { + writer.WritePropertyName(method); + writer.WriteStartObject(); + writer.WritePropertyName("methodID"); + writer.WriteValue(module.GetMethodId(method)); + writer.WriteEndObject(); + } + writer.WriteEndObject(); + writer.WriteEndObject(); + } + writer.WriteEndObject(); + } + + /// + /// Builder for instances. + /// + public sealed class Builder + { + private readonly List _modules = + new List(); + + /// + /// Adds a JavaScript module of the given type. + /// + /// Type of JavaScript module. + /// The builder instance. + public Builder Add() where T : IJavaScriptModule, new() + { + var moduleId = _modules.Count; + _modules.Add(new JavaScriptModuleRegistration(moduleId, typeof(T))); + return this; + } + + /// + /// Builds the instance. + /// + /// The instance. + public JavaScriptModulesConfig Build() + { + return new JavaScriptModulesConfig(_modules); + } + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/NativeArgumentsParseException.cs b/ReactWindows/ReactNative/Bridge/NativeArgumentsParseException.cs new file mode 100644 index 00000000000..d4a4125259e --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/NativeArgumentsParseException.cs @@ -0,0 +1,31 @@ +using System; + +namespace ReactNative.Bridge +{ + /// + /// An exception thrown when converting between JavaScript and native arguments. + /// + public class NativeArgumentsParseException : ArgumentException + { + /// + /// Instantiates the . + /// + /// The exception message. + /// The parameter name. + public NativeArgumentsParseException(string message, string paramName) + : base(message, paramName) + { + } + + /// + /// Instantiates the . + /// + /// The exception message. + /// The parameter name. + /// The inner exception. + public NativeArgumentsParseException(string message, string paramName, Exception innerException) + : base(message, paramName, innerException) + { + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/NativeModuleBase.cs b/ReactWindows/ReactNative/Bridge/NativeModuleBase.cs new file mode 100644 index 00000000000..911acdded8a --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/NativeModuleBase.cs @@ -0,0 +1,423 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Reflection; +using ReactNative.Tracing; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; + +namespace ReactNative.Bridge +{ + /// + /// Base class for catalyst native modules. Implementations can be linked + /// to lifecycle events, such as the creation and disposal of the + /// by overriding the appropriate methods. + /// + /// Native methods are exposed to JavaScript with the + /// annotation. These methods may only + /// use arguments that can be parsed by or + /// , which maps from a JavaScript function and can + /// be used only as a last parameter, or in the case of success and error + /// callback pairs, the last two arguments respectively. + /// + /// All methods annotated with must + /// return . + /// + /// Please note that it is not allowed to have multiple methods annotated + /// with that share the same name. + /// + /// + /// Default implementations of and + /// are provided for convenience. + /// Subclasses need not call these base methods should they choose to + /// override them. + /// + public abstract class NativeModuleBase : INativeModule + { + private static readonly IReadOnlyDictionary s_emptyConstants + = new Dictionary(); + + private readonly IReadOnlyDictionary _methods; + + /// + /// Instantiates a . + /// + protected NativeModuleBase() + { + _methods = InitializeMethods(); + } + + /// + /// Return true if you intend to override some other native module that + /// was registered, e.g., as part of a different package (such as the + /// core one). Trying to override without returning true from this + /// method is considered an error and will throw an exception during + /// initialization. By default, all modules return false. + /// + public virtual bool CanOverrideExistingModule + { + get + { + return false; + } + } + + /// + /// Get the constants exported by this module. + /// + public virtual IReadOnlyDictionary Constants + { + get + { + return s_emptyConstants; + } + } + + /// + /// Get the methods callabke from JavaScript on this module. + /// + public IReadOnlyDictionary Methods + { + get + { + if (_methods == null) + { + throw new InvalidOperationException("Module has not been initialized."); + } + + return _methods; + } + } + + /// + /// Get the name of the module. + /// + /// + /// This will be the name used to require() this module + /// from JavaScript. + /// + public abstract string Name + { + get; + } + + /// + /// Called after the creation of a , in + /// order to initialize native modules that require the catalyst or + /// JavaScript modules. + /// + public virtual void Initialize() + { + } + + /// + /// Called before a is disposed. + /// + public virtual void OnCatalystInstanceDispose() + { + } + + /// + /// Create the set of constants to configure the global environment. + /// + /// The set of constants. + /// + /// This virtual method will be called during . + /// + protected virtual IReadOnlyDictionary CreateConstants() + { + return new Dictionary(); + } + + private IReadOnlyDictionary InitializeMethods() + { + var declaredMethods = GetType().GetTypeInfo().DeclaredMethods; + var exportedMethods = new List(); + foreach (var method in declaredMethods) + { + if (method.IsDefined(typeof(ReactMethodAttribute))) + { + exportedMethods.Add(method); + } + } + + var methodMap = new Dictionary(exportedMethods.Count); + foreach (var method in exportedMethods) + { + var existingMethod = default(INativeMethod); + if (methodMap.TryGetValue(method.Name, out existingMethod)) + { + throw new NotSupportedException( + string.Format( + CultureInfo.InvariantCulture, + "React module '{0}' with name '{1}' has more than one ReactMethod with the name '{2}'.", + GetType(), + Name, + method.Name)); + } + + methodMap.Add(method.Name, new NativeMethod(this, method)); + } + + return methodMap; + } + + 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>(() => GenerateExpression(instance, method).Compile()); + + if (method.IsAsync()) + { + throw new NotImplementedException("Async methods not yet supported."); + } + + Type = METHOD_TYPE_REMOTE; + } + + public string Type + { + get; + } + + public void Invoke(ICatalystInstance catalystInstance, JArray jsArguments) + { + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "callNativeModuleMethod")) + { + _invokeDelegate.Value(catalystInstance, jsArguments); + } + } + + private static ConstructorInfo s_newArgumentNullException = (ConstructorInfo)ReflectionHelpers.InfoOf(() => new ArgumentNullException(default(string))); + private static ConstructorInfo s_newArgumentException = (ConstructorInfo)ReflectionHelpers.InfoOf(() => new ArgumentException(default(string), default(string))); + private static ConstructorInfo s_newNativeArgumentParseException = (ConstructorInfo)ReflectionHelpers.InfoOf(() => new NativeArgumentsParseException(default(string), default(string))); + private static ConstructorInfo s_newNativeArgumentParseExceptionInner = (ConstructorInfo)ReflectionHelpers.InfoOf(() => new NativeArgumentsParseException(default(string), default(string), default(Exception))); + private static ConstructorInfo s_newCallback = (ConstructorInfo)ReflectionHelpers.InfoOf(() => new Callback(default(int), default(ICatalystInstance))); + private static MethodInfo s_valueInt = ((MethodInfo)ReflectionHelpers.InfoOf((JToken token) => token.Value())).GetGenericMethodDefinition().MakeGenericMethod(typeof(int)); + private static MethodInfo s_toObjectGeneric = ((MethodInfo)ReflectionHelpers.InfoOf((JToken token) => token.ToObject())).GetGenericMethodDefinition(); + private static MethodInfo s_stringFormat = (MethodInfo)ReflectionHelpers.InfoOf(() => string.Format(default(IFormatProvider), default(string), default(object))); + private static MethodInfo s_getIndex = (MethodInfo)ReflectionHelpers.InfoOf((JArray arr) => arr[0]); + private static PropertyInfo s_countProperty = (PropertyInfo)ReflectionHelpers.InfoOf((JArray arr) => arr.Count); + + private static Expression> GenerateExpression(NativeModuleBase module, MethodInfo method) + { + var parameterInfos = method.GetParameters(); + var n = parameterInfos.Length; + + var parameterExpressions = new ParameterExpression[n]; + var extractExpressions = new Expression[n]; + + var catalystInstanceParameter = Expression.Parameter(typeof(ICatalystInstance), "catalystInstance"); + var jsArgumentsParameter = Expression.Parameter(typeof(JArray), "jsArguments"); + + for (var i = 0; i < n; ++i) + { + var parameterInfo = parameterInfos[i]; + var parameterType = parameterInfo.ParameterType; + var parameterExpression = Expression.Parameter(parameterType, parameterInfo.Name); + parameterExpressions[i] = parameterExpression; + extractExpressions[i] = GenerateExtractExpression( + parameterInfo.ParameterType, + parameterExpression, + Expression.Call(jsArgumentsParameter, s_getIndex, Expression.Constant(i)), + catalystInstanceParameter, + jsArgumentsParameter.Name, + module.Name, + method.Name, + i); + } + + var blockStatements = new Expression[parameterInfos.Length + 4]; + + // + // if (catalystInstance == null) + // throw new ArgumentNullException(nameof(catalystInstance)); + // + blockStatements[0] = CreateNullCheckExpression(catalystInstanceParameter); + + // + // if (jsArguments == null) + // throw new ArgumentNullException(nameof(jsArguments)); + // + blockStatements[1] = CreateNullCheckExpression(jsArgumentsParameter); + + // + // if (jsArguments.Count != valueOf(parameterInfos.Count)) + // throw new NativeArgumentsParseException( + // string.Format( + // CultureInfo.InvariantCulture, + // "Module 'moduleName' method 'methodName' got '{0}' arguments, expected 'parameterCount'." + // jsArguments.Count)); + // + blockStatements[2] = Expression.IfThen( + Expression.NotEqual( + Expression.MakeMemberAccess(jsArgumentsParameter, s_countProperty), + Expression.Constant(parameterInfos.Length) + ), + Expression.Throw( + Expression.New( + s_newNativeArgumentParseException, + Expression.Call( + s_stringFormat, + Expression.Constant(CultureInfo.InvariantCulture), + Expression.Constant( + string.Format( + CultureInfo.InvariantCulture, + "Module '{0}' method '{1}' got '{{0}}' arguments, expected '{2}'.", + module.Name, + method.Name, + parameterInfos.Length) + ), + Expression.Convert( + Expression.MakeMemberAccess(jsArgumentsParameter, s_countProperty), + typeof(object) + ) + ), + Expression.Constant(jsArgumentsParameter.Name) + ) + ) + ); + + // + // p0 = Extract(jsArguments[0]); + // p1 = Extract(jsArguments[1]); + // ... + // pn = Extract(jsArguments[n]); + // + Array.Copy(extractExpressions, 0, blockStatements, 3, parameterInfos.Length); + + blockStatements[blockStatements.Length - 1] = Expression.Call( + Expression.Constant(module), + method, + parameterExpressions); + + return Expression.Lambda>( + Expression.Block(parameterExpressions, blockStatements), + catalystInstanceParameter, + jsArgumentsParameter + ); + } + + private static Expression GenerateExtractExpression( + Type type, + Expression leftExpression, + Expression tokenExpression, + Expression catalystInstanceExpression, + string parameterName, + string moduleName, + string methodName, + int argumentIndex) + { + // + // try + // { + // ... + // } + // catch (Exception ex) + // { + // throw new NativeArgumentParseException( + // string.Format( + // CultureInfo.InvariantCulture, + // "Error extracting argument for module 'moduleName' method 'methodName' at index '{0}'.", + // argumentIndex), + // paramName, + // ex); + // } + // + var catchBlock = Expression.Parameter(typeof(Exception), "ex").Let(ex => + Expression.Catch( + ex, + 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(parameterName), + ex + ) + ) + ) + ); + + var valueExpression = default(Expression); + if (type == typeof(ICallback)) + { + valueExpression = Expression.Parameter(typeof(int), "id").Let(id => + Expression.Block( + new[] { id }, + Expression.Assign( + id, + Expression.Call(s_valueInt, tokenExpression) + ), + Expression.New(s_newCallback, id, catalystInstanceExpression) + ) + ); + } + else + { + var toObjectMethod = s_toObjectGeneric.MakeGenericMethod(type); + valueExpression = Expression.Call(tokenExpression, toObjectMethod); + } + + return Expression.TryCatch( + Expression.Block( + typeof(void), + Expression.Assign(leftExpression, valueExpression) + ), + catchBlock + ); + } + + private static Expression CreateNullCheckExpression(ParameterExpression parameter) + where T : class + { + return Expression.IfThen( + Expression.Equal( + parameter, + Expression.Default(typeof(T)) + ), + Expression.Throw(Expression.New(s_newArgumentNullException, Expression.Constant(parameter.Name))) + ); + } + + 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/NativeModuleRegistry.cs b/ReactWindows/ReactNative/Bridge/NativeModuleRegistry.cs new file mode 100644 index 00000000000..9da026fe0ee --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/NativeModuleRegistry.cs @@ -0,0 +1,269 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using ReactNative.Tracing; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace ReactNative.Bridge +{ + /// + /// A set of native APIs exposed to a particular JavaScript instance. + /// + public sealed class NativeModuleRegistry + { + private readonly IReadOnlyList _moduleTable; + private readonly IReadOnlyDictionary _moduleInstances; + private readonly IList _batchCompleteListenerModules; + + private NativeModuleRegistry( + IReadOnlyList moduleTable, + IReadOnlyDictionary moduleInstances) + { + _moduleTable = moduleTable; + _moduleInstances = moduleInstances; + _batchCompleteListenerModules = _moduleTable + .Select(moduleDefinition => moduleDefinition.Target) + .OfType() + .ToList(); + } + + /// + /// The set of native modules exposed. + /// + public IEnumerable Modules + { + get + { + return _moduleInstances.Values; + } + } + + /// + /// Gets a module instance of a specific type. + /// + /// Type of module instance. + /// The module instance. + public T GetModule() where T : INativeModule + { + var instance = default(INativeModule); + if (_moduleInstances.TryGetValue(typeof(T), out instance)) + { + return (T)instance; + } + + throw new InvalidOperationException("No module instance for type '{0}'."); + } + + /// + /// Write the module descriptions to the given . + /// + /// The JSON writer. + public /* TODO: internal? */ void WriteModuleDescriptions(JsonWriter writer) + { + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "CreateJSON")) + { + writer.WriteStartObject(); + foreach (var moduleDef in _moduleTable) + { + writer.WritePropertyName(moduleDef.Name); + moduleDef.WriteModuleDescription(writer); + } + writer.WriteEndObject(); + } + } + + /// + /// Invoke a method on a native module. + /// + /// The catalyst instance. + /// The module ID. + /// The method ID. + /// The parameters. + internal /* TODO: public? */ void Invoke( + ICatalystInstance catalystInstance, + int moduleId, + int methodId, + JArray parameters) + { + if (moduleId < 0) + throw new ArgumentOutOfRangeException("Invalid module ID: " + moduleId, nameof(moduleId)); + if (_moduleTable.Count < moduleId) + throw new ArgumentOutOfRangeException("Call to unknown module: " + moduleId, nameof(moduleId)); + + _moduleTable[moduleId].Invoke(catalystInstance, methodId, parameters); + } + + /// + /// Hook to notify modules that the has + /// been initialized. + /// + internal /* TODO: public? */ void NotifyCatalystInstanceInitialize() + { + DispatcherHelpers.AssertOnDispatcher(); + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "NativeModuleRegistry_NotifyCatalystInstanceInitialize")) + { + foreach (var module in _moduleInstances.Values) + { + module.Initialize(); + } + } + } + + /// + /// Hook to notify modules that the has + /// been disposed. + /// + internal /* TODO: public? */ void NotifyCatalystInstanceDispose() + { + DispatcherHelpers.AssertOnDispatcher(); + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "NativeModuleRegistry_NotifyCatalystInstanceDestroy")) + { + foreach (var module in _moduleInstances.Values) + { + module.OnCatalystInstanceDispose(); + } + } + } + + class ModuleDefinition + { + private readonly int _id; + private readonly IList _methods; + + public ModuleDefinition(int id, string name, INativeModule target) + { + _id = id; + Name = name; + Target = target; + _methods = new List(target.Methods.Count); + + foreach (var entry in target.Methods) + { + _methods.Add( + new MethodRegistration( + entry.Key, + "NativeCall__" + target.Name + "_" + entry.Key, + entry.Value)); + } + } + + public int Id { get; } + + public string Name { get; } + + public INativeModule Target { get; } + + public void Invoke(ICatalystInstance catalystInstance, int methodId, JArray parameters) + { + _methods[methodId].Method.Invoke(catalystInstance, parameters); + } + + public void WriteModuleDescription(JsonWriter writer) + { + writer.WriteStartObject(); + writer.WritePropertyName("moduleID"); + writer.WriteValue(_id); + writer.WritePropertyName("methods"); + writer.WriteStartObject(); + for (var i = 0; i < _methods.Count; ++i) + { + var method = _methods[i]; + writer.WritePropertyName(method.Name); + writer.WriteStartObject(); + writer.WritePropertyName("methodID"); + writer.WriteValue(i); + writer.WritePropertyName("type"); + writer.WriteValue(method.Method.Type); + writer.WriteEndObject(); + } + writer.WriteEndObject(); + writer.WritePropertyName("constants"); + JObject.FromObject(Target.Constants).WriteTo(writer); + writer.WriteEndObject(); + } + + class MethodRegistration + { + public MethodRegistration(string name, string tracingName, INativeMethod method) + { + Name = name; + TracingName = tracingName; + Method = method; + } + + public string Name { get; } + + public string TracingName { get; } + + public INativeMethod Method { get; } + } + } + + /// + /// Builder for . + /// + public sealed class Builder + { + private readonly IDictionary _modules = + new Dictionary(); + + /// + /// Add a native module to the builder. + /// + /// The native module. + /// The builder instance. + public Builder Add(INativeModule module) + { + if (module == null) + throw new ArgumentNullException(nameof(module)); + if (module.Name == null) + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "Native module '{0}' cannot have a null `Name`.", + module.GetType()), + nameof(module)); + + var existing = default(INativeModule); + if (_modules.TryGetValue(module.Name, out existing) && !module.CanOverrideExistingModule) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Native module '{0}' tried to override '{1}' for module name '{2}'. " + + "If this was your intention, override `CanOverrideExistingModule`.", + module.GetType().Name, + existing.GetType().Name, + module.Name)); + + } + + _modules[module.Name] = module; + + return this; + } + + /// + /// Build a instance. + /// + /// The instance. + public NativeModuleRegistry Build() + { + var moduleTable = new List(_modules.Count); + var moduleInstances = new Dictionary(_modules.Count); + + var idx = 0; + foreach (var module in _modules.Values) + { + var moduleDef = new ModuleDefinition(idx++, module.Name, module); + moduleTable.Add(moduleDef); + moduleInstances.Add(module.GetType(), module); + } + + return new NativeModuleRegistry(moduleTable, moduleInstances); + } + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/Queue/CatalystQueueConfiguration.cs b/ReactWindows/ReactNative/Bridge/Queue/CatalystQueueConfiguration.cs new file mode 100644 index 00000000000..cb6480f893c --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/Queue/CatalystQueueConfiguration.cs @@ -0,0 +1,98 @@ +using System; + +namespace ReactNative.Bridge.Queue +{ + /// + /// Specifies which s must be used to run + /// the various contexts of execution within catalyst (dispatcher, native + /// modules, and JS). Some of these queue *may* be the same but should be + /// coded against as if they are different. + /// + class CatalystQueueConfiguration : ICatalystQueueConfiguration + { + private readonly MessageQueueThread _dispatcherQueueThread; + private readonly MessageQueueThread _nativeModulesQueueThread; + private readonly MessageQueueThread _jsQueueThread; + + private CatalystQueueConfiguration( + MessageQueueThread dispatcherQueueThread, + MessageQueueThread nativeModulesQueueThread, + MessageQueueThread jsQueueThread) + { + _dispatcherQueueThread = dispatcherQueueThread; + _nativeModulesQueueThread = nativeModulesQueueThread; + _jsQueueThread = jsQueueThread; + } + + /// + /// The main UI thread. + /// + public IMessageQueueThread DispatcherQueueThread + { + get + { + return _dispatcherQueueThread; + } + } + + /// + /// The native modules thread. + /// + public IMessageQueueThread NativeModulesQueueThread + { + get + { + return _nativeModulesQueueThread; + } + } + + /// + /// The JavaScript thread. + /// + public IMessageQueueThread JSQueueThread + { + get + { + return _jsQueueThread; + } + } + + /// + /// Disposes the queue configuration. + /// + /// + /// Should be called whenever the corresponding + /// is disposed. + /// + public void Dispose() + { + _dispatcherQueueThread.Dispose(); + _nativeModulesQueueThread.Dispose(); + _jsQueueThread.Dispose(); + } + + /// + /// Factory for the configuration. + /// + /// The configuration specification. + /// The exception handler. + /// The queue configuration. + public static CatalystQueueConfiguration Create( + CatalystQueueConfigurationSpec spec, + Action exceptionHandler) + { + var dispatcherThreadSpec = MessageQueueThreadSpec.DispatcherThreadSpec; + var dispatcherThread = MessageQueueThread.Create(dispatcherThreadSpec, exceptionHandler); + + var jsThread = spec.JSQueueThreadSpec != dispatcherThreadSpec + ? MessageQueueThread.Create(spec.JSQueueThreadSpec, exceptionHandler) + : dispatcherThread; + + var nativeModulesThread = spec.NativeModulesQueueThreadSpec != dispatcherThreadSpec + ? MessageQueueThread.Create(spec.NativeModulesQueueThreadSpec, exceptionHandler) + : dispatcherThread; + + return new CatalystQueueConfiguration(dispatcherThread, nativeModulesThread, jsThread); + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/Queue/CatalystQueueConfigurationSpec.cs b/ReactWindows/ReactNative/Bridge/Queue/CatalystQueueConfigurationSpec.cs new file mode 100644 index 00000000000..89a4284236b --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/Queue/CatalystQueueConfigurationSpec.cs @@ -0,0 +1,111 @@ +using System; + +namespace ReactNative.Bridge.Queue +{ + /// + /// Specification for creating a . + /// This exists so the is able to set + /// exception handlers on the s it uses. + /// + public sealed class CatalystQueueConfigurationSpec + { + private CatalystQueueConfigurationSpec( + MessageQueueThreadSpec nativeModulesQueueThreadSpec, + MessageQueueThreadSpec jsQueueThreadSpec) + { + NativeModulesQueueThreadSpec = nativeModulesQueueThreadSpec; + JSQueueThreadSpec = jsQueueThreadSpec; + } + + /// + /// The native modules specification. + /// + public MessageQueueThreadSpec NativeModulesQueueThreadSpec + { + get; + } + + /// + /// The JavaScript specification. + /// + public MessageQueueThreadSpec JSQueueThreadSpec + { + get; + } + + /// + /// The default instance. + /// + public static CatalystQueueConfigurationSpec Default { get; } = CreateDefault(); + + private static CatalystQueueConfigurationSpec CreateDefault() + { + return new Builder() + { + JSQueueThreadSpec = MessageQueueThreadSpec.Create("js", MessageQueueThreadKind.BackgroundSingleThread), + NativeModulesQueueThreadSpec = MessageQueueThreadSpec.Create("native_modules", MessageQueueThreadKind.BackgroundAnyThread), + } + .Build(); + } + + /// + /// Builder for . + /// + public sealed class Builder + { + private MessageQueueThreadSpec _nativeModulesQueueThreadSpec; + private MessageQueueThreadSpec _jsQueueThreadSpec; + + /// + /// Set the native modules . + /// + public MessageQueueThreadSpec NativeModulesQueueThreadSpec + { + set + { + if (_nativeModulesQueueThreadSpec != null) + { + throw new InvalidOperationException("Setting native modules queue thread spec multiple times!"); + } + + _nativeModulesQueueThreadSpec = value; + } + } + + /// + /// Set the JavaScript . + /// + public MessageQueueThreadSpec JSQueueThreadSpec + { + set + { + if (_jsQueueThreadSpec != null) + { + throw new InvalidOperationException("Setting native modules queue thread spec multiple times!"); + } + + _jsQueueThreadSpec = value; + } + } + + /// + /// Build the . + /// + /// The instance. + public CatalystQueueConfigurationSpec Build() + { + if (_nativeModulesQueueThreadSpec == null) + { + throw new InvalidOperationException("Native modules queue thread spec has not been set."); + } + + if (_jsQueueThreadSpec == null) + { + throw new InvalidOperationException("JS queue thread spec has not been set."); + } + + return new CatalystQueueConfigurationSpec(_nativeModulesQueueThreadSpec, _jsQueueThreadSpec); + } + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/Queue/ICatalystQueueConfiguration.cs b/ReactWindows/ReactNative/Bridge/Queue/ICatalystQueueConfiguration.cs new file mode 100644 index 00000000000..8babde85271 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/Queue/ICatalystQueueConfiguration.cs @@ -0,0 +1,28 @@ +using System; + +namespace ReactNative.Bridge.Queue +{ + /// + /// Specifies which s must be used to run + /// the various contexts of execution within catalyst (dispatcher, native + /// modules, and JS). Some of these queue *may* be the same but should be + /// coded against as if they are different. + /// + public interface ICatalystQueueConfiguration : IDisposable + { + /// + /// The main UI thread. + /// + IMessageQueueThread DispatcherQueueThread { get; } + + /// + /// The native modules thread. + /// + IMessageQueueThread NativeModulesQueueThread { get; } + + /// + /// The JavaScript thread. + /// + IMessageQueueThread JSQueueThread { get; } + } +} diff --git a/ReactWindows/ReactNative/Bridge/Queue/IMessageQueueThread.cs b/ReactWindows/ReactNative/Bridge/Queue/IMessageQueueThread.cs new file mode 100644 index 00000000000..0176805fc28 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/Queue/IMessageQueueThread.cs @@ -0,0 +1,30 @@ +using System; + +namespace ReactNative.Bridge.Queue +{ + /// + /// Encapsulates an action queue. + /// + public interface IMessageQueueThread + { + /// + /// Runs the given action on this thread. + /// + /// + /// The action will be submitted to the end of the event queue + /// even if it is being submitted from the same queue Thread. + /// + /// The action. + void RunOnQueue(Action action); + + /// + /// Checks whether the current thread is also the thread + /// associated with this . + /// + /// + /// true if the current thread is associated with this + /// instance, false otherwise. + /// + bool IsOnThread(); + } +} diff --git a/ReactWindows/ReactNative/Bridge/Queue/LimitedConcurrencyTaskScheduler.cs b/ReactWindows/ReactNative/Bridge/Queue/LimitedConcurrencyTaskScheduler.cs new file mode 100644 index 00000000000..1d2a193277e --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/Queue/LimitedConcurrencyTaskScheduler.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Windows.System.Threading; + +namespace ReactNative.Bridge.Queue +{ + /// + /// Provides a task scheduler that ensures a maximum concurrency level while + /// running on top of the thread pool. + /// + class LimitedConcurrencyLevelTaskScheduler : TaskScheduler + { + // Indicates whether the current thread is processing work items. + [ThreadStatic] + private static bool _currentThreadIsProcessingItems; + + // The list of tasks to be executed + private readonly LinkedList _tasks = new LinkedList(); // protected by lock(_tasks) + + // The maximum concurrency level allowed by this scheduler. + private readonly int _maxDegreeOfParallelism; + + // Indicates whether the scheduler is currently processing work items. + private int _delegatesQueuedOrRunning = 0; + + /// + /// Creates a new instance with the specified degree of parallelism. + /// + /// The degrees of parallelism. + public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) + { + if (maxDegreeOfParallelism < 1) + { + throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism)); + } + + _maxDegreeOfParallelism = maxDegreeOfParallelism; + } + + /// + /// Queues a task to the scheduler. + /// + /// The task to enqueue. + protected sealed override void QueueTask(Task task) + { + // Add the task to the list of tasks to be processed. If there aren't enough + // delegates currently queued or running to process tasks, schedule another. + lock (_tasks) + { + _tasks.AddLast(task); + if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism) + { + ++_delegatesQueuedOrRunning; + NotifyThreadPoolOfPendingWork(); + } + } + } + + /// + /// Inform the ThreadPool that there's work to be executed for this scheduler. + /// + private async void NotifyThreadPoolOfPendingWork() + { + await ThreadPool.RunAsync(_ => + { + // Note that the current thread is now processing work items. + // This is necessary to enable inlining of tasks into this thread. + _currentThreadIsProcessingItems = true; + + try + { + // Process all available items in the queue. + while (true) + { + Task item; + lock (_tasks) + { + // When there are no more items to be processed, + // note that we're done processing, and get out. + if (_tasks.Count == 0) + { + --_delegatesQueuedOrRunning; + break; + } + + // Get the next item from the queue + item = _tasks.First.Value; + _tasks.RemoveFirst(); + } + + // Execute the task we pulled out of the queue + TryExecuteTask(item); + } + } + // We're done processing items on the current thread + finally + { + _currentThreadIsProcessingItems = false; + } + }, + WorkItemPriority.Normal); + } + + /// + /// Attempts to execute the specified task on the current thread. + /// + /// The task to execute. + /// Task queue flag. + /// An indicator if the task was executed inline. + protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + // If this thread isn't already processing a task, we don't support inlining + if (!_currentThreadIsProcessingItems) + { + return false; + } + + // If the task was previously queued, remove it from the queue + if (taskWasPreviouslyQueued) + { + // Try to run the task. + if (TryDequeue(task)) + { + return TryExecuteTask(task); + } + else + { + return false; + } + } + else + { + return TryExecuteTask(task); + } + } + + /// + /// Attempt to remove a previously scheduled task from the scheduler. + /// + /// The task to dequeue. + /// An indicator if the task was dequeued. + protected sealed override bool TryDequeue(Task task) + { + lock (_tasks) + { + return _tasks.Remove(task); + } + } + + /// + /// Gets the maximum concurrency level supported by this scheduler. + /// + public sealed override int MaximumConcurrencyLevel { get { return _maxDegreeOfParallelism; } } + + /// + /// Gets an enumerable of the tasks currently scheduled on this scheduler. + /// + /// + protected sealed override IEnumerable GetScheduledTasks() + { + bool lockTaken = false; + try + { + Monitor.TryEnter(_tasks, ref lockTaken); + if (lockTaken) + { + return _tasks; + } + else + { + throw new NotSupportedException(); + } + } + finally + { + if (lockTaken) + { + Monitor.Exit(_tasks); + } + } + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThread.cs b/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThread.cs new file mode 100644 index 00000000000..8afacc2f35f --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThread.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.System.Threading; +using Windows.UI.Core; + +namespace ReactNative.Bridge.Queue +{ + /// + /// Encapsulates an action queue. + /// + public abstract class MessageQueueThread : IMessageQueueThread, IDisposable + { + private bool _disposed; + + private MessageQueueThread() { } + + /// + /// Flags if the is disposed. + /// + protected bool IsDisposed + { + get + { + return _disposed; + } + } + + /// + /// Checks if the caller is running on the queue instance. + /// + /// + /// true if the caller is calling from the queue, false + /// otherwise. + /// + public bool IsOnThread() + { + AssertNotDisposed(); + + return IsOnThreadCore(); + } + + /// + /// Queues an action to run. + /// + /// The action. + public void RunOnQueue(Action action) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + AssertNotDisposed(); + + Enqueue(action); + } + + /// + /// Disposes the action queue. + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Enqueues the action. + /// + /// The action. + protected abstract void Enqueue(Action action); + + /// + /// Checks if the caller is running on the queue instance. + /// + /// + /// true if the caller is calling from the queue, false + /// otherwise. + /// + protected abstract bool IsOnThreadCore(); + + /// + /// Disposes the action queue. + /// + /// + /// false if dispose was triggered by a finalizer, true + /// otherwise. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + } + } + + private void AssertNotDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException("this"); + } + } + + /// + /// Factory to create the action queue. + /// + /// The action queue specification. + /// The exception handler. + /// The action queue instance. + public static MessageQueueThread Create( + MessageQueueThreadSpec spec, + Action handler) + { + if (spec == null) + throw new ArgumentNullException(nameof(spec)); + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + switch (spec.Kind) + { + case MessageQueueThreadKind.DispatcherThread: + return new DispatcherMessageQueueThread(spec.Name, handler); + case MessageQueueThreadKind.BackgroundSingleThread: + return new SingleBackgroundMessageQueueThread(spec.Name, handler); + case MessageQueueThreadKind.BackgroundAnyThread: + return new AnyBackgroundMessageQueueThread(spec.Name, handler); + default: + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Unknown thread type '{0}' with name '{1}'.", + spec.Kind, + spec.Name)); + } + } + + class DispatcherMessageQueueThread : MessageQueueThread + { + private readonly string _name; + private readonly Subject _actionObservable; + private readonly IDisposable _subscription; + + public DispatcherMessageQueueThread(string name, Action handler) + { + _name = name; + _actionObservable = new Subject(); + _subscription = _actionObservable + .ObserveOnDispatcher() + .Subscribe(action => + { + try + { + action(); + } + catch (Exception ex) + { + handler(ex); + } + }); + } + + protected override void Enqueue(Action action) + { + _actionObservable.OnNext(action); + } + + protected override bool IsOnThreadCore() + { + return CoreWindow.GetForCurrentThread().Dispatcher != null; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _actionObservable.Dispose(); + _subscription.Dispose(); + } + } + + class SingleBackgroundMessageQueueThread : MessageQueueThread + { + private static readonly Action s_canary = new Action(() => { }); + + private readonly string _name; + private readonly Action _handler; + private readonly BlockingCollection _queue; + private readonly ThreadLocal _indicator; + private readonly IAsyncAction _asyncAction; + + public SingleBackgroundMessageQueueThread(string name, Action handler) + { + _name = name; + _handler = handler; + _queue = new BlockingCollection(); + _indicator = new ThreadLocal(); + _asyncAction = ThreadPool.RunAsync(_ => + { + _indicator.Value = true; + Run(); + }, + WorkItemPriority.Normal); + } + + protected override bool IsOnThreadCore() + { + return _indicator.Value; + } + + protected override void Enqueue(Action action) + { + _queue.Add(action); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + // Unblock the background thread. + Enqueue(s_canary); + } + + private void Run() + { + while (true) + { + var action = _queue.Take(); + if (IsDisposed) + { + break; + } + + try + { + action(); + } + catch (Exception ex) + { + _handler(ex); + } + } + } + } + + class AnyBackgroundMessageQueueThread : MessageQueueThread + { + private readonly string _name; + private readonly Action _handler; + private readonly TaskScheduler _taskScheduler; + private readonly TaskFactory _taskFactory; + + public AnyBackgroundMessageQueueThread(string name, Action handler) + { + _name = name; + _handler = handler; + _taskScheduler = new LimitedConcurrencyLevelTaskScheduler(1); + _taskFactory = new TaskFactory(_taskScheduler); + } + + protected override async void Enqueue(Action action) + { + await _taskFactory.StartNew(() => + { + try + { + action(); + } + catch (Exception ex) + { + _handler(ex); + } + }); + } + + protected override bool IsOnThreadCore() + { + return TaskScheduler.Current == _taskScheduler; + } + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThreadExtensions.cs b/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThreadExtensions.cs new file mode 100644 index 00000000000..d7a9d129b6e --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThreadExtensions.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; + +namespace ReactNative.Bridge.Queue +{ + /// + /// Extension methods for s. + /// + public static class MessageQueueThreadExtensions + { + /// + /// Asserts , throwing if the false. + /// + /// The message queue thread. + /// + /// Thrown if the assertion fails. + /// + public static void AssertIsOnThread(this IMessageQueueThread actionQueue) + { + if (!actionQueue.IsOnThread()) + { + throw new InvalidOperationException("Thread access assertion failed."); + } + } + + /// + /// Calls a function on a message queue and returns a task to await the response. + /// + /// Type of response. + /// The message queue thread. + /// The function. + /// A task to await the result. + public static Task CallOnQueue(this IMessageQueueThread actionQueue, Func func) + { + var taskCompletionSource = new TaskCompletionSource(); + + actionQueue.RunOnQueue(async () => + { + var result = func(); + + // TaskCompletionSource.SetResult can call continuations + // on the awaiter of the task completion source. + await Task.Run(() => taskCompletionSource.SetResult(result)); + }); + + return taskCompletionSource.Task; + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThreadKind.cs b/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThreadKind.cs new file mode 100644 index 00000000000..1a6f582dba5 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThreadKind.cs @@ -0,0 +1,24 @@ +namespace ReactNative.Bridge.Queue +{ + /// + /// Types of . + /// + public enum MessageQueueThreadKind + { + /// + /// Dispatcher thread type. + /// + DispatcherThread, + + /// + /// Single background thread type. + /// + BackgroundSingleThread, + + /// + /// Any background thread type. + /// + BackgroundAnyThread, + } + +} diff --git a/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThreadSpec.cs b/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThreadSpec.cs new file mode 100644 index 00000000000..add7d2d10ad --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/Queue/MessageQueueThreadSpec.cs @@ -0,0 +1,47 @@ +using System; + +namespace ReactNative.Bridge.Queue +{ + /// + /// Specification for creating a . + /// + public class MessageQueueThreadSpec + { + private MessageQueueThreadSpec(MessageQueueThreadKind kind, string name) + { + Name = name; + Kind = kind; + } + + /// + /// The name of the . + /// + public string Name { get; } + + /// + /// The type of the . + /// + internal MessageQueueThreadKind Kind { get; } + + /// + /// Singleton dispatcher specification. + /// + public static MessageQueueThreadSpec DispatcherThreadSpec { get; } = new MessageQueueThreadSpec(MessageQueueThreadKind.DispatcherThread, "main_ui"); + + /// + /// Factory for creating s. + /// + /// The name. + /// The kind. + /// The instance. + public static MessageQueueThreadSpec Create(string name, MessageQueueThreadKind kind) + { + if (kind == MessageQueueThreadKind.DispatcherThread) + { + throw new NotSupportedException("Use the singleton MainUiThreadSpec instance."); + } + + return new MessageQueueThreadSpec(kind, name); + } + } +} diff --git a/ReactWindows/ReactNative/Bridge/ReactContext.cs b/ReactWindows/ReactNative/Bridge/ReactContext.cs new file mode 100644 index 00000000000..de402ead173 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/ReactContext.cs @@ -0,0 +1,6 @@ +namespace ReactNative.Bridge +{ + public class ReactContext + { + } +} diff --git a/ReactWindows/ReactNative/Bridge/ReactContextNativeModuleBase.cs b/ReactWindows/ReactNative/Bridge/ReactContextNativeModuleBase.cs new file mode 100644 index 00000000000..4a97b8b6b70 --- /dev/null +++ b/ReactWindows/ReactNative/Bridge/ReactContextNativeModuleBase.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ReactNative.Bridge +{ + class ReactContextNativeModuleBase + { + } +} diff --git a/ReactWindows/ReactNative/Common/ReactConstants.cs b/ReactWindows/ReactNative/Common/ReactConstants.cs new file mode 100644 index 00000000000..07523c5c7fd --- /dev/null +++ b/ReactWindows/ReactNative/Common/ReactConstants.cs @@ -0,0 +1,13 @@ +namespace ReactNative.Common +{ + /// + /// Set of constants used. + /// + public static class ReactConstants + { + /// + /// Trace tag for React components. + /// + public const string Tag = "React"; + } +} diff --git a/ReactWindows/ReactNative/Core/ReactInstanceManager.cs b/ReactWindows/ReactNative/Core/ReactInstanceManager.cs new file mode 100644 index 00000000000..07ce79aa759 --- /dev/null +++ b/ReactWindows/ReactNative/Core/ReactInstanceManager.cs @@ -0,0 +1,61 @@ + +namespace ReactNative.Core +{ + using ReactNative.Bridge; + + /// + /// This class is managing instances of {@link CatalystInstance}. It expose a way to configure + /// catalyst instance using {@link ReactPackage} and keeps track of the lifecycle of that + /// instance.It also sets up connection between the instance and developers support functionality + /// of the framework. + /// + /// An instance of this manager is required to start JS application in { @link ReactRootView} (see + /// {@link ReactRootView#startReactApplication} for more info). + /// + /// The lifecycle of the instance of {@link ReactInstanceManager} should be bound to the activity + /// that owns the { @link ReactRootView } that is used to render react application using this + /// instance manager (see {@link ReactRootView#startReactApplication}). It's required to pass + /// owning activity's lifecycle events to the instance manager (see {@link #onPause}, + /// {@link #onDestroy} and {@link #onResume}). + /// + public abstract class ReactInstanceManager + { + public interface ReactInstanceEventListener + { + /** + * Called when the react context is initialized (all modules registered). Always called on the + * UI thread. + */ + void onReactContextInitialized(ReactContext context); + } + + /// + /// Trigger react context initialization asynchronously in a background async task. + /// + public abstract void createReactContextInBackground(); + + /// + /// return whether createReactContextInBackground has been called + /// + /// + public abstract bool hasStartedCreatingInitialContext(); + + /// + /// Gets the URL where the last bundle was loaded from. + /// + /// URL where the last bundle was loaded from. + public abstract string getSourceUrl(); + + /// + /// Attach given {@param rootView} to a catalyst instance manager and start JS application + /// + /// + public abstract void attachMeasuredRootView(ReactRootView rootView); + + /// + /// Detach given rootView from current catalyst instance. + /// + /// + public abstract void detachRootView(ReactRootView rootView); + } +} diff --git a/ReactWindows/ReactNative/Core/ReactRootView.cs b/ReactWindows/ReactNative/Core/ReactRootView.cs new file mode 100644 index 00000000000..36f6c7c474b --- /dev/null +++ b/ReactWindows/ReactNative/Core/ReactRootView.cs @@ -0,0 +1,45 @@ + +namespace ReactNative.Core +{ + using Newtonsoft.Json.Linq; + using ReactNative.UIManager; + using System; + + /// + /// Default root view for catalyst apps. Provides the ability to listen for size changes so that a UI manager can re-layout its elements + /// + public class ReactRootView : SizeMonitoringFrameLayout, RootView + { + private ReactInstanceManager mReactInstanceManager; + private string mJSModuleName; + private bool mIsAttachedToWindow = false; + private bool mIsAttachedToInstance = false; + + public void startReactApplication(ReactInstanceManager reactInstanceManager, string moduleName) + { + //TODO: Add thread queue impl hook + //UiThreadUtil.assertOnUiThread(); + + mReactInstanceManager = reactInstanceManager; + mJSModuleName = moduleName; + + if (!mReactInstanceManager.hasStartedCreatingInitialContext()) + { + mReactInstanceManager.createReactContextInBackground(); + } + } + + //TODO: Implement this method for emitting events + private void sendEvent(string eventName, JArray parameters) + { + throw new NotImplementedException(); + + if (mReactInstanceManager != null) + { + // mReactInstanceManager.getCurrentReactContext() + // .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + //.emit(eventName, params); + } + } + } +} diff --git a/ReactWindows/ReactNative/Hosting/Bridge/ChakraJavaScriptExecutor.cs b/ReactWindows/ReactNative/Hosting/Bridge/ChakraJavaScriptExecutor.cs new file mode 100644 index 00000000000..18ecaaded5c --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/Bridge/ChakraJavaScriptExecutor.cs @@ -0,0 +1,133 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using System; +using System.Diagnostics; + +namespace ReactNative.Hosting.Bridge +{ + class ChakraJavaScriptExecutor : IJavaScriptExecutor + { + private readonly JavaScriptRuntime _runtime; + private readonly JavaScriptValue _globalObject; + private readonly JavaScriptValue _requireFunction; + + public ChakraJavaScriptExecutor() + { + _runtime = JavaScriptRuntime.Create(); + _globalObject = JavaScriptValue.GlobalObject; + var requireId = JavaScriptPropertyId.FromString("require"); + _requireFunction = _globalObject.GetProperty(requireId); + + InitializeChakra(); + + // TODO: resolve how to inject React JavaScript library + } + + public JToken Call(string moduleName, string methodName, JArray arguments) + { + // Get the module + var moduleString = JavaScriptValue.FromString(moduleName); + var requireArguments = new[] { _globalObject, moduleString }; + var module = _requireFunction.CallFunction(requireArguments); + + // Get the method + var propertyId = JavaScriptPropertyId.FromString(methodName); + var method = module.GetProperty(propertyId); + + // Set up the arguments to pass in + var callArguments = new JavaScriptValue[arguments.Count + 1]; + callArguments[0] = _globalObject; // TODO: What is first argument? + + for (var i = 0; i < arguments.Count; ++i) + { + callArguments[i + 1] = JTokenToJavaScriptValueConverter.Convert(arguments[i]); + } + + // Invoke the function + var result = method.CallFunction(callArguments); + + // Convert the result + return JavaScriptValueToJTokenConverter.Convert(result); + } + + public void SetGlobalVariable(string propertyName, JToken value) + { + var javaScriptValue = JTokenToJavaScriptValueConverter.Convert(value); + var propertyId = JavaScriptPropertyId.FromString(propertyName); + _globalObject.SetProperty(propertyId, javaScriptValue, true); + } + + private void InitializeChakra() + { + // Set the current context + var context = _runtime.CreateContext(); + JavaScriptContext.Current = context; + + // Set the WinRT namespace (TODO: needed?) + Native.ThrowIfError( + Native.JsProjectWinRTNamespace("Windows")); + +#if DEBUG + // Start debugging. + JavaScriptContext.StartDebugging(); +#endif + + var consolePropertyId = default(JavaScriptPropertyId); + Native.ThrowIfError( + Native.JsGetPropertyIdFromName("console", out consolePropertyId)); + + var consoleObject = JavaScriptValue.CreateObject(); + _globalObject.SetProperty(consolePropertyId, consoleObject, true); + + DefineHostCallback(consoleObject, "log", ConsoleCallback, IntPtr.Zero); + DefineHostCallback(consoleObject, "warn", ConsoleCallback, IntPtr.Zero); + DefineHostCallback(consoleObject, "error", ConsoleCallback, IntPtr.Zero); + + Debug.WriteLine("Chakra initialization successful."); + } + + private static void DefineHostCallback( + JavaScriptValue obj, + string callbackName, + JavaScriptNativeFunction callback, + IntPtr callbackData) + { + var propertyId = JavaScriptPropertyId.FromString(callbackName); + var function = JavaScriptValue.CreateFunction(callback, callbackData); + obj.SetProperty(propertyId, function, true); + } + + private static JavaScriptValue ConsoleCallback( + JavaScriptValue callee, + bool isConstructCall, + JavaScriptValue[] arguments, + ushort argumentCount, + IntPtr callbackData) + { + try + { + Debug.Write("JS console> "); + + // First argument is this-context (? @TODO), ignore... + foreach (var argument in arguments) + { + Debug.Write(argument.ToString() + " "); + } + + Debug.WriteLine(""); + } + catch (Exception ex) + { + Debug.WriteLine( + "#EXCEPTION in ChakraExecutor::ConsoleCallback()! " + ex.Message); + } + + return JavaScriptValue.Invalid; + } + + public void Dispose() + { + _runtime.Dispose(); + } + } +} diff --git a/ReactWindows/ReactNative/Hosting/Bridge/JTokenToJavaScriptValueConverter.cs b/ReactWindows/ReactNative/Hosting/Bridge/JTokenToJavaScriptValueConverter.cs new file mode 100644 index 00000000000..f2931444b97 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/Bridge/JTokenToJavaScriptValueConverter.cs @@ -0,0 +1,117 @@ +using Newtonsoft.Json.Linq; +using System; + +namespace ReactNative.Hosting.Bridge +{ + sealed class JTokenToJavaScriptValueConverter + { + private static readonly JTokenToJavaScriptValueConverter s_instance = + new JTokenToJavaScriptValueConverter(); + + private JTokenToJavaScriptValueConverter() { } + + public static JavaScriptValue Convert(JToken token) + { + return s_instance.Visit(token); + } + + private JavaScriptValue Visit(JToken token) + { + if (token == null) + throw new ArgumentNullException(nameof(token)); + + switch (token.Type) + { + case JTokenType.Array: + return VisitArray((JArray)token); + case JTokenType.Boolean: + return VisitBoolean((JValue)token); + case JTokenType.Float: + return VisitFloat((JValue)token); + case JTokenType.Integer: + return VisitInteger((JValue)token); + case JTokenType.Null: + return VisitNull(token); + case JTokenType.Object: + return VisitObject((JObject)token); + case JTokenType.String: + return VisitString((JValue)token); + case JTokenType.Undefined: + return VisitUndefined(token); + case JTokenType.Constructor: + case JTokenType.Property: + case JTokenType.Comment: + case JTokenType.Date: + case JTokenType.Raw: + case JTokenType.Bytes: + case JTokenType.Guid: + case JTokenType.Uri: + case JTokenType.TimeSpan: + case JTokenType.None: + default: + throw new NotSupportedException(); + } + } + + private JavaScriptValue VisitArray(JArray token) + { + var n = token.Count; + var values = new JavaScriptValue[n]; + for (var i = 0; i < n; ++i) + { + values[i] = Visit(token[i]); + } + + var array = JavaScriptValue.CreateArray((uint)n); + for (var i = 0; i < n; ++i) + { + array.SetIndexedProperty(JavaScriptValue.FromInt32(i), values[i]); + } + + return array; + } + + private JavaScriptValue VisitBoolean(JValue token) + { + return JavaScriptValue.FromBoolean(token.Value()); + } + + private JavaScriptValue VisitFloat(JValue token) + { + return JavaScriptValue.FromDouble(token.Value()); + } + + private JavaScriptValue VisitInteger(JValue token) + { + return JavaScriptValue.FromDouble(token.Value()); + } + + private JavaScriptValue VisitNull(JToken token) + { + return JavaScriptValue.Null; + } + + private JavaScriptValue VisitObject(JObject token) + { + var jsonObject = JavaScriptValue.CreateObject(); + foreach (var entry in token) + { + var value = Visit(entry.Value); + var propertyId = JavaScriptPropertyId.FromString(entry.Key); + jsonObject.SetProperty(propertyId, value, true); + } + + return jsonObject; + } + + private JavaScriptValue VisitString(JValue token) + { + return JavaScriptValue.FromString(token.Value()); + } + + private JavaScriptValue VisitUndefined(JToken token) + { + return JavaScriptValue.Undefined; + } + } +} diff --git a/ReactWindows/ReactNative/Hosting/Bridge/JavaScriptValueToJTokenConverter.cs b/ReactWindows/ReactNative/Hosting/Bridge/JavaScriptValueToJTokenConverter.cs new file mode 100644 index 00000000000..a92b17dcf2f --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/Bridge/JavaScriptValueToJTokenConverter.cs @@ -0,0 +1,108 @@ +using Newtonsoft.Json.Linq; +using System; + +namespace ReactNative.Hosting.Bridge +{ + sealed class JavaScriptValueToJTokenConverter + { + private static readonly JToken s_true = new JValue(true); + private static readonly JToken s_false = new JValue(false); + private static readonly JToken s_null = JValue.CreateNull(); + private static readonly JToken s_undefined = JValue.CreateUndefined(); + + private static readonly JavaScriptValueToJTokenConverter s_instance = + new JavaScriptValueToJTokenConverter(); + + private JavaScriptValueToJTokenConverter() { } + + public static JToken Convert(JavaScriptValue value) + { + return s_instance.Visit(value); + } + + private JToken Visit(JavaScriptValue value) + { + switch (value.ValueType) + { + case JavaScriptValueType.Array: + return VisitArray(value); + case JavaScriptValueType.Boolean: + return VisitBoolean(value); + case JavaScriptValueType.Null: + return VisitNull(value); + case JavaScriptValueType.Number: + return VisitNumber(value); + case JavaScriptValueType.Object: + return VisitObject(value); + case JavaScriptValueType.String: + return VisitString(value); + case JavaScriptValueType.Undefined: + return VisitUndefined(value); + case JavaScriptValueType.Function: + case JavaScriptValueType.Error: + default: + throw new NotSupportedException(); + } + } + + private JToken VisitArray(JavaScriptValue value) + { + var count = 0; + var array = new JArray(); + while (true) + { + var index = JavaScriptValue.FromInt32(count++); + if (!value.HasIndexedProperty(index)) + { + var element = value.GetIndexedProperty(index); + array.Add(Visit(element)); + } + else + { + break; + } + } + + return array; + } + + private JToken VisitBoolean(JavaScriptValue value) + { + return value.ToBoolean() ? s_true : s_false; + } + + private JToken VisitNull(JavaScriptValue value) + { + return s_null; + } + + private JToken VisitNumber(JavaScriptValue value) + { + return JToken.FromObject(value.ToObject()); + } + + private JToken VisitObject(JavaScriptValue value) + { + var jsonObject = new JObject(); + var properties = Visit(value.GetOwnPropertyNames()).ToObject(); + foreach (var property in properties) + { + var propertyId = JavaScriptPropertyId.FromString(property); + var propertyValue = value.GetProperty(propertyId); + jsonObject.Add(property, Visit(propertyValue)); + } + + return jsonObject; + } + + private JToken VisitString(JavaScriptValue value) + { + return JValue.CreateString(value.ToString()); + } + + private JToken VisitUndefined(JavaScriptValue value) + { + return s_undefined; + } + } +} diff --git a/ReactWindows/ReactNative/Hosting/Bridge/ReactBridge.cs b/ReactWindows/ReactNative/Hosting/Bridge/ReactBridge.cs new file mode 100644 index 00000000000..c49b3d9195e --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/Bridge/ReactBridge.cs @@ -0,0 +1,153 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using ReactNative.Bridge.Queue; + +namespace ReactNative.Hosting.Bridge +{ + /// + /// Class to the JavaScript execution environment and means of transport + /// for messages between JavaScript and the native environment. + /// + public class ReactBridge : IReactBridge + { + private readonly IJavaScriptExecutor _jsExecutor; + private readonly IReactCallback _reactCallback; + private readonly IMessageQueueThread _nativeModulesQueueThread; + + /// + /// Instantiates the . + /// + /// The JavaScript executor. + /// The native callback handler. + /// + /// The native modules queue thread. + /// + public ReactBridge( + IJavaScriptExecutor jsExecutor, + IReactCallback reactCallback, + IMessageQueueThread nativeModulesQueueThread) + { + _jsExecutor = jsExecutor; + _reactCallback = reactCallback; + _nativeModulesQueueThread = nativeModulesQueueThread; + } + + /// + /// Calls a JavaScript function. + /// + /// The module ID. + /// The method ID. + /// The arguments. + public void CallFunction(int moduleId, int methodId, JArray arguments) + { + var allArgs = new JArray + { + moduleId, + methodId, + arguments, + }; + + var message = new JObject + { + { "module", "BatchedBridge" }, + { "method", "callFunctionReturnFlushedQueue" }, + { "context", 15 }, + { "args", allArgs }, + }; + + var messageArray = new JArray + { + message, + }; + + // TODO: actually introduce batching here... + var processBatchArgs = new JArray + { + messageArray, + }; + + var response = _jsExecutor.Call("BatchedBridge", "processBatch", processBatchArgs); + + ProcessResponse(response); + } + + /// + /// Invokes a JavaScript callback. + /// + /// The callback ID. + /// The arguments. + public void InvokeCallback(int callbackId, JArray arguments) + { + var allArgs = new JArray + { + callbackId, + arguments, + }; + + var message = new JObject + { + { "module", "BatchedBridge" }, + { "method", "invokeCallbackAndReturnFlushedQueue" }, + { "args", allArgs }, + }; + + var messageArray = new JArray + { + message, + }; + + var processBatchArgs = new JArray + { + messageArray, + }; + + var response = _jsExecutor.Call("BatchedBridge", "processBatch", processBatchArgs); + + ProcessResponse(response); + } + + /// + /// Sets a global JavaScript variable. + /// + /// The property name. + /// The JSON-encoded value. + public void SetGlobalVariable(string propertyName, string jsonEncodedArgument) + { + _jsExecutor.SetGlobalVariable(propertyName, JToken.Parse(jsonEncodedArgument)); + } + + /// + /// Disposes the bridge. + /// + public void Dispose() + { + } + + private void ProcessResponse(JToken response) + { + var messages = response as JArray; + if (messages == null) + { + return; + } + + var moduleIds = messages[0].ToObject(); + var methodIds = messages[0].ToObject(); + var paramsArray = (JArray)messages[2]; + + _nativeModulesQueueThread.RunOnQueue(() => + { + for (var i = 0; i < moduleIds.Length; i++) + { + var moduleId = moduleIds[i]; + var methodId = methodIds[i]; + var args = (JArray)paramsArray[i]; + + _reactCallback.Invoke(moduleId, methodId, args); + }; + + _reactCallback.OnBatchComplete(); + }); + } + } +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptBackgroundWorkItemCallback.cs b/ReactWindows/ReactNative/Hosting/JavaScriptBackgroundWorkItemCallback.cs new file mode 100644 index 00000000000..18ac1aa4536 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptBackgroundWorkItemCallback.cs @@ -0,0 +1,14 @@ +namespace ReactNative.Hosting +{ + using System; + + /// + /// A background work item callback. + /// + /// + /// This is passed to the host's thread service (if provided) to allow the host to + /// invoke the work item callback on the background thread of its choice. + /// + /// Data argument passed to the thread service. + public delegate void JavaScriptBackgroundWorkItemCallback(IntPtr callbackData); +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptBeforeCollectCallback.cs b/ReactWindows/ReactNative/Hosting/JavaScriptBeforeCollectCallback.cs new file mode 100644 index 00000000000..06643a89dba --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptBeforeCollectCallback.cs @@ -0,0 +1,10 @@ +namespace ReactNative.Hosting +{ + using System; + + /// + /// A callback called before collection. + /// + /// The state passed to SetBeforeCollectCallback. + public delegate void JavaScriptBeforeCollectCallback(IntPtr callbackState); +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptContext.cs b/ReactWindows/ReactNative/Hosting/JavaScriptContext.cs new file mode 100644 index 00000000000..bfa6ca20f84 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptContext.cs @@ -0,0 +1,489 @@ +namespace ReactNative.Hosting +{ + using System; + + /// + /// A script context. + /// + /// + /// + /// Each script context contains its own global object, distinct from the global object in + /// other script contexts. + /// + /// + /// Many Chakra hosting APIs require an "active" script context, which can be set using + /// Current. Chakra hosting APIs that require a current context to be set will note + /// that explicitly in their documentation. + /// + /// + public struct JavaScriptContext + { + /// + /// The reference. + /// + private readonly IntPtr reference; + + /// + /// Initializes a new instance of the struct. + /// + /// The reference. + internal JavaScriptContext(IntPtr reference) + { + this.reference = reference; + } + + /// + /// Gets an invalid context. + /// + public static JavaScriptContext Invalid + { + get { return new JavaScriptContext(IntPtr.Zero); } + } + + /// + /// Gets or sets the current script context on the thread. + /// + public static JavaScriptContext Current + { + get + { + JavaScriptContext reference; + Native.ThrowIfError(Native.JsGetCurrentContext(out reference)); + return reference; + } + + set + { + Native.ThrowIfError(Native.JsSetCurrentContext(value)); + } + } + + /// + /// Gets a value indicating whether the runtime of the current context is in an exception state. + /// + /// + /// + /// If a call into the runtime results in an exception (either as the result of running a + /// script or due to something like a conversion failure), the runtime is placed into an + /// "exception state." All calls into any context created by the runtime (except for the + /// exception APIs) will fail with InExceptionState until the exception is + /// cleared. + /// + /// + /// If the runtime of the current context is in the exception state when a callback returns + /// into the engine, the engine will automatically rethrow the exception. + /// + /// + /// Requires an active script context. + /// + /// + public static bool HasException + { + get + { + bool hasException; + Native.ThrowIfError(Native.JsHasException(out hasException)); + return hasException; + } + } + + /// + /// Gets a value indicating whether the heap of the current context is being enumerated. + /// + /// + /// Requires an active script context. + /// + public static bool IsEnumeratingHeap + { + get + { + bool isEnumerating; + Native.ThrowIfError(Native.JsIsEnumeratingHeap(out isEnumerating)); + return isEnumerating; + } + } + + /// + /// Gets the runtime that the context belongs to. + /// + public JavaScriptRuntime Runtime + { + get + { + JavaScriptRuntime handle; + Native.ThrowIfError(Native.JsGetRuntime(this, out handle)); + return handle; + } + } + + /// + /// Gets a value indicating whether the context is a valid context or not. + /// + public bool IsValid + { + get { return reference != IntPtr.Zero; } + } + + /// + /// Tells the runtime to do any idle processing it need to do. + /// + /// + /// + /// If idle processing has been enabled for the current runtime, calling Idle will + /// inform the current runtime that the host is idle and that the runtime can perform + /// memory cleanup tasks. + /// + /// + /// Idle will also return the number of system ticks until there will be more idle work + /// for the runtime to do. Calling Idle before this number of ticks has passed will do + /// no work. + /// + /// + /// Requires an active script context. + /// + /// + /// + /// The next system tick when there will be more idle work to do. Returns the + /// maximum number of ticks if there no upcoming idle work to do. + /// + public static uint Idle() + { + uint ticks; + Native.ThrowIfError(Native.JsIdle(out ticks)); + return ticks; + } + + /// + /// Parses a script and returns a Function representing the script. + /// + /// + /// Requires an active script context. + /// + /// The script to parse. + /// + /// A cookie identifying the script that can be used by script contexts that have debugging enabled. + /// + /// The location the script came from. + /// A Function representing the script code. + public static JavaScriptValue ParseScript(string script, JavaScriptSourceContext sourceContext, string sourceName) + { + JavaScriptValue result; + Native.ThrowIfError(Native.JsParseScript(script, sourceContext, sourceName, out result)); + return result; + } + + /// + /// Parses a serialized script and returns a Function representing the script. + /// + /// + /// Requires an active script context. + /// + /// The script to parse. + /// The serialized script. + /// + /// A cookie identifying the script that can be used by script contexts that have debugging enabled. + /// + /// The location the script came from. + /// A Function representing the script code. + public static JavaScriptValue ParseScript(string script, byte[] buffer, JavaScriptSourceContext sourceContext, string sourceName) + { + JavaScriptValue result; + Native.ThrowIfError(Native.JsParseSerializedScript(script, buffer, sourceContext, sourceName, out result)); + return result; + } + + /// + /// Parses a script and returns a Function representing the script. + /// + /// + /// Requires an active script context. + /// + /// The script to parse. + /// A Function representing the script code. + public static JavaScriptValue ParseScript(string script) + { + return ParseScript(script, JavaScriptSourceContext.None, string.Empty); + } + + /// + /// Parses a serialized script and returns a Function representing the script. + /// + /// + /// Requires an active script context. + /// + /// The script to parse. + /// The serialized script. + /// A Function representing the script code. + public static JavaScriptValue ParseScript(string script, byte[] buffer) + { + return ParseScript(script, buffer, JavaScriptSourceContext.None, string.Empty); + } + + /// + /// Executes a script. + /// + /// + /// Requires an active script context. + /// + /// The script to run. + /// + /// A cookie identifying the script that can be used by script contexts that have debugging enabled. + /// + /// The location the script came from. + /// The result of the script, if any. + public static JavaScriptValue RunScript(string script, JavaScriptSourceContext sourceContext, string sourceName) + { + JavaScriptValue result; + Native.ThrowIfError(Native.JsRunScript(script, sourceContext, sourceName, out result)); + return result; + } + + /// + /// Runs a serialized script. + /// + /// + /// Requires an active script context. + /// + /// The source code of the serialized script. + /// The serialized script. + /// + /// A cookie identifying the script that can be used by script contexts that have debugging enabled. + /// + /// The location the script came from. + /// The result of the script, if any. + public static JavaScriptValue RunScript(string script, byte[] buffer, JavaScriptSourceContext sourceContext, string sourceName) + { + JavaScriptValue result; + Native.ThrowIfError(Native.JsRunSerializedScript(script, buffer, sourceContext, sourceName, out result)); + return result; + } + + /// + /// Executes a script. + /// + /// + /// Requires an active script context. + /// + /// The script to run. + /// The result of the script, if any. + public static JavaScriptValue RunScript(string script) + { + return RunScript(script, JavaScriptSourceContext.None, string.Empty); + } + + /// + /// Runs a serialized script. + /// + /// + /// Requires an active script context. + /// + /// The source code of the serialized script. + /// The serialized script. + /// The result of the script, if any. + public static JavaScriptValue RunScript(string script, byte[] buffer) + { + return RunScript(script, buffer, JavaScriptSourceContext.None, string.Empty); + } + + /// + /// Serializes a parsed script to a buffer than can be reused. + /// + /// + /// + /// SerializeScript parses a script and then stores the parsed form of the script in a + /// runtime-independent format. The serialized script then can be deserialized in any + /// runtime without requiring the script to be re-parsed. + /// + /// + /// Requires an active script context. + /// + /// + /// The script to serialize. + /// The buffer to put the serialized script into. Can be null. + /// + /// The size of the buffer, in bytes, required to hold the serialized script. + /// + public static ulong SerializeScript(string script, byte[] buffer) + { + var bufferSize = (ulong)buffer.Length; + Native.ThrowIfError(Native.JsSerializeScript(script, buffer, ref bufferSize)); + return bufferSize; + } + + /// + /// Returns the exception that caused the runtime of the current context to be in the + /// exception state and resets the exception state for that runtime. + /// + /// + /// + /// If the runtime of the current context is not in an exception state, this API will throw + /// JsErrorInvalidArgument. If the runtime is disabled, this will return an exception + /// indicating that the script was terminated, but it will not clear the exception (the + /// exception will be cleared if the runtime is re-enabled using + /// EnableRuntimeExecution). + /// + /// + /// Requires an active script context. + /// + /// + /// The exception for the runtime of the current context. + public static JavaScriptValue GetAndClearException() + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsGetAndClearException(out reference)); + return reference; + } + + /// + /// Sets the runtime of the current context to an exception state. + /// + /// + /// + /// If the runtime of the current context is already in an exception state, this API will + /// throw JsErrorInExceptionState. + /// + /// + /// Requires an active script context. + /// + /// + /// + /// The JavaScript exception to set for the runtime of the current context. + /// + public static void SetException(JavaScriptValue exception) + { + Native.ThrowIfError(Native.JsSetException(exception)); + } + + /// + /// Starts debugging in the context. + /// + /// The debug application to use for debugging. + public static void StartDebugging() + { + Native.ThrowIfError(Native.JsStartDebugging()); + } + + /// + /// Starts profiling in the current context. + /// + /// + /// Requires an active script context. + /// + /// The profiling callback to use. + /// The profiling events to callback with. + /// A context to pass to the profiling callback. + public static void StartProfiling(Native.IActiveScriptProfilerCallback callback, Native.ProfilerEventMask eventMask, int context) + { + Native.ThrowIfError(Native.JsStartProfiling(callback, eventMask, context)); + } + + /// + /// Stops profiling in the current context. + /// + /// + /// + /// Will not return an error if profiling has not started. + /// + /// + /// Requires an active script context. + /// + /// + /// + /// The reason for stopping profiling to pass to the profiler callback. + /// + public static void StopProfiling(int reason) + { + Native.ThrowIfError(Native.JsStopProfiling(reason)); + } + + /// + /// Enumerates the heap of the current context. + /// + /// + /// + /// While the heap is being enumerated, the current context cannot be removed, and all calls to + /// modify the state of the context will fail until the heap enumerator is released. + /// + /// + /// Requires an active script context. + /// + /// + /// A heap enumerator. + public static Native.IActiveScriptProfilerHeapEnum EnumerateHeap() + { + Native.IActiveScriptProfilerHeapEnum enumerator; + Native.ThrowIfError(Native.JsEnumerateHeap(out enumerator)); + return enumerator; + } + + /// + /// Adds a reference to a script context. + /// + /// + /// Calling AddRef ensures that the context will not be freed until Release is called. + /// + /// The object's new reference count. + public uint AddRef() + { + uint count; + Native.ThrowIfError(Native.JsContextAddRef(this, out count)); + return count; + } + + /// + /// Releases a reference to a script context. + /// + /// + /// Removes a reference to a context that was created by AddRef. + /// + /// The object's new reference count. + public uint Release() + { + uint count; + Native.ThrowIfError(Native.JsContextRelease(this, out count)); + return count; + } + + /// + /// A scope automatically sets a context to current and resets the original context + /// when disposed. + /// + public struct Scope : IDisposable + { + /// + /// The previous context. + /// + private readonly JavaScriptContext previousContext; + + /// + /// Whether the structure has been disposed. + /// + private bool disposed; + + /// + /// Initializes a new instance of the struct. + /// + /// The context to create the scope for. + public Scope(JavaScriptContext context) + { + disposed = false; + previousContext = Current; + Current = context; + } + + /// + /// Disposes the scope and sets the previous context to current. + /// + public void Dispose() + { + if (disposed) + { + return; + } + + Current = previousContext; + disposed = true; + } + } + } +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptEngineException.cs b/ReactWindows/ReactNative/Hosting/JavaScriptEngineException.cs new file mode 100644 index 00000000000..86098f48818 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptEngineException.cs @@ -0,0 +1,30 @@ +namespace ReactNative.Hosting +{ + using System; + using System.Runtime.Serialization; + + /// + /// An exception that occurred in the workings of the JavaScript engine itself. + /// + public sealed class JavaScriptEngineException : JavaScriptException + { + /// + /// Initializes a new instance of the class. + /// + /// The error code returned. + public JavaScriptEngineException(JavaScriptErrorCode code) : + this(code, "A fatal exception has occurred in a JavaScript runtime") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error code returned. + /// The error message. + public JavaScriptEngineException(JavaScriptErrorCode code, string message) : + base(code, message) + { + } + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptErrorCode.cs b/ReactWindows/ReactNative/Hosting/JavaScriptErrorCode.cs new file mode 100644 index 00000000000..7abf8462c08 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptErrorCode.cs @@ -0,0 +1,158 @@ +namespace ReactNative.Hosting +{ + using System.Diagnostics.CodeAnalysis; + + /// + /// An error code returned from a Chakra hosting API. + /// + public enum JavaScriptErrorCode : uint + { + /// + /// Success error code. + /// + NoError = 0, + + /// + /// Category of errors that relates to incorrect usage of the API itself. + /// + CategoryUsage = 0x10000, + + /// + /// An argument to a hosting API was invalid. + /// + InvalidArgument, + + /// + /// An argument to a hosting API was null in a context where null is not allowed. + /// + NullArgument, + + /// + /// The hosting API requires that a context be current, but there is no current context. + /// + NoCurrentContext, + + /// + /// The engine is in an exception state and no APIs can be called until the exception is + /// cleared. + /// + InExceptionState, + + /// + /// A hosting API is not yet implemented. + /// + NotImplemented, + + /// + /// A hosting API was called on the wrong thread. + /// + WrongThread, + + /// + /// A runtime that is still in use cannot be disposed. + /// + RuntimeInUse, + + /// + /// A bad serialized script was used, or the serialized script was serialized by a + /// different version of the Chakra engine. + /// + BadSerializedScript, + + /// + /// The runtime is in a disabled state. + /// + InDisabledState, + + /// + /// Runtime does not support reliable script interruption. + /// + CannotDisableExecution, + + /// + /// A heap enumeration is currently underway in the script context. + /// + HeapEnumInProgress, + + /// + /// A hosting API that operates on Object values was called with a non-Object value. + /// + ArgumentNotObject, + + /// + /// A script context is in the middle of a profile callback. + /// + InProfileCallback, + + /// + /// A thread service callback is currently underway. + /// + InThreadServiceCallback, + + /// + /// Scripts cannot be serialized in debug contexts. + /// + CannotSerializeDebugScript, + + /// + /// The context cannot be put into a debug state because it is already in a debug state. + /// + AlreadyDebuggingContext, + + /// + /// The context cannot start profiling because it is already profiling. + /// + AlreadyProfilingContext, + + /// + /// Idle notification given when the host did not enable idle processing. + /// + IdleNotEnabled, + + /// + /// Category of errors that relates to errors occurring within the engine itself. + /// + CategoryEngine = 0x20000, + + /// + /// The Chakra engine has run out of memory. + /// + OutOfMemory, + + /// + /// Category of errors that relates to errors in a script. + /// + CategoryScript = 0x30000, + + /// + /// A JavaScript exception occurred while running a script. + /// + ScriptException, + + /// + /// JavaScript failed to compile. + /// + ScriptCompile, + + /// + /// A script was terminated due to a request to suspend a runtime. + /// + ScriptTerminated, + + /// + /// A script was terminated because it tried to use "eval" or "function" and eval was disabled. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Eval is a valid function name.")] + ScriptEvalDisabled, + + /// + /// Category of errors that are fatal and signify failure of the engine. + /// + CategoryFatal = 0x40000, + + /// + /// A fatal error in the engine has occurred. + /// + Fatal, + } +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptException.cs b/ReactWindows/ReactNative/Hosting/JavaScriptException.cs new file mode 100644 index 00000000000..888c17aaff2 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptException.cs @@ -0,0 +1,44 @@ +namespace ReactNative.Hosting +{ + using System; + using System.Runtime.Serialization; + + /// + /// An exception returned from the Chakra engine. + /// + public class JavaScriptException : Exception + { + /// + /// The error code. + /// + private readonly JavaScriptErrorCode code; + + /// + /// Initializes a new instance of the class. + /// + /// The error code returned. + public JavaScriptException(JavaScriptErrorCode code) : + this(code, "A fatal exception has occurred in a JavaScript runtime") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error code returned. + /// The error message. + public JavaScriptException(JavaScriptErrorCode code, string message) : + base(message) + { + this.code = code; + } + + /// + /// Gets the error code. + /// + public JavaScriptErrorCode ErrorCode + { + get { return code; } + } + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptFatalException.cs b/ReactWindows/ReactNative/Hosting/JavaScriptFatalException.cs new file mode 100644 index 00000000000..7927401f4c8 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptFatalException.cs @@ -0,0 +1,30 @@ +namespace ReactNative.Hosting +{ + using System; + using System.Runtime.Serialization; + + /// + /// A fatal exception occurred. + /// + public sealed class JavaScriptFatalException : JavaScriptException + { + /// + /// Initializes a new instance of the class. + /// + /// The error code returned. + public JavaScriptFatalException(JavaScriptErrorCode code) : + this(code, "A fatal exception has occurred in a JavaScript runtime") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error code returned. + /// The error message. + public JavaScriptFatalException(JavaScriptErrorCode code, string message) : + base(code, message) + { + } + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptMemoryAllocationCallback.cs b/ReactWindows/ReactNative/Hosting/JavaScriptMemoryAllocationCallback.cs new file mode 100644 index 00000000000..df24f05dc00 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptMemoryAllocationCallback.cs @@ -0,0 +1,17 @@ +namespace ReactNative.Hosting +{ + using System; + + /// + /// User implemented callback routine for memory allocation events + /// + /// The state passed to SetRuntimeMemoryAllocationCallback. + /// The type of type allocation event. + /// The size of the allocation. + /// + /// For the Allocate event, returning true allows the runtime to continue with + /// allocation. Returning false indicates the allocation request is rejected. The return value + /// is ignored for other allocation events. + /// + public delegate bool JavaScriptMemoryAllocationCallback(IntPtr callbackState, JavaScriptMemoryEventType allocationEvent, UIntPtr allocationSize); +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptMemoryEventType.cs b/ReactWindows/ReactNative/Hosting/JavaScriptMemoryEventType.cs new file mode 100644 index 00000000000..83c0b3d54ec --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptMemoryEventType.cs @@ -0,0 +1,23 @@ +namespace ReactNative.Hosting +{ + /// + /// Allocation callback event type. + /// + public enum JavaScriptMemoryEventType + { + /// + /// Indicates a request for memory allocation. + /// + Allocate = 0, + + /// + /// Indicates a memory freeing event. + /// + Free = 1, + + /// + /// Indicates a failed allocation event. + /// + Failure = 2 + } +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptNativeFunction.cs b/ReactWindows/ReactNative/Hosting/JavaScriptNativeFunction.cs new file mode 100644 index 00000000000..ac119ff1a9f --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptNativeFunction.cs @@ -0,0 +1,19 @@ +using System; + +namespace ReactNative.Hosting +{ + using System.Runtime.InteropServices; + + /// + /// A function callback. + /// + /// + /// A Function object that represents the function being invoked. + /// + /// Indicates whether this is a regular call or a 'new' call. + /// The arguments to the call. + /// The number of arguments. + /// Callback data, if any. + /// The result of the call, if any. + public delegate JavaScriptValue JavaScriptNativeFunction(JavaScriptValue callee, bool isConstructCall, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] JavaScriptValue[] arguments, ushort argumentCount, IntPtr callbackData); +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptObjectFinalizeCallback.cs b/ReactWindows/ReactNative/Hosting/JavaScriptObjectFinalizeCallback.cs new file mode 100644 index 00000000000..8a8f8385eca --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptObjectFinalizeCallback.cs @@ -0,0 +1,12 @@ +namespace ReactNative.Hosting +{ + using System; + + /// + /// A finalization callback. + /// + /// + /// The external data that was passed in when creating the object being finalized. + /// + public delegate void JavaScriptObjectFinalizeCallback(IntPtr data); +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptPropertyId.cs b/ReactWindows/ReactNative/Hosting/JavaScriptPropertyId.cs new file mode 100644 index 00000000000..7af468d1b20 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptPropertyId.cs @@ -0,0 +1,141 @@ +namespace ReactNative.Hosting +{ + using System; + + /// + /// A property identifier. + /// + /// + /// Property identifiers are used to refer to properties of JavaScript objects instead of using + /// strings. + /// + public struct JavaScriptPropertyId : IEquatable + { + /// + /// The id. + /// + private readonly IntPtr id; + + /// + /// Initializes a new instance of the struct. + /// + /// The ID. + internal JavaScriptPropertyId(IntPtr id) + { + this.id = id; + } + + /// + /// Gets an invalid ID. + /// + public static JavaScriptPropertyId Invalid + { + get { return new JavaScriptPropertyId(IntPtr.Zero); } + } + + /// + /// Gets the name associated with the property ID. + /// + /// + /// + /// Requires an active script context. + /// + /// + public string Name + { + get + { + string name; + Native.ThrowIfError(Native.JsGetPropertyNameFromId(this, out name)); + return name; + } + } + + /// + /// Gets the property ID associated with the name. + /// + /// + /// + /// Property IDs are specific to a context and cannot be used across contexts. + /// + /// + /// Requires an active script context. + /// + /// + /// + /// The name of the property ID to get or create. The name may consist of only digits. + /// + /// The property ID in this runtime for the given name. + public static JavaScriptPropertyId FromString(string name) + { + JavaScriptPropertyId id; + Native.ThrowIfError(Native.JsGetPropertyIdFromName(name, out id)); + return id; + } + + /// + /// The equality operator for property IDs. + /// + /// The first property ID to compare. + /// The second property ID to compare. + /// Whether the two property IDs are the same. + public static bool operator ==(JavaScriptPropertyId left, JavaScriptPropertyId right) + { + return left.Equals(right); + } + + /// + /// The inequality operator for property IDs. + /// + /// The first property ID to compare. + /// The second property ID to compare. + /// Whether the two property IDs are not the same. + public static bool operator !=(JavaScriptPropertyId left, JavaScriptPropertyId right) + { + return !left.Equals(right); + } + + /// + /// Checks for equality between property IDs. + /// + /// The other property ID to compare. + /// Whether the two property IDs are the same. + public bool Equals(JavaScriptPropertyId other) + { + return id == other.id; + } + + /// + /// Checks for equality between property IDs. + /// + /// The other property ID to compare. + /// Whether the two property IDs are the same. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is JavaScriptPropertyId && Equals((JavaScriptPropertyId)obj); + } + + /// + /// The hash code. + /// + /// The hash code of the property ID. + public override int GetHashCode() + { + return id.ToInt32(); + } + + /// + /// Converts the property ID to a string. + /// + /// The name of the property ID. + public override string ToString() + { + return Name; + } + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptRuntime.cs b/ReactWindows/ReactNative/Hosting/JavaScriptRuntime.cs new file mode 100644 index 00000000000..06525f579dd --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptRuntime.cs @@ -0,0 +1,223 @@ +namespace ReactNative.Hosting +{ + using System; + + /// + /// A Chakra runtime. + /// + /// + /// + /// Each Chakra runtime has its own independent execution engine, JIT compiler, and garbage + /// collected heap. As such, each runtime is completely isolated from other runtimes. + /// + /// + /// Runtimes can be used on any thread, but only one thread can call into a runtime at any + /// time. + /// + /// + /// NOTE: A JavaScriptRuntime, unlike other objects in the Chakra hosting API, is not + /// garbage collected since it contains the garbage collected heap itself. A runtime will + /// continue to exist until Dispose is called. + /// + /// + public struct JavaScriptRuntime : IDisposable + { + /// + /// The handle. + /// + private IntPtr handle; + + /// + /// Gets a value indicating whether the runtime is valid. + /// + public bool IsValid + { + get { return handle != IntPtr.Zero; } + } + + /// + /// Gets the current memory usage for a runtime. + /// + /// + /// Memory usage can be always be retrieved, regardless of whether or not the runtime is active + /// on another thread. + /// + public UIntPtr MemoryUsage + { + get + { + UIntPtr memoryUsage; + Native.ThrowIfError(Native.JsGetRuntimeMemoryUsage(this, out memoryUsage)); + return memoryUsage; + } + } + + /// + /// Gets or sets the current memory limit for a runtime. + /// + /// + /// The memory limit of a runtime can be always be retrieved, regardless of whether or not the + /// runtime is active on another thread. + /// + public UIntPtr MemoryLimit + { + get + { + UIntPtr memoryLimit; + Native.ThrowIfError(Native.JsGetRuntimeMemoryLimit(this, out memoryLimit)); + return memoryLimit; + } + + set + { + Native.ThrowIfError(Native.JsSetRuntimeMemoryLimit(this, value)); + } + } + + /// + /// Gets or sets a value indicating whether script execution is disabled in the runtime. + /// + public bool Disabled + { + get + { + bool isDisabled; + Native.ThrowIfError(Native.JsIsRuntimeExecutionDisabled(this, out isDisabled)); + return isDisabled; + } + + set + { + Native.ThrowIfError(value + ? Native.JsDisableRuntimeExecution(this) + : Native.JsEnableRuntimeExecution(this)); + } + } + + /// + /// Creates a new runtime. + /// + /// The attributes of the runtime to be created. + /// The thread service for the runtime. Can be null. + /// The runtime created. + public static JavaScriptRuntime Create(JavaScriptRuntimeAttributes attributes, JavaScriptThreadServiceCallback threadServiceCallback) + { + JavaScriptRuntime handle; + Native.ThrowIfError(Native.JsCreateRuntime(attributes, threadServiceCallback, out handle)); + return handle; + } + + /// + /// Creates a new runtime. + /// + /// The attributes of the runtime to be created. + /// The runtime created. + public static JavaScriptRuntime Create(JavaScriptRuntimeAttributes attributes) + { + return Create(attributes, null); + } + + /// + /// Creates a new runtime. + /// + /// The runtime created. + public static JavaScriptRuntime Create() + { + return Create(JavaScriptRuntimeAttributes.None, null); + } + + /// + /// Disposes a runtime. + /// + /// + /// Once a runtime has been disposed, all resources owned by it are invalid and cannot be used. + /// If the runtime is active (i.e. it is set to be current on a particular thread), it cannot + /// be disposed. + /// + public void Dispose() + { + if (IsValid) + { + Native.ThrowIfError(Native.JsDisposeRuntime(this)); + } + + handle = IntPtr.Zero; + } + + /// + /// Performs a full garbage collection. + /// + public void CollectGarbage() + { + Native.ThrowIfError(Native.JsCollectGarbage(this)); + } + + /// + /// Sets a memory allocation callback for specified runtime + /// + /// + /// + /// Registering a memory allocation callback will cause the runtime to call back to the host + /// whenever it acquires memory from, or releases memory to, the OS. The callback routine is + /// called before the runtime memory manager allocates a block of memory. The allocation will + /// be rejected if the callback returns false. The runtime memory manager will also invoke the + /// callback routine after freeing a block of memory, as well as after allocation failures. + /// + /// + /// The callback is invoked on the current runtime execution thread, therefore execution is + /// blocked until the callback completes. + /// + /// + /// The return value of the callback is not stored; previously rejected allocations will not + /// prevent the runtime from invoking the callback again later for new memory allocations. + /// + /// + /// + /// User provided state that will be passed back to the callback. + /// + /// + /// Memory allocation callback to be called for memory allocation events. + /// + public void SetMemoryAllocationCallback(IntPtr callbackState, JavaScriptMemoryAllocationCallback allocationCallback) + { + Native.ThrowIfError(Native.JsSetRuntimeMemoryAllocationCallback(this, callbackState, allocationCallback)); + } + + /// + /// Sets a callback function that is called by the runtime before garbage collection. + /// + /// + /// + /// The callback is invoked on the current runtime execution thread, therefore execution is + /// blocked until the callback completes. + /// + /// + /// The callback can be used by hosts to prepare for garbage collection. For example, by + /// releasing unnecessary references on Chakra objects. + /// + /// + /// + /// User provided state that will be passed back to the callback. + /// + /// The callback function being set. + public void SetBeforeCollectCallback(IntPtr callbackState, JavaScriptBeforeCollectCallback beforeCollectCallback) + { + Native.ThrowIfError(Native.JsSetRuntimeBeforeCollectCallback(this, callbackState, beforeCollectCallback)); + } + + /// + /// Creates a debug script context for running scripts. + /// + /// + /// Each script context has its own global object that is isolated from all other script + /// contexts. + /// + /// The created script context. + public JavaScriptContext CreateContext() + { + JavaScriptContext reference; + Native.ThrowIfError(Native.JsCreateContext(this, out reference)); + return reference; + } + } +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptRuntimeAttributes.cs b/ReactWindows/ReactNative/Hosting/JavaScriptRuntimeAttributes.cs new file mode 100644 index 00000000000..765ebcb3f3f --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptRuntimeAttributes.cs @@ -0,0 +1,46 @@ +namespace ReactNative.Hosting +{ + using System; + using System.Diagnostics.CodeAnalysis; + + /// + /// Attributes of a runtime. + /// + [Flags] + public enum JavaScriptRuntimeAttributes + { + /// + /// No special attributes. + /// + None = 0x00000000, + + /// + /// The runtime will not do any work (such as garbage collection) on background threads. + /// + DisableBackgroundWork = 0x00000001, + + /// + /// The runtime should support reliable script interruption. This increases the number of + /// places where the runtime will check for a script interrupt request at the cost of a + /// small amount of runtime performance. + /// + AllowScriptInterrupt = 0x00000002, + + /// + /// Host will call Idle, so enable idle processing. Otherwise, the runtime will manage + /// memory slightly more aggressively. + /// + EnableIdleProcessing = 0x00000004, + + /// + /// Runtime will not generate native code. + /// + DisableNativeCodeGeneration = 0x00000008, + + /// + /// Using Eval or Function constructor will throw an exception. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Eval is a valid function name.")] + DisableEval = 0x00000010, + } +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptRuntimeVersion.cs b/ReactWindows/ReactNative/Hosting/JavaScriptRuntimeVersion.cs new file mode 100644 index 00000000000..266a51a4580 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptRuntimeVersion.cs @@ -0,0 +1,23 @@ +namespace ReactNative.Hosting +{ + /// + /// Version of the runtime. + /// + public enum JavaScriptRuntimeVersion + { + /// + /// Create runtime with IE10 version. + /// + Version10 = 0, + + /// + /// Create runtime with IE11 version. + /// + Version11 = 1, + + /// + /// Create runtime with highest version present on the machine at runtime. + /// + VersionEdge = -1, + } +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptScriptException.cs b/ReactWindows/ReactNative/Hosting/JavaScriptScriptException.cs new file mode 100644 index 00000000000..5ae42613eae --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptScriptException.cs @@ -0,0 +1,49 @@ +namespace ReactNative.Hosting +{ + using System; + using System.Runtime.Serialization; + + /// + /// A script exception. + /// + public sealed class JavaScriptScriptException : JavaScriptException + { + /// + /// The error. + /// + private readonly JavaScriptValue error; + + /// + /// Initializes a new instance of the class. + /// + /// The error code returned. + /// The JavaScript error object. + public JavaScriptScriptException(JavaScriptErrorCode code, JavaScriptValue error) : + this(code, error, "JavaScript Exception") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error code returned. + /// The JavaScript error object. + /// The error message. + public JavaScriptScriptException(JavaScriptErrorCode code, JavaScriptValue error, string message) : + base(code, message) + { + this.error = error; + } + + /// + /// Gets a JavaScript object representing the script error. + /// + public JavaScriptValue Error + { + get + { + return error; + } + } + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptSourceContext.cs b/ReactWindows/ReactNative/Hosting/JavaScriptSourceContext.cs new file mode 100644 index 00000000000..1e7dc92af1f --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptSourceContext.cs @@ -0,0 +1,184 @@ +namespace ReactNative.Hosting +{ + using System; + + /// + /// A cookie that identifies a script for debugging purposes. + /// + public struct JavaScriptSourceContext : IEquatable + { + /// + /// The context. + /// + private readonly IntPtr context; + + /// + /// Initializes a new instance of the struct. + /// + /// The context. + private JavaScriptSourceContext(IntPtr context) + { + this.context = context; + } + + /// + /// Gets an empty source context. + /// + public static JavaScriptSourceContext None + { + get { return new JavaScriptSourceContext(new IntPtr(-1)); } + } + + /// + /// The equality operator for source contexts. + /// + /// The first source context to compare. + /// The second source context to compare. + /// Whether the two source contexts are the same. + public static bool operator ==(JavaScriptSourceContext left, JavaScriptSourceContext right) + { + return left.Equals(right); + } + + /// + /// The inequality operator for source contexts. + /// + /// The first source context to compare. + /// The second source context to compare. + /// Whether the two source contexts are not the same. + public static bool operator !=(JavaScriptSourceContext left, JavaScriptSourceContext right) + { + return !left.Equals(right); + } + + /// + /// Subtracts an offset from the value of the source context. + /// + /// The source context to subtract the offset from. + /// The offset to subtract. + /// A new source context that reflects the subtraction of the offset from the context. + public static JavaScriptSourceContext operator -(JavaScriptSourceContext context, int offset) + { + return FromIntPtr(context.context - offset); + } + + /// + /// Subtracts an offset from the value of the source context. + /// + /// The source context to subtract the offset from. + /// The offset to subtract. + /// A new source context that reflects the subtraction of the offset from the context. + public static JavaScriptSourceContext Subtract(JavaScriptSourceContext left, int right) + { + return left - right; + } + + /// + /// Decrements the value of the source context. + /// + /// The source context to decrement. + /// A new source context that reflects the decrementing of the context. + public static JavaScriptSourceContext operator --(JavaScriptSourceContext context) + { + return FromIntPtr(context.context - 1); + } + + /// + /// Decrements the value of the source context. + /// + /// The source context to decrement. + /// A new source context that reflects the decrementing of the context. + public static JavaScriptSourceContext Decrement(JavaScriptSourceContext left) + { + return --left; + } + + /// + /// Adds an offset from the value of the source context. + /// + /// The source context to add the offset to. + /// The offset to add. + /// A new source context that reflects the addition of the offset to the context. + public static JavaScriptSourceContext operator +(JavaScriptSourceContext context, int offset) + { + return FromIntPtr(context.context + offset); + } + + /// + /// Adds an offset from the value of the source context. + /// + /// The source context to add the offset to. + /// The offset to add. + /// A new source context that reflects the addition of the offset to the context. + public static JavaScriptSourceContext Add(JavaScriptSourceContext left, int right) + { + return left + right; + } + + /// + /// Increments the value of the source context. + /// + /// The source context to increment. + /// A new source context that reflects the incrementing of the context. + public static JavaScriptSourceContext operator ++(JavaScriptSourceContext context) + { + return FromIntPtr(context.context + 1); + } + + /// + /// Increments the value of the source context. + /// + /// The source context to increment. + /// A new source context that reflects the incrementing of the context. + public static JavaScriptSourceContext Increment(JavaScriptSourceContext left) + { + return ++left; + } + + /// + /// Creates a new source context. + /// + /// + /// The cookie for the source context. + /// + /// The new source context. + public static JavaScriptSourceContext FromIntPtr(IntPtr cookie) + { + return new JavaScriptSourceContext(cookie); + } + + /// + /// Checks for equality between source contexts. + /// + /// The other source context to compare. + /// Whether the two source contexts are the same. + public bool Equals(JavaScriptSourceContext other) + { + return context == other.context; + } + + /// + /// Checks for equality between source contexts. + /// + /// The other source context to compare. + /// Whether the two source contexts are the same. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is JavaScriptSourceContext && Equals((JavaScriptSourceContext)obj); + } + + /// + /// The hash code. + /// + /// The hash code of the source context. + public override int GetHashCode() + { + return context.ToInt32(); + } + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptThreadServiceCallback.cs b/ReactWindows/ReactNative/Hosting/JavaScriptThreadServiceCallback.cs new file mode 100644 index 00000000000..b9a3a3e337b --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptThreadServiceCallback.cs @@ -0,0 +1,18 @@ +namespace ReactNative.Hosting +{ + using System; + + /// + /// A thread service callback. + /// + /// + /// The host can specify a background thread service when creating a runtime. If + /// specified, then background work items will be passed to the host using this callback. The + /// host is expected to either begin executing the background work item immediately and return + /// true or return false and the runtime will handle the work item in-thread. + /// + /// The callback for the background work item. + /// The data argument to be passed to the callback. + /// Whether the thread service will execute the callback. + public delegate bool JavaScriptThreadServiceCallback(JavaScriptBackgroundWorkItemCallback callbackFunction, IntPtr callbackData); +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptUsageException.cs b/ReactWindows/ReactNative/Hosting/JavaScriptUsageException.cs new file mode 100644 index 00000000000..73fd9cff602 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptUsageException.cs @@ -0,0 +1,30 @@ +namespace ReactNative.Hosting +{ + using System; + using System.Runtime.Serialization; + + /// + /// An API usage exception occurred. + /// + public sealed class JavaScriptUsageException : JavaScriptException + { + /// + /// Initializes a new instance of the class. + /// + /// The error code returned. + public JavaScriptUsageException(JavaScriptErrorCode code) : + this(code, "A fatal exception has occurred in a JavaScript runtime") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error code returned. + /// The error message. + public JavaScriptUsageException(JavaScriptErrorCode code, string message) : + base(code, message) + { + } + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptValue.cs b/ReactWindows/ReactNative/Hosting/JavaScriptValue.cs new file mode 100644 index 00000000000..bce6f4383b3 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptValue.cs @@ -0,0 +1,885 @@ +namespace ReactNative.Hosting +{ + using System; + using System.Runtime.InteropServices; + + /// + /// A JavaScript value. + /// + /// + /// A JavaScript value is one of the following types of values: Undefined, Null, Boolean, + /// String, Number, or Object. + /// + public struct JavaScriptValue + { + /// + /// The reference. + /// + private readonly IntPtr reference; + + /// + /// Initializes a new instance of the struct. + /// + /// The reference. + private JavaScriptValue(IntPtr reference) + { + this.reference = reference; + } + + /// + /// Gets an invalid value. + /// + public static JavaScriptValue Invalid + { + get { return new JavaScriptValue(IntPtr.Zero); } + } + + /// + /// Gets the value of undefined in the current script context. + /// + /// + /// Requires an active script context. + /// + public static JavaScriptValue Undefined + { + get + { + JavaScriptValue value; + Native.ThrowIfError(Native.JsGetUndefinedValue(out value)); + return value; + } + } + + /// + /// Gets the value of null in the current script context. + /// + /// + /// Requires an active script context. + /// + public static JavaScriptValue Null + { + get + { + JavaScriptValue value; + Native.ThrowIfError(Native.JsGetNullValue(out value)); + return value; + } + } + + /// + /// Gets the value of true in the current script context. + /// + /// + /// Requires an active script context. + /// + public static JavaScriptValue True + { + get + { + JavaScriptValue value; + Native.ThrowIfError(Native.JsGetTrueValue(out value)); + return value; + } + } + + /// + /// Gets the value of false in the current script context. + /// + /// + /// Requires an active script context. + /// + public static JavaScriptValue False + { + get + { + JavaScriptValue value; + Native.ThrowIfError(Native.JsGetFalseValue(out value)); + return value; + } + } + + /// + /// Gets the global object in the current script context. + /// + /// + /// Requires an active script context. + /// + public static JavaScriptValue GlobalObject + { + get + { + JavaScriptValue value; + Native.ThrowIfError(Native.JsGetGlobalObject(out value)); + return value; + } + } + + /// + /// Gets a value indicating whether the value is valid. + /// + public bool IsValid + { + get { return reference != IntPtr.Zero; } + } + + /// + /// Gets the JavaScript type of the value. + /// + /// + /// Requires an active script context. + /// + /// The type of the value. + public JavaScriptValueType ValueType + { + get + { + JavaScriptValueType type; + Native.ThrowIfError(Native.JsGetValueType(this, out type)); + return type; + } + } + + /// + /// Gets the length of a String value. + /// + /// + /// Requires an active script context. + /// + /// The length of the string. + public int StringLength + { + get + { + int length; + Native.ThrowIfError(Native.JsGetStringLength(this, out length)); + return length; + } + } + + /// + /// Gets or sets the prototype of an object. + /// + /// + /// Requires an active script context. + /// + public JavaScriptValue Prototype + { + get + { + JavaScriptValue prototypeReference; + Native.ThrowIfError(Native.JsGetPrototype(this, out prototypeReference)); + return prototypeReference; + } + + set + { + Native.ThrowIfError(Native.JsSetPrototype(this, value)); + } + } + + /// + /// Gets a value indicating whether an object is extensible or not. + /// + /// + /// Requires an active script context. + /// + public bool IsExtensionAllowed + { + get + { + bool allowed; + Native.ThrowIfError(Native.JsGetExtensionAllowed(this, out allowed)); + return allowed; + } + } + + /// + /// Gets a value indicating whether an object is an external object. + /// + /// + /// Requires an active script context. + /// + public bool HasExternalData + { + get + { + bool hasExternalData; + Native.ThrowIfError(Native.JsHasExternalData(this, out hasExternalData)); + return hasExternalData; + } + } + + /// + /// Gets or sets the data in an external object. + /// + /// + /// Requires an active script context. + /// + public IntPtr ExternalData + { + get + { + IntPtr data; + Native.ThrowIfError(Native.JsGetExternalData(this, out data)); + return data; + } + + set + { + Native.ThrowIfError(Native.JsSetExternalData(this, value)); + } + } + + /// + /// Creates a Boolean value from a bool value. + /// + /// + /// Requires an active script context. + /// + /// The value to be converted. + /// The converted value. + public static JavaScriptValue FromBoolean(bool value) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsBoolToBoolean(value, out reference)); + return reference; + } + + /// + /// Creates a Number value from a double value. + /// + /// + /// Requires an active script context. + /// + /// The value to be converted. + /// The new Number value. + public static JavaScriptValue FromDouble(double value) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsDoubleToNumber(value, out reference)); + return reference; + } + + /// + /// Creates a Number value from a int value. + /// + /// + /// Requires an active script context. + /// + /// The value to be converted. + /// The new Number value. + public static JavaScriptValue FromInt32(int value) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsIntToNumber(value, out reference)); + return reference; + } + + /// + /// Creates a String value from a string pointer. + /// + /// + /// Requires an active script context. + /// + /// The string to convert to a String value. + /// The new String value. + public static JavaScriptValue FromString(string value) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsPointerToString(value, new UIntPtr((uint)value.Length), out reference)); + return reference; + } + + /// + /// Creates a JavaScript value that is a projection of the passed in object. + /// + /// + /// Requires an active script context. + /// + /// An object to be projected. + /// A JavaScript value that is a projection of the object. + public static JavaScriptValue FromObject(object value) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsVariantToValue(ref value, out reference)); + return reference; + } + + /// + /// Creates a new Object. + /// + /// + /// Requires an active script context. + /// + /// The new Object. + public static JavaScriptValue CreateObject() + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsCreateObject(out reference)); + return reference; + } + + /// + /// Creates a new Object that stores some external data. + /// + /// + /// Requires an active script context. + /// + /// External data that the object will represent. May be null. + /// + /// A callback for when the object is finalized. May be null. + /// + /// The new Object. + public static JavaScriptValue CreateExternalObject(IntPtr data, JavaScriptObjectFinalizeCallback finalizer) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsCreateExternalObject(data, finalizer, out reference)); + return reference; + } + + /// + /// Creates a new JavaScript function. + /// + /// + /// Requires an active script context. + /// + /// The method to call when the function is invoked. + /// The new function object. + public static JavaScriptValue CreateFunction(JavaScriptNativeFunction function) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsCreateFunction(function, IntPtr.Zero, out reference)); + return reference; + } + + /// + /// Creates a new JavaScript function. + /// + /// + /// Requires an active script context. + /// + /// The method to call when the function is invoked. + /// Data to be provided to all function callbacks. + /// The new function object. + public static JavaScriptValue CreateFunction(JavaScriptNativeFunction function, IntPtr callbackData) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsCreateFunction(function, callbackData, out reference)); + return reference; + } + + /// + /// Creates a JavaScript array object. + /// + /// + /// Requires an active script context. + /// + /// The initial length of the array. + /// The new array object. + public static JavaScriptValue CreateArray(uint length) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsCreateArray(length, out reference)); + return reference; + } + + /// + /// Creates a new JavaScript error object + /// + /// + /// Requires an active script context. + /// + /// Message for the error object. + /// The new error object. + public static JavaScriptValue CreateError(JavaScriptValue message) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsCreateError(message, out reference)); + return reference; + } + + /// + /// Creates a new JavaScript RangeError error object + /// + /// + /// Requires an active script context. + /// + /// Message for the error object. + /// The new error object. + public static JavaScriptValue CreateRangeError(JavaScriptValue message) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsCreateRangeError(message, out reference)); + return reference; + } + + /// + /// Creates a new JavaScript ReferenceError error object + /// + /// + /// Requires an active script context. + /// + /// Message for the error object. + /// The new error object. + public static JavaScriptValue CreateReferenceError(JavaScriptValue message) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsCreateReferenceError(message, out reference)); + return reference; + } + + /// + /// Creates a new JavaScript SyntaxError error object + /// + /// + /// Requires an active script context. + /// + /// Message for the error object. + /// The new error object. + public static JavaScriptValue CreateSyntaxError(JavaScriptValue message) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsCreateSyntaxError(message, out reference)); + return reference; + } + + /// + /// Creates a new JavaScript TypeError error object + /// + /// + /// Requires an active script context. + /// + /// Message for the error object. + /// The new error object. + public static JavaScriptValue CreateTypeError(JavaScriptValue message) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsCreateTypeError(message, out reference)); + return reference; + } + + /// + /// Creates a new JavaScript URIError error object + /// + /// + /// Requires an active script context. + /// + /// Message for the error object. + /// The new error object. + public static JavaScriptValue CreateUriError(JavaScriptValue message) + { + JavaScriptValue reference; + Native.ThrowIfError(Native.JsCreateURIError(message, out reference)); + return reference; + } + + /// + /// Adds a reference to the object. + /// + /// + /// This only needs to be called on objects that are not going to be stored somewhere on + /// the stack. Calling AddRef ensures that the JavaScript object the value refers to will not be freed + /// until Release is called + /// + /// The object's new reference count. + public uint AddRef() + { + uint count; + Native.ThrowIfError(Native.JsAddRef(this, out count)); + return count; + } + + /// + /// Releases a reference to the object. + /// + /// + /// Removes a reference that was created by AddRef. + /// + /// The object's new reference count. + public uint Release() + { + uint count; + Native.ThrowIfError(Native.JsRelease(this, out count)); + return count; + } + + /// + /// Retrieves the bool value of a Boolean value. + /// + /// + /// Requires an active script context. + /// + /// The converted value. + public bool ToBoolean() + { + bool value; + Native.ThrowIfError(Native.JsBooleanToBool(this, out value)); + return value; + } + + /// + /// Retrieves the double value of a Number value. + /// + /// + /// + /// This function retrieves the value of a Number value. It will fail with + /// InvalidArgument if the type of the value is not Number. + /// + /// + /// Requires an active script context. + /// + /// + /// The double value. + public double ToDouble() + { + double value; + Native.ThrowIfError(Native.JsNumberToDouble(this, out value)); + return value; + } + + /// + /// Retrieves the string pointer of a String value. + /// + /// + /// + /// This function retrieves the string pointer of a String value. It will fail with + /// InvalidArgument if the type of the value is not String. + /// + /// + /// Requires an active script context. + /// + /// + /// The string. + public new string ToString() + { + IntPtr buffer; + UIntPtr length; + Native.ThrowIfError(Native.JsStringToPointer(this, out buffer, out length)); + + return Marshal.PtrToStringUni(buffer, (int)length); + } + + /// + /// Retrieves the object representation of an Object value. + /// + /// + /// Requires an active script context. + /// + /// The object representation of the value. + public object ToObject() + { + object value; + Native.ThrowIfError(Native.JsValueToVariant(this, out value)); + return value; + } + + /// + /// Converts the value to Boolean using regular JavaScript semantics. + /// + /// + /// Requires an active script context. + /// + /// The converted value. + public JavaScriptValue ConvertToBoolean() + { + JavaScriptValue booleanReference; + Native.ThrowIfError(Native.JsConvertValueToBoolean(this, out booleanReference)); + return booleanReference; + } + + /// + /// Converts the value to Number using regular JavaScript semantics. + /// + /// + /// Requires an active script context. + /// + /// The converted value. + public JavaScriptValue ConvertToNumber() + { + JavaScriptValue numberReference; + Native.ThrowIfError(Native.JsConvertValueToNumber(this, out numberReference)); + return numberReference; + } + + /// + /// Converts the value to String using regular JavaScript semantics. + /// + /// + /// Requires an active script context. + /// + /// The converted value. + public JavaScriptValue ConvertToString() + { + JavaScriptValue stringReference; + Native.ThrowIfError(Native.JsConvertValueToString(this, out stringReference)); + return stringReference; + } + + /// + /// Converts the value to Object using regular JavaScript semantics. + /// + /// + /// Requires an active script context. + /// + /// The converted value. + public JavaScriptValue ConvertToObject() + { + JavaScriptValue objectReference; + Native.ThrowIfError(Native.JsConvertValueToObject(this, out objectReference)); + return objectReference; + } + + /// + /// Sets an object to not be extensible. + /// + /// + /// Requires an active script context. + /// + public void PreventExtension() + { + Native.ThrowIfError(Native.JsPreventExtension(this)); + } + + /// + /// Gets a property descriptor for an object's own property. + /// + /// + /// Requires an active script context. + /// + /// The ID of the property. + /// The property descriptor. + public JavaScriptValue GetOwnPropertyDescriptor(JavaScriptPropertyId propertyId) + { + JavaScriptValue descriptorReference; + Native.ThrowIfError(Native.JsGetOwnPropertyDescriptor(this, propertyId, out descriptorReference)); + return descriptorReference; + } + + /// + /// Gets the list of all properties on the object. + /// + /// + /// Requires an active script context. + /// + /// An array of property names. + public JavaScriptValue GetOwnPropertyNames() + { + JavaScriptValue propertyNamesReference; + Native.ThrowIfError(Native.JsGetOwnPropertyNames(this, out propertyNamesReference)); + return propertyNamesReference; + } + + /// + /// Determines whether an object has a property. + /// + /// + /// Requires an active script context. + /// + /// The ID of the property. + /// Whether the object (or a prototype) has the property. + public bool HasProperty(JavaScriptPropertyId propertyId) + { + bool hasProperty; + Native.ThrowIfError(Native.JsHasProperty(this, propertyId, out hasProperty)); + return hasProperty; + } + + /// + /// Gets an object's property. + /// + /// + /// Requires an active script context. + /// + /// The ID of the property. + /// The value of the property. + public JavaScriptValue GetProperty(JavaScriptPropertyId id) + { + JavaScriptValue propertyReference; + Native.ThrowIfError(Native.JsGetProperty(this, id, out propertyReference)); + return propertyReference; + } + + /// + /// Sets an object's property. + /// + /// + /// Requires an active script context. + /// + /// The ID of the property. + /// The new value of the property. + /// The property set should follow strict mode rules. + public void SetProperty(JavaScriptPropertyId id, JavaScriptValue value, bool useStrictRules) + { + Native.ThrowIfError(Native.JsSetProperty(this, id, value, useStrictRules)); + } + + /// + /// Deletes an object's property. + /// + /// + /// Requires an active script context. + /// + /// The ID of the property. + /// The property set should follow strict mode rules. + /// Whether the property was deleted. + public JavaScriptValue DeleteProperty(JavaScriptPropertyId propertyId, bool useStrictRules) + { + JavaScriptValue returnReference; + Native.ThrowIfError(Native.JsDeleteProperty(this, propertyId, useStrictRules, out returnReference)); + return returnReference; + } + + /// + /// Defines a new object's own property from a property descriptor. + /// + /// + /// Requires an active script context. + /// + /// The ID of the property. + /// The property descriptor. + /// Whether the property was defined. + public bool DefineProperty(JavaScriptPropertyId propertyId, JavaScriptValue propertyDescriptor) + { + bool result; + Native.ThrowIfError(Native.JsDefineProperty(this, propertyId, propertyDescriptor, out result)); + return result; + } + + /// + /// Test if an object has a value at the specified index. + /// + /// + /// Requires an active script context. + /// + /// The index to test. + /// Whether the object has an value at the specified index. + public bool HasIndexedProperty(JavaScriptValue index) + { + bool hasProperty; + Native.ThrowIfError(Native.JsHasIndexedProperty(this, index, out hasProperty)); + return hasProperty; + } + + /// + /// Retrieve the value at the specified index of an object. + /// + /// + /// Requires an active script context. + /// + /// The index to retrieve. + /// The retrieved value. + public JavaScriptValue GetIndexedProperty(JavaScriptValue index) + { + JavaScriptValue propertyReference; + Native.ThrowIfError(Native.JsGetIndexedProperty(this, index, out propertyReference)); + return propertyReference; + } + + /// + /// Set the value at the specified index of an object. + /// + /// + /// Requires an active script context. + /// + /// The index to set. + /// The value to set. + public void SetIndexedProperty(JavaScriptValue index, JavaScriptValue value) + { + Native.ThrowIfError(Native.JsSetIndexedProperty(this, index, value)); + } + + /// + /// Delete the value at the specified index of an object. + /// + /// + /// Requires an active script context. + /// + /// The index to delete. + public void DeleteIndexedProperty(JavaScriptValue index) + { + Native.ThrowIfError(Native.JsDeleteIndexedProperty(this, index)); + } + + /// + /// Compare two JavaScript values for equality. + /// + /// + /// + /// This function is equivalent to the "==" operator in JavaScript. + /// + /// + /// Requires an active script context. + /// + /// + /// The object to compare. + /// Whether the values are equal. + public bool Equals(JavaScriptValue other) + { + bool equals; + Native.ThrowIfError(Native.JsEquals(this, other, out equals)); + return equals; + } + + /// + /// Compare two JavaScript values for strict equality. + /// + /// + /// + /// This function is equivalent to the "===" operator in JavaScript. + /// + /// + /// Requires an active script context. + /// + /// + /// The object to compare. + /// Whether the values are strictly equal. + public bool StrictEquals(JavaScriptValue other) + { + bool equals; + Native.ThrowIfError(Native.JsStrictEquals(this, other, out equals)); + return equals; + } + + /// + /// Invokes a function. + /// + /// + /// Requires an active script context. + /// + /// The arguments to the call. + /// The Value returned from the function invocation, if any. + public JavaScriptValue CallFunction(params JavaScriptValue[] arguments) + { + JavaScriptValue returnReference; + + if (arguments.Length > ushort.MaxValue) + { + throw new ArgumentOutOfRangeException("arguments"); + } + + Native.ThrowIfError(Native.JsCallFunction(this, arguments, (ushort)arguments.Length, out returnReference)); + return returnReference; + } + + /// + /// Invokes a function as a constructor. + /// + /// + /// Requires an active script context. + /// + /// The arguments to the call. + /// The Value returned from the function invocation. + public JavaScriptValue ConstructObject(params JavaScriptValue[] arguments) + { + JavaScriptValue returnReference; + + if (arguments.Length > ushort.MaxValue) + { + throw new ArgumentOutOfRangeException("arguments"); + } + + Native.ThrowIfError(Native.JsConstructObject(this, arguments, (ushort)arguments.Length, out returnReference)); + return returnReference; + } + } +} diff --git a/ReactWindows/ReactNative/Hosting/JavaScriptValueType.cs b/ReactWindows/ReactNative/Hosting/JavaScriptValueType.cs new file mode 100644 index 00000000000..3014e80ad28 --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/JavaScriptValueType.cs @@ -0,0 +1,53 @@ +namespace ReactNative.Hosting +{ + /// + /// The JavaScript type of a JavaScriptValue. + /// + public enum JavaScriptValueType + { + /// + /// The value is the undefined value. + /// + Undefined = 0, + + /// + /// The value is the null value. + /// + Null = 1, + + /// + /// The value is a JavaScript number value. + /// + Number = 2, + + /// + /// The value is a JavaScript string value. + /// + String = 3, + + /// + /// The value is a JavaScript Boolean value. + /// + Boolean = 4, + + /// + /// The value is a JavaScript object value. + /// + Object = 5, + + /// + /// The value is a JavaScript function object value. + /// + Function = 6, + + /// + /// The value is a JavaScript error object value. + /// + Error = 7, + + /// + /// The value is a JavaScript array object value. + /// + Array = 8, + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative/Hosting/Native.cs b/ReactWindows/ReactNative/Hosting/Native.cs new file mode 100644 index 00000000000..c805ada700b --- /dev/null +++ b/ReactWindows/ReactNative/Hosting/Native.cs @@ -0,0 +1,644 @@ +namespace ReactNative.Hosting +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.InteropServices; + + /// + /// Native interfaces. + /// + public static class Native + { + /// + /// Event mask for profiling. + /// + public enum ProfilerEventMask + { + /// + /// Trace calls to script functions. + /// + TraceScriptFunctionCall = 0x1, + + /// + /// Trace calls to built-in functions. + /// + TraceNativeFunctionCall = 0x2, + + /// + /// Trace calls to DOM methods. + /// + TraceDomFunctionCall = 0x4, + + /// + /// Trace all calls except DOM methods. + /// + TraceAll = (TraceScriptFunctionCall | TraceNativeFunctionCall), + + /// + /// Trace all calls. + /// + TraceAllWithDom = (TraceAll | TraceDomFunctionCall) + } + + /// + /// Profiled script type. + /// + public enum ProfilerScriptType + { + /// + /// A user script. + /// + User, + + /// + /// A dynamic script. + /// + Dynamic, + + /// + /// A native script. + /// + Native, + + /// + /// A DOM-related script. + /// + Dom + } + + /// + /// IProcessDebugManager32 COM interface. + /// + [Guid("51973C2f-CB0C-11d0-B5C9-00A0244A0E7A")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IProcessDebugManager32 + { + /// + /// Creates a new debug application. + /// + /// The new debug application. + void CreateApplication(out IDebugApplication32 debugApplication); + + /// + /// Gets the default debug application. + /// + /// The default debug application. + void GetDefaultApplication(out IDebugApplication32 debugApplication); + + /// + /// Adds a new debug application. + /// + /// The new debug application. + /// An engine-defined cookie. + void AddApplication(IDebugApplication32 debugApplication, out uint cookie); + + /// + /// Removes a debug application. + /// + /// The cookie of the debug application to remove. + void RemoveApplication(uint cookie); + + /// + /// Creates a debug document helper. + /// + /// The outer unknown. + /// The new debug document helper. + void CreateDebugDocumentHelper(object outerUnknown, out IDebugDocumentHelper32 helper); + } + + /// + /// IProcessDebugManager64 COM interface. + /// + [Guid("56b9fC1C-63A9-4CC1-AC21-087D69A17FAB")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IProcessDebugManager64 + { + /// + /// Creates a new debug application. + /// + /// The new debug application. + void CreateApplication(out IDebugApplication64 debugApplication); + + /// + /// Gets the default debug application. + /// + /// The default debug application. + void GetDefaultApplication(out IDebugApplication64 debugApplication); + + /// + /// Adds a new debug application. + /// + /// The new debug application. + /// An engine-defined cookie. + void AddApplication(IDebugApplication64 debugApplication, out uint cookie); + + /// + /// Removes a debug application. + /// + /// The cookie of the debug application to remove. + void RemoveApplication(uint cookie); + + /// + /// Creates a debug document helper. + /// + /// The outer unknown. + /// The new debug document helper. + void CreateDebugDocumentHelper(object outerUnknown, out IDebugDocumentHelper64 helper); + } + + /// + /// IDebugApplication32 COM interface. + /// + [Guid("51973C32-CB0C-11d0-B5C9-00A0244A0E7A")] + public interface IDebugApplication32 + { + } + + /// + /// IDebugApplication64 COM interface. + /// + [Guid("4dedc754-04c7-4f10-9e60-16a390fe6e62")] + public interface IDebugApplication64 + { + } + + /// + /// IDebugDocumentHelper32 COM interface. + /// + [Guid("51973C26-CB0C-11d0-B5C9-00A0244A0E7A")] + public interface IDebugDocumentHelper32 + { + } + + /// + /// IDebugDocumentHelper64 COM interface. + /// + [Guid("c4c7363c-20fd-47f9-bd82-4855e0150871")] + public interface IDebugDocumentHelper64 + { + } + + /// + /// IActiveScriptProfilerCallback COM interface. + /// + [Guid("740eca23-7d9d-42e5-ba9d-f8b24b1c7a9b")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IActiveScriptProfilerCallback + { + /// + /// Called when the profile is started. + /// + /// The context provided when profiling was started. + void Initialize(uint context); + + /// + /// Called when profiling is stopped. + /// + /// The reason code provided when profiling was stopped. + void Shutdown(uint reason); + + /// + /// Called when a script is compiled. + /// + /// The ID of the script. + /// The type of the script. + /// The debug document context, if any. + void ScriptCompiled(int scriptId, ProfilerScriptType type, IntPtr debugDocumentContext); + + /// + /// Called when a function is compiled. + /// + /// The ID of the function. + /// The ID of the script. + /// The name of the function. + /// The function name hint. + /// The debug document context, if any. + void FunctionCompiled(int functionId, int scriptId, [MarshalAs(UnmanagedType.LPWStr)] string functionName, [MarshalAs(UnmanagedType.LPWStr)] string functionNameHint, IntPtr debugDocumentContext); + + /// + /// Called when a function is entered. + /// + /// The ID of the script. + /// The ID of the function. + void OnFunctionEnter(int scriptId, int functionId); + + /// + /// Called when a function is exited. + /// + /// The ID of the script. + /// The ID of the function. + void OnFunctionExit(int scriptId, int functionId); + } + + /// + /// IActiveScriptProfilerCallback2 COM interface. + /// + [Guid("31B7F8AD-A637-409C-B22F-040995B6103D")] + public interface IActiveScriptProfilerCallback2 : IActiveScriptProfilerCallback + { + /// + /// Called when a function is entered by name. + /// + /// The name of the function. + /// The type of the function. + void OnFunctionEnterByName(string functionName, ProfilerScriptType type); + + /// + /// Called when a function is exited by name. + /// + /// The name of the function. + /// The type of the function. + void OnFunctionExitByName(string functionName, ProfilerScriptType type); + } + + /// + /// IActiveScriptProfilerHeapEnum COM interface. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Name defined in COM.")] + [Guid("32E4694E-0D37-419B-B93D-FA20DED6E8EA")] + public interface IActiveScriptProfilerHeapEnum + { + } + + /// + /// Throws if a native method returns an error code. + /// + /// The error. + internal static void ThrowIfError(JavaScriptErrorCode error) + { + if (error != JavaScriptErrorCode.NoError) + { + switch (error) + { + case JavaScriptErrorCode.InvalidArgument: + throw new JavaScriptUsageException(error, "Invalid argument."); + + case JavaScriptErrorCode.NullArgument: + throw new JavaScriptUsageException(error, "Null argument."); + + case JavaScriptErrorCode.NoCurrentContext: + throw new JavaScriptUsageException(error, "No current context."); + + case JavaScriptErrorCode.InExceptionState: + throw new JavaScriptUsageException(error, "Runtime is in exception state."); + + case JavaScriptErrorCode.NotImplemented: + throw new JavaScriptUsageException(error, "Method is not implemented."); + + case JavaScriptErrorCode.WrongThread: + throw new JavaScriptUsageException(error, "Runtime is active on another thread."); + + case JavaScriptErrorCode.RuntimeInUse: + throw new JavaScriptUsageException(error, "Runtime is in use."); + + case JavaScriptErrorCode.BadSerializedScript: + throw new JavaScriptUsageException(error, "Bad serialized script."); + + case JavaScriptErrorCode.InDisabledState: + throw new JavaScriptUsageException(error, "Runtime is disabled."); + + case JavaScriptErrorCode.CannotDisableExecution: + throw new JavaScriptUsageException(error, "Cannot disable execution."); + + case JavaScriptErrorCode.AlreadyDebuggingContext: + throw new JavaScriptUsageException(error, "Context is already in debug mode."); + + case JavaScriptErrorCode.HeapEnumInProgress: + throw new JavaScriptUsageException(error, "Heap enumeration is in progress."); + + case JavaScriptErrorCode.ArgumentNotObject: + throw new JavaScriptUsageException(error, "Argument is not an object."); + + case JavaScriptErrorCode.InProfileCallback: + throw new JavaScriptUsageException(error, "In a profile callback."); + + case JavaScriptErrorCode.InThreadServiceCallback: + throw new JavaScriptUsageException(error, "In a thread service callback."); + + case JavaScriptErrorCode.CannotSerializeDebugScript: + throw new JavaScriptUsageException(error, "Cannot serialize a debug script."); + + case JavaScriptErrorCode.AlreadyProfilingContext: + throw new JavaScriptUsageException(error, "Already profiling this context."); + + case JavaScriptErrorCode.IdleNotEnabled: + throw new JavaScriptUsageException(error, "Idle is not enabled."); + + case JavaScriptErrorCode.OutOfMemory: + throw new JavaScriptEngineException(error, "Out of memory."); + + case JavaScriptErrorCode.ScriptException: + { + JavaScriptValue errorObject; + JavaScriptErrorCode innerError = JsGetAndClearException(out errorObject); + + if (innerError != JavaScriptErrorCode.NoError) + { + throw new JavaScriptFatalException(innerError); + } + + throw new JavaScriptScriptException(error, errorObject, "Script threw an exception."); + } + + case JavaScriptErrorCode.ScriptCompile: + { + JavaScriptValue errorObject; + JavaScriptErrorCode innerError = JsGetAndClearException(out errorObject); + + if (innerError != JavaScriptErrorCode.NoError) + { + throw new JavaScriptFatalException(innerError); + } + + throw new JavaScriptScriptException(error, errorObject, "Compile error."); + } + + case JavaScriptErrorCode.ScriptTerminated: + throw new JavaScriptScriptException(error, JavaScriptValue.Invalid, "Script was terminated."); + + case JavaScriptErrorCode.ScriptEvalDisabled: + throw new JavaScriptScriptException(error, JavaScriptValue.Invalid, "Eval of strings is disabled in this runtime."); + + case JavaScriptErrorCode.Fatal: + throw new JavaScriptFatalException(error); + + default: + throw new JavaScriptFatalException(error); + } + } + } + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateRuntime(JavaScriptRuntimeAttributes attributes, JavaScriptThreadServiceCallback threadService, out JavaScriptRuntime runtime); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCollectGarbage(JavaScriptRuntime handle); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsDisposeRuntime(JavaScriptRuntime handle); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetRuntimeMemoryUsage(JavaScriptRuntime runtime, out UIntPtr memoryUsage); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetRuntimeMemoryLimit(JavaScriptRuntime runtime, out UIntPtr memoryLimit); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsSetRuntimeMemoryLimit(JavaScriptRuntime runtime, UIntPtr memoryLimit); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsSetRuntimeMemoryAllocationCallback(JavaScriptRuntime runtime, IntPtr callbackState, JavaScriptMemoryAllocationCallback allocationCallback); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsSetRuntimeBeforeCollectCallback(JavaScriptRuntime runtime, IntPtr callbackState, JavaScriptBeforeCollectCallback beforeCollectCallback); + + [DllImport("chakra.dll", EntryPoint = "JsAddRef")] + internal static extern JavaScriptErrorCode JsContextAddRef(JavaScriptContext reference, out uint count); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsAddRef(JavaScriptValue reference, out uint count); + + [DllImport("chakra.dll", EntryPoint = "JsRelease")] + internal static extern JavaScriptErrorCode JsContextRelease(JavaScriptContext reference, out uint count); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsRelease(JavaScriptValue reference, out uint count); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateContext(JavaScriptRuntime runtime, out JavaScriptContext newContext); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetCurrentContext(out JavaScriptContext currentContext); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsSetCurrentContext(JavaScriptContext context); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetRuntime(JavaScriptContext context, out JavaScriptRuntime runtime); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsStartDebugging(); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsIdle(out uint nextIdleTick); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsParseScript(string script, JavaScriptSourceContext sourceContext, string sourceUrl, out JavaScriptValue result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsRunScript(string script, JavaScriptSourceContext sourceContext, string sourceUrl, out JavaScriptValue result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsSerializeScript(string script, byte[] buffer, ref ulong bufferSize); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsParseSerializedScript(string script, byte[] buffer, JavaScriptSourceContext sourceContext, string sourceUrl, out JavaScriptValue result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsRunSerializedScript(string script, byte[] buffer, JavaScriptSourceContext sourceContext, string sourceUrl, out JavaScriptValue result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetPropertyIdFromName(string name, out JavaScriptPropertyId propertyId); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetPropertyNameFromId(JavaScriptPropertyId propertyId, out string name); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetUndefinedValue(out JavaScriptValue undefinedValue); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetNullValue(out JavaScriptValue nullValue); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetTrueValue(out JavaScriptValue trueValue); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetFalseValue(out JavaScriptValue falseValue); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsBoolToBoolean(bool value, out JavaScriptValue booleanValue); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsBooleanToBool(JavaScriptValue booleanValue, out bool boolValue); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsConvertValueToBoolean(JavaScriptValue value, out JavaScriptValue booleanValue); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetValueType(JavaScriptValue value, out JavaScriptValueType type); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsDoubleToNumber(double doubleValue, out JavaScriptValue value); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsIntToNumber(int intValue, out JavaScriptValue value); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsNumberToDouble(JavaScriptValue value, out double doubleValue); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsConvertValueToNumber(JavaScriptValue value, out JavaScriptValue numberValue); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetStringLength(JavaScriptValue sringValue, out int length); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsPointerToString(string value, UIntPtr stringLength, out JavaScriptValue stringValue); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsStringToPointer(JavaScriptValue value, out IntPtr stringValue, out UIntPtr stringLength); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsConvertValueToString(JavaScriptValue value, out JavaScriptValue stringValue); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsVariantToValue(ref object var, out JavaScriptValue value); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsValueToVariant(JavaScriptValue obj, out object var); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetGlobalObject(out JavaScriptValue globalObject); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateObject(out JavaScriptValue obj); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateExternalObject(IntPtr data, JavaScriptObjectFinalizeCallback finalizeCallback, out JavaScriptValue obj); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsConvertValueToObject(JavaScriptValue value, out JavaScriptValue obj); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetPrototype(JavaScriptValue obj, out JavaScriptValue prototypeObject); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsSetPrototype(JavaScriptValue obj, JavaScriptValue prototypeObject); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetExtensionAllowed(JavaScriptValue obj, out bool value); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsPreventExtension(JavaScriptValue obj); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetProperty(JavaScriptValue obj, JavaScriptPropertyId propertyId, out JavaScriptValue value); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetOwnPropertyDescriptor(JavaScriptValue obj, JavaScriptPropertyId propertyId, out JavaScriptValue propertyDescriptor); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetOwnPropertyNames(JavaScriptValue obj, out JavaScriptValue propertyNames); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsSetProperty(JavaScriptValue obj, JavaScriptPropertyId propertyId, JavaScriptValue value, bool useStrictRules); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsHasProperty(JavaScriptValue obj, JavaScriptPropertyId propertyId, out bool hasProperty); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsDeleteProperty(JavaScriptValue obj, JavaScriptPropertyId propertyId, bool useStrictRules, out JavaScriptValue result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsDefineProperty(JavaScriptValue obj, JavaScriptPropertyId propertyId, JavaScriptValue propertyDescriptor, out bool result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsHasIndexedProperty(JavaScriptValue obj, JavaScriptValue index, out bool result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetIndexedProperty(JavaScriptValue obj, JavaScriptValue index, out JavaScriptValue result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsSetIndexedProperty(JavaScriptValue obj, JavaScriptValue index, JavaScriptValue value); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsDeleteIndexedProperty(JavaScriptValue obj, JavaScriptValue index); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsEquals(JavaScriptValue obj1, JavaScriptValue obj2, out bool result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsStrictEquals(JavaScriptValue obj1, JavaScriptValue obj2, out bool result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsHasExternalData(JavaScriptValue obj, out bool value); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetExternalData(JavaScriptValue obj, out IntPtr externalData); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsSetExternalData(JavaScriptValue obj, IntPtr externalData); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateArray(uint length, out JavaScriptValue result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCallFunction(JavaScriptValue function, JavaScriptValue[] arguments, ushort argumentCount, out JavaScriptValue result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsConstructObject(JavaScriptValue function, JavaScriptValue[] arguments, ushort argumentCount, out JavaScriptValue result); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateFunction(JavaScriptNativeFunction nativeFunction, IntPtr externalData, out JavaScriptValue function); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateError(JavaScriptValue message, out JavaScriptValue error); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateRangeError(JavaScriptValue message, out JavaScriptValue error); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateReferenceError(JavaScriptValue message, out JavaScriptValue error); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateSyntaxError(JavaScriptValue message, out JavaScriptValue error); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateTypeError(JavaScriptValue message, out JavaScriptValue error); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsCreateURIError(JavaScriptValue message, out JavaScriptValue error); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsHasException(out bool hasException); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsGetAndClearException(out JavaScriptValue exception); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsSetException(JavaScriptValue exception); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsDisableRuntimeExecution(JavaScriptRuntime runtime); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsEnableRuntimeExecution(JavaScriptRuntime runtime); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsIsRuntimeExecutionDisabled(JavaScriptRuntime runtime, out bool isDisabled); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsStartProfiling(IActiveScriptProfilerCallback callback, ProfilerEventMask eventMask, int context); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsStopProfiling(int reason); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsEnumerateHeap(out IActiveScriptProfilerHeapEnum enumerator); + + [DllImport("chakra.dll")] + internal static extern JavaScriptErrorCode JsIsEnumeratingHeap(out bool isEnumeratingHeap); + + [DllImport("chakra.dll", CharSet = CharSet.Unicode)] + internal static extern JavaScriptErrorCode JsProjectWinRTNamespace(string namespaceName); + + /// + /// ProcessDebugManager COM interface. + /// + [ComImport] + [Guid("78A51822-51F4-11D0-8F20-00805F2CD064")] + public class ProcessDebugManager + { + } + } +} diff --git a/ReactWindows/ReactNative/Modules/Core/RCTNativeAppEventEmitter.cs b/ReactWindows/ReactNative/Modules/Core/RCTNativeAppEventEmitter.cs new file mode 100644 index 00000000000..04f9f1073e9 --- /dev/null +++ b/ReactWindows/ReactNative/Modules/Core/RCTNativeAppEventEmitter.cs @@ -0,0 +1,20 @@ +using ReactNative.Bridge; + +namespace ReactNative.Modules.Core +{ + /// + /// Native app event emitter. + /// + public class RCTNativeAppEventEmitter : JavaScriptModuleBase + { + /// + /// Emit a native app event. + /// + /// The event name. + /// The event data. + public void emit(string eventName, object data) + { + Invoke(nameof(emit), eventName, data); + } + } +} diff --git a/ReactWindows/ReactNative/Properties/AssemblyInfo.cs b/ReactWindows/ReactNative/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..1d25425b68b --- /dev/null +++ b/ReactWindows/ReactNative/Properties/AssemblyInfo.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ReactNative")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ReactNative")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/ReactWindows/ReactNative/Properties/ReactNative.rd.xml b/ReactWindows/ReactNative/Properties/ReactNative.rd.xml new file mode 100644 index 00000000000..f4bbd23eceb --- /dev/null +++ b/ReactWindows/ReactNative/Properties/ReactNative.rd.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/ReactWindows/ReactNative/ReactMethodAttribute.cs b/ReactWindows/ReactNative/ReactMethodAttribute.cs new file mode 100644 index 00000000000..af5f8b679e4 --- /dev/null +++ b/ReactWindows/ReactNative/ReactMethodAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace ReactNative +{ + /// + /// An attribute for annotating methods in an + /// . + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class ReactMethodAttribute : Attribute + { + } +} diff --git a/ReactWindows/ReactNative/ReactNative.csproj b/ReactWindows/ReactNative/ReactNative.csproj new file mode 100644 index 00000000000..b2b9d5b749f --- /dev/null +++ b/ReactWindows/ReactNative/ReactNative.csproj @@ -0,0 +1,192 @@ + + + + + Debug + AnyCPU + {C7673AD5-E3AA-468C-A5FD-FA38154E205C} + Library + Properties + ReactNative + ReactNative + en-US + UAP + 10.0.10586.0 + 10.0.10240.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + x86 + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + + + x86 + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + + + ARM + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM + false + prompt + + + ARM + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM + false + prompt + + + x64 + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + + + x64 + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 14.0 + + + + \ No newline at end of file diff --git a/ReactWindows/ReactNative/Reflection/ExpressionExtensions.cs b/ReactWindows/ReactNative/Reflection/ExpressionExtensions.cs new file mode 100644 index 00000000000..1e3535ea89a --- /dev/null +++ b/ReactWindows/ReactNative/Reflection/ExpressionExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq.Expressions; + +namespace ReactNative.Reflection +{ + /// + /// Helper methods for expressions. + /// + static class ExpressionExtensions + { + /// + /// Transforms an expression into a parameter that can be used in + /// multiple places in a larger expression. Particularly useful for + /// declaring parameters in a . + /// + /// Expression type. + /// Result type. + /// The expression. + /// The result selector. + /// The result. + public static T Let(this TExpression expression, Func selector) + where TExpression : Expression + { + return selector(expression); + } + } +} diff --git a/ReactWindows/ReactNative/Reflection/MethodInfoHelpers.cs b/ReactWindows/ReactNative/Reflection/MethodInfoHelpers.cs new file mode 100644 index 00000000000..797868958d0 --- /dev/null +++ b/ReactWindows/ReactNative/Reflection/MethodInfoHelpers.cs @@ -0,0 +1,13 @@ +using System.Reflection; +using System.Threading.Tasks; + +namespace ReactNative.Reflection +{ + /// + /// Helper methods for . + /// + static class MethodInfoHelpers + { + + } +} diff --git a/ReactWindows/ReactNative/Reflection/ReflectionHelpers.cs b/ReactWindows/ReactNative/Reflection/ReflectionHelpers.cs new file mode 100644 index 00000000000..19d0406d007 --- /dev/null +++ b/ReactWindows/ReactNative/Reflection/ReflectionHelpers.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; + +namespace ReactNative.Reflection +{ + /// + /// Helpers for . + /// + static class ReflectionHelpers + { + /// + /// Checks if a method is asynchronous. + /// + /// The method. + /// + /// true if the method is asynchronous, false otherwise. + /// + public static bool IsAsync(this MethodInfo methodInfo) + { + return typeof(Task).IsAssignableFrom(methodInfo.ReturnType); + } + + /// + /// Gets the at the root of the expression. + /// + /// The expression. + /// The reflected member. + public static MemberInfo InfoOf(Expression expression) + { + return _InfoOf(expression.Body); + } + + /// + /// Gets the at the root of the expression. + /// + /// Type of result. + /// The expression. + /// The reflected member. + public static MemberInfo InfoOf(Expression> expression) + { + return _InfoOf(expression.Body); + } + + /// + /// Gets the at the root of the expression. + /// + /// Type of input. + /// Type of result. + /// The expression. + /// The reflected member. + + public static MemberInfo InfoOf(Expression> expression) + { + return _InfoOf(expression.Body); + } + + /// + /// Gets the at the root of the expression. + /// + /// The expression. + /// The reflected member. + public static MemberInfo InfoOf(Expression expression) + { + return _InfoOf(expression); + } + + private static MemberInfo _InfoOf(Expression expression) + { + switch (expression.NodeType) + { + case ExpressionType.Call: + var callExpression = (MethodCallExpression)expression; + return callExpression.Method; + case ExpressionType.MemberAccess: + var memberExpression = (MemberExpression)expression; + return memberExpression.Member; + case ExpressionType.New: + var newExpression = (NewExpression)expression; + return newExpression.Constructor; + } + + throw new InvalidOperationException("Expected either a method call, member access, or new expression."); + } + } +} diff --git a/ReactWindows/ReactNative/Tracing/EventSourceManager.cs b/ReactWindows/ReactNative/Tracing/EventSourceManager.cs new file mode 100644 index 00000000000..6857234cee9 --- /dev/null +++ b/ReactWindows/ReactNative/Tracing/EventSourceManager.cs @@ -0,0 +1,15 @@ +using System.Diagnostics.Tracing; + +namespace ReactNative.Tracing +{ + /// + /// Static for the application. + /// + static class EventSourceManager + { + /// + /// The instance. + /// + public static EventSource Instance { get; } = new EventSource("ReactNative"); + } +} diff --git a/ReactWindows/ReactNative/Tracing/TraceDisposable.cs b/ReactWindows/ReactNative/Tracing/TraceDisposable.cs new file mode 100644 index 00000000000..dc78ce867b7 --- /dev/null +++ b/ReactWindows/ReactNative/Tracing/TraceDisposable.cs @@ -0,0 +1,55 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.Tracing; + +namespace ReactNative.Tracing +{ + /// + /// Disposable implementation for tracing operations. + /// + /// + /// This implementation is created as a struct to minimize the heap impact + /// for tracing operations. + /// + struct /* do not make class */ TraceDisposable : IDisposable + { + private static readonly Stopwatch s_stopwatch = Stopwatch.StartNew(); + + private readonly int _traceId; + private readonly string _title; + private readonly long _timestamp; + + /// + /// Instantiates the . + /// + /// The trace ID. + /// The event title. + public TraceDisposable(int traceId, string title) + { + _traceId = traceId; + _title = title; + _timestamp = s_stopwatch.ElapsedTicks; + } + + /// + /// Disposed the instance, capturing the trace. + /// + public void Dispose() + { + EventSourceManager.Instance.Write(_title, new EventData(_traceId, TimeSpan.FromTicks(s_stopwatch.ElapsedTicks - _timestamp))); + } + + [EventData] + struct EventData + { + public EventData(int source, TimeSpan elapsed) + { + Source = source; + Elapsed = elapsed; + } + + public int Source { get; } + public TimeSpan Elapsed { get; } + } + } +} diff --git a/ReactWindows/ReactNative/Tracing/Tracer.cs b/ReactWindows/ReactNative/Tracing/Tracer.cs new file mode 100644 index 00000000000..8b171b93e68 --- /dev/null +++ b/ReactWindows/ReactNative/Tracing/Tracer.cs @@ -0,0 +1,46 @@ +namespace ReactNative.Tracing +{ + /// + /// Tracing helpers for the application. + /// + static class Tracer + { + /// + /// Trace ID for bridge events. + /// + public const int TRACE_TAG_REACT_BRIDGE = 0; + + /// + /// Trace ID for application events. + /// + public const int TRACE_TAG_REACT_APPS = 1; + + /// + /// Trace ID for view events. + /// + public const int TRACE_TAG_REACT_VIEW = 2; + + /// + /// Creates a disposable to trace an operation from start to finish. + /// + /// The trace ID. + /// The event title. + /// + /// The instance to dispose when the operation is finished. + /// + public static TraceDisposable Trace(int traceId, string title) + { + return new TraceDisposable(traceId, title); + } + + /// + /// Writes a trace. + /// + /// The trace tag. + /// The trace message. + public static void Write(string tag, string message) + { + EventSourceManager.Instance.Write(tag, message); + } + } +} diff --git a/ReactWindows/ReactNative/UIManager/AppRegistry.cs b/ReactWindows/ReactNative/UIManager/AppRegistry.cs new file mode 100644 index 00000000000..2f904d3dc76 --- /dev/null +++ b/ReactWindows/ReactNative/UIManager/AppRegistry.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; + +namespace ReactNative.UIManager +{ + /// + /// The application registry. + /// + public class AppRegistry : JavaScriptModuleBase + { + /// + /// Run the application. + /// + /// The app key. + /// The app parameters. + void runApplication(string appKey, JObject appParameters) + { + Invoke(nameof(runApplication), appKey, appParameters); + } + + /// + /// Unmount the application. + /// + /// The root tag node. + void unmountApplicationComponentAtRootTag(int rootTagNode) + { + Invoke(nameof(unmountApplicationComponentAtRootTag), rootTagNode); + } + } +} diff --git a/ReactWindows/ReactNative/UIManager/Events/RCTEventEmitter.cs b/ReactWindows/ReactNative/UIManager/Events/RCTEventEmitter.cs new file mode 100644 index 00000000000..5917d66bfff --- /dev/null +++ b/ReactWindows/ReactNative/UIManager/Events/RCTEventEmitter.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; + +namespace ReactNative.Modules.Core +{ + /// + /// JavaScript event emitter. + /// + public sealed class RCTEventEmitter : JavaScriptModuleBase + { + /// + /// Receive an event. + /// + /// The target tag. + /// The event name. + /// The event data. + public void receiveEvent(int targetTag, string eventName, JObject @event) + { + Invoke(nameof(receiveEvent), targetTag, eventName, @event); + } + + /// + /// Receives touches. + /// + /// The event name. + /// The touches. + /// The changed indices. + public void receiveTouches(string eventName, JArray touches, JArray changedIndices) + { + Invoke(nameof(receiveTouches), touches, changedIndices); + } + } +} diff --git a/ReactWindows/ReactNative/UIManager/RootView.cs b/ReactWindows/ReactNative/UIManager/RootView.cs new file mode 100644 index 00000000000..1b54cde1ff4 --- /dev/null +++ b/ReactWindows/ReactNative/UIManager/RootView.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ReactNative.UIManager +{ + /// + /// Interface for the root native view of a React native application. + /// + public interface RootView + { + /// + /// Called when a child starts a native gesture (e.g. a scroll in a ScrollView). + /// + /// + //void onChildStartedNativeGesture(CalibrationEventArgs wpEvent); + } +} diff --git a/ReactWindows/ReactNative/UIManager/SizeMonitoringFrameLayout.cs b/ReactWindows/ReactNative/UIManager/SizeMonitoringFrameLayout.cs new file mode 100644 index 00000000000..6ccd774388a --- /dev/null +++ b/ReactWindows/ReactNative/UIManager/SizeMonitoringFrameLayout.cs @@ -0,0 +1,46 @@ + +namespace ReactNative.UIManager +{ + /// + /// allows registering for size change events. The main purpose for this class is to hide complexity of ReactRootView + /// + public class SizeMonitoringFrameLayout + { + public interface OnSizeChangedListener + { + void onSizeChanged(int width, int height, int oldWidth, int oldHeight); + } + + private OnSizeChangedListener mOnSizeChangedListener; + + /*public SizeMonitoringFrameLayout(Context context) + { + super(context); + } + + public SizeMonitoringFrameLayout(Context context, AttributeSet attrs) + { + super(context, attrs); + } + + public SizeMonitoringFrameLayout(Context context, AttributeSet attrs, int defStyle) + { + super(context, attrs, defStyle); + }*/ + + public void setOnSizeChangedListener(OnSizeChangedListener onSizeChangedListener) + { + mOnSizeChangedListener = onSizeChangedListener; + } + + protected void onSizeChanged(int w, int h, int oldw, int oldh) + { + // base.onSizeChanged(w, h, oldw, oldh); + + if (mOnSizeChangedListener != null) + { + mOnSizeChangedListener.onSizeChanged(w, h, oldw, oldh); + } + } + } +} diff --git a/ReactWindows/ReactNative/project.json b/ReactWindows/ReactNative/project.json new file mode 100644 index 00000000000..f94633cebfe --- /dev/null +++ b/ReactWindows/ReactNative/project.json @@ -0,0 +1,18 @@ +{ + "dependencies": { + "Microsoft.NETCore.UniversalWindowsPlatform": "5.0.0", + "Newtonsoft.Json": "7.0.1", + "Rx-Xaml": "2.2.5" + }, + "frameworks": { + "uap10.0": {} + }, + "runtimes": { + "win10-arm": {}, + "win10-arm-aot": {}, + "win10-x86": {}, + "win10-x86-aot": {}, + "win10-x64": {}, + "win10-x64-aot": {} + } +} \ No newline at end of file