Building Hybrid Apps With ChakraCore

About The Author

Eric Rozell is a Senior Software Engineer on the Developer Experience team at Microsoft. He currently leads the effort to bring React Native to the Universal … More about Eric ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

The key reason for Eric Rozell’s investigation of ChakraCore was to support the React Native framework on the Universal Windows Platform, which is a framework for declaring applications using JavaScript and the React programming model. Embedding ChakraCore in a C# application is quite easy and in this article, Eric Rozell will show you how.

There are many reasons to embed JavaScript capabilities into an app. One example may be to take a dependency on a JavaScript library that has not yet been ported to the language you’re developing in. Another reason could be your desire to allow users to eval small routines or functions in JavaScript, e.g., in data processing applications.

A detailed overview on how Hybrid Apps communicate with Windows, the Browser, and the JRST Hosting API
ChakraCore is the core part of the JavaScript engine that powers Microsoft Edge and Windows apps written in HTML/CSS/JS. It supports Just-in-time (JIT) compilation of JavaScript, garbage collection, as well as the JavaScript Runtime (JSRT) APIs, which allows you to easily embed ChakraCore in your applications.

ChakraCore provides a high performance JavaScript engine that powers the Microsft Edge browser and Windows applications written with WinJS. The key reason for our investigation of ChakraCore was to support the React Native framework on the Universal Windows Platform, a framework for declaring applications using JavaScript and the React programming model.

Hello, ChakraCore

Embedding ChakraCore in a C# application is quite easy. To start, grab a copy of the JavaScript runtime wrapper from GitHub. Include this code directly in your project or build your own library dependency out of it, whichever better suits your needs. There is also a very simple console application that shows how to evaluate JavaScript source code and convert values from the JavaScript runtime into C# strings.

Building Apps With ChakraCore

There are a few extra steps involved when building C# applications with ChakraCore embedded. As of the time of writing, there are no public binaries for ChakraCore. But don’t fret. Building ChakraCore is as easy as this:

  1. Clone the ChakraCore Git repository.
  2. Open the solution in Visual Studio (VS 2015 and the Windows 10 SDK are required if you wish to build for ARM).
  3. Build the solution from Visual Studio.
  4. The build output will be placed in Build\VcBuild\bin relative to your Git root folder.

If you wish to build from the command line, open up a Developer Command Prompt for Visual Studio, navigate to the Git root folder for ChakraCore, and run:

msbuild Build\Chakra.Core.sln /p:Configuration=Debug /p:Platform=x86

You’ll want to replace the Configuration and Platform parameters with the proper settings for your build.

Now that you have a version of ChakraCore.dll, you have some options for how to ship it with your application. The simplest way is to just copy and paste the binary into your build output folder. For convenience, I drafted a simple MSBuild target to include in your .csproj to automatically copy these binaries for you each time you build:

<Target Name="AfterBuild">
  <ItemGroup>
    <ChakraDependencies Include="$(ReferencesPath)\ChakraCore.*" />
  </ItemGroup>
  <Copy SourceFiles="@(ChakraDependencies)" DestinationFolder="$(OutputPath) " />
</Target>

For those who don’t speak MSBuild, one of the MSBuild conventions is to run targets in your project named AfterBuild after the build is complete. The above bit of XML roughly translates to “after the build is complete, search the references path for files that match the pattern ChakraCore.* and copy those files to the output directory.” You’ll need to set the $(ReferencesPath) property in your .csproj as well.

If you are building your application for multiple platforms, it helps to drop the ChakraCore.dll dependencies in folder names based off of your build configuration and platform. E.g., consider the following structure:

├── References
    ├── x86
        ├── Debug
            ├── ChakraCore.dll
            ├── ChakraCore.pdb
        ├── Release
            ├── ...
    ├── x64
        ├── ...
    ├── ARM
        ├── ...

That way you can declare the MSBuild property $(ReferencesPath) based on your build properties, e.g.

References\$(Configuration)\$(Platform)\

JavaScript Value Types In ChakraCore

The first step to building more complex applications with ChakraCore is understanding the data model. JavaScript is a dynamic, untyped language that supports first-class functions. The data model for JavaScript values in ChakraCore supports these designs. Here are the value types supported in Chakra:

  • Undefined,
  • Null,
  • Number,
  • String,
  • Boolean,
  • Object,
  • Function,
  • Error,
  • Array.

String Conversion With Serialization And Parsing

There are a number of ways of marshalling data from the CLR to the JavaScript runtime. A simple way is to parse and serialize the data as a JSON string once it enters the runtime, as follows:

var jsonObject = JavaScriptValue.GlobalObject.GetProperty(
    JavaScriptPropertyId.FromString("JSON"));
    var stringify = jsonObject.GetProperty(
    JavaScriptPropertyId.FromString("stringify"))
    var parse = jsonObject.GetProperty(
    JavaScriptPropertyId.FromString("parse"));

    var jsonInput = @"{""foo"":42}";
    var stringInput = JavaScriptValue.FromString(jsonInput);
    var parsedInput = parse.CallFunction(JavaScriptValue.GlobalObject, stringInput);
    var stringOutput = stringify.CallFunction(JavaScriptValue.GlobalObject, parsedInput);
    var jsonOutput = stringOutput.ToString();

Debug.Assert(jsonInput == jsonOutput);

In the above code, we marshal the JSON data, {“foo”:42} into the runtime as a string and, parse the data using the JSON.parse function. The result is a JavaScript object, which we use as input to the JSON.stringify function, then use the ToString() method on the result value to put the result back into a .NET string. Obviously, the idea would be to use the parsedInput object as an input to your logic running in Chakra, and apply the stringify function only when you need to marshal data back out.

Direct Object Model Conversion (With Json.NET)

An alternative approach to the string-based approach in the previous section would be to use the Chakra native APIs to construct the objects directly in the JavaScript runtime. While you can choose whatever JSON data model you desire for your C# application, we chose Json.NET due to its popularity and performance characteristics. The basic outcome we are looking for is a function from JavaScriptValue (the Chakra data model) to JToken (the Json.NET data model) and the inverse function from JToken to JavaScriptValue. Since JSON is a tree data structure, a recursive visitor is a good approach for implementing the converters.

Here is the logic for the visitor class that converts values from JavaScriptValue to JToken:

public 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 array = new JArray();
        var propertyId = JavaScriptPropertyId.FromString("length");
        var length = (int)value.GetProperty(propertyId).ToDouble();
        for (var i = 0; i < length; ++i)
        {
            var index = JavaScriptValue.FromInt32(i);
            var element = value.GetIndexedProperty(index);
            array.Add(Visit(element));
        }

        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)
    {
        var number = value.ToDouble();

        return number % 1 == 0
            ? new JValue((long)number)
            : new JValue(number);
    }

    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;
    }
}

And here is the inverse logic from JToken to JavaScript value:

public 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);
            default:
                throw new NotSupportedException();
        }
    }

    private JavaScriptValue VisitArray(JArray token)
    {
        var n = token.Count;
        var array = AddRef(JavaScriptValue.CreateArray((uint)n));
        for (var i = 0; i < n; ++i)
        {
            var value = Visit(token[i]);
            array.SetIndexedProperty(JavaScriptValue.FromInt32(i), value);
            value.Release();
        }

        return array;
    }

    private JavaScriptValue VisitBoolean(JValue token)
    {
        return token.Value()
            ? JavaScriptValue.True
            : JavaScriptValue.False;
    }

    private JavaScriptValue VisitFloat(JValue token)
    {
        return AddRef(JavaScriptValue.FromDouble(token.Value()));
    }

    private JavaScriptValue VisitInteger(JValue token)
    {
        return AddRef(JavaScriptValue.FromDouble(token.Value()));
    }

    private JavaScriptValue VisitNull(JToken token)
    {
        return JavaScriptValue.Null;
    }

    private JavaScriptValue VisitObject(JObject token)
    {
        var jsonObject = AddRef(JavaScriptValue.CreateObject());
        foreach (var entry in token)
        {
            var value = Visit(entry.Value);
            var propertyId = JavaScriptPropertyId.FromString(entry.Key);
            jsonObject.SetProperty(propertyId, value, true);
            value.Release();
        }

        return jsonObject;
    }

    private JavaScriptValue VisitString(JValue token)
    {
        return AddRef(JavaScriptValue.FromString(token.Value()));
    }

    private JavaScriptValue VisitUndefined(JToken token)
    {
        return JavaScriptValue.Undefined;
    }

    private JavaScriptValue AddRef(JavaScriptValue value)
    {
        value.AddRef();
        return value;
    }
}

As with any recursive algorithm, there are base cases and recursion steps. In this case, the base cases are the “leaf nodes” of the JSON tree (i.e., undefined, null, numbers, Booleans, strings) and the recursive steps occur when we encounter arrays and objects.

The goal of direct object model conversion is to lessen pressure on the garbage collector as serialization and parsing will generate a lot of intermediate strings. Bear in mind that your choice of .NET object model for JSON (Json.NET in the examples above) may also have an impact on your decision to use the direct object model conversion method outlined in this section or the string serialization / parsing method outlined in the previous section. If your decision is based purely on throughput, and your application is not GC-bound, the string-marshaling approach will outperform the direct object model conversion (especially with the back-and-forth overhead from native to managed code for large JSON trees).

You should evaluate the performance impact of either approach on your scenario before choosing one or the other. To assist in that investigation, I’ve published a simple tool for calculating throughput and garbage collection impact for both the CLR and Chakra on GitHub.

ChakraCore Threading Requirements

The ChakraCore runtime is single-threaded in the sense that only one thread may have access to it at a time. This does not mean, however, that you must designate a thread to do all the work on the JavaScriptRuntime (although it may be easier to do so).

Setting up the JavaScript runtime is relatively straightforward:

var runtime = JavaScriptRuntime.Create();

Before you can use this runtime on any thread, you must first set the context for a particular thread:

var context = runtime.CreateContext();
    JavaScriptContext.Current = context;

When you are done using that thread for JavaScript work for the time being, be sure to reset the JavaScript context to an invalid value:

JavaScriptContext.Current = JavaScriptContext.Invalid;

At some later point, on any other thread, simply recreate or reassign the context as above. If you attempt to assign the context simultaneously on two different threads for the same runtime, ChakraCore will throw an exception like this:

var t1 = Task.Run(() =>
{
    JavaScriptContext.Current = runtime.CreateContext();
    Task.Delay(1000).Wait();
    JavaScriptContext.Current = JavaScriptContext.Invalid;
});

    var t2 = Task.Run(() =>
{
    JavaScriptContext.Current = runtime.CreateContext();
    Task.Delay(1000).Wait();
    JavaScriptContext.Current = JavaScriptContext.Invalid;
});

    Task.WaitAll(t1, t2);

While it is appropriate to throw an exception, nothing should prevent you from using multiple threads concurrently for two different runtimes. Similarly, if you attempt to dispose the runtime without first resetting the context to an invalid value, ChakraCore will throw an exception notifying that the runtime is in use:

using (var runtime = JavaScriptRuntime.Create())
{
    var context = runtime.CreateContext();
    JavaScriptContext.Current = context;
}

If you encounter the “runtime is in use” exception that stems from disposing the runtime before unsetting the context, double check your JavaScript thread activity for any asynchronous behavior. The way async/await works in C# generally allows for any thread from the thread pool to carry out a continuation after the completion of an asynchronous operation. For ChakraCore to function properly, the context must be unset by the exact same physical thread (not logical thread) that set it initially. For more information, consult the Microsoft Developer Network site on Task Parallelism.

Thread Queue Options

In our implementation of the React Native on Windows, we considered a few different approaches to ensuring all JavaScript operations were single threaded. React Native has three main threads of activity, the UI thread, the background native module thread, and the JavaScript thread. Since JavaScript work can originate from either the native module thread or the UI thread, and generally speaking each thread does not block waiting for completion of activity on any other thread, we also have the requirement of implementing a FIFO queue for the JavaScript work.

ThreadPool Thread Capture

One of the options we considered was to block a thread pool thread permanently for evaluating JavaScript operations. Here’s the sample code for that:

// Initializes the thread queue
var queue = new BlockingCollection();
var asyncAction = ThreadPool.RunAsync(
    _ =>
    {
        JavaScriptContext.Current = context;

        while (true)
        {
            var action = queue.Take();
            if (... /* Check disposal */) break;

            try { action(); }
            catch (Exception ex) { ... /* Handle exceptions */ }
        }

        JavaScriptContext.Current = JavaScriptContext.Invalid;
    },
    WorkItemPriority.Normal);

// Enqueues work
queue.Add(() => JavaScriptContext.RunScript(... /* JavaScript */);

The benefit to this approach is its simplicity in that we know a single thread is running all of the JavaScript operations. The detriment is that we’re permanently blocking a thread pool thread, so it cannot be used for other work.

Task Scheduler

Another approach we considered uses the .NET framework’s TaskScheduler. There are a few ways to create a task scheduler that limits concurrency and guarantees FIFO, but for simplicity, we use this one from MSDN.

// Initializes the thread queue
    var taskScheduler =
     new LimitedConcurrencyLevelTaskScheduler(1);
    var taskFactory = new TaskFactory(taskScheduler);

// Enqueues work
    taskFactory.StartNew(() =>
{
    if (... /* Check disposed */) return;
    try { JavaScriptContext.RunScript(... /* JavaScript */); }
    catch (Exception ex) { ... /* Handle exception */}
});

The benefit to this approach is that it does not require any blocking operations.

ChakraCore Runtime Considerations

Garbage Collection

One of the main deterrents from using ChakraCore in conjunction with another managed language like C# is the complexity of competing garbage collectors. ChakraCore has a few hooks to give you more control over how garbage collection in the JavaScript runtime is managed. For more information, check out the documentation on runtime resource usage.

Conclusion: To JIT Or Not To JIT?

Depending on your application, you may want to weigh the overhead of the JIT compiler against the frequency at which you run certain functions. In case you do decide that the overhead of the JIT compiler is not worth the trade-off, here’s how you can disable it:

var runtime = 
    JavaScriptRuntime.Create(JavaScriptRuntimeAttributes.DisableNativeCodeGeneration);

The option to run the just-in-time (JIT) compiler in ChakraCore is also optional — all the JavaScript code will be fully interpreted even without the JIT compiler.

More Hands On With Web And Mobile Apps

Check out these helpful resources on building web and mobile apps:

For further updates on the topic and new features, please view the original documentation.

Further Reading

Smashing Editorial (ms, at, il, mrn)