Mobile Corner

Hosting JavaScript in a Windows 10 App

An introduction to the Chakra JavaScript engine and how it can be used within a Windows 10 app to execute JavaScript.

Initially, the concept of executing JavaScript within a Universal Windows Platform (UWP) app might seem a little weird. However, with the ever-increasing need to build cross-platform apps there are scenarios where it makes sense.

One of the most interesting aspects of JavaScript is that it has become the one language that nearly all developers have had some exposure to and is independent of any particular device or OS. While driven initially by Web browsers in order to deliver interactive capabilities for Web sites, JavaScript is now often used outside the browser.

On the continuum of cross-platform technologies, at one end there are responsive Web sites, capable of delivering a good mobile experience; on the other end is building a native application for each platform. Neither option is ideal: The mobile experience misses out on device capabilities, and for most platforms there's no way for the Web site to be featured in the Windows Store, making it hard for developers to promote and make sales; plus, building native applications is expensive and it's hard to ensure the same business logic is applied correctly.

There are some good middle-ground options, such as PhoneGap and the tools offered by Xamarin. However, an option that isn't often considered is to develop the core business logic of the application in JavaScript. There would be a native application developed for each platform, yet the core business logic, written in JavaScript, is shared between the platforms.

In this article I'll demonstrate the use of the Chakra JavaScript engine, which powers the Microsoft Edge browser in Windows 10, in a UWP app.

Before I get started I'd like to recognize that the starting point for the code I'll use in this article has come from Paul Vick's chakra-host GitHub repository, which includes samples and wrappers for both managed and native code. I've made only minor changes to these wrappers to allow them to work within a UWP app and the full source code is available in the download accompanying this article. Paul's blog also has a number of great posts discussing some of the aspects of the Chakra execution engine in more detail.

I'm going to step through creating a basic JavaScript interpreter that accepts input from the user in a TextBox on the left of the screen (see Figure 1) and displays the output on the right side of the screen when the user clicks the Execute button.

[Click on image for larger view.] Figure 1. Simple JavaScript Interpreter

As you can see the UX for this app is relatively simple. After starting a new app based on the Blank Application (Universal Windows) project template, the MainPage is split into two columns with a TextBox and Button on the left side, and a ListView to display the output on the right side, as shown in the XAMLin Listing 1.

Listing 1: Building the Main Page
<Page x:Class="JavascriptHostSample.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.ColumnDefinitions>
      <ColumnDefinition />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <RelativePanel>
      <TextBox Text="{x:Bind CurrentViewModel.CodeToExecute, Mode=TwoWay}"
               AcceptsReturn="True"
               TextWrapping="Wrap"
               RelativePanel.AlignTopWithPanel="True"
               RelativePanel.AlignLeftWithPanel="True"
               RelativePanel.AlignRightWithPanel="True"
               RelativePanel.Above="ExecuteButton" />

      <Button Click="{x:Bind CurrentViewModel.Execute}"
              RelativePanel.AlignLeftWithPanel="True"
              RelativePanel.AlignRightWithPanel="True"
              RelativePanel.AlignBottomWithPanel="True"
              x:Name="ExecuteButton"
              HorizontalAlignment="Stretch">Execute</Button>
    </RelativePanel>
    <ListView Grid.Column="1"
      ItemsSource="{x:Bind CurrentViewModel.Output}">
      <ListView.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding}" />
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </Grid>
</Page>

Note that in this case we're using a RelativePanel to arrange the TextBox and Button. This could just have easily been done using multiple rows within the Grid. If you haven't already started using the RelativePanel it's worth reviewing this code and getting familiar with it as it becomes important when you're attempting to build UXes that work across the wide range of devices that are targetable by UWP apps.

In order to interact with the Chakra JavaScript engine I'm going to import the helper classes that Vick has made available in his samples (all classes within the Hosting folder, as well as the Profiler class). These wrap the calls to the Chakra engine, as well as wrapping arguments and result values to make them easier to work with from managed code.

When the user runs the app, enters some JavaScript in the TextBox and clicks the Execute button, the app needs to invoke the JavaScript using the Chakra engine and then return the result to be displayed on the screen. I'll start by creating a MainViewModel class that will be used to receive the JavaScript code to execute from the user (CodeToExecute property), as well as passing any output back to the user (Output property), as shown in Listing 2.

Listing 2: Creating a MainViewModel Class
public sealed partial class MainPage 
{
  public MainPage()
  {
    DataContext = new MainViewModel();
    InitializeComponent();
  }
  public MainViewModel CurrentViewModel => DataContext as MainViewModel;
}

public class MainViewModel
{
  public string CodeToExecute { get; set; }

  public ObservableCollection<string> Output { get; } = new ObservableCollection<string>();
  public void Execute()
  {
  }
}

In order to invoke the JavaScript the MainViewModel will need to access the Chakra engine. This is done by creating a runtime, which you can think of as an instance of the JavaScript execution engine, which has everything needed to load and execute JavaScript. However, in addition to a runtime, JavaScript needs to be invoked within an execution context.

At a simplicity level you can think of an execution context as being the sandbox that the JavaScript code is going to be invoked within. For example, each execution context has an independent global object and definitions within one execution context don't appear in another context. This is quite an interesting aspect of the Chakra engine as you can dynamically switch between multiple contexts to ensure isolation of data, definitions and so on. In the case of this example, I'll be creating a new context each time the user clicks the Execute button, this ensures that variables declared in a previous execution won't be available or interfere with the code being executed.

Listing 3 creates a new JavaScriptRuntime (the wrapper class included from Vick's sample code) instance in the constructor of the MainViewModel. Each time the Execute method is invoked a new JavaScriptContext is created and set to be the current context. The Scope class makes it easy to switch between multiple contexts, if required.

Listing 3: Creating a JavaScriptRuntime Instance
private JavaScriptRuntime Runtime { get; }
public MainViewModel()
{
  Runtime = JavaScriptRuntime.Create();
}

public void Execute()
{
  using (var context = Runtime.CreateContext())
  using (new JavaScriptContext.Scope(context))
  {
    try
    {
      var result = JavaScriptContext.RunScript(CodeToExecute);
      var numberResult = result.ConvertToNumber();
      var doubleResult = numberResult.ToDouble();
      Output.Add("Result: " + doubleResult);

    }
    catch (Exception ex)
    {
      Output.Add("Exception:" + ex.Message);
    }

  }
}

Currently, the only output the user sees is what's been returned at the end of invoking the script. So, for example, an input of "1+1" would return "2," but an input of "var x = 1+1" would return NaN, as there really isn't a return value. A typical command-line application would typically use Console.WriteLine to display progress to the user. In the case of the Chakra engine there's no console to which the JavaScript code can write. However, it is possible to create a function that calls back into managed code, and from there an entry can be added to the Output collection.

Defining a callback to managed code involves creating a function in JavaScript and associating it with a delegate. The following helper function accepts parameters that determine the JavaScript object on which the function will be created, the name of the function to be created and the delegate in managed code to be executed when the JavaScript function is invoked:

private void DefineCallback
  (JavaScriptValue hostObject, string callbackName, JavaScriptNativeFunction callbackDelegate)
{
  var propertyId = JavaScriptPropertyId.FromString(callbackName);

  var function = JavaScriptValue.CreateFunction(callbackDelegate);

  hostObject.SetProperty(propertyId, function, true);
}

In this case you're going to create a function called "echo," which can be called in order to pass data out from the executing Javascript to the host application where it can be displayed in the Output area. The Echo method, whose signature matches the JavaScriptNativeFunction delegate, is defined in Listing 4.

Listing 4: Echo Function
private JavaScriptValue Echo(JavaScriptValue callee, 
  bool isConstructCall, JavaScriptValue[] arguments,
  ushort argumentCount, IntPtr callbackData)
{
  for (uint index = 1; index < argumentCount; index++)
  {
    Output.Add(arguments[index].ConvertToString().ToString());
  }

  return JavaScriptValue.True;
}

private void DefineEcho()
{
  var globalObject = JavaScriptValue.GlobalObject;

  DefineCallback(globalObject, "echo", Echo);
}

The DefineEcho method is used to define the "echo" method on the global JavaScript object. This means it can be invoked by simply calling echo(….);. It's unlikely that you'll want to define functions directly on the global object, as this can be considered bad practice due to the risk it may conflict with other JavaScript code that might be loaded (for example if you were to load a third-party JavaScript library, there's no guarantee that it hasn't defined an "echo" method on the global object). To better encapsulate this, and other functions that you may wish to define, you can define a different object where you'll define the function:

private void DefineEcho()
{
  var globalObject = JavaScriptValue.GlobalObject;

  var hostObject = JavaScriptValue.CreateObject();
  var hostPropertyId = JavaScriptPropertyId.FromString("managedhost");
  globalObject.SetProperty(hostPropertyId, hostObject, true);
            
  DefineCallback(hostObject, "echo", Echo);
}

In this code a new object is created and added to the "managedhost" property on the global object (note that while there's still the possibility of a conflict on this property, with an appropriate named property the risk of this is much lower, and this object will hold all the functions that are to be defined), and then the "echo" function is defined on the new object. The only difference is that in JavaScript the function then needs to be invoked by calling managedhost.echo(….);.

When working with the Chakra engine there are a couple of points that are worth observing. The first relates to the previous example where a conduit was opened between the executing JavaScript code and the host application. Both the Chakra engine and the host application have a garbage collection system that will clear up unreferenced objects.

Because the ability to call back to the host application from JavaScript relies on both the JavaScript function and the managed code delegate to exist, it's important to make sure they don't get accidentally cleaned up. What can be difficult to diagnose is that the execution of the garbage collection appears indeterministic and, thus, hard to track down which objects have been cleaned up.

The previous example seemed to work fine. However, there is a risk that the anonymous delegate that gets created pointing to the Echo method can be collected because it's no longer being referenced by any entity within the host application. For example, in the following code I'm forcing the garbage collector:

DefineEcho();

GC.Collect();

var result = JavaScriptContext.RunScript(CodeToExecute);

When the JavaScript code is run you'll then see an AccessViolationException bubble up if the code attempts to invoke the managedhost.echo(…) method (see Figure 2). This is because the delegate passed into the DefineCallback method has been collected because it's no longer referenced by any managed object.

[Click on image for larger view.] Figure 2. Exception Raised When Managed Object Collected

Luckily, there is a simple solution, which is to explicitly create and then maintain a reference to the delegate for the Echo method. The DefineEcho method now looks like the following code:

private JavaScriptNativeFunction EchoDelegate { get; set; }

private void DefineEcho()
{
  var globalObject = JavaScriptValue.GlobalObject;

  var hostObject = JavaScriptValue.CreateObject();
  var hostPropertyId = JavaScriptPropertyId.FromString("managedhost");
  globalObject.SetProperty(hostPropertyId, hostObject, true);
  EchoDelegate = Echo;
  DefineCallback(hostObject, "echo", EchoDelegate);
}

It's also possible when working with the Chakra engine to use Visual Studio to debug the JavaScript code being invoked. Visual Studio can only attach either managed code or JavaScript debugger; to switch between debugger types, open the Project Properties window for the UWP project (if you want to do this for a console application you need to open the generated EXE in Visual Studio via File | Open | Project/Solution and then select the EXE file and select what debugger type to use) and change the Application process dropdown in the Debugger type section of the Debug tab (see Figure 3).

[Click on image for larger view.] Figure 3. Switching to Script Debugging

In addition to switching to Script debugging, you also need to enable debugging within the Chakra engine. There's a helper method in the execution context wrapper:

JavaScriptContext.StartDebugging();

With debugging enabled and the Script debugger type selected, when the JavaScript code is executed within the application, a new item is added to Solution Explorer, representing the script that has been loaded within the execution context (see Figure 4).

[Click on image for larger view.] Figure 4. Debugging JavaScript

While you can open the script block by double-clicking on the item in the Solution Explorer, setting a breakpoint won't do anything since a new execution context is being created each time the Execute button is clicked. However, by calling debugger at the beginning of the JavaScript code, Visual Studio will break execution at this point, allowing you to step through and even inspect values, as shown in Figure 5 (It actually seems that you need to either invoke the code twice or put two calls to debugger in order for Visual Studio to open the script block and pause execution).

[Click on image for larger view.] Figure 5. Hitting a Breakpoint

Wrapping Up
I've demonstrated how the Chakra JavaScript engine can be accessed from a UWP app. This is an extremely powerful mechanism that can be used for code sharing between a mobile app on different platforms, as well as business-logic code used to power Web sites.

It's important to handle exceptions and ensure functions and variables aren't accidentally garbage collected. The interaction model between the managed host application and the JavaScript code being executed is not trivial and there are a full range of methods that are exposed by the JavaScript Runtime, documented on the MSDN Library.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.