Le projet Edge.js vous offre la possibilité d'exécuter du code Node.js et du code .NET dans un même processus. Dans cet article, je vais aborder les raisons qui ont motivé ce projet, je décrirai les mécanismes de base fournis par Edge.js et explorerai quelques scénarii dans lesquels Edge.js peut vous aider à développer votre application Node.js.
Pourquoi utiliser Edge.js ?
Bien que de nombreuses applications puissent être écrites exclusivement avec Node.js, certaines situations requièrent ou peuvent bénéficier de la combinaison de Node.js et de .NET. Vous pouvez vouloir utiliser .NET et Node.js dans votre application pour diverses raisons. Le Framework .NET et les packages NuGet constituent un riche écosystème de fonctionnalités pouvant complémenter celui de Node.js et des modules NPM. Vous avez peut-être des composants .NET préexistants que vous souhaiteriez utiliser dans une application Node.js. Vous pouvez vouloir utiliser les capacités de multi-threading de la CLR pour exécuter des calculs sollicitant le CPU et qui ne seraient pas adaptés au modèle "single-threaded" de Node.js. Ou vous pourriez enfin préférer utiliser le Framework .NET et C# là où vous auriez dû écrire des extensions natives à Node.js en C/C++, afin d'accéder aux mécanismes spécifiques au système d'exploitation qui ne seraient pas encore exposés via Node.js.
Si vous prenez la décision de concevoir votre application en utilisant à la fois Node.js et .NET, vous aurez à séparer les composants Node.js et .NET dans des processus séparés et à établir une forme quelconque de communication interprocessus, possiblement en HTTP :
Edge.js propose une approche alternative pour la composition des systèmes hétérogènes tels que celui présenté ci-dessus. Il vous permet d'exécuter le code Node.js et le code .NET au sein d'un unique processus et apporte un mécanisme d'interopérabilité entre V8 et la CLR :
Utiliser Edge.js pour exécuter Node.js et .NET dans un seul processus plutôt que de diviser l'application en plusieurs processus apporte deux avantages principaux : une meilleure performance et une complexité moindre. Les mesures de performances effectuées sur ce scénario montrent que l'appel "in-process" par Edge.js de Node.js vers C# est 32 fois plus rapide que le même appel effectué en HTTP entre deux processus locaux. Avoir à traiter avec un processus unique plutôt qu'avec deux et un canal de communication réduit également la complexité que vous aurez à gérer sur le plan du déploiement et de la maintenance.
Bienvenue .NET ! depuis Node.js
Je vais utiliser l'exemple basique d'un appel depuis Node.js vers C# pour expliquer les concepts clefs de Node.js :
La première ligne importe le module edge installé précédemment depuis NPM. Edge.js est un module Node.js natif. Ce qui est particulier avec Edge.js, c'est qu'au moment où il est chargé, il lance l'hébergement de la CLR à l'intérieur du processus node.exe.
Le module edge expose une fonction unique appelée func. Vu de haut, la fonction prend du code CLR en paramètre et retourne une fonction JavaScript qui va faire office de proxy vers le code CLR. La fonction func accepte du code CLR sous divers formats, du code source littéral jusqu'à du code CLR précompilé en passant par un nom de fichier. Dans les lignes 3 à 8 ci-dessus, le programme spécifie une expression lambda asynchrone, avec "async", sous forme de code C# littéral. Edge.js extrait alors le code source et le compile dans une assembly CLR en mémoire. Il crée ensuite et retourne une fonction proxy JavaScript autour du code CLR que l'application assigne à la variable hello à la ligne 3. Notez que la compilation n'est effectuée qu'une seule fois par appel à edge.func. Les résultats sont mis en cache. Si vous appelez deux fois edge.func avec le même littéral, vous obtiendrez la même instance de Func<object, Task<object>>, récupérée depuis le cache.
La fonction hello créée par Edge.js en tant que proxy du code C# est appelée ligne 10 avec le pattern async standard de Node.js. La fonction prend un paramètre unique (la chaîne de caractères Node.js) et une fonction de "callback" acceptant une erreur et une valeur pour le résultat. Le paramètre en entrée est passé à la lambda asynchrone C# à la ligne 4 et celui-ci est concaténé à la chaîne de caractères welcome dans l'expression à la ligne 6. La nouvelle chaîne obtenue en C# est ensuite passée à Edge.js dans le paramètre result quand la callback JavaScript est invoquée à la ligne 10. Celle-ci affiche alors dans la console : .NET welcomes Node.js.
Edge.js fournit un modèle d'interopérabilité prescriptif entre Node.js et .NET lorsqu'il sont exécutés "in-process". Tous les appels directs depuis JavaScript vers une fonction CLR ne sont pas permis. La contrainte est que la fonction CLR doit être un délégué de type Func<object, Task<object>>. Toutefois, c'est un mécanisme qui offre suffisamment de flexibilité pour passer n'importe quelle donnée de JavaScript vers .NET et retourner toute donnée de .NET vers Node.js. De plus, il impose que le code .NET soit exécuté de façon asynchrone, afin qu'il reste naturellement intégré avec la nature "single-threaded" de Node.js. Voici comment le délégué Func<object, Task<object>> est retranscrit vers le pattern async de Node.js :
Le pattern d'interopérabilité ne vous empêche d'accéder à aucune partie du Framework .NET. Il implique cependant l'écriture d'une couche supplémentaire d'adaptation pour exposer la fonctionnalité .NET souhaitée comme un délégué Func<object, Task<object>>. Au sein de cette couche d'adaptation, Il est essentiel que soit prise en charge la problématique des APIs bloquantes incluses dans le Framework .NET, par exemple en s'appuyant sur le "thread pool" de la CLR pour exécuter l'opération sans bloquer la boucle d'évènements de Node.js.
Données et fonctions
Bien que Edge.js ne vous permette de passer qu'un seul paramètre entre Node.js et .NET, celui-ci peut très bien être un type complexe. Lorsque vous appelez le code .NET depuis Node.js, Edge.js est capable de marshaler tous les types JavaScript : des primitives aux objets et tableaux. Quand vous passez des données de .NET vers Node.js, Edge.js peut marshaler tous les types atomiques de la CLR, comme des instances d'objets CLR, des listes, des collections, des dictionnaires. Conceptuellement, vous pouvez voir l'échange de données entre les tas V8 et la CLR ("heaps") comme une sérialisation JSON effectuée par l'un des environnement et une de-sérialisation JSON par l'autre. En réalité, Edge.js n'utilise pas de sérialisation JSON mais marshale plutôt directement en mémoire la donnée entre les deux systèmes de types, sans représentation intermédiaire sous forme de chaîne de caractères, ce qui est bien plus efficace que la sérialisation JSON.
Edge.js marshale les données par valeur, une copie de la donnée est donc créée sur le tas V8 ou le tas de la CLR quand l'exécution passe la frontière entre V8 et CLR. Une exception à cette règle est à noter : si Edge.js marshale la donnée par valeur, il marshale les fonctions par référence. Jetons un œil à cet exemple pour illustrer la puissance de ce concept :
Dans cet exemple, Node.js appelle la fonction addAndMultiplyBy2 implémentée en C#. Cette fonction prend deux nombres et retourne leur somme multipliée par 2. Pour les besoins de l'exercice, faisons l'hypothèse que C# sait comment ajouter mais ne sait pas comment multiplier. Le code C# va avoir besoin de faire appel à JavaScript pour la multiplication après avoir calculé la somme.
Pour réaliser ce scénario, la fonction multiplyBy2 définie dans l'application Node.js lignes 18 à 20 est passée en même temps que les deux opérandes au code C# lors de l'appel à la fonction addAndMultiplyBy2, ligne 23. Vous pouvez constater que la fonction multiplyBy2 est bien conforme au pattern prescriptif d'interopérabilité d'Edge.js. Ceci permet à Edge.js de créer un proxy .NET à la fonction, sous la forme d'un délégué Func<object, Task<object>>. Le proxy JavaScript est appelé par C# à la ligne 10 afin d'effectuer la multiplication de la somme calculée aux lignes 8-9.
Les fonctions conformes au pattern prescriptif d'interopérabilité peuvent aussi être marshalées de .NET vers Node.js. Etre capable de marshaler des fonctions dans un sens et dans l'autre, entre V8 et la CLR, est un concept extrêmement puissant, particulièrement lorsqu'il est combiné avec une "closure". Prenez l'exemple suivant :
Aux lignes 1 à 7, Edge.js crée une fonction JavaScript createCounter en tant que proxy d'une expression lambda C#. Le paramètre passé à la fonction createCounter à la ligne 9 est assigné à une variable locale à la ligne 3. Là où c'est intéressant, c'est aux lignes 4 et 5 : le résultat de la expression lambda async est une instance d'un délégué Func<object, Task<object>>, dont l'implémentation (ligne 5) embarque la variable locale de la ligne 3 dans sa "closure". Quand Edge.js marshale cette instance en retour vers Node.js sous forme de fonction JavaScript et l'assigne à la variable counter à la ligne 9, alors la fonction JavaScript counter contient effectivement une closure vers l'état de la CLR. Cela se vérifie lorsque, lignes 10 et 11, les deux appels successifs à la fonction counter retournent une valeur à chaque fois incrémentée. Ceci est le résultat des appels à la fonction Func<object, Task<object>> implémentée ligne 5 qui incrémente la valeur de la variable locale de la ligne 3.
La capacité de marshaler les fonctions entre V8 et la CLR, combinée avec le concept de closure, est donc un mécanisme puissant permettant au code .NET d'exposer des fonctionnalités d'objets CLR à Node.js. La variable locale de la ligne 3 du dernier exemple aurait très bien pu être une instance de la class Person.
Construisons quelque chose
Examinons quelques exemples pratiques sur la façon d'utiliser Edge.js dans une application Node.js.
Node.js est construit sur une architecture à thread unique, "single-threaded". Afin de s'assurer que l'application reste réactive, aucun code bloquant ne peut s'exécuter en son sein. La plupart des applications exécutent les calculs "CPU-bound", c’est-à-dire faisant usage intensif du CPU, en dehors du processus Node.js. Le processus externe utilise généralement une technologie autre que Node.js. Avec Edge.js, ce scénario devient beaucoup plus simple à implémenter. Il permet à votre application d'exécuter des logiques "CPU-bound" sur un thread du pool de threads de la CLR, au sein du processus Node.js. Donc, pendant que les calculs "CPU-bound" s'exécutent via le thread pool de la CLR, l'application Node.js portée par le thread V8 reste réactive. Dès que l'opération "CPU-bound" se termine, Edge.js fait en sorte de synchroniser les threads afin que la callback de complétion JavaScript soit exécutée par le thread V8. L'exemple suivant montre l'utilisation de fonctionnalités .NET pour convertir des formats d'images.
La fonction convertImageToJpg utilise les fonctionnalités de System.Drawing de .NET pour convertir une image au format PNG en JPG. Cette conversion est une opération de calcul intensif exécutée dans l'implémentation en C# par un thread du thread pool de la CLR créé ligne 6 par un appel à Task.Run. Pendant que le calcul est effectué, le thread V8 est libre de prendre en charge les évènements à traiter entre temps. Le code C# se place en attente de la fin du traitement de l'image en utilisant le mot clef await ligne 6. C'est une fois que l'image aura été convertie que la fonction convertImageToJpg sera complétée en invoquant la Callback JavaScript des lignes 14 et 15 sur le thread V8.
Un autre exemple où Edge.js s'avère bien pratique est pour l'accès aux données depuis MS SQL Server. Pour le développeur Node.js, il n'y a actuellement aucune option aussi complète et mature pour accéder à MS SQL Server qu'ADO.NET, du Framework .NET. Prenez l'application Node.js suivante :
Ligne 1, Edge.js crée une fonction sql en compilant le code ADO.NET du fichier sql.csx. La fonction sql prend une chaine de caractères contenant une commande T-SQL, l'exécute de façon asynchrone en utilisant ADO.NET et retourne le résultat à Node.js. Le fichier sql.csx supporte les quatre opérations CRUD sur une base MS SQL Server en moins de 100 lignes de code ADO.NET en C# :
L'implémentation fournie dans le fichier sql.csx s'appuie sur les APIs asynchrones d'ADO.NET pour accéder à MS SQL Server et exécuter la commande T-SQL passée depuis Node.js.
Les deux cas d'utilisation présentés ci-dessus ne sont que quelques exemples parmi de nombreux autres où Edge.js peut vous aider à développer vos applications Node.js. Vous pourrez en trouver beaucoup d'autres sur le site GitHub de Edge.js.
La Roadmap
Edge.js est un projet Open Source sous licence Apache 2.0. Il est actuellement en développement actif et les contributions sont les bienvenues. Vous pouvez jeter un œil à la liste des work items sur lesquels vous pouvez utiliser votre temps et votre expertise.
Bien que tous les exemples dans cet article utilisent C#, Edge.js supporte n'importe quel langage CLR. Les extensions actuelles fournissent le support du scripting avec F#, Python et PowerShell. Le modèle d'extensibilité du langage permet d'ajouter facilement des compilateurs pour les autres langages CLR.
Pour l'heure, Edge.js requiert le Framework .NET et ne fonctionne donc que sous Windows. Cependant, le support de Mono est actuellement en cours de développement actif et permettra d'exécuter une application Edge.js sous MacOS et *nix en plus de Windows.
Au sujet de l'auteur
Tomasz Janczuk est un ingénieur en développement logiciel chez Microsoft. Il se concentre actuellement sur Node.js et Windows Azure. Avant cela, il a travaillé sur le Framework .NET et les Web Services. Sur son temps libre, il participe à de nombreuses activités outdoor dans le Nord-Ouest pacifique et au-delà. Vous pouvez le suivre sur Twitter, @tjanczuk, jeter un œil à sa page GitHub, ou lire son blog pour plus d'informations.