Partilhar via


Executar .NET do JavaScript

Esse artigo explica como executar o .NET do JavaScript (JS) usando JS[JSImport]/[JSExport] interoperabilidade.

Para obter orientações adicionais, confira as diretrizes de Configuração e hospedagem de aplicativos WebAssembly do .NET no repositório GitHub do .NET Runtime (dotnet/runtime).

Os aplicativos existentes JS podem usar o suporte do WebAssembly do lado do cliente expandido para reutilizar bibliotecas .NET de JS ou para compilar novas estruturas e aplicativos baseados em .NET.

Observação

Esse artigo se concentra na execução do .NET de aplicativos JS sem nenhuma dependência no Blazor. Para obter diretrizes sobre como usar [JSImport]/[JSExport] interoperabilidade em aplicativos Blazor WebAssembly, consulte interoperabilidade JavaScript JSImportar/JSExportar com ASP.NET Core Blazor.

Essas abordagens são apropriadas quando você espera apenas executar no WebAssembly (WASM). As bibliotecas podem fazer uma verificação de runtime para determinar se o aplicativo está em execução no WASM chamando OperatingSystem.IsBrowser.

Pré-requisitos

Instale a versão mais recente do SDK do .NET.

Instale a carga de trabalho wasm-tools em um shell de comando administrativo, que traz os destinos do MSBuild relacionados:

dotnet workload install wasm-tools

As ferramentas também podem ser instaladas por meio do instalador do Visual Studio na carga de trabalho de ASP.NET e desenvolvimento da Web no instalador do Visual Studio. Selecione a opção ferramentas de build do .NET WebAssembly na lista de componentes opcionais.

Opcionalmente, instale a carga de trabalho wasm-experimental, que contém modelos de projeto experimentais para começar a usar o .NET no WebAssembly em um aplicativo de navegador (aplicativo navegador WebAssembly) ou em um aplicativo de console baseado em Node.js (aplicativo de console WebAssembly). Essa carga de trabalho não será necessária se você planeja integrar JS[JSImport]/[JSExport] a interoperabilidade a um aplicativo existente JS.

dotnet workload install wasm-experimental

Os modelos também podem ser instalados no pacote NuGet Microsoft.NET.Runtime.WebAssembly.Templates com o seguinte comando:

dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates

Para obter mais informações, consulte a seção Modelos experimentais de carga de trabalho e projeto.

Namespace

A API de interoperabilidade JS descrita neste artigo é controlada por atributos no namespace System.Runtime.InteropServices.JavaScript.

Configuração do projeto

Para configurar um projeto (.csproj) para habilitar a JS interoperabilidade:

  • Defina o moniker da estrutura de destino (espaço reservado{TARGET FRAMEWORK}):

    <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
    

    Há suporte para .NET 7 (net7.0) ou posterior.

  • Habilite a propriedade AllowUnsafeBlocks, que permite que o gerador de código no compilador Roslyn use ponteiros para JS interoperabilidade:

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    Aviso

    A API de interoperabilidade JS requer a habilitação de AllowUnsafeBlocks. Tenha cuidado ao implementar seu próprio código não seguro em aplicativos .NET, o que pode introduzir riscos de segurança e estabilidade. Para obter mais informações, consulte Código não seguro, tipos de ponteiro e ponteiros de função.

A seguir há um exemplo de arquivo de projeto (.csproj) após a configuração. O espaço reservado {TARGET FRAMEWORK} é a estrutura de destino:

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

  <PropertyGroup>
    <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

</Project>
  • Defina o moniker da estrutura de destino:

    <TargetFramework>net7.0</TargetFramework>
    

    Há suporte para .NET 7 (net7.0) ou posterior.

  • Especifique browser-wasm para o identificador de runtime:

    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    
  • Especifique um tipo de saída executável:

    <OutputType>Exe</OutputType>
    
  • Habilite a propriedade AllowUnsafeBlocks, que permite que o gerador de código no compilador Roslyn use ponteiros para JS interoperabilidade:

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    Aviso

    A API de interoperabilidade JS requer a habilitação de AllowUnsafeBlocks. Tenha cuidado ao implementar seu próprio código não seguro em aplicativos .NET, o que pode introduzir riscos de segurança e estabilidade. Para obter mais informações, consulte Código não seguro, tipos de ponteiro e ponteiros de função.

  • Especifique WasmMainJSPath para apontar para um arquivo no disco. Esse arquivo é publicado com o aplicativo, mas o uso do arquivo não é necessário se você estiver integrando o .NET a um aplicativo existente JS.

    No exemplo a seguir, o arquivo JS no disco é main.js, mas qualquer nome de arquivo JS é permitido:

    <WasmMainJSPath>main.js</WasmMainJSPath>
    

Exemplo de arquivo de projeto (.csproj) após a configuração:

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

Interoperabilidade do JavaScript em WASM

As APIs no exemplo a seguir são importadas de dotnet.js. Essas APIs permitem configurar módulos nomeados que podem ser importados para o código C# e chamar em métodos expostos pelo código .NET, incluindo Program.Main.

Importante

"Importar" e "exportar" ao longo deste artigo são definidos da perspectiva do .NET:

  • Um aplicativo importa métodos JS para que eles possam ser chamados do .NET.
  • O aplicativo exporta métodos .NET para que eles possam ser chamados do JS.

No exemplo a seguir:

  • O arquivo dotnet.js é usado para criar e iniciar o runtime do WebAssembly do .NET. dotnet.js é gerado como parte da saída de build do aplicativo.

    Importante

    Para se integrar a um aplicativo existente, copie o conteúdo da pasta de saída† de publicação para os ativos de implantação do aplicativo existente para que ele possa ser atendido com o restante do aplicativo. Para implantações de produção, publique o aplicativo com o comando dotnet publish -c Release em um shell de comando e implante o conteúdo da pasta de saída com o aplicativo.

    †A pasta de saída de publicação é o local de destino do seu perfil de publicação. O padrão para um perfil de Release no .NET 8 ou posterior é bin/Release/{TARGET FRAMEWORK}/publish, em que o espaço reservado {TARGET FRAMEWORK} é a estrutura de destino (por exemplo, net8.0).

  • dotnet.create() configura o runtime do WebAssembly do .NET.

  • setModuleImports associa um nome a um módulo de funções JS para importação para o .NET. O módulo JS contém uma função dom.setInnerText, que aceita um seletor de elemento e tempo para exibir o tempo atual do cronômetro na interface do usuário. O nome do módulo pode ser qualquer cadeia de caracteres (ele não precisa ser um nome de arquivo), mas deve corresponder ao nome usado com o JSImportAttribute (explicado posteriormente neste artigo). A função dom.setInnerText é importada para C# e chamada pelo método C# SetInnerText. O método SetInnerText é mostrado posteriormente nesta seção.

  • exports.StopwatchSample.Reset() chama o .NET (StopwatchSample.Reset) de JS. O método C# Reset reiniciará o cronômetro se ele estiver em execução ou redefini-lo se ele não estiver em execução. O método Reset é mostrado posteriormente nesta seção.

  • exports.StopwatchSample.Toggle() chama o .NET (StopwatchSample.Toggle) de JS. O método C# Toggle inicia ou interrompe o cronômetro, dependendo se ele estiver em execução ou não. O método Toggle é mostrado posteriormente nesta seção.

  • runMain() executa Program.Main.

  • setModuleImports associa um nome a um módulo de funções JS para importação para o .NET. O módulo JS contém uma função window.location.href, que retorna o endereço da página atual (URL). O nome do módulo pode ser qualquer cadeia de caracteres (ele não precisa ser um nome de arquivo), mas deve corresponder ao nome usado com o JSImportAttribute (explicado posteriormente neste artigo). A função window.location.href é importada para C# e chamada pelo método C# GetHRef. O método GetHRef é mostrado posteriormente nesta seção.

  • exports.MyClass.Greeting() chama o .NET (MyClass.Greeting) de JS. O método C# Greeting retorna uma cadeia de caracteres que inclui o resultado da chamada da função window.location.href. O método Greeting é mostrado posteriormente nesta seção.

  • dotnet.run() executa Program.Main.

Módulo JS:

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

const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotnet
  .withApplicationArguments("start")
  .create();

setModuleImports('main.js', {
  dom: {
    setInnerText: (selector, time) => 
      document.querySelector(selector).innerText = time
  }
});

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);

document.getElementById('reset').addEventListener('click', e => {
  exports.StopwatchSample.Reset();
  e.preventDefault();
});

const pauseButton = document.getElementById('pause');
pauseButton.addEventListener('click', e => {
  const isRunning = exports.StopwatchSample.Toggle();
  pauseButton.innerText = isRunning ? 'Pause' : 'Start';
  e.preventDefault();
});

await runMain();
import { dotnet } from './_framework/dotnet.js'

const { setModuleImports, getAssemblyExports, getConfig } = await dotnet
  .withDiagnosticTracing(false)
  .withApplicationArgumentsFromQuery()
  .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();
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();

Para importar uma função JS para que ela possa ser chamada de C#, use o novo JSImportAttribute em uma assinatura de método correspondente. O primeiro parâmetro para o JSImportAttribute é o nome da função JS a ser importada e o segundo parâmetro é o nome do módulo.

No exemplo a seguir, a função dom.setInnerText é chamada do módulo main.js quando o método SetInnerText é chamado:

[JSImport("dom.setInnerText", "main.js")]
internal static partial void SetInnerText(string selector, string content);

No exemplo a seguir, a função window.location.href é chamada do módulo main.js quando o método GetHRef é chamado:

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

Na assinatura do método importado, você pode usar tipos .NET para parâmetros e valores retornados, que sofrem realização de marshal automaticamente pelo runtime. Use JSMarshalAsAttribute<T> para controlar como os parâmetros do método importado sofrem realização de marshal. Por exemplo, você pode optar por realizar marshaling de um long como System.Runtime.InteropServices.JavaScript.JSType.Number ou System.Runtime.InteropServices.JavaScript.JSType.BigInt. Você pode passar Action/Func<TResult> retornos de chamada como parâmetros, que sofrem realização de marshal como funções chamáveis JS. Você pode passar referências de JS e objeto gerenciado e elas sofrem realização de marshal como objetos proxy, mantendo o objeto ativo no limite até que o proxy seja coletado. Você também pode importar e exportar métodos assíncronos com um resultado Task, que sofrem realização de marshal como JS promessas. A maioria dos tipos que sofrem realização de marshal funciona em ambas as direções, como parâmetros e como valores retornados, em métodos importados e exportados.

A tabela a seguir indica os mapeamentos de tipo com suporte.

.NET JavaScript Nullable TaskparaPromise Opcional JSMarshalAs Array of
Boolean Boolean Com suporte Com suporte Com suporte Sem suporte
Byte Number Com suporte Com suporte Com suporte Com suporte
Char String Com suporte Com suporte Com suporte Sem suporte
Int16 Number Com suporte Com suporte Com suporte Sem suporte
Int32 Number Com suporte Com suporte Com suporte Com suporte
Int64 Number Com suporte Com suporte Sem suporte Sem suporte
Int64 BigInt Com suporte Com suporte Sem suporte Sem suporte
Single Number Com suporte Com suporte Com suporte Sem suporte
Double Number Com suporte Com suporte Com suporte Com suporte
IntPtr Number Com suporte Com suporte Com suporte Sem suporte
DateTime Date Com suporte Com suporte Sem suporte Sem suporte
DateTimeOffset Date Com suporte Com suporte Sem suporte Sem suporte
Exception Error Sem suporte Com suporte Com suporte Sem suporte
JSObject Object Sem suporte Com suporte Com suporte Com suporte
String String Sem suporte Com suporte Com suporte Com suporte
Object Any Sem suporte Com suporte Sem suporte Com suporte
Span<Byte> MemoryView Sem suporte Sem suporte Sem suporte Sem suporte
Span<Int32> MemoryView Sem suporte Sem suporte Sem suporte Sem suporte
Span<Double> MemoryView Sem suporte Sem suporte Sem suporte Sem suporte
ArraySegment<Byte> MemoryView Sem suporte Sem suporte Sem suporte Sem suporte
ArraySegment<Int32> MemoryView Sem suporte Sem suporte Sem suporte Sem suporte
ArraySegment<Double> MemoryView Sem suporte Sem suporte Sem suporte Sem suporte
Task Promise Sem suporte Sem suporte Com suporte Sem suporte
Action Function Sem suporte Sem suporte Sem suporte Sem suporte
Action<T1> Function Sem suporte Sem suporte Sem suporte Sem suporte
Action<T1, T2> Function Sem suporte Sem suporte Sem suporte Sem suporte
Action<T1, T2, T3> Function Sem suporte Sem suporte Sem suporte Sem suporte
Func<TResult> Function Sem suporte Sem suporte Sem suporte Sem suporte
Func<T1, TResult> Function Sem suporte Sem suporte Sem suporte Sem suporte
Func<T1, T2, TResult> Function Sem suporte Sem suporte Sem suporte Sem suporte
Func<T1, T2, T3, TResult> Function Sem suporte Sem suporte Sem suporte Sem suporte

As seguintes condições se aplicam ao mapeamento de tipos e valores que sofrem realização de marshal:

  • A coluna Array of indica se o tipo .NET pode sofrer realização de marshal como um JSArray. Exemplo: C# int[] (Int32) mapeado para JSArray de Numbers.
  • Ao passar um valor JS para C# com um valor do tipo errado, a estrutura gera uma exceção na maioria dos casos. A estrutura não executa verificação de tipo em tempo de compilação no JS.
  • JSObject, Exception, Task e ArraySegment criam GCHandle e um proxy. Você pode disparar o descarte no código do desenvolvedor ou permitir que a coleta de lixo (GC, na sigla em inglês) do .NET descarte os objetos posteriormente. Esses tipos têm uma sobrecarga significativa de desempenho.
  • Array: o marshaling de uma matriz cria uma cópia da matriz no JS ou no .NET.
  • MemoryView
    • MemoryView é uma classe JS para o runtime do .NET WebAssembly realizar marshaling de Span e ArraySegment.
    • Ao contrário do marshaling de uma matriz, realizar marshaling de um Span ou ArraySegment não cria uma cópia da memória subjacente.
    • MemoryView só pode ter uma instância criada corretamente pelo runtime do WebAssembly do .NET. Portanto, não é possível importar uma função JS como um método .NET que tenha um parâmetro de Span ou ArraySegment.
    • MemoryView criado para um Span só é válido durante a chamada de interoperabilidade. Como Span é alocado na pilha de chamadas, que não persiste após a chamada de interoperabilidade, não é possível exportar um método .NET que retorna um Span.
    • MemoryView criado para um ArraySegment sobrevive após a chamada de interoperabilidade e é útil para compartilhar um buffer. Chamar dispose() em um MemoryView criado para um ArraySegment descarta o proxy e desafixa a matriz .NET subjacente. É recomendável chamar dispose() em um bloco try-finally para MemoryView.

As funções acessíveis no namespace global podem ser importadas usando o prefixo globalThis no nome da função e o atributo [JSImport] sem fornecer um nome de módulo. No exemplo a seguir, console.log é prefixado com globalThis. A função importada é chamada pelo método C# Log, que aceita uma mensagem de cadeia de caracteres C# (message) e realiza marshaling da cadeia de caracteres C# em um JSString para console.log:

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

Para exportar um método .NET para que ele possa ser chamado do JS, use o JSExportAttribute.

No exemplo a seguir, cada método é exportado para JS e pode ser chamado de funções JS:

  • O método Toggle inicia ou interrompe o cronômetro dependendo de seu estado de execução.
  • O método Reset reiniciará o cronômetro se ele estiver em execução ou redefini-lo se ele não estiver em execução.
  • O método IsRunning indica se o cronômetro está em execução.
[JSExport]
internal static bool Toggle()
{
    if (stopwatch.IsRunning)
    {
        stopwatch.Stop();
        return false;
    }
    else
    {
        stopwatch.Start();
        return true;
    }
}

[JSExport]
internal static void Reset()
{
    if (stopwatch.IsRunning)
        stopwatch.Restart();
    else
        stopwatch.Reset();

    Render();
}

[JSExport]
internal static bool IsRunning() => stopwatch.IsRunning;

No exemplo a seguir, o método Greeting retorna uma cadeia de caracteres que inclui o resultado da chamada do método GetHRef. Conforme mostrado anteriormente, o método C# GetHref chama JS para a função window.location.href do módulo main.js. window.location.href retorna o endereço da página atual (URL):

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

Modelos experimentais de carga de trabalho e projeto

Para demonstrar a funcionalidade de interoperabilidade JS e obter modelos JS de projeto de interoperabilidade, instale a carga de trabalho wasm-experimental:

dotnet workload install wasm-experimental

A carga de trabalho wasm-experimental contém dois modelos de projeto: wasmbrowser e wasmconsole. Esses modelos são experimentais no momento, o que significa que o fluxo de trabalho do desenvolvedor para os modelos está evoluindo. No entanto, o .NET e as APIs JS usadas nos modelos têm suporte no .NET 8 e fornecem uma base para o uso do .NET em WASM do JS.

Os modelos também podem ser instalados no pacote NuGet Microsoft.NET.Runtime.WebAssembly.Templates com o seguinte comando:

dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates

Aplicativo de navegador

Você pode criar um aplicativo de navegador com o modelo wasmbrowser na linha de comando, que cria um aplicativo Web que demonstra como usar o .NET e JS juntos em um navegador:

dotnet new wasmbrowser

Como alternativa no Visual Studio, você pode criar o aplicativo usando o modelo de projeto WebAssembly Browser App.

Compile o aplicativo do Visual Studio ou use a CLI do .NET:

dotnet build

Compile e execute o aplicativo do Visual Studio ou use a CLI do .NET:

dotnet run

Como alternativa, instale e use o dotnet serve comando:

dotnet serve -d:bin/$(Configuration)/{TARGET FRAMEWORK}/publish

No exemplo anterior, o espaço reservado {TARGET FRAMEWORK} é o moniker da estrutura de destino.

Aplicativo de console do Node.js

Você pode criar um aplicativo de console com o modelo wasmconsole, que cria um aplicativo executado em WASM como um aplicativo de console Node.js ou V8:

dotnet new wasmconsole

Como alternativa no Visual Studio, você pode criar o aplicativo usando o modelo de projeto WebAssembly Console App.

Compile o aplicativo do Visual Studio ou use a CLI do .NET:

dotnet build

Compile e execute o aplicativo do Visual Studio ou use a CLI do .NET:

dotnet run

Como alternativa, inicie qualquer servidor de arquivos estático no diretório de saída de publicação que contenha o arquivo main.mjs:

node bin/$(Configuration)/{TARGET FRAMEWORK}/{PATH}/main.mjs

No exemplo anterior, o espaço reservado {TARGET FRAMEWORK} é o moniker da estrutura de destino, e o espaço reservado {PATH} é o caminho para o arquivo main.mjs.

Recursos adicionais