Raf's laboratory Abstracts Feed Raffaele Rialdi personal website

I am Raf logo

In-process interoperability between Javascript/Node and .NET Core

July 22, 2020
http://www.iamraf.net/Articles/in-process-interoperability-between-javascriptnode

Few years ago, I wanted to make an experiment with Electron, the cross-platform UI framework (also used from Visual Studio Code) that enables reusing web technologies to write desktop applications. After experimenting with Javascript first and Typescipt, I was still out of my comfort zone, due all the runtime errors that are typical of the dynamic languages.

For this reason I started thinking how I could implement some interop to use C# View Models in Electron. My primary goal was to avoid out-of process calls as ElectronNET did. I really wanted performance and avoid the n-th process opened from Electon. Crazy start but hey, I love experimenting.

Since Electron is based on NodeJs which in turn is based on V8 (Chrome engine), I quickly planned a number of basic but important proof of concepts:

  1. Writing a basic C++ plugin for NodeJS with the goal of creating dynamic types and members that are immediately usable from Javascript
  2. Host the CoreCLR from the NodeJS C++ plugin
  3. Load an assembly mine from .NET to make "service calls" from C++
  4. Execute some C# code from Javascript and vice-versa.

Of course these were only the basic points, but enough to give me the proof of the basic building blocks of my project (and enough for this blog post of course)

Phase 1: A basic C++ plugin for NodeJS

This was pretty easy. Apart from starting from scratch with the examples provided in NodeJS and setting up the toolset chain, they worked pretty nicely. I immediately discovered important features hidden in NodeJS and V8 that every developer should know:

  • Everything, even a single boolean, is wrapped inside an object in V8. And this explains  why V8 based browsers like Chrome eat your RAM
  • The type system is wider than the one exposed in Javascript. For example, Javascript numbers are 54 bit floating points but V8 is able to distinguish between integers and floating points. And it uses some guessing method to understand if a number is an integer. If you are asking why it does that, the reason is performance; in fact if you use a floating point in a for-loop, it runs way much slower.
  • The threading stuff is done using libuv library where the main thread is where everything of the javascript stuff must happen.
  • Most of the times Javascript is compiled and not interpreted. This gives you performance in many scenarios, but of course is not able to do great optimizations as a compiler can/should do.

The good part of this first addon/plugin is that I could dynamically create new types. I mean literally add not only new types during the execution, but members as well.

By default C++ NodeJS addons declare types using macros and they are defined during the addon startup. But if you crack the macros and look what they do, turning them into pure method calls is easy, and you can call at any moment.

Phase 2: Hosting the CoreCLR from the C++ NodeJS plugin

At that time, the .NET Core hosting APIs were still not final, but I could easily write the needed code without too much efforts. I spent most of the time in writing a cross-platform version of hosting the CoreCLR. The APIs resemble the one used in the past to host the .NET Framework, but in .NET Core they are exposed just as pure C export invocations instead of COM calls.

Phase 3: Load an initializer assembly from .NET

Once you have hosted the CoreCLR, it is pretty easy to invoke some managed code using an old technique called Reverse PInvoke. It works like PInvoke but in the reverse direction, with C++ as a client and C# exposing the method called from the native code.

Instead of Reverse Pinvoke, starting from .NET 5 it will be possible to use C# instrinsic calls support used also by  https://github.com/microsoft/CsWinRT to make this step more performant.

In the final code of the plugin I added several "service calls" that allows me to either benefit to live in the native or managed world.

Phase 4: Execute some C# code from Javascript

At a certain time of your javascript code, you decide to invoke some C# code to access managed code. This makes use of one of those "service calls" asking the managed code to load an assembly and the managed type you want to use. From Javascript this is exposed as a call to my "xcore" plugin: the first argument is the full qualified name of the assembly (which includes the assembly name) and the type to be loaded in memory. Basically the same syntax and behavior of "Type.GetType".

xcore.loadClass("SampleLibrary.OrderSample.OrderManager, SampleLibrary");// xcore is the C++ plugin

Once the C# Sample library is loaded in memory, Javascript still doesn't know anything about it. Any dynamic invocation (like creating an instance, invoking static or instance methods) would hit the C++ plugin that nothing knows about this type. The result would be a legit runtime error.

For this reason, as soon as a type is loaded in memory, the very first thing to do is reading the metadata of the assembly so that the C++ plugin knows what members of that type should  be added in V8. Of course, C++ the implementation of those methods is just a single method that grab the member name and its parameters that have to be forwarded to .NET for being dispatched to the real type.

In the current implementation I use managed reflection to load the metadata in memory and "copy" it in the native couterpart (C++). But some time ago I tried to adopt the ECMA-335 C++ code that Kenny Kerr used in cppwinrt. It didn't work because the goal of cppwinrt is to load winmd files. The winmd files are ECMA-335 compliant but .NET implementation is wider and some case was not implemented in the cppwinrt implementation.

No worries, because (last year) I created a pull-request on GitHub, made a few changes with the help of Kenny, and my fix was finally accepted. That metadata library was finally able to load the .NET metadata in C++. I didn't have time to adopt the library in my code, but this is something that can dramatically improve the performance metadata loading in my project.

With this code, the C++ addon was able to cheat the Javascript code and make it think that the class andits members really existed. Ultimately I could then satisfy the current executions from Javascript:

var om = new xcore.OrderManager("raf"); // creating an instance
console.log(om.SelectedOrder.Name);// reading the Name property
var sum = om.Add(10, 20); // invoking methods

What happens when you invoke a member like the Add method?

The execution request is intercepted from the C++ plugin which takes the list of all the parameters that Javascript declared. At this point I validate whether the parameters match in terms of number and types:

  • Since V8 just guesses the type of the parameters, I can't tell much of the type.
  • The validation see if the guessed type can match the destination argument of the method exposed from C#, or if they are too different and we should fail
  • Does the method takes the right number of parameters? I also support optional parameters (C# params or parameters with a default value)

The validation can't be perfect since we don't have enough information from Javascript. But once the number of arguments is satisfied we can "try" to dispatch the call to the managed world.

On the managed code side, the service call takes the invocation request and tries to dispatch it (invoke the real code).

There were two possible strategies for dispatching a member invocation: reflection (but it would be slow) or code generation. Code generation is much harder but after the first call has great performance, and so I did using expression trees that:

  1. Cast the types as required from the Expressions
  2. Marshal the parameters from native to managed memory (this also can be improved by mean of MemoryMarshal class, available since .NET Core 3)
  3. Invoke the member (which is different depending whether the member is a property, a method, an async method or an event)

But the roundtrip is not fininished because we may have a return value.

Once the invokation is finished, I take the return value which is marshaled back to native memory and sent back to C++ via a service call. This way the C++ plugin can return the value to the javascript invocation.

This just gives an idea of what can be done. There are plenty of cases I supported in the plugin and each of those would require a separate post … I am not sure this would be interesting for many. The currently implemented hard cases are:

  • async invocation. This required a native queue to marshal the execution of the callback from the secondary threads to the main NodeJS thread which is managed by the libuv library
  • overload support. This has been very very tricky to support. Mingling the metadata to distinguish different types from javascript was very hard
  • event support. Even if javascript does not support events as a language feature, I wanted to support them with addEventHandler / removeEventHandler and it worked very nicely


What is missing and what else can be done

I didn't have much time to improve it but here there are few things that I would like to add:

  1. Adopt NodeJS N-API which would make the C++ plugin resistant to NodeJS version changes. I started working on that, but I found a bug (breaking change) in NodeJS APIs preventing this work to go on.
  2. Replacing the current metadata implementation with the native one. Since medata must be used from C++, it makes sense do grab it directly with native code
  3. Replacing memory marshalling with MemoryMarshal
  4. Replacing PInvokes with intrinsic calls C# support in .NET Core

The last but most important missing thing is "synchronizing" the Javascript Garbage Collector with .NET GC. Keeping trace of the objects lifetime is very tricky. There are new APIs in .NET that can help me but I definitely have to drill into this subject.

More info is published in a video you can find here:  https://github.com/raffaeler/xcore

I never published the sources because they are very tricky. If anyone has some interest in this project, please let me know via Twitter @raffaeler.



rated by 0 users



Share this page on Twitter


Privacy | Legal Copyright © Raffaele Rialdi 2009, Senior Software Developer, Consultant, p.iva IT01741850992, hosted by Vevy Europe Advanced Technologies Division. Site created by Raffaele Rialdi, 2009 - 2015 Hosted by: © 2008-2015 Vevy Europe S.p.A. - via Semeria, 16A - 16131 Genova - Italia - P.IVA 00269300109