Share via


Exécuter .NET à partir de JavaScript

Cet article explique comment exécuter .NET à partir de JavaScript (JS) en utilisant l’interopérabilité JS[JSImport]/[JSExport].

Pour obtenir des conseils supplémentaires, consultez les instructions relatives à la configuration et à l’hébergement d’applications WebAssembly .NET dans le référentiel GitHub .NET Runtime (dotnet/runtime). Nous prévoyons de mettre à jour cet article pour inclure de nouvelles informations dans les conseils inter-liés à la fin de 2023 ou au début de 2024.

Les applications JS existantes peuvent tirer parti de la prise en charge étendue de WebAssembly côté client dans .NET 7 ou version ultérieure pour réutiliser les bibliothèques .NET à partir de JS, ou pour créer des applications et des frameworks basés sur .NET.

Remarque

Cet article se concentre sur l’exécution de .NET à partir d’applications JS sans aucune dépendance par rapport à Blazor. Pour obtenir des conseils d’aide sur l’utilisation de l’interopérabilité [JSImport]/[JSExport] dans les applications Blazor WebAssembly, consultez Interopérabilité JSImport/JSExport JavaScript avec ASP.NET Core Blazor.

Ces approches sont appropriées quand vous comptez uniquement exécuter le code dans un environnement WebAssembly (WASM). Les bibliothèques peuvent effectuer une vérification au moment de l’exécution pour déterminer si l’application s’exécute sur WASM en appelant OperatingSystem.IsBrowser.

Prérequis

Kit de développement logiciel (SDK) .NET 7.0

Installez la dernière version du kit SDK .NET.

Installez la charge de travail wasm-tools, qui intègre les cibles MSBuild associées.

dotnet workload install wasm-tools

Si vous le souhaitez, installez la charge de travail wasm-experimental, qui contient des modèles de projet expérimentaux pour bien démarrer avec .NET sur WebAssembly dans une application de navigateur (application de navigateur WebAssembly) ou dans une application console basée sur Node.js (application console WebAssembly). Cette charge de travail n’est pas nécessaire si vous comptez intégrer l’interopérabilité JS[JSImport]/[JSExport] dans une application JS existante.

dotnet workload install wasm-experimental

Pour plus d’informations, consultez la section Modèles de charge de travail et de projet expérimentaux.

Espace de noms

L’API d’interopérabilité JS décrite dans cet article est contrôlée par des attributs de l’espace de noms System.Runtime.InteropServices.JavaScript.

Configuration du projet

Pour configurer un projet (.csproj) afin d’activer l’interopérabilité JS :

  • Ciblez net7.0 ou une version ultérieure :

    <TargetFramework>net7.0</TargetFramework>
    
  • Spécifiez browser-wasm pour l’identificateur de runtime :

    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    
  • Spécifiez un type de sortie d’exécutable :

    <OutputType>Exe</OutputType>
    
  • Activez la propriété AllowUnsafeBlocks, qui permet au générateur de code du compilateur Roslyn d’utiliser des pointeurs pour l’interopérabilité JS :

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    Avertissement

    L’API d’interopérabilité JS nécessite l’activation de AllowUnsafeBlocks. Soyez vigilant quand vous implémentez votre propre code non sécurisé dans des applications .NET, car cela peut présenter des risques pour la sécurité et la stabilité. Pour plus d’informations, consultez Code non sécurisé, types de pointeur et pointeurs de fonction.

  • Spécifiez WasmMainJSPath pour pointer vers un fichier sur le disque. Ce fichier est publié avec l’application, mais son utilisation n’est pas nécessaire si vous intégrez .NET à une application JS existante.

    Dans l’exemple suivant, le fichier JS situé sur le disque est main.js, mais tout nom de fichier JS est autorisé :

    <WasmMainJSPath>main.js</WasmMainJSPath>
    

Exemple de fichier projet (.csproj) après la configuration :

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <OutputType>Exe</OutputType>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <WasmMainJSPath>main.js</WasmMainJSPath>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Interopérabilité JavaScript sur WASM

Les API de l’exemple suivant sont importées à partir de dotnet.js. Ces API vous permettent de configurer des modules nommés qui peuvent être importés dans votre code C#, et d’appeler des méthodes exposées par votre code .NET, notamment Program.Main.

Important

« Importer » et « exporter » sont définis tout au long de cet article du point de vue de .NET :

  • Une application importe des méthodes JS pour qu’elles puissent être appelées à partir de .NET.
  • L’application exporte des méthodes .NET pour qu’elles puissent être appelées à partir de JS.

Dans l’exemple suivant :

  • Le fichier dotnet.js permet de créer et de démarrer le runtime .NET WebAssembly. dotnet.js est généré dans le cadre de la sortie de build de l’application, et se trouve dans le dossier AppBundle :

    bin/{BUILD CONFIGURATION}/{TARGET FRAMEWORK}/browser-wasm/AppBundle

    L’espace réservé {BUILD CONFIGURATION} représente la configuration de build (par exemple Debug, Release), et l’espace réservé {TARGET FRAMEWORK} représente le framework cible (par exemple net7.0).

    Important

    Pour effectuer l’intégration à une application existante, copiez le contenu du dossier AppBundle pour qu’il soit mis à disposition avec le reste de l’application. Pour les déploiements en production, publiez l’application avec la commande dotnet publish -c Release dans un interpréteur de commandes, puis déployez le dossier AppBundle avec l’application.

  • dotnet.create() configure le runtime .NET WebAssembly.

  • setModuleImports associe un nom à un module de fonctions JS à des fins d’importation dans .NET. Le module JS contient une fonction window.location.href, qui retourne l’adresse (URL) de la page active. Le nom du module peut être une chaîne (il n’est pas nécessaire qu’il s’agisse d’un nom de fichier), mais il doit correspondre au nom utilisé avec JSImportAttribute (expliqué plus loin dans cet article). La fonction window.location.href est importée dans C# et est appelée par la méthode C#GetHRef. La méthode GetHRef est présentée plus loin dans cette section.

  • exports.MyClass.Greeting() appelle .NET (MyClass.Greeting) à partir de JS. La méthode C# Greeting retourne une chaîne qui inclut le résultat de l’appel de la fonction window.location.href. La méthode Greeting est présentée plus loin dans cette section.

  • dotnet.run() exécute Program.Main.

Module JS :

import { dotnet } from './dotnet.js'

const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);

const { setModuleImports, getAssemblyExports, getConfig } = 
  await dotnet.create();

setModuleImports("main.js", {
  window: {
    location: {
      href: () => globalThis.window.location.href
    }
  }
});

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
const text = exports.MyClass.Greeting();
console.log(text);

document.getElementById("out").innerHTML = text;
await dotnet.run();

Pour importer une fonction JS afin qu’elle puisse être appelée à partir du code C#, utilisez le nouveau JSImportAttribute sur une signature de méthode correspondante. Le premier paramètre de JSImportAttribute est le nom de la fonction JS à importer, et le deuxième paramètre est le nom du module.

Dans l’exemple suivant, la fonction window.location.href est appelée à partir du module main.js quand la méthode GetHRef est appelée :

[JSImport("window.location.href", "main.js")]
internal static partial string GetHRef();

Dans la signature de méthode importée, vous pouvez utiliser des types .NET pour les paramètres et les valeurs de retour, qui sont marshalés automatiquement par le runtime. Utilisez JSMarshalAsAttribute<T> pour contrôler la façon dont les paramètres de méthode importés sont marshalés. Par exemple, vous pouvez choisir de marshaler long en tant que System.Runtime.InteropServices.JavaScript.JSType.Number ou System.Runtime.InteropServices.JavaScript.JSType.BigInt. Vous pouvez passer des rappels Action/Func<TResult> en tant que paramètres, qui sont marshalés en tant que fonctions JS pouvant être appelées. Vous pouvez passer à la fois des références d’objets JS et des références d’objets managés. Elles sont marshalées en tant qu’objets proxy, ce qui permet de conserver l’objet actif entre les environnements jusqu’à ce que le proxy soit traité pour un nettoyage de la mémoire. Vous pouvez également importer et exporter des méthodes asynchrones avec un résultat Task, qui sont marshalées en tant que promesses JS. La plupart des types marshalés fonctionnent dans les deux sens, en tant que paramètres et en tant que valeurs de retour, sur les méthodes importées et exportées.

Le tableau suivant indique les mappages de types pris en charge.

.NET JavaScript Nullable TaskàPromise JSMarshalAs facultatif Array of
Boolean Boolean Pris en charge Pris en charge Pris en charge Non pris en charge
Byte Number Pris en charge Pris en charge Pris en charge Pris en charge
Char String Pris en charge Pris en charge Pris en charge Non pris en charge
Int16 Number Pris en charge Pris en charge Pris en charge Non pris en charge
Int32 Number Pris en charge Pris en charge Pris en charge Pris en charge
Int64 Number Pris en charge Pris en charge Non pris en charge Non pris en charge
Int64 BigInt Pris en charge Pris en charge Non pris en charge Non pris en charge
Single Number Pris en charge Pris en charge Pris en charge Non pris en charge
Double Number Pris en charge Pris en charge Pris en charge Pris en charge
IntPtr Number Pris en charge Pris en charge Pris en charge Non pris en charge
DateTime Date Pris en charge Pris en charge Non pris en charge Non pris en charge
DateTimeOffset Date Pris en charge Pris en charge Non pris en charge Non pris en charge
Exception Error Non pris en charge Pris en charge Pris en charge Non pris en charge
JSObject Object Non pris en charge Pris en charge Pris en charge Pris en charge
String String Non pris en charge Pris en charge Pris en charge Pris en charge
Object Any Non pris en charge Pris en charge Non pris en charge Pris en charge
Span<Byte> MemoryView Non pris en charge Non pris en charge Non pris en charge Non pris en charge
Span<Int32> MemoryView Non pris en charge Non pris en charge Non pris en charge Non pris en charge
Span<Double> MemoryView Non pris en charge Non pris en charge Non pris en charge Non pris en charge
ArraySegment<Byte> MemoryView Non pris en charge Non pris en charge Non pris en charge Non pris en charge
ArraySegment<Int32> MemoryView Non pris en charge Non pris en charge Non pris en charge Non pris en charge
ArraySegment<Double> MemoryView Non pris en charge Non pris en charge Non pris en charge Non pris en charge
Task Promise Non pris en charge Non pris en charge Pris en charge Non pris en charge
Action Function Non pris en charge Non pris en charge Non pris en charge Non pris en charge
Action<T1> Function Non pris en charge Non pris en charge Non pris en charge Non pris en charge
Action<T1, T2> Function Non pris en charge Non pris en charge Non pris en charge Non pris en charge
Action<T1, T2, T3> Function Non pris en charge Non pris en charge Non pris en charge Non pris en charge
Func<TResult> Function Non pris en charge Non pris en charge Non pris en charge Non pris en charge
Func<T1, TResult> Function Non pris en charge Non pris en charge Non pris en charge Non pris en charge
Func<T1, T2, TResult> Function Non pris en charge Non pris en charge Non pris en charge Non pris en charge
Func<T1, T2, T3, TResult> Function Non pris en charge Non pris en charge Non pris en charge Non pris en charge

Les conditions suivantes s’appliquent au mappage de type et aux valeurs marshalées :

  • La colonne Array of indique si le type .NET peut être marshalé en tant que JSArray. Exemple : C# int[] (Int32) mappé à JSArray de Number.
  • Quand une valeur JS est passée en C# avec un type incorrect, le framework lève une exception dans la plupart des cas. Le framework n’effectue pas de contrôle de type au moment de la compilation en JS.
  • JSObject, Exception, Task et ArraySegment créent GCHandle et un proxy. Vous pouvez déclencher la suppression dans le code de développeur, ou autoriser le GC (nettoyage de la mémoire) .NET à supprimer les objets plus tard. Ces types entraînent une surcharge importante au niveau des performances.
  • Array : Le marshaling d’un tableau crée une copie du tableau en JS ou dans .NET.
  • MemoryView
    • MemoryView est une classe JS qui permet au runtime .NET WebAssembly de marshaler Span et ArraySegment.
    • Contrairement au marshaling d’un tableau, le marshaling de Span ou ArraySegment ne crée pas de copie de la mémoire sous-jacente.
    • MemoryView peut uniquement être correctement instancié par le runtime .NET WebAssembly. Il n’est donc pas possible d’importer une fonction JS en tant que méthode .NET qui a un paramètre Span ou ArraySegment.
    • Un MemoryView créé pour un Span est uniquement valide pour la durée de l’appel d’interopérabilité. Dans la mesure où Span est alloué sur la pile des appels, qui ne persiste pas après l’appel d’interopérabilité, il n’est pas possible d’exporter une méthode .NET qui retourne Span.
    • Un MemoryView créé pour un ArraySegment survit après l’appel d’interopérabilité, et est utile pour partager de la mémoire tampon. L’appel de dispose() sur un MemoryView créé pour ArraySegment supprime le proxy, et dissocie le tableau .NET sous-jacent. Nous vous recommandons d’appeler dispose() dans un bloc try-finally pour MemoryView.

Les fonctions accessibles sur l’espace de noms global peuvent être importées à l’aide du préfixe globalThis dans le nom de la fonction et à l’aide de l’attribut [JSImport] sans fournir de nom de module. Dans l’exemple suivant, console.log est précédé de globalThis. La fonction importée est appelée par la méthode C# Log, qui accepte un message de chaîne C# (message) et marshale la chaîne C# en JSString pour console.log :

[JSImport("globalThis.console.log")]
internal static partial void Log([JSMarshalAs<JSType.String>] string message);

Pour exporter une méthode .NET afin qu’elle puisse être appelée à partir de JS, utilisez JSExportAttribute.

Dans l’exemple suivant, la méthode Greeting retourne une chaîne qui inclut le résultat de l’appel de la méthode GetHRef. Comme cela a déjà été indiqué, la méthode C# GetHref appelle JS pour la fonction window.location.href à partir du module main.js. window.location.href retourne l’adresse (URL) de la page active :

[JSExport]
internal static string Greeting()
{
    var text = $"Hello, World! Greetings from {GetHRef()}";
    Console.WriteLine(text);

    return text;
}

Modèles de charge de travail et de projet expérimentaux

Pour illustrer la fonctionnalité d’interopérabilité JS et obtenir des modèles de projet d’interopérabilité JS, installez la charge de travail wasm-experimental :

dotnet workload install wasm-experimental

La charge de travail wasm-experimental contient deux modèles de projet : wasmbrowser et wasmconsole. Il s’agit de modèles expérimentaux pour le moment, ce qui signifie que le workflow de développeur associé aux modèles évolue. Toutefois, les API .NET et JS utilisées dans les modèles sont prises en charge dans .NET 7 et fournissent les bases de l’utilisation de .NET sur WASM à partir de JS.

Application de navigateur

Vous pouvez créer une application de navigateur avec le modèle wasmbrowser. Cela vous permet de créer une application web illustrant l’utilisation simultanée de .NET et de JS dans un navigateur :

dotnet new wasmbrowser

Générez l’application à partir de Visual Studio ou à l’aide de l’interface .NET CLI :

dotnet build

L’application générée se trouve dans le répertoire bin/{BUILD CONFIGURATION}/{TARGET FRAMEWORK}/browser-wasm/AppBundle. L’espace réservé {BUILD CONFIGURATION} correspond à la configuration de build (par exemple Debug, Release). L’espace réservé {TARGET FRAMEWORK} correspond au moniker de framework cible (par exemple net7.0).

Générez et exécutez l’application à partir de Visual Studio ou à l’aide de l’interface .NET CLI :

dotnet run

Vous pouvez également démarrer un serveur de fichiers statiques à partir du répertoire AppBundle :

dotnet serve -d:bin/$(Configuration)/{TARGET FRAMEWORK}/browser-wasm/AppBundle

Dans l’exemple précédent, l’espace réservé {TARGET FRAMEWORK} correspond au moniker de framework cible (par exemple net7.0).

Application console Node.js

Vous pouvez créer une application console avec le modèle wasmconsole. Cela vous permet de créer une application qui s’exécute sur WASM en tant qu’application console Node.js ou V8 :

dotnet new wasmconsole

Générez l’application à partir de Visual Studio ou à l’aide de l’interface .NET CLI :

dotnet build

L’application générée se trouve dans le répertoire bin/{BUILD CONFIGURATION}/{TARGET FRAMEWORK}/browser-wasm/AppBundle. L’espace réservé {BUILD CONFIGURATION} correspond à la configuration de build (par exemple Debug, Release). L’espace réservé {TARGET FRAMEWORK} correspond au moniker de framework cible (par exemple net7.0).

Générez et exécutez l’application à partir de Visual Studio ou à l’aide de l’interface .NET CLI :

dotnet run

Vous pouvez également démarrer un serveur de fichiers statiques à partir du répertoire AppBundle :

node bin/$(Configuration)/{TARGET FRAMEWORK}/browser-wasm/AppBundle/main.mjs

Dans l’exemple précédent, l’espace réservé {TARGET FRAMEWORK} correspond au moniker de framework cible (par exemple net7.0).

Ressources supplémentaires