The Edge.js project enables you to run Node.js and .NET code in one process. In this article I discuss the motivation behind the project, describe the basic mechanisms Edge.js provides, and explore a few scenarios where it can help you develop your Node.js application.
Why use Edge.js?
While many applications can be written exclusively in Node.js, there are situations that require or benefit from a combination of Node.js and .NET. You may want to use .NET and Node.js in your application for several reasons. .NET framework and NuGet packages provide a rich ecosystem of functionality that complements that of Node.js and NPM modules. You may have pre-existing .NET components you want to reuse in a Node.js application. You may want to use multi-threaded CLR to run CPU-bound computations that are not well suited for single-threaded Node.js. Or you may prefer to use .NET Framework and C# as opposed to writing native Node.js extensions in C/C++ to access mechanisms specific to the operating system not already exposed through Node.js.
Once you decided that your application will use both Node.js and .NET, you have to separate the Node.js and .NET components with a process boundary and establish some form of inter-process communication between the processes, which could be HTTP:
Edge.js provides an alternative way of composing heterogeneous systems like the one above. It allows you to run Node.js and .NET code in a single process and provides an interop mechanism between V8 and CLR:
There are two main benefits of using Edge.js to run Node.js and .NET in one process instead of splitting the application into two processes: better performance, and reduced complexity. Performance measurements of one scenario show that in-process Edge.js call from Node.js to C# is 32 times faster than the same call made using HTTP between two local processes. Dealing with a single process instead of two processes with a communication channel between them reduces the deployment and maintenance complexity you need to handle.
.NET welcomes Node.js
I will use this basic example of calling from Node.js to C# to explain key concepts of Edge.js:
The first line imports the edge module previously installed from NPM. Edge.js is a native Node.js module. What is special about Edge.js is that the moment it is loaded it starts hosting CLR inside the node.exe process.
The edge module exposes a single function called func. At a high level, the function takes CLR code as a parameter, and returns a JavaScript function which is a proxy to the CLR code. The func function accepts CLR code in a variety of formats, from literal source code, a file name, to a pre-compiled CLR. In lines 3-8 above, the program specifies an async lambda expression as literal C# code. Edge.js extracts that source code and compiles it into an in-memory CLR assembly. Then it creates and returns a JavaScript proxy function around the CLR code which the application assigns to the hello variable in line 3. Note that this compilation is done once per call to edge.func and the results are cached. Moreover, if you call edge.func with the same string literal twice, you will get back the same instance of the Func<object,Task<object>> from the cache.
The hello function created by Edge.js as a proxy to C# code is called in line 10 using the standard async pattern of Node.js. The function takes a single parameter (the Node.js string) and a callback function that accepts an error and a single result value. The input parameter is passed to the C# async lambda expression in line 4, and the expression appends it to the .NET welcomes string in line 6. The new string constructed in C# is then used by Edge.js as the result parameter when invoking the JavaScript callback function in line 10. The JavaScript callback function prints it to the console: .NET welcomes Node.js.
Edge.js provides a prescriptive interop model between Node.js and .NET code running in-process. It does not allow JavaScript to directly call into just any CLR function. The CLR function must be a Func<object,Task<object>> delegate. This mechanism allows enough flexibility to pass any data from Node.js to .NET and return any data from .NET to Node.js. At the same time it requires that the .NET code executes asynchronously which allows it to be naturally integrated with the single-threaded Node.js code. This is how the Func<object,Task<object>> delegate maps to the concepts of the Node.js async pattern:
The interop pattern does not prevent you from accessing any parts of .NET framework, but it will often require you to write an extra adapter layer to expose the desired .NET functionality as a Func<object,Task<object>> delegate. This adapter layer requires that you explicitly address the issue of blocking APIs in .NET by possibly running the operation on a CLR thread pool to avoid blocking the Node.js event loop.
Data and functions
Although Edge.js allows you to pass only one parameter between Node.js and .NET, that parameter may be a complex type. When calling .NET code from Node.js, Edge.js can marshal all standard JavaScript types: from primitives to objects and arrays. When passing data from .NET to Node.js, Edge.js can marshal all atomic CLR types as well as CLR object instances, lists, collections, and dictionaries. Conceptually you can think of passing data between V8 and CLR heaps similarly to serializing data to JSON in one environment and deserializing from JSON in the other. However, Edge.js does not use actual JSON serialization in the process. Instead, it marshals data between V8 and CLR type system directly in memory without the intermediate string representation, which is far more efficient than JSON serialization and deserialization.
Edge.js marshals all data by value, so a copy of the data is created on the V8 or CLR heap when execution crosses the V8/CLR boundary. This rule has one notable exception: separately from marshaling data by value, Edge.js will marshal functions by reference. Let’s have a look at this example to illustrate this powerful concept:
In this example, Node.js will call addAndMultiplyBy2 function implemented in C#. The function takes two numbers, and returns their sum multiplied by 2. For the purpose of this example let’s assume C# knows how to add but does not know how to multiply. The C# code will need to call back to JavaScript to perform the multiplication after computing the sum.
To enable this scenario, Node.js application will define a multiplyBy2 function in lines 18-20 and pass it along with the two operands to C# code when calling the addAndMultiplyBy2 function in line 23. Notice how the shape of the multiplyBy2 function matches the prescriptive interop pattern required by Edge.js. This enables Edge.js to create a .NET proxy to the multiplyBy2 JavaScript function as a Func<object,Task<object>> delegate in .NET. The JavaScript function proxy is then called by the C# code in line 10 to perform the multiplication of the sum computed in lines 8-9.
Functions following the prescriptive interop pattern can also be marshaled from .NET to Node.js. Being able to marshal functions in either direction between V8 and CLR is a very powerful concept, especially when combined with a closure. Consider the following example:
In lines 1-7, Edge.js creates a JavaScript function createCounter which is a proxy to a C# lambda expression. The parameter passed to the createCounter function in line 9 is assigned to a C# local variable in line 3. The interesting part comes in lines 4-5: the result of the C# async lambda expression is a Func<object,Task<object>> delegate instance, implementation of which (line 5) includes the local variable from line 3 in its closure. When Edge.js marshals this Func<object,Task<object>> instance back to Node.js as a JavaScript function and assigns it to the counter variable in line 9, the counter JavaScript function effectively contains a closure over CLR state. This is proven true in lines 10-11, where calling the counter function twice returns an ever increasing value. This is the result of every call to the Func<object,Task<object>> implemented in line 5 increasing the value of the local variable from line 3.
The ability to marshal functions between V8 and CLR combined with the concept of a closure is a very powerful mechanism. It enables .NET code to expose functionality of CLR objects to Node.js. The local variable in line 3 in the last example could as well be an instance of a Person class.
Let’s build something
Let’s have a look at a few practical examples of how you can use Edge.js in a Node.js application.
Node.js has a single-threaded architecture. No blocking code can execute as part of the application if it is to remain responsive. Most Node.js applications execute CPU-bound computations out of process. The external process typically uses technology other than Node.js. Edge.js makes this scenario much simpler to implement. It allows your Node.js application to execute CPU-bound logic on a CLR thread pool within the Node.js process. While the CPU-bound computation executes on a CLR thread pool thread, the node.js application running on the V8 thread remains responsive. When the CPU-bound operation finishes, Edge.js synchronizes threads such that the JavaScript completion callback is executed on the V8 thread. Consider this example of using .NET functionality to convert image formats:
The convertImageToJpg function uses System.Drawing functionality in .NET to convert a PNG image to the JPG format. This is a computationally intensive operation, therefore the C# implementation runs the conversion on a CLR thread pool thread created in line 6 with a call to Task.Run. While this computation executes, the singleton V8 thread in the process is free to process subsequent events. The C# code waits for the completion of the image conversion with the await keyword in line 6. Only after the image has been converted will the convertImageToJpg complete by invoking the JavaScript callback code in lines 14-15 on the V8 thread.
Another example where Edge.js comes handy is accessing data in MS SQL. None of the options of accessing MS SQL data available for a Node.js developer today are as complete or mature as ADO.NET functionality in .NET Framework. Edge.js gives you a simple way of using ADO.NET in your Node.js application. Consider this Node.js application:
In line 1, Edge.js creates the sql function by compiling ADO.NET code in the sql.csx file. The sql function accepts a string with a T-SQL command, executes it asynchronously using ADO.NET, and returns the results back to Node.js. The sql.csx file supports the four CRUD operations against MS SQL database using less than 100 lines of ADO.NET code in C#:
The implementation in sql.csx file uses asynchronous ADO.NET APIs to access MS SQL and execute T-SQL command provided to it from Node.js.
The two examples above represent just a small portion of scenarios where Edge.js helps you write Node.js applications. You can see more samples at the Edge.js GitHub site.
The Roadmap
Edge.js is an open source project licensed under Apache 2.0. It is currently under active development and contributions are welcome. You can check the list of work items that could use your time and expertise.
Although all the examples in this article were using C#, Edge.js supports running code in any CLR language within a Node.js application. Current extensions provide support for scripting F#, Python, and PowerShell. The language extensibility model allows you to easily add compilers for other CLR languages.
Edge.js currently requires .NET Framework and therefore only works on Windows. However, support for Mono is under active development and will enable running Edge.js application on MacOS and *nix in addition to Windows.
About the Author
Tomasz Janczuk is a software engineer at Microsoft. His current focus is node.js and Windows Azure. Before that he worked on .NET Framework and web services. In his free time he engages in a lot of outdoor activities in the Pacific Northwest and beyond. You can follow him on Twitter, @tjanczuk, check out his GitHub page or read his blog for more information.