JavaScript [JSImport]/[JSExport] 相互運用

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

この記事では、JS[JSImport]/[JSExport] 相互運用機能を使用して JavaScript (JS) から .NET を実行する方法について説明します。

その他のガイダンスについては、.NET ランタイム (dotnet/runtime) GitHub リポジトリの .NET WebAssembly アプリケーションの構成とホストに関するガイダンスを参照してください。

既存の JS アプリでは、拡張されたクライアント側 WebAssembly サポートを使用して、JS から .NET ライブラリを再利用することや、新しい .NET ベースのアプリやフレームワークを構築することができます。

Note

この記事では、Blazor に依存することなく、JS アプリから .NET を実行することに重点を置いています。 Blazor WebAssembly アプリでの [JSImport]/[JSExport] 相互運用機能の使用に関するガイダンスについては、「ASP.NET Core Blazor を使用した JavaScript JSImport/JSExport 相互運用」をご覧ください。

これらの方法は、WebAssembly (WASM) でのみ実行する必要がある場合に適しています。 ライブラリでは、OperatingSystem.IsBrowser を呼び出すことによって、アプリが WASM で実行されているかどうかを判断するランタイム チェックを行うことができます。

前提条件

.NET Core SDK (最新バージョン)

管理コマンド シェルで wasm-tools ワークロードをインストールします。これにより、関連する MSBuild ターゲットを導入できます。

dotnet workload install wasm-tools

これらのツールをインストールするには、Visual Studio インストーラーの [ASP.NET と Web の開発] ワークロードにある Visual Studio のインストーラーを使用することもできます。 省略可能なコンポーネント一覧から [.NET WebAssembly ビルド ツール] オプションを選びます。

必要に応じて、ブラウザー アプリ (WebAssembly ブラウザー アプリ) または Node.js ベースのコンソール アプリ (WebAssembly コンソール アプリ) で .NET on WebAssembly の使用を開始するための試験段階のプロジェクト テンプレートを含む、wasm-experimental ワークロードをインストールします。 JS[JSImport]/[JSExport] 相互運用を既存の JS アプリに統合する予定がある場合、このワークロードは必要ありません。

dotnet workload install wasm-experimental

次のコマンドを使用して、Microsoft.NET.Runtime.WebAssembly.Templates NuGet パッケージからテンプレートをインストールすることもできます。

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

詳細については、「試験段階のワークロードとプロジェクト テンプレート」セクションを参照してください。

名前空間

この記事で説明する JS 相互運用 API は、System.Runtime.InteropServices.JavaScript 名前空間の属性によって制御されます。

プロジェクトの構成

プロジェクト (.csproj) を構成して JS 相互運用を有効にするには:

  • ターゲット フレームワーク モニカー ({TARGET FRAMEWORK} プレースホルダー) を設定します。

    <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
    

    .NET 7 (net7.0) 以降がサポートされています。

  • AllowUnsafeBlocks プロパティを有効にします。これにより、Roslyn コンパイラのコード ジェネレーターでは JS 相互運用のためにポインター使用できるようになります。

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    警告

    JS 相互運用 API では、AllowUnsafeBlocks を有効にする必要があります。 .NET アプリで独自のアンセーフ コードを実装する場合は注意してください。セキュリティと安定性のリスクを招く可能性があります。 詳細については、「アンセーフ コード、ポインター型、関数ポインター」を参照してください。

構成後のプロジェクト ファイル例 (.csproj) を次に示します。 {TARGET FRAMEWORK} プレースホルダーはターゲット フレームワークです。

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

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

</Project>
  • ターゲット フレームワーク モニカーを設定します。

    <TargetFramework>net7.0</TargetFramework>
    

    .NET 7 (net7.0) 以降がサポートされています。

  • ランタイム識別子の browser-wasm を指定します。

    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    
  • 実行可能な出力ファイルの種類を指定します。

    <OutputType>Exe</OutputType>
    
  • AllowUnsafeBlocks プロパティを有効にします。これにより、Roslyn コンパイラのコード ジェネレーターでは JS 相互運用のためにポインター使用できるようになります。

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    警告

    JS 相互運用 API では、AllowUnsafeBlocks を有効にする必要があります。 .NET アプリで独自のアンセーフ コードを実装する場合は注意してください。セキュリティと安定性のリスクを招く可能性があります。 詳細については、「アンセーフ コード、ポインター型、関数ポインター」を参照してください。

  • ディスク上のファイルを指すように WasmMainJSPath を指定します。 このファイルはアプリと共に発行されますが、.NET を既存の JS アプリに統合する場合は、このファイルを使用する必要はありません。

    次の例では、ディスク上の JS ファイルは main.js ですが、いずれの JS ファイル名も許容されます。

    <WasmMainJSPath>main.js</WasmMainJSPath>
    

構成後のプロジェクト ファイル (.csproj) の例:

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

WASM での JavaScript 相互運用

次の例の API は dotnet.js からインポートされます。 これらの API を使用すると、C# コードにインポートできる名前付きモジュールを設定し、Program.Main など、.NET コードによって公開されるメソッドを呼び出すことができます。

重要

この記事全体の "インポート" と "エクスポート" は、.NET の観点から定義されています。

  • アプリによって JS メソッドがインポートされ、.NET から呼び出せるようになります。
  • アプリによって .NET メソッドがエクスポートされ、JS から呼び出せるようになります。

次に例を示します。

  • dotnet.js ファイルは、.NET WebAssembly ランタイムの作成と起動に使用されます。 dotnet.js は、アプリのビルド出力の一部として生成されます。

    重要

    既存のアプリと統合するには、発行出力フォルダー† の内容を既存のアプリのデプロイ資産にコピーして、アプリの rest と共に提供できるようにします。 運用環境のデプロイの場合は、コマンド シェルで dotnet publish -c Release コマンドを使用してアプリを発行し、出力フォルダーの内容をアプリと共にデプロイします。

    † 発行出力フォルダーは、発行プロファイルのターゲットの場所です。 .NET 8 以降の Release プロファイルの既定値は bin/Release/{TARGET FRAMEWORK}/publish です。この {TARGET FRAMEWORK} プレースホルダーはターゲット フレームワークです (たとえば net8.0)。

  • dotnet.create() では、.NET WebAssembly ランタイムを設定します。

  • setModuleImports では、.NET にインポートするための JS 関数のモジュールに名前を関連付けます。 JS モジュールには dom.setInnerText 関数があります。これを使うと、要素セレクターと時間を受け取り、現在のストップウォッチ時間を UI に表示することができます。 モジュールの名前を任意の文字列にすることはできますが (ファイル名である必要はありません)、JSImportAttribute で使用される名前と一致する必要があります (この記事の後半で説明します)。 dom.setInnerText 関数は C# にインポートされ、C# メソッド SetInnerText によって呼び出されます。 SetInnerText メソッドは、このセクションの後半に示されています。

  • exports.StopwatchSample.Reset() では、JS から .NET (StopwatchSample.Reset) への呼び出しを行います。 Reset C# メソッドを使うと、ストップウォッチが実行中の場合は再起動し、実行されていない場合はリセットすることができます。 Reset メソッドは、このセクションの後半に示されています。

  • exports.StopwatchSample.Toggle() では、JS から .NET (StopwatchSample.Toggle) への呼び出しを行います。 Toggle C# メソッドを使うと、現在実行中かどうかに応じてストップウォッチを開始または停止できます。 Toggle メソッドは、このセクションの後半に示されています。

  • runMain() では Program.Main を実行します。

  • setModuleImports では、.NET にインポートするための JS 関数のモジュールに名前を関連付けます。 JS モジュールには、現在のページ アドレス (URL) を返す window.location.href 関数が含まれています。 モジュールの名前を任意の文字列にすることはできますが (ファイル名である必要はありません)、JSImportAttribute で使用される名前と一致する必要があります (この記事の後半で説明します)。 window.location.href 関数は C# にインポートされ、C# メソッド GetHRef によって呼び出されます。 GetHRef メソッドは、このセクションの後半に示されています。

  • exports.MyClass.Greeting() では、JS から .NET (MyClass.Greeting) への呼び出しを行います。 Greeting C# メソッドでは、window.location.href 関数を呼び出した結果を含む文字列を返します。 Greeting メソッドは、このセクションの後半に示されています。

  • dotnet.run() では Program.Main を実行します。

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();

C# から呼び出せるように JS 関数をインポートするには、一致するメソッド シグネチャで新しい JSImportAttribute を使用します。 JSImportAttribute の最初のパラメーターは、インポートする JS 関数の名前で、2 番目のパラメーターはモジュールの名前です。

次の例では、dom.setInnerText 関数は、SetInnerText メソッドが呼び出されたときに main.js モジュールから呼び出されます。

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

次の例では、window.location.href 関数は、GetHRef メソッドが呼び出されたときに main.js モジュールから呼び出されます。

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

インポートされたメソッド シグネチャでは、パラメーターと戻り値に .NET 型を使用でき、これらはランタイムによって自動的にマーシャリングされます。 インポートされたメソッド パラメーターのマーシャリング方法を制御するには、JSMarshalAsAttribute<T> を使用します。 たとえば、longSystem.Runtime.InteropServices.JavaScript.JSType.Number または System.Runtime.InteropServices.JavaScript.JSType.BigInt としてマーシャリングするように選択できます。 Action/Func<TResult> コールバックをパラメーターとして渡すことができます。これは呼び出し可能な JS 関数としてマーシャリングされます。 JS とマネージド オブジェクト参照の両方を渡すことができ、それらはプロキシ オブジェクトとしてマーシャリングされ、プロキシがガベージ コレクションされるまでオブジェクトが境界を越えて維持されます。 Task の結果によって非同期メソッドをインポートおよびエクスポートすることもできます。これらは JS Promise としてマーシャリングされます。 マーシャリングされた型のほとんどは、インポートされたメソッドとエクスポートされたメソッドの両方で、パラメーターおよび戻り値として、両方向で動作します。

グローバル名前空間でアクセスできる関数をインポートするには、関数名に globalThis プレフィックスを使い、モジュール名を指定せずに [JSImport] 属性を使います。 次の例では、console.log の前に globalThis を付けています。 インポートされた関数は C# Log メソッドによって呼び出されます。C# 文字列メッセージ (message) を受け取り、C# 文字列を console.log の JSString にマーシャリングします。

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

.NET メソッドをエクスポートして JS から呼び出せるようにするには、JSExportAttribute を使用します。

次の例では、各メソッドが JS にエクスポートされ、JS 関数から呼び出すことができるようになります。

  • Toggle メソッドを使うと、ストップウォッチの実行状態に応じてストップウォッチを開始または停止することができます。
  • Reset メソッドを使うと、ストップウォッチが実行中の場合は再起動し、実行されていない場合はリセットすることができます。
  • IsRunning メソッドを使うと、ストップウォッチが実行されているかどうかを示すことができます。
[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;

次の例では、Greeting メソッドにより、GetHRef メソッドを呼び出した結果を含む文字列が返されます。 前述のように、GetHref C# メソッドでは、main.js モジュールから window.location.href 関数の JS を呼び出します。 window.location.href では、現在のページ アドレス (URL) が返されます。

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

試験段階のワークロードとプロジェクト テンプレート

JS 相互運用機能を示し、JS 相互運用プロジェクト テンプレート取得するには、wasm-experimental ワークロードをインストールします。

dotnet workload install wasm-experimental

wasm-experimental ワークロードには、wasmbrowserwasmconsole の 2 つのプロジェクト テンプレートが含まれています。 これらのテンプレートは、現時点では試験段階にあります。これは、テンプレートの開発者ワークフローが進化していることを意味します。 ただし、テンプレートで使う .NET と JS の API は .NET 8 でサポートされており、JS から WASM 上で .NET を使用するための基盤となります。

次のコマンドを使用して、Microsoft.NET.Runtime.WebAssembly.Templates NuGet パッケージからテンプレートをインストールすることもできます。

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

ブラウザー アプリ

コマンド ラインから wasmbrowser テンプレートを使ってブラウザー アプリを作成できます。これにより、ブラウザーで .NET と JS を一緒に使用する例を示す Web アプリが作成されます。

dotnet new wasmbrowser

また、Visual Studio で WebAssembly Browser App プロジェクト テンプレートを使ってアプリを作成することもできます。

Visual Studio から、または .NET CLI を使用してアプリをビルドします。

dotnet build

Visual Studio から、または .NET CLI を使用して、アプリをビルドして実行します。

dotnet run

または、dotnet serve コマンドをインストールして使用します。

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

上の例の {TARGET FRAMEWORK} プレースホルダーはターゲット フレームワーク モニカーです。

Node.js コンソール アプリ

wasmconsole テンプレートを使用してコンソール アプリを作成できます。その場合、 Node.js または V8 コンソール アプリとして WASM で実行されるアプリが作成されます。

dotnet new wasmconsole

また、Visual Studio で WebAssembly Console App プロジェクト テンプレートを使ってアプリを作成することもできます。

Visual Studio から、または .NET CLI を使用してアプリをビルドします。

dotnet build

Visual Studio から、または .NET CLI を使用して、アプリをビルドして実行します。

dotnet run

または、main.mjs ファイルが格納されている発行出力ディレクトリから静的ファイル サーバーを起動します。

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

上の例の {TARGET FRAMEWORK} プレースホルダーはターゲット フレームワーク モニカーです。また、{PATH} プレースホルダーは main.mjs ファイルのパスです。

その他のリソース