[browser] Allow JS modules to be injected as pre-loaded assets (#90392)
authorPavel Savara <pavel.savara@gmail.com>
Fri, 11 Aug 2023 18:34:52 +0000 (20:34 +0200)
committerGitHub <noreply@github.com>
Fri, 11 Aug 2023 18:34:52 +0000 (20:34 +0200)
Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com>
src/mono/sample/wasm/browser-minimal-config/main.js
src/mono/wasm/runtime/dotnet.d.ts
src/mono/wasm/runtime/loader/assets.ts
src/mono/wasm/runtime/loader/run.ts
src/mono/wasm/runtime/startup.ts
src/mono/wasm/runtime/types/index.ts

index c2f518f461f43da3109aca099adef7ecb207ed85..acd81b2899a393526ca93c07c8af01360c3eb2e4 100644 (file)
@@ -1,24 +1,35 @@
 import { dotnet } from "./_framework/dotnet.js";
+import * as runtime from "./_framework/dotnet.runtime.js";
 
-async function fetchBinary(uri) {
-    return new Uint8Array(await (await fetch(uri)).arrayBuffer());
+async function fetchBinary(url) {
+    return (await fetch(url, { cache: "no-cache" })).arrayBuffer();
+}
+
+function fetchWasm(url) {
+    return {
+        response: fetch(url, { cache: "no-cache" }),
+        url,
+        name: url.substring(url.lastIndexOf("/") + 1)
+    };
 }
 
 const assets = [
     {
         name: "dotnet.native.js",
+        // demo dynamic import
+        moduleExports: import("./_framework/dotnet.native.js"),
         behavior: "js-module-native"
     },
-    {
-        name: "dotnet.js",
-        behavior: "js-module-dotnet"
-    },
     {
         name: "dotnet.runtime.js",
+        // demo static import
+        moduleExports: runtime,
         behavior: "js-module-runtime"
     },
     {
         name: "dotnet.native.wasm",
+        // demo pending download promise
+        pendingDownload: fetchWasm("./_framework/dotnet.native.wasm"),
         behavior: "dotnetwasm"
     },
     {
@@ -31,7 +42,8 @@ const assets = [
     },
     {
         name: "Wasm.Browser.Config.Sample.wasm",
-        buffer: await fetchBinary("./_framework/Wasm.Browser.Config.Sample.wasm"),
+        // demo buffer promise
+        buffer: fetchBinary("./_framework/Wasm.Browser.Config.Sample.wasm"),
         behavior: "assembly"
     },
     {
index 1d5e74e2a9fbbc84e4a6ba60d9f46fe336bb786b..a25058f2a2e80507d406236c3a785d7753b16ee6 100644 (file)
@@ -266,7 +266,12 @@ interface AssetEntry {
      * If provided, runtime doesn't have to fetch the data.
      * Runtime would set the buffer to null after instantiation to free the memory.
      */
-    buffer?: ArrayBuffer;
+    buffer?: ArrayBuffer | Promise<ArrayBuffer>;
+    /**
+     * If provided, runtime doesn't have to import it's JavaScript modules.
+     * This will not work for multi-threaded runtime.
+     */
+    moduleExports?: any | Promise<any>;
     /**
      * It's metadata + fetch-like Promise<Response>
      * If provided, the runtime doesn't have to initiate the download. It would just await the response.
index b39ce6a48b98fb271dbf6d2fb6b90b52e66b1a74..ba18815a08db80f99f8f63c1d45ce8e3b2218bb5 100644 (file)
@@ -201,10 +201,11 @@ export async function mono_download_assets(): Promise<void> {
                 const asset = await downloadPromise;
                 if (asset.buffer) {
                     if (!skipInstantiateByAssetTypes[asset.behavior]) {
-                        mono_assert(asset.buffer && typeof asset.buffer === "object", "asset buffer must be array or buffer like");
+                        mono_assert(asset.buffer && typeof asset.buffer === "object", "asset buffer must be array-like or buffer-like or promise of these");
                         mono_assert(typeof asset.resolvedUrl === "string", "resolvedUrl must be string");
                         const url = asset.resolvedUrl!;
-                        const data = new Uint8Array(asset.buffer!);
+                        const buffer = await asset.buffer;
+                        const data = new Uint8Array(buffer);
                         cleanupAsset(asset);
 
                         // wait till after onRuntimeInitialized and after memory snapshot is loaded or skipped
@@ -487,7 +488,7 @@ async function start_asset_download_sources(asset: AssetEntryInternal): Promise<
         return asset.pendingDownloadInternal.response;
     }
     if (asset.buffer) {
-        const buffer = asset.buffer;
+        const buffer = await asset.buffer;
         if (!asset.resolvedUrl) {
             asset.resolvedUrl = "undefined://" + asset.name;
         }
@@ -707,6 +708,7 @@ export function cleanupAsset(asset: AssetEntryInternal) {
     asset.pendingDownloadInternal = null as any; // GC
     asset.pendingDownload = null as any; // GC
     asset.buffer = null as any; // GC
+    asset.moduleExports = null as any; // GC
 }
 
 function fileName(name: string) {
index e6bd20142258cac089fd6bc611b366e71318e44e..a3cb7977a30f0cf941e9942408bce09a6135e9f1 100644 (file)
@@ -428,16 +428,29 @@ export async function createEmscripten(moduleFactory: DotnetModuleConfig | ((api
         : createEmscriptenMain();
 }
 
+// in the future we can use feature detection to load different flavors
 function importModules() {
     const jsModuleRuntimeAsset = resolve_single_asset_path("js-module-runtime");
     const jsModuleNativeAsset = resolve_single_asset_path("js-module-native");
-    mono_log_debug(`Attempting to import '${jsModuleRuntimeAsset.resolvedUrl}' for ${jsModuleRuntimeAsset.name}`);
-    mono_log_debug(`Attempting to import '${jsModuleNativeAsset.resolvedUrl}' for ${jsModuleNativeAsset.name}`);
-    return [
-        // keep js module names dynamic by using config, in the future we can use feature detection to load different flavors
-        import(/* webpackIgnore: true */jsModuleRuntimeAsset.resolvedUrl!),
-        import(/* webpackIgnore: true */jsModuleNativeAsset.resolvedUrl!),
-    ];
+
+    let jsModuleRuntimePromise: Promise<RuntimeModuleExportsInternal>;
+    let jsModuleNativePromise: Promise<NativeModuleExportsInternal>;
+
+    if (typeof jsModuleRuntimeAsset.moduleExports === "object") {
+        jsModuleRuntimePromise = jsModuleRuntimeAsset.moduleExports;
+    } else {
+        mono_log_debug(`Attempting to import '${jsModuleRuntimeAsset.resolvedUrl}' for ${jsModuleRuntimeAsset.name}`);
+        jsModuleRuntimePromise = import(/* webpackIgnore: true */jsModuleRuntimeAsset.resolvedUrl!);
+    }
+
+    if (typeof jsModuleNativeAsset.moduleExports === "object") {
+        jsModuleNativePromise = jsModuleNativeAsset.moduleExports;
+    } else {
+        mono_log_debug(`Attempting to import '${jsModuleNativeAsset.resolvedUrl}' for ${jsModuleNativeAsset.name}`);
+        jsModuleNativePromise = import(/* webpackIgnore: true */jsModuleNativeAsset.resolvedUrl!);
+    }
+
+    return [jsModuleRuntimePromise, jsModuleNativePromise];
 }
 
 async function initializeModules(es6Modules: [RuntimeModuleExportsInternal, NativeModuleExportsInternal]) {
index 777d54d23b93883405c74a210dbf2bacc343e0e2..f67a8b7c18ba903ad1cd4ecb1459dceb5b34b68d 100644 (file)
@@ -480,6 +480,7 @@ async function instantiate_wasm_module(
         assetToLoad.pendingDownloadInternal = null as any; // GC
         assetToLoad.pendingDownload = null as any; // GC
         assetToLoad.buffer = null as any; // GC
+        assetToLoad.moduleExports = null as any; // GC
 
         mono_log_debug("instantiate_wasm_module done");
 
index 9c0ea73b0ec7001ce07c57619f0d8742724b6e3c..816080c85e817ab2a5428697c611c1284a479533 100644 (file)
@@ -205,7 +205,14 @@ export interface AssetEntry {
      * If provided, runtime doesn't have to fetch the data. 
      * Runtime would set the buffer to null after instantiation to free the memory.
      */
-    buffer?: ArrayBuffer
+    buffer?: ArrayBuffer | Promise<ArrayBuffer>,
+
+    /**
+     * If provided, runtime doesn't have to import it's JavaScript modules.
+     * This will not work for multi-threaded runtime.
+     */
+    moduleExports?: any | Promise<any>,
+
     /**
      * It's metadata + fetch-like Promise<Response>
      * If provided, the runtime doesn't have to initiate the download. It would just await the response.