diff --git a/src/native/corehost/browserhost/CMakeLists.txt b/src/native/corehost/browserhost/CMakeLists.txt index bd3b54b9ffb521..c6254318d65634 100644 --- a/src/native/corehost/browserhost/CMakeLists.txt +++ b/src/native/corehost/browserhost/CMakeLists.txt @@ -72,9 +72,9 @@ if (UPPERCASE_CMAKE_BUILD_TYPE STREQUAL DEBUG) ) endif () -# TODO-WASM -sWASM_BIGINT=1 -# TODO-WASM -emit-llvm -# TODO-WASM --source-map-base http://microsoft.com +# WASM-TODO -sWASM_BIGINT=1 +# WASM-TODO -emit-llvm +# WASM-TODO --source-map-base http://microsoft.com target_link_options(browserhost PRIVATE -sINITIAL_MEMORY=134217728 -sMAXIMUM_MEMORY=2147483648 @@ -85,7 +85,7 @@ target_link_options(browserhost PRIVATE -sEXPORT_ES6=1 -sEXIT_RUNTIME=0 -sEXPORTED_RUNTIME_METHODS=UTF8ToString,cwrap,ccall,HEAPU8,HEAPU32,HEAPU64,BROWSER_HOST - -sEXPORTED_FUNCTIONS=_posix_memalign,_free,stackAlloc,stackRestore,stackSave,_browserHostInitializeCoreCLR,_browserHostExecuteAssembly,___cpp_exception + -sEXPORTED_FUNCTIONS=_posix_memalign,_free,stackAlloc,stackRestore,stackSave,_BrowserHost_InitializeCoreCLR,_BrowserHost_ExecuteAssembly,___cpp_exception -sEXPORT_NAME=createDotnetRuntime -fwasm-exceptions -msimd128 diff --git a/src/native/corehost/browserhost/browserhost.cpp b/src/native/corehost/browserhost/browserhost.cpp index 2bfd4850e3ede0..f975fedfeba9a8 100644 --- a/src/native/corehost/browserhost/browserhost.cpp +++ b/src/native/corehost/browserhost/browserhost.cpp @@ -50,9 +50,9 @@ extern "C" const void* GlobalizationResolveDllImport(const char* name); const void* CompressionResolveDllImport(const char* name); - bool browserHostExternalAssemblyProbe(const char* pathPtr, /*out*/ void **outDataStartPtr, /*out*/ int64_t* outSize); - void browserHostResolveMain(int exitCode); - void browserHostRejectMain(const char *reason); + bool BrowserHost_ExternalAssemblyProbe(const char* pathPtr, /*out*/ void **outDataStartPtr, /*out*/ int64_t* outSize); + void BrowserHost_ResolveMain(int exitCode); + void BrowserHost_RejectMain(const char *reason); } // The current CoreCLR instance details. @@ -64,13 +64,6 @@ static void log_error_info(const char* line) std::fprintf(stderr, "log error: %s\n", line); } -static bool external_assembly_probe(const char* path, /*out*/ void **data_start, /*out*/ int64_t* size) -{ - *size = 0; - *data_start = nullptr; - return browserHostExternalAssemblyProbe(path, data_start, size);; -} - static const void* pinvoke_override(const char* library_name, const char* entry_point_name) { if (strcmp(library_name, "libSystem.Native") == 0) @@ -107,9 +100,11 @@ static std::vector propertyKeys; static std::vector propertyValues; static pal::char_t ptr_to_string_buffer[STRING_LENGTH("0xffffffffffffffff") + 1]; -extern "C" int browserHostInitializeCoreCLR(void) +// WASM-TODO: pass TPA via argument, not env +// WASM-TODO: pass app_path via argument, not env +// WASM-TODO: pass search_paths via argument, not env +extern "C" int BrowserHost_InitializeCoreCLR(void) { - //WASM-TODO: does getenv return UTF8 ? pal::getenv(HOST_PROPERTY_APP_PATHS, &app_path); pal::getenv(HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES, &search_paths); pal::getenv(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES, &tpa); @@ -124,7 +119,7 @@ extern "C" int browserHostInitializeCoreCLR(void) host_runtime_contract host_contract = { sizeof(host_runtime_contract), nullptr }; host_contract.pinvoke_override = &pinvoke_override; - host_contract.external_assembly_probe = &external_assembly_probe; + host_contract.external_assembly_probe = &BrowserHost_ExternalAssemblyProbe; pal::snwprintf(ptr_to_string_buffer, ARRAY_SIZE(ptr_to_string_buffer), _X("0x%zx"), (size_t)(&host_contract)); @@ -144,7 +139,8 @@ extern "C" int browserHostInitializeCoreCLR(void) } // WASM-TODO: browser needs async entrypoint -extern "C" int browserHostExecuteAssembly(const char* assemblyPath) +// WASM-TODO: don't coreclr_shutdown_2 when browser +extern "C" int BrowserHost_ExecuteAssembly(const char* assemblyPath) { int exit_code; int retval = coreclr_execute_assembly(CurrentClrInstance, CurrentAppDomainId, 0, nullptr, assemblyPath, (uint32_t*)&exit_code); @@ -164,11 +160,11 @@ extern "C" int browserHostExecuteAssembly(const char* assemblyPath) std::fprintf(stderr, "coreclr_shutdown_2 failed - Error: 0x%08x\n", retval); exit_code = -1; // WASM-TODO: this is too trivial - browserHostRejectMain("coreclr_shutdown_2 failed"); + BrowserHost_RejectMain("coreclr_shutdown_2 failed"); } // WASM-TODO: this is too trivial // because nothing runs continuations yet and also coreclr_execute_assembly is sync looping - browserHostResolveMain(exit_code); + BrowserHost_ResolveMain(exit_code); return retval; } diff --git a/src/native/corehost/browserhost/cross-module.ts b/src/native/corehost/browserhost/cross-module.ts new file mode 100644 index 00000000000000..fb464fa0869070 --- /dev/null +++ b/src/native/corehost/browserhost/cross-module.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../../libs/Common/JavaScript/cross-module"; diff --git a/src/native/corehost/browserhost/host/cross-linked.ts b/src/native/corehost/browserhost/host/cross-linked.ts new file mode 100644 index 00000000000000..4feb7945cefe29 --- /dev/null +++ b/src/native/corehost/browserhost/host/cross-linked.ts @@ -0,0 +1,5 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { } from "../../../libs/Common/JavaScript/cross-linked"; + diff --git a/src/native/corehost/browserhost/host/host.ts b/src/native/corehost/browserhost/host/host.ts new file mode 100644 index 00000000000000..7f8784b3f81551 --- /dev/null +++ b/src/native/corehost/browserhost/host/host.ts @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { CharPtr, VoidPtr, VoidPtrPtr } from "./types"; +import { } from "./cross-linked"; // ensure ambient symbols are declared + +const loadedAssemblies : Map = new Map(); + +export function registerDllBytes(bytes: Uint8Array, asset: { name: string }) { + const sp = Module.stackSave(); + try { + const sizeOfPtr = 4; + const ptrPtr = Module.stackAlloc(sizeOfPtr); + if (Module._posix_memalign(ptrPtr as any, 16, bytes.length)) { + throw new Error("posix_memalign failed"); + } + + const ptr = Module.HEAPU32[ptrPtr as any >>> 2]; + Module.HEAPU8.set(bytes, ptr); + loadedAssemblies.set(asset.name, { ptr, length: bytes.length }); + } finally { + Module.stackRestore(sp); + } +} + +// bool BrowserHost_ExternalAssemblyProbe(const char* pathPtr, /*out*/ void **outDataStartPtr, /*out*/ int64_t* outSize); +export function BrowserHost_ExternalAssemblyProbe(pathPtr:CharPtr, outDataStartPtr:VoidPtrPtr, outSize:VoidPtr) { + const path = Module.UTF8ToString(pathPtr); + const assembly = loadedAssemblies.get(path); + if (assembly) { + Module.HEAPU32[outDataStartPtr as any >>> 2] = assembly.ptr; + // int64_t target + Module.HEAPU32[outSize as any >>> 2] = assembly.length; + Module.HEAPU32[((outSize as any) + 4) >>> 2] = 0; + return true; + } + Module.HEAPU32[outDataStartPtr as any >>> 2] = 0; + Module.HEAPU32[outSize as any >>> 2] = 0; + Module.HEAPU32[((outSize as any) + 4) >>> 2] = 0; + return false; +} + +export function BrowserHost_ResolveMain(exitCode:number) { + dotnetLoaderExports.resolveRunMainPromise(exitCode); +} + +export function BrowserHost_RejectMain(reason:any) { + dotnetLoaderExports.rejectRunMainPromise(reason); +} + +// WASM-TODO: take ideas from Mono +// - second call to exit should be silent +// - second call to exit not override the first exit code +// - improve reason extraction +// - install global handler for unhandled exceptions and promise rejections +// - raise ExceptionHandling.RaiseAppDomainUnhandledExceptionEvent() +export function exit(exit_code: number, reason: any): void { + const reasonStr = reason ? (reason.stack ? reason.stack || reason.message : reason.toString()) : ""; + if (exit_code !== 0) { + dotnetLogger.error(`Exit with code ${exit_code} ${reason ? "and reason: " + reasonStr : ""}`); + } + if (dotnetJSEngine.IS_NODE) { + (globalThis as any).process.exit(exit_code); + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function runMain(mainAssemblyName?: string, args?: string[]): Promise { + // int BrowserHost_ExecuteAssembly(char * assemblyPath) + const res = Module.ccall("BrowserHost_ExecuteAssembly", "number", ["string"], [mainAssemblyName]) as number; + if (res != 0) { + const reason = new Error("Failed to execute assembly"); + exit(res, reason); + throw reason; + } + + return dotnetLoaderExports.getRunMainPromise(); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function runMainAndExit(mainAssemblyName?: string, args?: string[]): Promise { + try { + await runMain(mainAssemblyName, args); + } catch (error) { + exit(1, error); + throw error; + } + exit(0, null); + return 0; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function setEnvironmentVariable(name: string, value: string): void { + throw new Error("Not implemented"); +} diff --git a/src/native/corehost/browserhost/host/index.ts b/src/native/corehost/browserhost/host/index.ts new file mode 100644 index 00000000000000..ba49fe37b95065 --- /dev/null +++ b/src/native/corehost/browserhost/host/index.ts @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { InternalExchange, BrowserHostExports, RuntimeAPI, BrowserHostExportsTable } from "./types"; +import { InternalExchangeIndex } from "./types"; +import { } from "./cross-linked"; // ensure ambient symbols are declared + +import { exit, runMain, runMainAndExit, setEnvironmentVariable, registerDllBytes } from "./host"; +import { + setHeapB32, setHeapB8, setHeapU8, setHeapU16, setHeapU32, setHeapI8, setHeapI16, setHeapI32, setHeapI52, setHeapU52, setHeapI64Big, setHeapF32, setHeapF64, + getHeapB32, getHeapB8, getHeapU8, getHeapU16, getHeapU32, getHeapI8, getHeapI16, getHeapI32, getHeapI52, getHeapU52, getHeapI64Big, getHeapF32, getHeapF64, + localHeapViewI8, localHeapViewI16, localHeapViewI32, localHeapViewI64Big, localHeapViewU8, localHeapViewU16, localHeapViewU32, localHeapViewF32, localHeapViewF64, + isSharedArrayBuffer, +} from "./memory"; + +export function dotnetInitializeModule(internals: InternalExchange): void { + const runtimeApiLocal: Partial = { + runMain, + runMainAndExit, + setEnvironmentVariable, + exit, + setHeapB32, setHeapB8, setHeapU8, setHeapU16, setHeapU32, setHeapI8, setHeapI16, setHeapI32, setHeapI52, setHeapU52, setHeapI64Big, setHeapF32, setHeapF64, + getHeapB32, getHeapB8, getHeapU8, getHeapU16, getHeapU32, getHeapI8, getHeapI16, getHeapI32, getHeapI52, getHeapU52, getHeapI64Big, getHeapF32, getHeapF64, + localHeapViewI8, localHeapViewI16, localHeapViewI32, localHeapViewI64Big, localHeapViewU8, localHeapViewU16, localHeapViewU32, localHeapViewF32, localHeapViewF64, + }; + + const hostNativeExportsLocal: BrowserHostExports = { + registerDllBytes, + isSharedArrayBuffer + }; + dotnetSetInternals(internals); + Object.assign(internals[InternalExchangeIndex.RuntimeAPI], runtimeApiLocal); + internals[InternalExchangeIndex.BrowserHostExportsTable] = tabulateBrowserHostExports(hostNativeExportsLocal); + const updates = internals[InternalExchangeIndex.InternalUpdatesCallbacks]; + if (!updates.includes(dotnetUpdateModuleInternals)) updates.push(dotnetUpdateModuleInternals); + dotnetUpdateAllInternals(); + + function tabulateBrowserHostExports(map:BrowserHostExports):BrowserHostExportsTable { + // keep in sync with dotnetUpdateModuleInternals() + return [ + map.registerDllBytes, + map.isSharedArrayBuffer, + ]; + } +} + +export { BrowserHost_ExternalAssemblyProbe, BrowserHost_ResolveMain, BrowserHost_RejectMain } from "./host"; diff --git a/src/native/corehost/browserhost/host/memory.ts b/src/native/corehost/browserhost/host/memory.ts new file mode 100644 index 00000000000000..a4f402f63c72f2 --- /dev/null +++ b/src/native/corehost/browserhost/host/memory.ts @@ -0,0 +1,259 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { MemOffset, NumberOrPointer, VoidPtr } from "./types"; +import { } from "./cross-linked"; // ensure ambient symbols are declared + +export const max_int64_big = BigInt("9223372036854775807"); +export const min_int64_big = BigInt("-9223372036854775808"); +export const sharedArrayBufferDefined = typeof SharedArrayBuffer !== "undefined"; + +export function assert_int_in_range(value: Number, min: Number, max: Number) { + dotnetAssert.check(Number.isSafeInteger(value), () => `Value is not an integer: ${value} (${typeof (value)})`); + dotnetAssert.check(value >= min && value <= max, () => `Overflow: value ${value} is out of ${min} ${max} range`); +} + +export function _zero_region(byteOffset: VoidPtr, sizeBytes: number): void { + localHeapViewU8().fill(0, byteOffset, byteOffset + sizeBytes); +} + +/** note: boolean is 8 bits not 32 bits when inside a structure or array */ +export function setHeapB32(offset: MemOffset, value: number | boolean): void { + const boolValue = !!value; + if (typeof (value) === "number") + assert_int_in_range(value, 0, 1); + Module.HEAP32[offset >>> 2] = boolValue ? 1 : 0; +} + +export function setHeapB8(offset: MemOffset, value: number | boolean): void { + const boolValue = !!value; + if (typeof (value) === "number") + assert_int_in_range(value, 0, 1); + Module.HEAPU8[offset] = boolValue ? 1 : 0; +} + +export function setHeapU8(offset: MemOffset, value: number): void { + assert_int_in_range(value, 0, 0xFF); + Module.HEAPU8[offset] = value; +} + +export function setHeapU16(offset: MemOffset, value: number): void { + assert_int_in_range(value, 0, 0xFFFF); + Module.HEAPU16[offset >>> 1] = value; +} + +// does not check for growable heap +export function setHeapU16_local(localView: Uint16Array, offset: MemOffset, value: number): void { + assert_int_in_range(value, 0, 0xFFFF); + localView[offset >>> 1] = value; +} + +// does not check for overflow nor growable heap +export function setHeapU16_unchecked(offset: MemOffset, value: number): void { + Module.HEAPU16[offset >>> 1] = value; +} + +// does not check for overflow nor growable heap +export function setHeapU32_unchecked(offset: MemOffset, value: NumberOrPointer): void { + Module.HEAPU32[offset >>> 2] = value; +} + +export function setHeapU32(offset: MemOffset, value: NumberOrPointer): void { + assert_int_in_range(value, 0, 0xFFFF_FFFF); + Module.HEAPU32[offset >>> 2] = value; +} + +export function setHeapI8(offset: MemOffset, value: number): void { + assert_int_in_range(value, -0x80, 0x7F); + Module.HEAP8[offset] = value; +} + +export function setHeapI16(offset: MemOffset, value: number): void { + assert_int_in_range(value, -0x8000, 0x7FFF); + Module.HEAP16[offset >>> 1] = value; +} + +export function setHeapI32_unchecked(offset: MemOffset, value: number): void { + Module.HEAP32[offset >>> 2] = value; +} + +export function setHeapI32(offset: MemOffset, value: number): void { + assert_int_in_range(value, -0x8000_0000, 0x7FFF_FFFF); + Module.HEAP32[offset >>> 2] = value; +} + +/** + * Throws for values which are not 52 bit integer. See Number.isSafeInteger() + */ +export function setHeapI52(offset: MemOffset, value: number): void { + dotnetAssert.check(Number.isSafeInteger(value), () => `Value is not a safe integer: ${value} (${typeof (value)})`); + throw new Error("WASM-TODO"); +} + +/** + * Throws for values which are not 52 bit integer or are negative. See Number.isSafeInteger(). + */ +export function setHeapU52(offset: MemOffset, value: number): void { + dotnetAssert.check(Number.isSafeInteger(value), () => `Value is not a safe integer: ${value} (${typeof (value)})`); + dotnetAssert.check(value >= 0, "Can't convert negative Number into UInt64"); + throw new Error("WASM-TODO"); +} + +export function setHeapI64Big(offset: MemOffset, value: bigint): void { + dotnetAssert.check(typeof value === "bigint", () => `Value is not an bigint: ${value} (${typeof (value)})`); + dotnetAssert.check(value >= min_int64_big && value <= max_int64_big, () => `Overflow: value ${value} is out of ${min_int64_big} ${max_int64_big} range`); + + Module.HEAP64[offset >>> 3] = value; +} + +export function setHeapF32(offset: MemOffset, value: number): void { + dotnetAssert.check(typeof value === "number", () => `Value is not a Number: ${value} (${typeof (value)})`); + Module.HEAPF32[offset >>> 2] = value; +} + +export function setHeapF64(offset: MemOffset, value: number): void { + dotnetAssert.check(typeof value === "number", () => `Value is not a Number: ${value} (${typeof (value)})`); + Module.HEAPF64[offset >>> 3] = value; +} + +export function getHeapB32(offset: MemOffset): boolean { + const value = (Module.HEAPU32[offset >>> 2]); + if (value > 1 && !(getHeapB32 as any).warnDirtyBool) { + (getHeapB32 as any).warnDirtyBool = true; + dotnetLogger.warn(`getB32: value at ${offset} is not a boolean, but a number: ${value}`); + } + return !!value; +} + +export function getHeapB8(offset: MemOffset): boolean { + return !!(Module.HEAPU8[offset]); +} + +export function getHeapU8(offset: MemOffset): number { + return Module.HEAPU8[offset]; +} + +export function getHeapU16(offset: MemOffset): number { + return Module.HEAPU16[offset >>> 1]; +} + +// does not check for growable heap +export function getHeapU16_local(localView: Uint16Array, offset: MemOffset): number { + return localView[offset >>> 1]; +} + +export function getHeapU32(offset: MemOffset): number { + return Module.HEAPU32[offset >>> 2]; +} + +// does not check for growable heap +export function getHeapU32_local(localView: Uint32Array, offset: MemOffset): number { + return localView[offset >>> 2]; +} + +export function getHeapI8(offset: MemOffset): number { + return Module.HEAP8[offset]; +} + +export function getHeapI16(offset: MemOffset): number { + return Module.HEAP16[offset >>> 1]; +} + +// does not check for growable heap +export function getHeapI16_local(localView: Int16Array, offset: MemOffset): number { + return localView[offset >>> 1]; +} + +export function getHeapI32(offset: MemOffset): number { + return Module.HEAP32[offset >>> 2]; +} + +// does not check for growable heap +export function getHeapI32_local(localView: Int32Array, offset: MemOffset): number { + return localView[offset >>> 2]; +} + +/** + * Throws for Number.MIN_SAFE_INTEGER > value > Number.MAX_SAFE_INTEGER + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function getHeapI52(offset: MemOffset): number { + throw new Error("WASM-TODO"); +} + +/** + * Throws for 0 > value > Number.MAX_SAFE_INTEGER + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function getHeapU52(offset: MemOffset): number { + throw new Error("WASM-TODO"); +} + +export function getHeapI64Big(offset: MemOffset): bigint { + return Module.HEAP64[offset >>> 3]; +} + +export function getHeapF32(offset: MemOffset): number { + return Module.HEAPF32[offset >>> 2]; +} + +export function getHeapF64(offset: MemOffset): number { + return Module.HEAPF64[offset >>> 3]; +} + +// returns memory view which is valid within current synchronous call stack +export function localHeapViewI8(): Int8Array { + return Module.HEAP8; +} + +// returns memory view which is valid within current synchronous call stack +export function localHeapViewI16(): Int16Array { + return Module.HEAP16; +} + +// returns memory view which is valid within current synchronous call stack +export function localHeapViewI32(): Int32Array { + return Module.HEAP32; +} + +// returns memory view which is valid within current synchronous call stack +export function localHeapViewI64Big(): BigInt64Array { + return Module.HEAP64; +} + +// returns memory view which is valid within current synchronous call stack +export function localHeapViewU8(): Uint8Array { + return Module.HEAPU8; +} + +// returns memory view which is valid within current synchronous call stack +export function localHeapViewU16(): Uint16Array { + return Module.HEAPU16; +} + +// returns memory view which is valid within current synchronous call stack +export function localHeapViewU32(): Uint32Array { + return Module.HEAPU32; +} + +// returns memory view which is valid within current synchronous call stack +export function localHeapViewF32(): Float32Array { + return Module.HEAPF32; +} + +// returns memory view which is valid within current synchronous call stack +export function localHeapViewF64(): Float64Array { + return Module.HEAPF64; +} + +export function copyBytes(srcPtr: VoidPtr, dstPtr: VoidPtr, bytes: number): void { + const heap = localHeapViewU8(); + heap.copyWithin(dstPtr as any, srcPtr as any, srcPtr as any + bytes); +} + +export function isSharedArrayBuffer(buffer: any): buffer is SharedArrayBuffer { + // BEWARE: In some cases, `instanceof SharedArrayBuffer` returns false even though buffer is an SAB. + // Patch adapted from https://github.com/emscripten-core/emscripten/pull/16994 + // See also https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag + return sharedArrayBufferDefined && buffer[Symbol.toStringTag] === "SharedArrayBuffer"; +} diff --git a/src/native/corehost/browserhost/host/types.ts b/src/native/corehost/browserhost/host/types.ts new file mode 100644 index 00000000000000..2786379af7369f --- /dev/null +++ b/src/native/corehost/browserhost/host/types.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../types"; diff --git a/src/native/corehost/browserhost/libBrowserHost.footer.js b/src/native/corehost/browserhost/libBrowserHost.footer.js new file mode 100644 index 00000000000000..2e623505a03e2e --- /dev/null +++ b/src/native/corehost/browserhost/libBrowserHost.footer.js @@ -0,0 +1,73 @@ +//! Licensed to the .NET Foundation under one or more agreements. +//! The .NET Foundation licenses this file to you under the MIT license. + +/** + * This is root of **Emscripten library** that would become part of `dotnet.native.js` + * It implements the corehost and a part of public JS API related to memory and runtime hosting. + */ + +(function (exports) { + function libFactory() { + const lib = { + $BROWSER_HOST: { + selfInitialize: () => { + if (typeof dotnetInternals !== "undefined") { + BROWSER_HOST.dotnetInternals = dotnetInternals; + + const exports = {}; + libBrowserHostFn(exports); + exports.dotnetInitializeModule(dotnetInternals); + BROWSER_HOST.assignExports(exports, BROWSER_HOST); + + const HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES = "TRUSTED_PLATFORM_ASSEMBLIES"; + const HOST_PROPERTY_ENTRY_ASSEMBLY_NAME = "ENTRY_ASSEMBLY_NAME"; + const HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES = "NATIVE_DLL_SEARCH_DIRECTORIES"; + const HOST_PROPERTY_APP_PATHS = "APP_PATHS"; + + const config = dotnetInternals[2/*InternalExchangeIndex.LoaderConfig*/]; + if (!config.resources.assembly || + !config.resources.coreAssembly || + config.resources.coreAssembly.length === 0 || + !config.mainAssemblyName || + !config.virtualWorkingDirectory || + !config.environmentVariables) { + throw new Error("Invalid runtime config, cannot initialize the runtime."); + } + const assemblyPaths = config.resources.assembly.map(a => a.virtualPath); + const coreAssemblyPaths = config.resources.coreAssembly.map(a => a.virtualPath); + ENV[HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES] = config.environmentVariables[HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES] = [...coreAssemblyPaths, ...assemblyPaths].join(":"); + ENV[HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES] = config.environmentVariables[HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES] = config.virtualWorkingDirectory; + ENV[HOST_PROPERTY_APP_PATHS] = config.environmentVariables[HOST_PROPERTY_APP_PATHS] = config.virtualWorkingDirectory; + ENV[HOST_PROPERTY_ENTRY_ASSEMBLY_NAME] = config.environmentVariables[HOST_PROPERTY_ENTRY_ASSEMBLY_NAME] = config.mainAssemblyName; + } + }, + }, + $libBrowserHostFn: libBrowserHost, + $BROWSER_HOST__postset: "BROWSER_HOST.selfInitialize()", + }; + + // this executes the function at link time in order to capture exports + // this is what Emscripten does for linking JS libraries + // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#javascript-limits-in-library-files + // it would execute the code at link time and call .toString() on functions to move it to the final output + // this process would loose any closure references, unless they are passed to `__deps` and also explicitly given to the linker + // JS name mangling and minification also applies, see src\native\rollup.config.defines.js and `reserved` there + const exports = {} + libBrowserHost(exports); + let commonDeps = ["$libBrowserHostFn", "$DOTNET", "$DOTNET_INTEROP", "$ENV"]; + let assignExportsBuilder = ""; + for (const exportName of Reflect.ownKeys(exports)) { + const name = String(exportName); + if (name === "dotnetInitializeModule") continue; + lib[name] = () => "dummy"; + assignExportsBuilder += `_${String(name)} = exports.${String(name)};\n`; + } + lib.$BROWSER_HOST.assignExports = new Function("exports", assignExportsBuilder); + lib["$BROWSER_HOST__deps"] = commonDeps; + + autoAddDeps(lib, "$BROWSER_HOST"); + addToLibrary(lib); + } + libFactory(); + return exports; +})({}); \ No newline at end of file diff --git a/src/native/corehost/browserhost/libBrowserHost.js b/src/native/corehost/browserhost/libBrowserHost.js deleted file mode 100644 index b9b27dd9436849..00000000000000 --- a/src/native/corehost/browserhost/libBrowserHost.js +++ /dev/null @@ -1,550 +0,0 @@ -//! Licensed to the .NET Foundation under one or more agreements. -//! The .NET Foundation licenses this file to you under the MIT license. -//! This is generated file, see src/native/libs/Browser/rollup.config.defines.js - - -var libBrowserHost = (function (exports) { - 'use strict'; - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - let Module; - let runtimeApi; - let Logger = {}; - let Assert = {}; - let JSEngine = {}; - let loaderExports = {}; - let runtimeExports = {}; - let hostExports = {}; - let interopExports = {}; - let nativeBrowserExports = {}; - let dotnetInternals; - function getInternals() { - return dotnetInternals; - } - function setInternals(internal) { - dotnetInternals = internal; - runtimeApi = dotnetInternals.runtimeApi; - Module = dotnetInternals.runtimeApi.Module; - } - function updateAllInternals() { - if (dotnetInternals.updates === undefined) { - dotnetInternals.updates = []; - } - for (const updateImpl of dotnetInternals.updates) { - updateImpl(); - } - } - function updateMyInternals() { - if (Object.keys(loaderExports).length === 0 && dotnetInternals.loaderExportsTable) { - loaderExports = {}; - Logger = {}; - Assert = {}; - JSEngine = {}; - expandLE(dotnetInternals.loaderExportsTable, Logger, Assert, JSEngine, loaderExports); - } - if (Object.keys(runtimeExports).length === 0 && dotnetInternals.runtimeExportsTable) { - runtimeExports = {}; - expandRE(dotnetInternals.runtimeExportsTable, runtimeExports); - } - if (Object.keys(hostExports).length === 0 && dotnetInternals.hostNativeExportsTable) { - hostExports = {}; - expandHE(dotnetInternals.hostNativeExportsTable, hostExports); - } - if (Object.keys(interopExports).length === 0 && dotnetInternals.interopJavaScriptNativeExportsTable) { - interopExports = {}; - expandJSNE(dotnetInternals.interopJavaScriptNativeExportsTable, interopExports); - } - if (Object.keys(nativeBrowserExports).length === 0 && dotnetInternals.nativeBrowserExportsTable) { - nativeBrowserExports = {}; - expandNBE(dotnetInternals.nativeBrowserExportsTable, nativeBrowserExports); - } - } - /** - * Functions below allow our JS modules to exchange internal interfaces by passing tables of functions in known order instead of using string symbols. - * IMPORTANT: If you need to add more functions, make sure that you add them at the end of the table, so that the order of existing functions does not change. - */ - function tabulateLE(logger, assert, loaderExports) { - return [ - logger.info, - logger.warn, - logger.error, - assert.check, - loaderExports.ENVIRONMENT_IS_NODE, - loaderExports.ENVIRONMENT_IS_SHELL, - loaderExports.ENVIRONMENT_IS_WEB, - loaderExports.ENVIRONMENT_IS_WORKER, - loaderExports.ENVIRONMENT_IS_SIDECAR, - loaderExports.browserHostResolveMain, - loaderExports.browserHostRejectMain, - loaderExports.getRunMainPromise, - ]; - } - function expandLE(table, logger, assert, jsEngine, loaderExports) { - const loggerLocal = { - info: table[0], - warn: table[1], - error: table[2], - }; - const assertLocal = { - check: table[3], - }; - const loaderExportsLocal = { - ENVIRONMENT_IS_NODE: table[4], - ENVIRONMENT_IS_SHELL: table[5], - ENVIRONMENT_IS_WEB: table[6], - ENVIRONMENT_IS_WORKER: table[7], - ENVIRONMENT_IS_SIDECAR: table[8], - browserHostResolveMain: table[9], - browserHostRejectMain: table[10], - getRunMainPromise: table[11], - }; - const jsEngineLocal = { - IS_NODE: loaderExportsLocal.ENVIRONMENT_IS_NODE(), - IS_SHELL: loaderExportsLocal.ENVIRONMENT_IS_SHELL(), - IS_WEB: loaderExportsLocal.ENVIRONMENT_IS_WEB(), - IS_WORKER: loaderExportsLocal.ENVIRONMENT_IS_WORKER(), - IS_SIDECAR: loaderExportsLocal.ENVIRONMENT_IS_SIDECAR(), - }; - Object.assign(loaderExports, loaderExportsLocal); - Object.assign(logger, loggerLocal); - Object.assign(assert, assertLocal); - Object.assign(jsEngine, jsEngineLocal); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function tabulateRE(map) { - return []; - } - function expandRE(table, runtime) { - Object.assign(runtime, {}); - } - function tabulateHE(map) { - return [ - map.registerDllBytes, - map.isSharedArrayBuffer, - ]; - } - function expandHE(table, native) { - const nativeLocal = { - registerDllBytes: table[0], - isSharedArrayBuffer: table[1], - }; - Object.assign(native, nativeLocal); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function tabulateJSNE(map) { - return []; - } - function expandJSNE(table, interop) { - const interopLocal = {}; - Object.assign(interop, interopLocal); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function tabulateNBE(map) { - return []; - } - function expandNBE(table, interop) { - const interopLocal = {}; - Object.assign(interop, interopLocal); - } - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - // see also `reserved` in `rollup.config.defines.js` - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - const loadedAssemblies = {}; - function registerDllBytes(bytes, asset) { - const sp = Module.stackSave(); - try { - const sizeOfPtr = 4; - const ptrPtr = Module.stackAlloc(sizeOfPtr); - if (Module._posix_memalign(ptrPtr, 16, bytes.length)) { - throw new Error("posix_memalign failed"); - } - const ptr = Module.HEAPU32[ptrPtr >>> 2]; - Module.HEAPU8.set(bytes, ptr); - loadedAssemblies[asset.name] = { ptr, length: bytes.length }; - } - finally { - Module.stackRestore(sp); - } - } - // bool browserHostExternalAssemblyProbe(const char* pathPtr, /*out*/ void **outDataStartPtr, /*out*/ int64_t* outSize); - function browserHostExternalAssemblyProbe(pathPtr, outDataStartPtr, outSize) { - const path = Module.UTF8ToString(pathPtr); - const assembly = loadedAssemblies[path]; - if (assembly) { - Module.HEAPU32[outDataStartPtr >>> 2] = assembly.ptr; - // int64_t target - Module.HEAPU32[outSize >>> 2] = assembly.length; - Module.HEAPU32[(outSize + 4) >>> 2] = 0; - return true; - } - Module.HEAPU32[outDataStartPtr >>> 2] = 0; - Module.HEAPU32[outSize >>> 2] = 0; - Module.HEAPU32[(outSize + 4) >>> 2] = 0; - return false; - } - function browserHostResolveMain(exitCode) { - loaderExports.browserHostResolveMain(exitCode); - } - function browserHostRejectMain(reason) { - loaderExports.browserHostRejectMain(reason); - } - // TODO-WASM: take ideas from Mono - // - second call to exit should be silent - // - second call to exit not override the first exit code - // - improve reason extraction - // - install global handler for unhandled exceptions and promise rejections - function exit(exit_code, reason) { - const reasonStr = reason ? (reason.stack ? reason.stack || reason.message : reason.toString()) : ""; - if (exit_code !== 0) { - Logger.error(`Exit with code ${exit_code} ${reason ? "and reason: " + reasonStr : ""}`); - } - if (JSEngine.IS_NODE) { - globalThis.process.exit(exit_code); - } - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async function runMain(mainAssemblyName, args) { - // int browserHostExecuteAssembly(char * assemblyPath) - const res = Module.ccall("browserHostExecuteAssembly", "number", ["string"], [mainAssemblyName]); - if (res != 0) { - const reason = new Error("Failed to execute assembly"); - exit(res, reason); - throw reason; - } - return loaderExports.getRunMainPromise(); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async function runMainAndExit(mainAssemblyName, args) { - try { - await runMain(mainAssemblyName, args); - } - catch (error) { - exit(1, error); - throw error; - } - exit(0, null); - return 0; - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function setEnvironmentVariable(name, value) { - throw new Error("Not implemented"); - } - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - const max_int64_big = BigInt("9223372036854775807"); - const min_int64_big = BigInt("-9223372036854775808"); - const sharedArrayBufferDefined = typeof SharedArrayBuffer !== "undefined"; - function assert_int_in_range(value, min, max) { - Assert.check(Number.isSafeInteger(value), () => `Value is not an integer: ${value} (${typeof (value)})`); - Assert.check(value >= min && value <= max, () => `Overflow: value ${value} is out of ${min} ${max} range`); - } - function _zero_region(byteOffset, sizeBytes) { - localHeapViewU8().fill(0, byteOffset, byteOffset + sizeBytes); - } - /** note: boolean is 8 bits not 32 bits when inside a structure or array */ - function setHeapB32(offset, value) { - const boolValue = !!value; - if (typeof (value) === "number") - assert_int_in_range(value, 0, 1); - Module.HEAP32[offset >>> 2] = boolValue ? 1 : 0; - } - function setHeapB8(offset, value) { - const boolValue = !!value; - if (typeof (value) === "number") - assert_int_in_range(value, 0, 1); - Module.HEAPU8[offset] = boolValue ? 1 : 0; - } - function setHeapU8(offset, value) { - assert_int_in_range(value, 0, 0xFF); - Module.HEAPU8[offset] = value; - } - function setHeapU16(offset, value) { - assert_int_in_range(value, 0, 0xFFFF); - Module.HEAPU16[offset >>> 1] = value; - } - // does not check for growable heap - function setHeapU16_local(localView, offset, value) { - assert_int_in_range(value, 0, 0xFFFF); - localView[offset >>> 1] = value; - } - // does not check for overflow nor growable heap - function setHeapU16_unchecked(offset, value) { - Module.HEAPU16[offset >>> 1] = value; - } - // does not check for overflow nor growable heap - function setHeapU32_unchecked(offset, value) { - Module.HEAPU32[offset >>> 2] = value; - } - function setHeapU32(offset, value) { - assert_int_in_range(value, 0, 4294967295); - Module.HEAPU32[offset >>> 2] = value; - } - function setHeapI8(offset, value) { - assert_int_in_range(value, -0x80, 0x7F); - Module.HEAP8[offset] = value; - } - function setHeapI16(offset, value) { - assert_int_in_range(value, -0x8000, 0x7FFF); - Module.HEAP16[offset >>> 1] = value; - } - function setHeapI32_unchecked(offset, value) { - Module.HEAP32[offset >>> 2] = value; - } - function setHeapI32(offset, value) { - assert_int_in_range(value, -2147483648, 2147483647); - Module.HEAP32[offset >>> 2] = value; - } - /** - * Throws for values which are not 52 bit integer. See Number.isSafeInteger() - */ - function setHeapI52(offset, value) { - Assert.check(Number.isSafeInteger(value), () => `Value is not a safe integer: ${value} (${typeof (value)})`); - throw new Error("TODO"); - // const error = cwraps.mono_wasm_f64_to_i52(offset, value); - // autoThrowI52(error); - } - /** - * Throws for values which are not 52 bit integer or are negative. See Number.isSafeInteger(). - */ - function setHeapU52(offset, value) { - Assert.check(Number.isSafeInteger(value), () => `Value is not a safe integer: ${value} (${typeof (value)})`); - Assert.check(value >= 0, "Can't convert negative Number into UInt64"); - throw new Error("TODO"); - //const error = cwraps.mono_wasm_f64_to_u52(offset, value); - //autoThrowI52(error); - } - function setHeapI64Big(offset, value) { - Assert.check(typeof value === "bigint", () => `Value is not an bigint: ${value} (${typeof (value)})`); - Assert.check(value >= min_int64_big && value <= max_int64_big, () => `Overflow: value ${value} is out of ${min_int64_big} ${max_int64_big} range`); - Module.HEAP64[offset >>> 3] = value; - } - function setHeapF32(offset, value) { - Assert.check(typeof value === "number", () => `Value is not a Number: ${value} (${typeof (value)})`); - Module.HEAPF32[offset >>> 2] = value; - } - function setHeapF64(offset, value) { - Assert.check(typeof value === "number", () => `Value is not a Number: ${value} (${typeof (value)})`); - Module.HEAPF64[offset >>> 3] = value; - } - function getHeapB32(offset) { - const value = (Module.HEAPU32[offset >>> 2]); - if (value > 1 && !getHeapB32.warnDirtyBool) { - getHeapB32.warnDirtyBool = true; - Logger.warn(`getB32: value at ${offset} is not a boolean, but a number: ${value}`); - } - return !!value; - } - function getHeapB8(offset) { - return !!(Module.HEAPU8[offset]); - } - function getHeapU8(offset) { - return Module.HEAPU8[offset]; - } - function getHeapU16(offset) { - return Module.HEAPU16[offset >>> 1]; - } - // does not check for growable heap - function getHeapU16_local(localView, offset) { - return localView[offset >>> 1]; - } - function getHeapU32(offset) { - return Module.HEAPU32[offset >>> 2]; - } - // does not check for growable heap - function getHeapU32_local(localView, offset) { - return localView[offset >>> 2]; - } - function getHeapI8(offset) { - return Module.HEAP8[offset]; - } - function getHeapI16(offset) { - return Module.HEAP16[offset >>> 1]; - } - // does not check for growable heap - function getHeapI16_local(localView, offset) { - return localView[offset >>> 1]; - } - function getHeapI32(offset) { - return Module.HEAP32[offset >>> 2]; - } - // does not check for growable heap - function getHeapI32_local(localView, offset) { - return localView[offset >>> 2]; - } - /** - * Throws for Number.MIN_SAFE_INTEGER > value > Number.MAX_SAFE_INTEGER - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function getHeapI52(offset) { - throw new Error("TODO"); - //const result = cwraps.mono_wasm_i52_to_f64(offset, runtimeHelpers._i52_error_scratch_buffer); - //const error = getI32(runtimeHelpers._i52_error_scratch_buffer); - //autoThrowI52(error); - //return result; - } - /** - * Throws for 0 > value > Number.MAX_SAFE_INTEGER - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function getHeapU52(offset) { - throw new Error("TODO"); - //const result = cwraps.mono_wasm_u52_to_f64(offset, runtimeHelpers._i52_error_scratch_buffer); - //const error = getI32(runtimeHelpers._i52_error_scratch_buffer); - //autoThrowI52(error); - //return result; - } - function getHeapI64Big(offset) { - return Module.HEAP64[offset >>> 3]; - } - function getHeapF32(offset) { - return Module.HEAPF32[offset >>> 2]; - } - function getHeapF64(offset) { - return Module.HEAPF64[offset >>> 3]; - } - // returns memory view which is valid within current synchronous call stack - function localHeapViewI8() { - return Module.HEAP8; - } - // returns memory view which is valid within current synchronous call stack - function localHeapViewI16() { - return Module.HEAP16; - } - // returns memory view which is valid within current synchronous call stack - function localHeapViewI32() { - return Module.HEAP32; - } - // returns memory view which is valid within current synchronous call stack - function localHeapViewI64Big() { - return Module.HEAP64; - } - // returns memory view which is valid within current synchronous call stack - function localHeapViewU8() { - return Module.HEAPU8; - } - // returns memory view which is valid within current synchronous call stack - function localHeapViewU16() { - return Module.HEAPU16; - } - // returns memory view which is valid within current synchronous call stack - function localHeapViewU32() { - return Module.HEAPU32; - } - // returns memory view which is valid within current synchronous call stack - function localHeapViewF32() { - return Module.HEAPF32; - } - // returns memory view which is valid within current synchronous call stack - function localHeapViewF64() { - return Module.HEAPF64; - } - function copyBytes(srcPtr, dstPtr, bytes) { - const heap = localHeapViewU8(); - heap.copyWithin(dstPtr, srcPtr, srcPtr + bytes); - } - function isSharedArrayBuffer(buffer) { - // BEWARE: In some cases, `instanceof SharedArrayBuffer` returns false even though buffer is an SAB. - // Patch adapted from https://github.com/emscripten-core/emscripten/pull/16994 - // See also https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag - return sharedArrayBufferDefined && buffer[Symbol.toStringTag] === "SharedArrayBuffer"; - } - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - function initialize(internals) { - const runtimeApiLocal = { - runMain, - runMainAndExit, - setEnvironmentVariable, - exit, - setHeapB32, setHeapB8, setHeapU8, setHeapU16, setHeapU32, setHeapI8, setHeapI16, setHeapI32, setHeapI52, setHeapU52, setHeapI64Big, setHeapF32, setHeapF64, - getHeapB32, getHeapB8, getHeapU8, getHeapU16, getHeapU32, getHeapI8, getHeapI16, getHeapI32, getHeapI52, getHeapU52, getHeapI64Big, getHeapF32, getHeapF64, - localHeapViewI8, localHeapViewI16, localHeapViewI32, localHeapViewI64Big, localHeapViewU8, localHeapViewU16, localHeapViewU32, localHeapViewF32, localHeapViewF64, - }; - const hostNativeExportsLocal = { - registerDllBytes, - isSharedArrayBuffer - }; - setInternals(internals); - Object.assign(internals.runtimeApi, runtimeApiLocal); - internals.hostNativeExportsTable = [...tabulateHE(hostNativeExportsLocal)]; - internals.updates.push(updateMyInternals); - updateAllInternals(); - } - - exports.browserHostExternalAssemblyProbe = browserHostExternalAssemblyProbe; - exports.browserHostRejectMain = browserHostRejectMain; - exports.browserHostResolveMain = browserHostResolveMain; - exports.initialize = initialize; - - return exports; - -}); -//! Licensed to the .NET Foundation under one or more agreements. -//! The .NET Foundation licenses this file to you under the MIT license. - -/** - * This is root of **Emscripten library** that would become part of `dotnet.native.js` - * It implements the corehost and a part of public JS API related to memory and runtime hosting. - */ - -(function (exports) { - function libFactory() { - const lib = { - $BROWSER_HOST: { - selfInitialize: () => { - if (typeof dotnetInternals !== "undefined") { - BROWSER_HOST.dotnetInternals = dotnetInternals; - - const exports = libBrowserHostFn(BROWSER_HOST); - exports.initialize(dotnetInternals); - BROWSER_HOST.assignExports(exports, BROWSER_HOST); - - const HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES = "TRUSTED_PLATFORM_ASSEMBLIES"; - const HOST_PROPERTY_ENTRY_ASSEMBLY_NAME = "ENTRY_ASSEMBLY_NAME"; - const HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES = "NATIVE_DLL_SEARCH_DIRECTORIES"; - const HOST_PROPERTY_APP_PATHS = "APP_PATHS"; - - const config = dotnetInternals.config; - const assemblyPaths = config.resources.assembly.map(a => a.virtualPath); - const coreAssemblyPaths = config.resources.coreAssembly.map(a => a.virtualPath); - ENV[HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES] = config.environmentVariables[HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES] = [...coreAssemblyPaths, ...assemblyPaths].join(":"); - ENV[HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES] = config.environmentVariables[HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES] = config.virtualWorkingDirectory; - ENV[HOST_PROPERTY_APP_PATHS] = config.environmentVariables[HOST_PROPERTY_APP_PATHS] = config.virtualWorkingDirectory; - ENV[HOST_PROPERTY_ENTRY_ASSEMBLY_NAME] = config.environmentVariables[HOST_PROPERTY_ENTRY_ASSEMBLY_NAME] = config.mainAssemblyName; - } - }, - }, - "$libBrowserHostFn": libBrowserHost, - "$BROWSER_HOST__postset": "BROWSER_HOST.selfInitialize();", - }; - - // this executes the function at compile time in order to capture export names - const exports = libBrowserHost({}); - let commonDeps = ["$libBrowserHostFn", "$DOTNET", "$DOTNET_INTEROP", "$ENV"]; - let assignExportsBuilder = ""; - for (const exportName of Reflect.ownKeys(exports)) { - const name = String(exportName); - if (name === "cross") continue; - if (name === "initialize") continue; - lib[name] = () => "dummy"; - assignExportsBuilder += `_${String(name)} = exports.${String(name)};\n`; - } - lib.$BROWSER_HOST.assignExports = new Function("exports", assignExportsBuilder); - lib["$BROWSER_HOST__deps"] = commonDeps; - - autoAddDeps(lib, "$BROWSER_HOST"); - addToLibrary(lib); - } - libFactory(); - return exports; -})({}); diff --git a/src/native/corehost/browserhost/loader/bootstrap.ts b/src/native/corehost/browserhost/loader/bootstrap.ts new file mode 100644 index 00000000000000..dbd116888d6bcd --- /dev/null +++ b/src/native/corehost/browserhost/loader/bootstrap.ts @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { LoadBootResourceCallback, JsModuleExports, JsAsset, AssemblyAsset, PdbAsset, WasmAsset, IcuAsset, EmscriptenModuleInternal } from "./types"; + +import { dotnetAssert, dotnetJSEngine, dotnetGetInternals, dotnetBrowserHostExports, dotnetUpdateAllInternals } from "./cross-module"; +import { getLoaderConfig } from "./config"; +import { BrowserHost_InitializeCoreCLR } from "./run"; +import { createPromiseController } from "./promise-controller"; + +const scriptUrlQuery = /*! webpackIgnore: true */import.meta.url; +const queryIndex = scriptUrlQuery.indexOf("?"); +const modulesUniqueQuery = queryIndex > 0 ? scriptUrlQuery.substring(queryIndex) : ""; +const scriptUrl = normalizeFileUrl(scriptUrlQuery); +const scriptDirectory = normalizeDirectoryUrl(scriptUrl); + +const nativeModulePromiseController = createPromiseController(() => { + dotnetUpdateAllInternals(); +}); + +// WASM-TODO: retry logic +// WASM-TODO: throttling logic +// WASM-TODO: load icu data +// WASM-TODO: invokeLibraryInitializers +// WASM-TODO: webCIL +// WASM-TODO: downloadOnly - blazor render mode auto pre-download. Really no start. + +export async function createRuntime(downloadOnly: boolean, loadBootResource?: LoadBootResourceCallback): Promise { + const config = getLoaderConfig(); + if (!config.resources || !config.resources.coreAssembly || !config.resources.coreAssembly.length) throw new Error("Invalid config, resources is not set"); + + const coreAssembliesPromise = Promise.all(config.resources.coreAssembly.map(fetchDll)); + const assembliesPromise = Promise.all(config.resources.assembly.map(fetchDll)); + const runtimeModulePromise = loadJSModule(config.resources.jsModuleRuntime[0], loadBootResource); + const nativeModulePromise = loadJSModule(config.resources.jsModuleNative[0], loadBootResource); + // WASM-TODO fetchWasm(config.resources.wasmNative[0]);// start loading early, no await + + const nativeModule = await nativeModulePromise; + const modulePromise = nativeModule.dotnetInitializeModule(dotnetGetInternals()); + nativeModulePromiseController.propagateFrom(modulePromise); + + const runtimeModule = await runtimeModulePromise; + const runtimeModuleReady = runtimeModule.dotnetInitializeModule(dotnetGetInternals()); + + await nativeModulePromiseController.promise; + await coreAssembliesPromise; + + if (!downloadOnly) { + BrowserHost_InitializeCoreCLR(); + } + + await assembliesPromise; + await runtimeModuleReady; +} + +async function loadJSModule(asset: JsAsset, loadBootResource?: LoadBootResourceCallback): Promise { + if (loadBootResource) throw new Error("TODO: loadBootResource is not implemented yet"); + if (asset.name && !asset.resolvedUrl) { + asset.resolvedUrl = locateFile(asset.name); + } + if (!asset.resolvedUrl) throw new Error("Invalid config, resources is not set"); + return await import(/* webpackIgnore: true */ asset.resolvedUrl); +} + +async function fetchDll(asset: AssemblyAsset): Promise { + if (asset.name && !asset.resolvedUrl) { + asset.resolvedUrl = locateFile(asset.name); + } + const bytes = await fetchBytes(asset); + await nativeModulePromiseController.promise; + + dotnetBrowserHostExports.registerDllBytes(bytes, asset); +} + +async function fetchBytes(asset: WasmAsset|AssemblyAsset|PdbAsset|IcuAsset): Promise { + dotnetAssert.check(asset && asset.resolvedUrl, "Bad asset.resolvedUrl"); + if (dotnetJSEngine.IS_NODE) { + const { promises: fs } = await import("fs"); + const { fileURLToPath } = await import(/*! webpackIgnore: true */"url"); + const isFileUrl = asset.resolvedUrl!.startsWith("file://"); + if (isFileUrl) { + asset.resolvedUrl = fileURLToPath(asset.resolvedUrl!); + } + const buffer = await fs.readFile(asset.resolvedUrl!); + return new Uint8Array(buffer); + } else { + const response = await fetch(asset.resolvedUrl!); + if (!response.ok) { + throw new Error(`Failed to load ${asset.resolvedUrl} with ${response.status} ${response.statusText}`); + } + const buffer = await response.arrayBuffer(); + return new Uint8Array(buffer); + } +} + +function locateFile(path: string) { + if ("URL" in globalThis) { + return new URL(path, scriptDirectory).toString(); + } + + if (isPathAbsolute(path)) return path; + return scriptDirectory + path + modulesUniqueQuery; +} + +function normalizeFileUrl(filename: string) { + // unix vs windows + // remove query string + return filename.replace(/\\/g, "/").replace(/[?#].*/, ""); +} + +function normalizeDirectoryUrl(dir: string) { + return dir.slice(0, dir.lastIndexOf("/")) + "/"; +} + +const protocolRx = /^[a-zA-Z][a-zA-Z\d+\-.]*?:\/\//; +const windowsAbsoluteRx = /[a-zA-Z]:[\\/]/; +function isPathAbsolute(path: string): boolean { + if (dotnetJSEngine.IS_NODE || dotnetJSEngine.IS_SHELL) { + // unix /x.json + // windows \x.json + // windows C:\x.json + // windows C:/x.json + return path.startsWith("/") || path.startsWith("\\") || path.indexOf("///") !== -1 || windowsAbsoluteRx.test(path); + } + + // anything with protocol is always absolute + // windows file:///C:/x.json + // windows http://C:/x.json + return protocolRx.test(path); +} diff --git a/src/native/corehost/browserhost/loader/config.ts b/src/native/corehost/browserhost/loader/config.ts new file mode 100644 index 00000000000000..5e6587cab616b2 --- /dev/null +++ b/src/native/corehost/browserhost/loader/config.ts @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { Assets, LoadBootResourceCallback, LoaderConfig, LoaderConfigInternal } from "./types"; + +export const netLoaderConfig: LoaderConfigInternal = {}; +let isConfigReady = false; + +export async function downloadConfig(url: string|undefined, loadBootResource?: LoadBootResourceCallback): Promise { + if (loadBootResource) throw new Error("TODO: loadBootResource is not implemented yet"); + if (isConfigReady) return; // only download if necessary + if (!url) { + url = "./dotnet.boot.js"; + } + + // url ends with .json + if (url.endsWith(".json")) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to download config from ${url}: ${response.status} ${response.statusText}`); + } + const newConfig = await response.json() as Partial; + mergeLoaderConfig(newConfig); + } else if (url.endsWith(".js") || url.endsWith(".mjs")) { + const module = await import(/* webpackIgnore: true */ url); + mergeLoaderConfig(module.config); + } + isConfigReady = true; +} + +export function getLoaderConfig(): LoaderConfig { + return netLoaderConfig; +} + +export function mergeLoaderConfig(source: Partial): void { + normalizeConfig(netLoaderConfig); + normalizeConfig(source); + mergeConfigs(netLoaderConfig, source); +} + +function mergeConfigs(target: LoaderConfigInternal, source: Partial): LoaderConfigInternal { + // no need to merge the same object + if (target === source || source === undefined || source === null) return target; + + mergeResources(target.resources!, source.resources!); + source.appendElementOnExit = source.appendElementOnExit !== undefined ? source.appendElementOnExit : target.appendElementOnExit; + source.logExitCode = source.logExitCode !== undefined ? source.logExitCode : target.logExitCode; + source.exitOnUnhandledError = source.exitOnUnhandledError !== undefined ? source.exitOnUnhandledError : target.exitOnUnhandledError; + source.loadAllSatelliteResources = source.loadAllSatelliteResources !== undefined ? source.loadAllSatelliteResources : target.loadAllSatelliteResources; + source.mainAssemblyName = source.mainAssemblyName !== undefined ? source.mainAssemblyName : target.mainAssemblyName; + source.virtualWorkingDirectory = source.virtualWorkingDirectory !== undefined ? source.virtualWorkingDirectory : target.virtualWorkingDirectory; + source.debugLevel = source.debugLevel !== undefined ? source.debugLevel : target.debugLevel; + source.diagnosticTracing = source.diagnosticTracing !== undefined ? source.diagnosticTracing : target.diagnosticTracing; + source.environmentVariables = { ...target.environmentVariables, ...source.environmentVariables }; + source.runtimeOptions = [...target.runtimeOptions!, ...source.runtimeOptions!]; + Object.assign(target, source); + if (target.resources!.coreAssembly!.length) { + isConfigReady = true; + } + return target; +} + +function mergeResources(target: Assets, source: Assets): Assets { + // no need to merge the same object + if (target === source || source === undefined || source === null) return target; + + source.coreAssembly = [...target.coreAssembly!, ...source.coreAssembly!]; + source.assembly = [...target.assembly!, ...source.assembly!]; + source.lazyAssembly = [...target.lazyAssembly!, ...source.lazyAssembly!]; + source.corePdb = [...target.corePdb!, ...source.corePdb!]; + source.pdb = [...target.pdb!, ...source.pdb!]; + source.jsModuleWorker = [...target.jsModuleWorker!, ...source.jsModuleWorker!]; + source.jsModuleNative = [...target.jsModuleNative!, ...source.jsModuleNative!]; + source.jsModuleDiagnostics = [...target.jsModuleDiagnostics!, ...source.jsModuleDiagnostics!]; + source.jsModuleRuntime = [...target.jsModuleRuntime!, ...source.jsModuleRuntime!]; + source.wasmSymbols = [...target.wasmSymbols!, ...source.wasmSymbols!]; + source.wasmNative = [...target.wasmNative!, ...source.wasmNative!]; + source.icu = [...target.icu!, ...source.icu!]; + source.vfs = [...target.vfs!, ...source.vfs!]; + source.modulesAfterConfigLoaded = [...target.modulesAfterConfigLoaded!, ...source.modulesAfterConfigLoaded!]; + source.modulesAfterRuntimeReady = [...target.modulesAfterRuntimeReady!, ...source.modulesAfterRuntimeReady!]; + source.extensions = { ...target.extensions!, ...source.extensions! }; + for (const key in source.satelliteResources) { + source.satelliteResources![key] = [...target.satelliteResources![key] || [], ...source.satelliteResources![key] || []]; + } + return Object.assign(target, source); +} + + +function normalizeConfig(target: LoaderConfigInternal) { + if (!target.resources) target.resources = {} as any; + normalizeResources(target.resources!); + if (!target.environmentVariables) target.environmentVariables = {}; + if (!target.runtimeOptions) target.runtimeOptions = []; + if (target.appendElementOnExit === undefined) target.appendElementOnExit = false; + if (target.logExitCode === undefined) target.logExitCode = false; + if (target.exitOnUnhandledError === undefined) target.exitOnUnhandledError = false; + if (target.loadAllSatelliteResources === undefined) target.loadAllSatelliteResources = false; + if (target.debugLevel === undefined) target.debugLevel = 0; + if (target.diagnosticTracing === undefined) target.diagnosticTracing = false; + if (target.virtualWorkingDirectory === undefined) target.virtualWorkingDirectory = "/"; +} + +function normalizeResources(target: Assets) { + if (!target.coreAssembly) target.coreAssembly = []; + if (!target.assembly) target.assembly = []; + if (!target.lazyAssembly) target.lazyAssembly = []; + if (!target.corePdb) target.corePdb = []; + if (!target.pdb) target.pdb = []; + if (!target.jsModuleWorker) target.jsModuleWorker = []; + if (!target.jsModuleNative) target.jsModuleNative = []; + if (!target.jsModuleDiagnostics) target.jsModuleDiagnostics = []; + if (!target.jsModuleRuntime) target.jsModuleRuntime = []; + if (!target.wasmSymbols) target.wasmSymbols = []; + if (!target.wasmNative) target.wasmNative = []; + if (!target.icu) target.icu = []; + if (!target.modulesAfterConfigLoaded) target.modulesAfterConfigLoaded = []; + if (!target.modulesAfterRuntimeReady) target.modulesAfterRuntimeReady = []; + if (!target.satelliteResources) target.satelliteResources = {}; + if (!target.extensions) target.extensions = {}; + if (!target.vfs) target.vfs = []; +} diff --git a/src/native/corehost/browserhost/loader/cross-module.ts b/src/native/corehost/browserhost/loader/cross-module.ts new file mode 100644 index 00000000000000..7e45f1da5f993b --- /dev/null +++ b/src/native/corehost/browserhost/loader/cross-module.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../cross-module"; diff --git a/src/native/corehost/browserhost/loader/dotnet.d.ts b/src/native/corehost/browserhost/loader/dotnet.d.ts index 3e878a4724ee72..c333eafe29af10 100644 --- a/src/native/corehost/browserhost/loader/dotnet.d.ts +++ b/src/native/corehost/browserhost/loader/dotnet.d.ts @@ -779,4 +779,5 @@ declare global { } declare const createDotnetRuntime: CreateDotnetRuntimeType; -export { type AssetBehaviors, type AssetEntry, type CreateDotnetRuntimeType, type DotnetHostBuilder, type DotnetModuleConfig, type EmscriptenModule, GlobalizationMode, type IMemoryView, type LoaderConfig, type ModuleAPI, type RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; +export { GlobalizationMode, createDotnetRuntime as default, dotnet, exit }; +export type { AssetBehaviors, AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, IMemoryView, LoaderConfig, ModuleAPI, RuntimeAPI }; diff --git a/src/native/corehost/browserhost/loader/dotnet.js b/src/native/corehost/browserhost/loader/dotnet.js deleted file mode 100644 index 9b96e58e74ef0c..00000000000000 --- a/src/native/corehost/browserhost/loader/dotnet.js +++ /dev/null @@ -1,948 +0,0 @@ -//! Licensed to the .NET Foundation under one or more agreements. -//! The .NET Foundation licenses this file to you under the MIT license. -//! This is generated file, see src/native/libs/Browser/rollup.config.defines.js - - -/*! bundlerFriendlyImports */ - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -let Module; -let runtimeApi; -let Logger = {}; -let Assert = {}; -let JSEngine = {}; -let loaderExports = {}; -let runtimeExports = {}; -let hostExports = {}; -let interopExports = {}; -let nativeBrowserExports = {}; -let dotnetInternals; -function getInternals() { - return dotnetInternals; -} -function setInternals(internal) { - dotnetInternals = internal; - runtimeApi = dotnetInternals.runtimeApi; - Module = dotnetInternals.runtimeApi.Module; -} -function updateAllInternals() { - if (dotnetInternals.updates === undefined) { - dotnetInternals.updates = []; - } - for (const updateImpl of dotnetInternals.updates) { - updateImpl(); - } -} -function updateMyInternals() { - if (Object.keys(loaderExports).length === 0 && dotnetInternals.loaderExportsTable) { - loaderExports = {}; - Logger = {}; - Assert = {}; - JSEngine = {}; - expandLE(dotnetInternals.loaderExportsTable, Logger, Assert, JSEngine, loaderExports); - } - if (Object.keys(runtimeExports).length === 0 && dotnetInternals.runtimeExportsTable) { - runtimeExports = {}; - expandRE(dotnetInternals.runtimeExportsTable, runtimeExports); - } - if (Object.keys(hostExports).length === 0 && dotnetInternals.hostNativeExportsTable) { - hostExports = {}; - expandHE(dotnetInternals.hostNativeExportsTable, hostExports); - } - if (Object.keys(interopExports).length === 0 && dotnetInternals.interopJavaScriptNativeExportsTable) { - interopExports = {}; - expandJSNE(dotnetInternals.interopJavaScriptNativeExportsTable, interopExports); - } - if (Object.keys(nativeBrowserExports).length === 0 && dotnetInternals.nativeBrowserExportsTable) { - nativeBrowserExports = {}; - expandNBE(dotnetInternals.nativeBrowserExportsTable, nativeBrowserExports); - } -} -/** - * Functions below allow our JS modules to exchange internal interfaces by passing tables of functions in known order instead of using string symbols. - * IMPORTANT: If you need to add more functions, make sure that you add them at the end of the table, so that the order of existing functions does not change. - */ -function tabulateLE(logger, assert, loaderExports) { - return [ - logger.info, - logger.warn, - logger.error, - assert.check, - loaderExports.ENVIRONMENT_IS_NODE, - loaderExports.ENVIRONMENT_IS_SHELL, - loaderExports.ENVIRONMENT_IS_WEB, - loaderExports.ENVIRONMENT_IS_WORKER, - loaderExports.ENVIRONMENT_IS_SIDECAR, - loaderExports.browserHostResolveMain, - loaderExports.browserHostRejectMain, - loaderExports.getRunMainPromise, - ]; -} -function expandLE(table, logger, assert, jsEngine, loaderExports) { - const loggerLocal = { - info: table[0], - warn: table[1], - error: table[2], - }; - const assertLocal = { - check: table[3], - }; - const loaderExportsLocal = { - ENVIRONMENT_IS_NODE: table[4], - ENVIRONMENT_IS_SHELL: table[5], - ENVIRONMENT_IS_WEB: table[6], - ENVIRONMENT_IS_WORKER: table[7], - ENVIRONMENT_IS_SIDECAR: table[8], - browserHostResolveMain: table[9], - browserHostRejectMain: table[10], - getRunMainPromise: table[11], - }; - const jsEngineLocal = { - IS_NODE: loaderExportsLocal.ENVIRONMENT_IS_NODE(), - IS_SHELL: loaderExportsLocal.ENVIRONMENT_IS_SHELL(), - IS_WEB: loaderExportsLocal.ENVIRONMENT_IS_WEB(), - IS_WORKER: loaderExportsLocal.ENVIRONMENT_IS_WORKER(), - IS_SIDECAR: loaderExportsLocal.ENVIRONMENT_IS_SIDECAR(), - }; - Object.assign(loaderExports, loaderExportsLocal); - Object.assign(logger, loggerLocal); - Object.assign(assert, assertLocal); - Object.assign(jsEngine, jsEngineLocal); -} -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function tabulateRE(map) { - return []; -} -function expandRE(table, runtime) { - Object.assign(runtime, {}); -} -function tabulateHE(map) { - return [ - map.registerDllBytes, - map.isSharedArrayBuffer, - ]; -} -function expandHE(table, native) { - const nativeLocal = { - registerDllBytes: table[0], - isSharedArrayBuffer: table[1], - }; - Object.assign(native, nativeLocal); -} -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function tabulateJSNE(map) { - return []; -} -function expandJSNE(table, interop) { - const interopLocal = {}; - Object.assign(interop, interopLocal); -} -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function tabulateNBE(map) { - return []; -} -function expandNBE(table, interop) { - const interopLocal = {}; - Object.assign(interop, interopLocal); -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -const config = {}; -let isConfigDownloaded = false; -async function downloadConfig(url, loadBootResource) { - if (loadBootResource) - throw new Error("TODO: loadBootResource is not implemented yet"); - if (isConfigDownloaded) - return; // only download config once - if (!url) { - url = "./dotnet.boot.js"; - } - // url ends with .json - if (url.endsWith(".json")) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to download config from ${url}: ${response.status} ${response.statusText}`); - } - const newConfig = await response.json(); - mergeConfig(newConfig); - } - else if (url.endsWith(".js") || url.endsWith(".mjs")) { - const module = await import(/* webpackIgnore: true */ url); - mergeConfig(module.config); - } - isConfigDownloaded = true; -} -function getConfig() { - return config; -} -function mergeConfig(source) { - normalizeConfig(config); - normalizeConfig(source); - mergeConfigs(config, source); -} -function mergeConfigs(target, source) { - // no need to merge the same object - if (target === source || source === undefined || source === null) - return target; - mergeResources(target.resources, source.resources); - source.appendElementOnExit = source.appendElementOnExit !== undefined ? source.appendElementOnExit : target.appendElementOnExit; - source.logExitCode = source.logExitCode !== undefined ? source.logExitCode : target.logExitCode; - source.exitOnUnhandledError = source.exitOnUnhandledError !== undefined ? source.exitOnUnhandledError : target.exitOnUnhandledError; - source.loadAllSatelliteResources = source.loadAllSatelliteResources !== undefined ? source.loadAllSatelliteResources : target.loadAllSatelliteResources; - source.mainAssemblyName = source.mainAssemblyName !== undefined ? source.mainAssemblyName : target.mainAssemblyName; - source.virtualWorkingDirectory = source.virtualWorkingDirectory !== undefined ? source.virtualWorkingDirectory : target.virtualWorkingDirectory; - source.debugLevel = source.debugLevel !== undefined ? source.debugLevel : target.debugLevel; - source.diagnosticTracing = source.diagnosticTracing !== undefined ? source.diagnosticTracing : target.diagnosticTracing; - source.environmentVariables = { ...target.environmentVariables, ...source.environmentVariables }; - source.runtimeOptions = [...target.runtimeOptions, ...source.runtimeOptions]; - Object.assign(target, source); - if (target.resources.coreAssembly.length) { - isConfigDownloaded = true; - } - return target; -} -function mergeResources(target, source) { - // no need to merge the same object - if (target === source || source === undefined || source === null) - return target; - source.coreAssembly = [...target.coreAssembly, ...source.coreAssembly]; - source.assembly = [...target.assembly, ...source.assembly]; - source.lazyAssembly = [...target.lazyAssembly, ...source.lazyAssembly]; - source.corePdb = [...target.corePdb, ...source.corePdb]; - source.pdb = [...target.pdb, ...source.pdb]; - source.jsModuleWorker = [...target.jsModuleWorker, ...source.jsModuleWorker]; - source.jsModuleNative = [...target.jsModuleNative, ...source.jsModuleNative]; - source.jsModuleDiagnostics = [...target.jsModuleDiagnostics, ...source.jsModuleDiagnostics]; - source.jsModuleRuntime = [...target.jsModuleRuntime, ...source.jsModuleRuntime]; - source.wasmSymbols = [...target.wasmSymbols, ...source.wasmSymbols]; - source.wasmNative = [...target.wasmNative, ...source.wasmNative]; - source.icu = [...target.icu, ...source.icu]; - source.vfs = [...target.vfs, ...source.vfs]; - source.modulesAfterConfigLoaded = [...target.modulesAfterConfigLoaded, ...source.modulesAfterConfigLoaded]; - source.modulesAfterRuntimeReady = [...target.modulesAfterRuntimeReady, ...source.modulesAfterRuntimeReady]; - source.extensions = { ...target.extensions, ...source.extensions }; - for (const key in source.satelliteResources) { - source.satelliteResources[key] = [...target.satelliteResources[key] || [], ...source.satelliteResources[key] || []]; - } - return Object.assign(target, source); -} -function normalizeConfig(target) { - if (!target.resources) - target.resources = {}; - normalizeResources(target.resources); - if (!target.environmentVariables) - target.environmentVariables = {}; - if (!target.runtimeOptions) - target.runtimeOptions = []; - if (target.appendElementOnExit === undefined) - target.appendElementOnExit = false; - if (target.logExitCode === undefined) - target.logExitCode = false; - if (target.exitOnUnhandledError === undefined) - target.exitOnUnhandledError = false; - if (target.loadAllSatelliteResources === undefined) - target.loadAllSatelliteResources = false; - if (target.debugLevel === undefined) - target.debugLevel = 0; - if (target.diagnosticTracing === undefined) - target.diagnosticTracing = false; - if (target.virtualWorkingDirectory === undefined) - target.virtualWorkingDirectory = "/"; - if (target.mainAssemblyName === undefined) - target.mainAssemblyName = "HelloWorld.dll"; -} -function normalizeResources(target) { - if (!target.coreAssembly) - target.coreAssembly = []; - if (!target.assembly) - target.assembly = []; - if (!target.lazyAssembly) - target.lazyAssembly = []; - if (!target.corePdb) - target.corePdb = []; - if (!target.pdb) - target.pdb = []; - if (!target.jsModuleWorker) - target.jsModuleWorker = []; - if (!target.jsModuleNative) - target.jsModuleNative = []; - if (!target.jsModuleDiagnostics) - target.jsModuleDiagnostics = []; - if (!target.jsModuleRuntime) - target.jsModuleRuntime = []; - if (!target.wasmSymbols) - target.wasmSymbols = []; - if (!target.wasmNative) - target.wasmNative = []; - if (!target.icu) - target.icu = []; - if (!target.modulesAfterConfigLoaded) - target.modulesAfterConfigLoaded = []; - if (!target.modulesAfterRuntimeReady) - target.modulesAfterRuntimeReady = []; - if (!target.satelliteResources) - target.satelliteResources = {}; - if (!target.extensions) - target.extensions = {}; - if (!target.vfs) - target.vfs = []; -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function exit(exit_code, reason) { - const reasonStr = reason ? (reason.stack ? reason.stack || reason.message : reason.toString()) : ""; - if (exit_code !== 0) { - Logger.error(`Exit with code ${exit_code} ${reason ? "and reason: " + reasonStr : ""}`); - } - if (JSEngine.IS_NODE) { - globalThis.process.exit(exit_code); - } -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -/// a unique symbol used to mark a promise as controllable -const promise_control_symbol = Symbol.for("wasm promise_control"); -/// Creates a new promise together with a controller that can be used to resolve or reject that promise. -/// Optionally takes callbacks to be called immediately after a promise is resolved or rejected. -function createPromiseController(afterResolve, afterReject) { - let promiseControl = null; - const promise = new Promise((resolve, reject) => { - promiseControl = { - isDone: false, - promise: null, - resolve: (data) => { - if (!promiseControl.isDone) { - promiseControl.isDone = true; - resolve(data); - if (afterResolve) { - afterResolve(); - } - } - }, - reject: (reason) => { - if (!promiseControl.isDone) { - promiseControl.isDone = true; - reject(reason); - if (afterReject) { - afterReject(); - } - } - }, - propagateFrom: (other) => { - other.then(promiseControl.resolve).catch(promiseControl.reject); - } - }; - }); - promiseControl.promise = promise; - const controllablePromise = promise; - controllablePromise[promise_control_symbol] = promiseControl; - return promiseControl; -} -function getPromiseController(promise) { - return promise[promise_control_symbol]; -} -function isControllablePromise(promise) { - return promise[promise_control_symbol] !== undefined; -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -let CoreCLRInitialized = false; -const runMainPromiseController = createPromiseController(); -function browserHostInitializeCoreCLR() { - if (CoreCLRInitialized) { - return; - } - // int browserHostInitializeCoreCLR(void) - const res = Module.ccall("browserHostInitializeCoreCLR", "number"); - if (res != 0) { - const reason = new Error("Failed to initialize CoreCLR"); - runMainPromiseController.reject(reason); - exit(res, reason); - } - CoreCLRInitialized = true; -} -function browserHostResolveMain(exitCode) { - runMainPromiseController.resolve(exitCode); -} -function browserHostRejectMain(reason) { - runMainPromiseController.reject(reason); -} -function getRunMainPromise() { - return runMainPromiseController.promise; -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -const scriptUrlQuery = /*! webpackIgnore: true */ import.meta.url; -const queryIndex = scriptUrlQuery.indexOf("?"); -const modulesUniqueQuery = queryIndex > 0 ? scriptUrlQuery.substring(queryIndex) : ""; -const scriptUrl = normalizeFileUrl(scriptUrlQuery); -const scriptDirectory = normalizeDirectoryUrl(scriptUrl); -const nativeModulePromiseController = createPromiseController(() => { - updateAllInternals(); -}); -// WASM-TODO: retry logic -// WASM-TODO: throttling logic -// WASM-TODO: load icu data -// WASM-TODO: invokeLibraryInitializers -// WASM-TODO: webCIL -async function createRuntime(downloadOnly, loadBootResource) { - const config = getConfig(); - if (!config.resources || !config.resources.coreAssembly || !config.resources.coreAssembly.length) - throw new Error("Invalid config, resources is not set"); - const coreAssembliesPromise = Promise.all(config.resources.coreAssembly.map(fetchDll)); - const assembliesPromise = Promise.all(config.resources.assembly.map(fetchDll)); - const runtimeModulePromise = loadModule(config.resources.jsModuleRuntime[0], loadBootResource); - const nativeModulePromise = loadModule(config.resources.jsModuleNative[0], loadBootResource); - const nativeModule = await nativeModulePromise; - const modulePromise = nativeModule.initialize(getInternals()); - nativeModulePromiseController.propagateFrom(modulePromise); - const runtimeModule = await runtimeModulePromise; - const runtimeModuleReady = runtimeModule.initialize(getInternals()); - await nativeModulePromiseController.promise; - await coreAssembliesPromise; - if (!downloadOnly) { - browserHostInitializeCoreCLR(); - } - await assembliesPromise; - await runtimeModuleReady; -} -async function loadModule(asset, loadBootResource) { - if (loadBootResource) - throw new Error("TODO: loadBootResource is not implemented yet"); - if (asset.name && !asset.resolvedUrl) { - asset.resolvedUrl = locateFile(asset.name); - } - if (!asset.resolvedUrl) - throw new Error("Invalid config, resources is not set"); - return await import(/* webpackIgnore: true */ asset.resolvedUrl); -} -function fetchWasm(asset) { - if (asset.name && !asset.resolvedUrl) { - asset.resolvedUrl = locateFile(asset.name); - } - return fetchBytes(asset); -} -async function fetchDll(asset) { - if (asset.name && !asset.resolvedUrl) { - asset.resolvedUrl = locateFile(asset.name); - } - const bytes = await fetchBytes(asset); - await nativeModulePromiseController.promise; - hostExports.registerDllBytes(bytes, asset); -} -async function fetchBytes(asset) { - Assert.check(asset && asset.resolvedUrl, "Bad asset.resolvedUrl"); - if (JSEngine.IS_NODE) { - const { promises: fs } = await import('fs'); - const { fileURLToPath } = await import(/*! webpackIgnore: true */ 'url'); - const isFileUrl = asset.resolvedUrl.startsWith("file://"); - if (isFileUrl) { - asset.resolvedUrl = fileURLToPath(asset.resolvedUrl); - } - const buffer = await fs.readFile(asset.resolvedUrl); - return new Uint8Array(buffer); - } - else { - const response = await fetch(asset.resolvedUrl); - if (!response.ok) { - throw new Error(`Failed to load ${asset.resolvedUrl} with ${response.status} ${response.statusText}`); - } - const buffer = await response.arrayBuffer(); - return new Uint8Array(buffer); - } -} -function locateFile(path) { - if ("URL" in globalThis) { - return new URL(path, scriptDirectory).toString(); - } - if (isPathAbsolute(path)) - return path; - return scriptDirectory + path + modulesUniqueQuery; -} -function normalizeFileUrl(filename) { - // unix vs windows - // remove query string - return filename.replace(/\\/g, "/").replace(/[?#].*/, ""); -} -function normalizeDirectoryUrl(dir) { - return dir.slice(0, dir.lastIndexOf("/")) + "/"; -} -const protocolRx = /^[a-zA-Z][a-zA-Z\d+\-.]*?:\/\//; -const windowsAbsoluteRx = /[a-zA-Z]:[\\/]/; -function isPathAbsolute(path) { - if (JSEngine.IS_NODE || JSEngine.IS_SHELL) { - // unix /x.json - // windows \x.json - // windows C:\x.json - // windows C:/x.json - return path.startsWith("/") || path.startsWith("\\") || path.indexOf("///") !== -1 || windowsAbsoluteRx.test(path); - } - // anything with protocol is always absolute - // windows file:///C:/x.json - // windows http://C:/x.json - return protocolRx.test(path); -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -let configUrl = undefined; -let applicationArguments = []; -let loadBootResourceCallback = undefined; -/* eslint-disable @typescript-eslint/no-unused-vars */ -class HostBuilder { - withConfig(config) { - mergeConfig(config); - return this; - } - withConfigSrc(configSrc) { - configUrl = configSrc; - return this; - } - withApplicationArguments(...args) { - applicationArguments = args; - return this; - } - withEnvironmentVariable(name, value) { - mergeConfig({ - environmentVariables: { - [name]: value - } - }); - return this; - } - withEnvironmentVariables(variables) { - mergeConfig({ - environmentVariables: variables - }); - return this; - } - withVirtualWorkingDirectory(vfsPath) { - mergeConfig({ - virtualWorkingDirectory: vfsPath - }); - return this; - } - withDiagnosticTracing(enabled) { - mergeConfig({ - diagnosticTracing: enabled - }); - return this; - } - withDebugging(level) { - mergeConfig({ - debugLevel: level - }); - return this; - } - withMainAssembly(mainAssemblyName) { - mergeConfig({ - mainAssemblyName: mainAssemblyName - }); - return this; - } - withApplicationArgumentsFromQuery() { - if (!globalThis.window) { - throw new Error("Missing window to the query parameters from"); - } - if (typeof globalThis.URLSearchParams == "undefined") { - throw new Error("URLSearchParams is supported"); - } - const params = new URLSearchParams(globalThis.window.location.search); - const values = params.getAll("arg"); - return this.withApplicationArguments(...values); - } - withApplicationEnvironment(applicationEnvironment) { - mergeConfig({ - applicationEnvironment: applicationEnvironment - }); - return this; - } - withApplicationCulture(applicationCulture) { - mergeConfig({ - applicationCulture: applicationCulture - }); - return this; - } - withResourceLoader(loadBootResource) { - loadBootResourceCallback = loadBootResource; - return this; - } - // internal - withModuleConfig(moduleConfig) { - Object.assign(Module, moduleConfig); - return this; - } - // internal - withConsoleForwarding() { - // TODO - return this; - } - // internal - withExitOnUnhandledError() { - // TODO - return this; - } - // internal - withAsyncFlushOnExit() { - // TODO - return this; - } - // internal - withExitCodeLogging() { - // TODO - return this; - } - // internal - withElementOnExit() { - // TODO - return this; - } - // internal - withInteropCleanupOnExit() { - // TODO - return this; - } - async download() { - try { - await downloadConfig(configUrl, loadBootResourceCallback); - return createRuntime(true, loadBootResourceCallback); - } - catch (err) { - exit(1, err); - throw err; - } - } - async create() { - try { - await downloadConfig(configUrl, loadBootResourceCallback); - await createRuntime(false, loadBootResourceCallback); - this.runtimeApi = runtimeApi; - return this.runtimeApi; - } - catch (err) { - exit(1, err); - throw err; - } - } - async run() { - try { - if (!this.runtimeApi) { - await this.create(); - } - const config = getConfig(); - return this.runtimeApi.runMainAndExit(config.mainAssemblyName, applicationArguments); - } - catch (err) { - exit(1, err); - throw err; - } - } -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -function initPolyfills() { - if (typeof globalThis.WeakRef !== "function") { - class WeakRefPolyfill { - constructor(value) { - this._value = value; - } - deref() { - return this._value; - } - } - globalThis.WeakRef = WeakRefPolyfill; - } - if (typeof globalThis.fetch !== "function") { - globalThis.fetch = fetchLike; - } -} -async function initPolyfillsAsync() { - if (JSEngine.IS_NODE) { - if (!globalThis.crypto) { - globalThis.crypto = {}; - } - if (!globalThis.crypto.getRandomValues) { - let nodeCrypto = undefined; - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: - nodeCrypto = await import(/*! webpackIgnore: true */ 'node:crypto'); - } - catch (err) { - // Noop, error throwing polyfill provided bellow - } - if (!nodeCrypto) { - globalThis.crypto.getRandomValues = () => { - throw new Error("Using node without crypto support. To enable current operation, either provide polyfill for 'globalThis.crypto.getRandomValues' or enable 'node:crypto' module."); - }; - } - else if (nodeCrypto.webcrypto) { - globalThis.crypto = nodeCrypto.webcrypto; - } - else if (nodeCrypto.randomBytes) { - const getRandomValues = (buffer) => { - if (buffer) { - buffer.set(nodeCrypto.randomBytes(buffer.length)); - } - }; - globalThis.crypto.getRandomValues = getRandomValues; - } - } - if (!globalThis.performance) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: - globalThis.performance = (await import(/*! webpackIgnore: true */ 'perf_hooks')).performance; - } - } -} -async function fetchLike(url, init) { - let node_fs = undefined; - let node_url = undefined; - try { - // this need to be detected only after we import node modules in onConfigLoaded - const hasFetch = typeof (globalThis.fetch) === "function"; - if (JSEngine.IS_NODE) { - const isFileUrl = url.startsWith("file://"); - if (!isFileUrl && hasFetch) { - return globalThis.fetch(url, init || { credentials: "same-origin" }); - } - if (!node_fs) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: - node_url = await import(/*! webpackIgnore: true */ 'url'); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: - node_fs = await import(/*! webpackIgnore: true */ 'fs'); - } - if (isFileUrl) { - url = node_url.fileURLToPath(url); - } - const arrayBuffer = await node_fs.promises.readFile(url); - return { - ok: true, - headers: { - length: 0, - get: () => null - }, - url, - arrayBuffer: () => arrayBuffer, - json: () => JSON.parse(arrayBuffer), - text: () => { - throw new Error("NotImplementedException"); - } - }; - } - else if (hasFetch) { - return globalThis.fetch(url, init || { credentials: "same-origin" }); - } - else if (typeof (read) === "function") { - // note that it can't open files with unicode names, like Strae.xml - // https://bugs.chromium.org/p/v8/issues/detail?id=12541 - return { - ok: true, - url, - headers: { - length: 0, - get: () => null - }, - arrayBuffer: () => { - return new Uint8Array(read(url, "binary")); - }, - json: () => { - return JSON.parse(read(url, "utf8")); - }, - text: () => read(url, "utf8") - }; - } - } - catch (e) { - return { - ok: false, - url, - status: 500, - headers: { - length: 0, - get: () => null - }, - statusText: "ERR28: " + e, - arrayBuffer: () => { - throw e; - }, - json: () => { - throw e; - }, - text: () => { - throw e; - } - }; - } - throw new Error("No fetch implementation available"); -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -let runtimeList; -class RuntimeList { - constructor() { - this.list = {}; - } - registerRuntime(api) { - if (api.runtimeId === undefined) { - api.runtimeId = Object.keys(this.list).length; - } - this.list[api.runtimeId] = new globalThis.WeakRef(api); - return api.runtimeId; - } - getRuntime(runtimeId) { - const wr = this.list[runtimeId]; - return wr ? wr.deref() : undefined; - } -} -function registerRuntime(api) { - const globalThisAny = globalThis; - // this code makes it possible to find dotnet runtime on a page via global namespace, even when there are multiple runtimes at the same time - if (!globalThisAny.getDotnetRuntime) { - globalThisAny.getDotnetRuntime = (runtimeId) => globalThisAny.getDotnetRuntime.__list.getRuntime(runtimeId); - globalThisAny.getDotnetRuntime.__list = runtimeList = new RuntimeList(); - } - else { - runtimeList = globalThisAny.getDotnetRuntime.__list; - } - return runtimeList.registerRuntime(api); -} - -var ProductVersion = "10.0.0-dev"; - -var BuildConfiguration = "Debug"; - -var GitHash = "3f5c7d9c448b99bdeae770df46ffc3c923f7ccf4"; - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// eslint-disable-next-line @typescript-eslint/no-unused-vars -async function invokeLibraryInitializers(functionName, args) { - throw new Error("Not implemented"); -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// WASMTODO inline the code -function check(condition, messageFactory) { - if (!condition) { - const message = typeof messageFactory === "string" ? messageFactory : messageFactory(); - throw new Error(`Assert failed: ${message}`); - } -} -/* eslint-disable no-console */ -const prefix = "CLR_WASM: "; -function info(msg, ...data) { - console.info(prefix + msg, ...data); -} -function warn(msg, ...data) { - console.warn(prefix + msg, ...data); -} -function error(msg, ...data) { - if (data && data.length > 0 && data[0] && typeof data[0] === "object") { - // don't log silent errors - if (data[0].silent) { - return; - } - if (data[0].toString) { - console.error(prefix + msg, data[0].toString()); - return; - } - } - console.error(prefix + msg, ...data); -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -function initialize() { - const ENVIRONMENT_IS_NODE = () => typeof process == "object" && typeof process.versions == "object" && typeof process.versions.node == "string"; - const ENVIRONMENT_IS_WEB_WORKER = () => typeof importScripts == "function"; - const ENVIRONMENT_IS_SIDECAR = () => ENVIRONMENT_IS_WEB_WORKER() && typeof dotnetSidecar !== "undefined"; // sidecar is emscripten main running in a web worker - const ENVIRONMENT_IS_WORKER = () => ENVIRONMENT_IS_WEB_WORKER() && !ENVIRONMENT_IS_SIDECAR(); // we redefine what ENVIRONMENT_IS_WORKER, we replace it in emscripten internals, so that sidecar works - const ENVIRONMENT_IS_WEB = () => typeof window == "object" || (ENVIRONMENT_IS_WEB_WORKER() && !ENVIRONMENT_IS_NODE()); - const ENVIRONMENT_IS_SHELL = () => !ENVIRONMENT_IS_WEB() && !ENVIRONMENT_IS_NODE(); - const runtimeApi = { - INTERNAL: {}, - Module: {}, - runtimeId: -1, - runtimeBuildInfo: { - productVersion: ProductVersion, - gitHash: GitHash, - buildConfiguration: BuildConfiguration, - wasmEnableThreads: false, - wasmEnableSIMD: true, - wasmEnableExceptionHandling: true, - }, - }; - const updates = []; - setInternals({ - config: config, - runtimeApi: runtimeApi, - updates, - }); - const runtimeApiFunctions = { - getConfig, - exit, - invokeLibraryInitializers, - }; - const loaderFunctions = { - ENVIRONMENT_IS_NODE, - ENVIRONMENT_IS_SHELL, - ENVIRONMENT_IS_WEB, - ENVIRONMENT_IS_WORKER, - ENVIRONMENT_IS_SIDECAR, - getRunMainPromise, - browserHostRejectMain, - browserHostResolveMain, - }; - const jsEngine = { - IS_NODE: ENVIRONMENT_IS_NODE(), - IS_SHELL: ENVIRONMENT_IS_SHELL(), - IS_WEB: ENVIRONMENT_IS_WEB(), - IS_WORKER: ENVIRONMENT_IS_WORKER(), - IS_SIDECAR: ENVIRONMENT_IS_SIDECAR(), - }; - const logger = { - info, - warn, - error, - }; - const assert = { - check, - }; - Object.assign(runtimeApi, runtimeApiFunctions); - Object.assign(Logger, logger); - Object.assign(Assert, assert); - Object.assign(JSEngine, jsEngine); - Object.assign(loaderExports, loaderFunctions); - dotnetInternals.loaderExportsTable = [...tabulateLE(Logger, Assert, loaderExports)]; - updates.push(updateMyInternals); - updateAllInternals(); - return runtimeApi; -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -initPolyfills(); -registerRuntime(initialize()); -await initPolyfillsAsync(); -const dotnet = new HostBuilder(); - -export { dotnet, exit }; diff --git a/src/native/corehost/browserhost/loader/dotnet.ts b/src/native/corehost/browserhost/loader/dotnet.ts new file mode 100644 index 00000000000000..a6bf53a108ab87 --- /dev/null +++ b/src/native/corehost/browserhost/loader/dotnet.ts @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +/** + * This is root of **JavaScript module** that would become of `dotnet.js`. + * It implements host for the browser together with `src/native/corehost/browserhost`. + * It exposes the public JS runtime APIs that is implemented in `Dotnet.Runtime.ts`. + * It's good to keep this file small. + */ + +import type { DotnetHostBuilder } from "./types"; + +import { HostBuilder } from "./host-builder"; +import { initPolyfills, initPolyfillsAsync } from "./polyfills"; +import { registerRuntime } from "./runtime-list"; +import { exit } from "./exit"; +import { dotnetInitializeModule } from "."; + +initPolyfills(); +registerRuntime(dotnetInitializeModule()); +await initPolyfillsAsync(); + +export const dotnet: DotnetHostBuilder | undefined = new HostBuilder() as DotnetHostBuilder; +export { exit }; diff --git a/src/native/corehost/browserhost/loader/exit.ts b/src/native/corehost/browserhost/loader/exit.ts new file mode 100644 index 00000000000000..5200aa2d9ac995 --- /dev/null +++ b/src/native/corehost/browserhost/loader/exit.ts @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnetJSEngine, dotnetLogger } from "./cross-module"; + +// WASM-TODO: redirect to host.ts +export function exit(exit_code: number, reason: any): void { + const reasonStr = reason ? (reason.stack ? reason.stack || reason.message : reason.toString()) : ""; + if (exit_code !== 0) { + dotnetLogger.error(`Exit with code ${exit_code} ${reason ? "and reason: " + reasonStr : ""}`); + } + if (dotnetJSEngine.IS_NODE) { + (globalThis as any).process.exit(exit_code); + } +} diff --git a/src/native/corehost/browserhost/loader/host-builder.ts b/src/native/corehost/browserhost/loader/host-builder.ts new file mode 100644 index 00000000000000..57fbbc8f834d24 --- /dev/null +++ b/src/native/corehost/browserhost/loader/host-builder.ts @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DotnetHostBuilder, LoaderConfig, RuntimeAPI, LoadBootResourceCallback, DotnetModuleConfig } from "./types"; + +import { Module, dotnetApi } from "./cross-module"; +import { downloadConfig, getLoaderConfig, mergeLoaderConfig } from "./config"; +import { createRuntime } from "./bootstrap"; +import { exit } from "./exit"; + +let configUrl: string | undefined = undefined; +let applicationArguments: string[] | undefined = []; +let loadBootResourceCallback: LoadBootResourceCallback | undefined = undefined; + +/* eslint-disable @typescript-eslint/no-unused-vars */ +export class HostBuilder implements DotnetHostBuilder { + private dotnetApi: RuntimeAPI | undefined; + withConfig(config: LoaderConfig): DotnetHostBuilder { + mergeLoaderConfig(config); + return this; + } + withConfigSrc(configSrc: string): DotnetHostBuilder { + configUrl = configSrc; + return this; + } + withApplicationArguments(...args: string[]): DotnetHostBuilder { + applicationArguments = args; + return this; + } + withEnvironmentVariable(name: string, value: string): DotnetHostBuilder { + mergeLoaderConfig({ + environmentVariables: { + [name]: value + } + }); + return this; + } + withEnvironmentVariables(variables: { [i: string]: string; }): DotnetHostBuilder { + mergeLoaderConfig({ + environmentVariables: variables + }); + return this; + } + withVirtualWorkingDirectory(vfsPath: string): DotnetHostBuilder { + mergeLoaderConfig({ + virtualWorkingDirectory: vfsPath + }); + return this; + } + withDiagnosticTracing(enabled: boolean): DotnetHostBuilder { + mergeLoaderConfig({ + diagnosticTracing: enabled + }); + return this; + } + withDebugging(level: number): DotnetHostBuilder { + mergeLoaderConfig({ + debugLevel: level + }); + return this; + } + withMainAssembly(mainAssemblyName: string): DotnetHostBuilder { + mergeLoaderConfig({ + mainAssemblyName: mainAssemblyName + }); + return this; + } + withApplicationArgumentsFromQuery(): DotnetHostBuilder { + if (!globalThis.window) { + throw new Error("Missing window to the query parameters from"); + } + + if (typeof globalThis.URLSearchParams == "undefined") { + throw new Error("URLSearchParams is supported"); + } + + const params = new URLSearchParams(globalThis.window.location.search); + const values = params.getAll("arg"); + return this.withApplicationArguments(...values); + } + withApplicationEnvironment(applicationEnvironment?: string): DotnetHostBuilder { + mergeLoaderConfig({ + applicationEnvironment: applicationEnvironment + }); + return this; + } + withApplicationCulture(applicationCulture?: string): DotnetHostBuilder { + mergeLoaderConfig({ + applicationCulture: applicationCulture + }); + return this; + } + withResourceLoader(loadBootResource?: LoadBootResourceCallback): DotnetHostBuilder { + loadBootResourceCallback = loadBootResource; + return this; + } + + // internal + withModuleConfig(moduleConfig: DotnetModuleConfig): DotnetHostBuilder { + Object.assign(Module, moduleConfig); + return this; + } + + async download(): Promise { + try { + await downloadConfig(configUrl, loadBootResourceCallback); + return createRuntime(true, loadBootResourceCallback); + } catch (err) { + exit(1, err); + throw err; + } + } + + async create(): Promise { + try { + await downloadConfig(configUrl, loadBootResourceCallback); + await createRuntime(false, loadBootResourceCallback); + this.dotnetApi = dotnetApi; + return this.dotnetApi; + } catch (err) { + exit(1, err); + throw err; + } + } + + async run(): Promise { + try { + if (!this.dotnetApi) { + await this.create(); + } + const config = getLoaderConfig(); + return this.dotnetApi!.runMainAndExit(config.mainAssemblyName, applicationArguments); + } catch (err) { + exit(1, err); + throw err; + } + } +} diff --git a/src/native/corehost/browserhost/loader/index.ts b/src/native/corehost/browserhost/loader/index.ts new file mode 100644 index 00000000000000..150f2e03520e1d --- /dev/null +++ b/src/native/corehost/browserhost/loader/index.ts @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { LoggerType, AssertType, RuntimeAPI, LoaderExports, NativeBrowserExportsTable, LoaderExportsTable, RuntimeExportsTable, InternalExchange, BrowserHostExportsTable, InteropJavaScriptExportsTable } from "./types"; +import { InternalExchangeIndex } from "../types"; + +import ProductVersion from "consts:productVersion"; +import BuildConfiguration from "consts:configuration"; +import GitHash from "consts:gitHash"; + +import { netLoaderConfig, getLoaderConfig } from "./config"; +import { exit } from "./exit"; +import { invokeLibraryInitializers } from "./lib-initializers"; +import { check, error, info, warn } from "./logging"; + +import { dotnetAssert, dotnetInternals, dotnetJSEngine, dotnetLoaderExports, dotnetLogger, dotnetSetInternals, dotnetUpdateAllInternals, dotnetUpdateModuleInternals } from "./cross-module"; +import { rejectRunMainPromise, resolveRunMainPromise, getRunMainPromise } from "./run"; + +export function dotnetInitializeModule(): RuntimeAPI { + const ENVIRONMENT_IS_NODE = () => typeof process == "object" && typeof process.versions == "object" && typeof process.versions.node == "string"; + const ENVIRONMENT_IS_WEB_WORKER = () => typeof importScripts == "function"; + const ENVIRONMENT_IS_SIDECAR = () => ENVIRONMENT_IS_WEB_WORKER() && typeof dotnetSidecar !== "undefined"; // sidecar is emscripten main running in a web worker + const ENVIRONMENT_IS_WORKER = () => ENVIRONMENT_IS_WEB_WORKER() && !ENVIRONMENT_IS_SIDECAR(); // we redefine what ENVIRONMENT_IS_WORKER, we replace it in emscripten internals, so that sidecar works + const ENVIRONMENT_IS_WEB = () => typeof window == "object" || (ENVIRONMENT_IS_WEB_WORKER() && !ENVIRONMENT_IS_NODE()); + const ENVIRONMENT_IS_SHELL = () => !ENVIRONMENT_IS_WEB() && !ENVIRONMENT_IS_NODE(); + + const dotnetApi: Partial = { + INTERNAL: {}, + Module: {} as any, + runtimeId: -1, + runtimeBuildInfo: { + productVersion: ProductVersion, + gitHash: GitHash, + buildConfiguration: BuildConfiguration, + wasmEnableThreads: false, + wasmEnableSIMD: true, + wasmEnableExceptionHandling: true, + }, + }; + + const internals:InternalExchange = [ + dotnetApi as RuntimeAPI, //0 + [dotnetUpdateModuleInternals], //1 + netLoaderConfig, //2 + null as any as RuntimeExportsTable, //3 + null as any as LoaderExportsTable, //4 + null as any as BrowserHostExportsTable, //5 + null as any as InteropJavaScriptExportsTable, //6 + null as any as NativeBrowserExportsTable, //7 + ]; + dotnetSetInternals(internals); + const runtimeApiFunctions: Partial = { + getConfig: getLoaderConfig, + exit, + invokeLibraryInitializers, + }; + const loaderFunctions: LoaderExports = { + ENVIRONMENT_IS_NODE, + ENVIRONMENT_IS_SHELL, + ENVIRONMENT_IS_WEB, + ENVIRONMENT_IS_WORKER, + ENVIRONMENT_IS_SIDECAR, + getRunMainPromise, + rejectRunMainPromise, + resolveRunMainPromise, + }; + const jsEngine = { + IS_NODE: ENVIRONMENT_IS_NODE(), + IS_SHELL: ENVIRONMENT_IS_SHELL(), + IS_WEB: ENVIRONMENT_IS_WEB(), + IS_WORKER: ENVIRONMENT_IS_WORKER(), + IS_SIDECAR: ENVIRONMENT_IS_SIDECAR(), + }; + const logger: LoggerType = { + info, + warn, + error, + }; + const assert: AssertType = { + check, + }; + Object.assign(dotnetApi, runtimeApiFunctions); + Object.assign(dotnetLogger, logger); + Object.assign(dotnetAssert, assert); + Object.assign(dotnetJSEngine, jsEngine); + Object.assign(dotnetLoaderExports, loaderFunctions); + dotnetInternals[InternalExchangeIndex.LoaderExportsTable] = dotnetTabLE(dotnetLogger, dotnetAssert, dotnetLoaderExports); + dotnetUpdateAllInternals(); + return dotnetApi as RuntimeAPI; + + function dotnetTabLE(logger:LoggerType, assert:AssertType, dotnetLoaderExports:LoaderExports):LoaderExportsTable { + // keep in sync with dotnetUpdateModuleInternals() + return [ + logger.info, + logger.warn, + logger.error, + assert.check, + dotnetLoaderExports.ENVIRONMENT_IS_NODE, + dotnetLoaderExports.ENVIRONMENT_IS_SHELL, + dotnetLoaderExports.ENVIRONMENT_IS_WEB, + dotnetLoaderExports.ENVIRONMENT_IS_WORKER, + dotnetLoaderExports.ENVIRONMENT_IS_SIDECAR, + dotnetLoaderExports.resolveRunMainPromise, + dotnetLoaderExports.rejectRunMainPromise, + dotnetLoaderExports.getRunMainPromise, + ]; + } +} diff --git a/src/native/corehost/browserhost/loader/lib-initializers.ts b/src/native/corehost/browserhost/loader/lib-initializers.ts new file mode 100644 index 00000000000000..171d2855d8bfa0 --- /dev/null +++ b/src/native/corehost/browserhost/loader/lib-initializers.ts @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function invokeLibraryInitializers(functionName: string, args: any[]): Promise { + throw new Error("Not implemented"); +} diff --git a/src/native/corehost/browserhost/loader/logging.ts b/src/native/corehost/browserhost/loader/logging.ts new file mode 100644 index 00000000000000..a51b8afd4114f2 --- /dev/null +++ b/src/native/corehost/browserhost/loader/logging.ts @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// WASM-TODO: inline the code + +export function check(condition: unknown, messageFactory: string | (() => string)): asserts condition { + if (!condition) { + const message = typeof messageFactory === "string" ? messageFactory : messageFactory(); + throw new Error(`dotnetAssert failed: ${message}`); + } +} + +/* eslint-disable no-console */ + +const prefix = "CLR_WASM: "; + +export function info(msg: string, ...data: any) { + console.info(prefix + msg, ...data); +} + +export function warn(msg: string, ...data: any) { + console.warn(prefix + msg, ...data); +} + +export function error(msg: string, ...data: any) { + if (data && data.length > 0 && data[0] && typeof data[0] === "object") { + // don't log silent errors + if (data[0].silent) { + return; + } + if (data[0].toString) { + console.error(prefix + msg, data[0].toString()); + return; + } + } + console.error(prefix + msg, ...data); +} diff --git a/src/native/corehost/browserhost/loader/polyfills.ts b/src/native/corehost/browserhost/loader/polyfills.ts new file mode 100644 index 00000000000000..860c125e8ce10e --- /dev/null +++ b/src/native/corehost/browserhost/loader/polyfills.ts @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnetJSEngine } from "./cross-module"; + +export function initPolyfills(): void { + if (typeof globalThis.WeakRef !== "function") { + class WeakRefPolyfill { + private _value: T | undefined; + + constructor(value: T) { + this._value = value; + } + + deref(): T | undefined { + return this._value; + } + } + globalThis.WeakRef = WeakRefPolyfill as any; + } + if (typeof globalThis.fetch !== "function") { + globalThis.fetch = fetchLike as any; + } +} + +export async function initPolyfillsAsync(): Promise { + if (dotnetJSEngine.IS_NODE) { + if (!globalThis.crypto) { + globalThis.crypto = {}; + } + if (!globalThis.crypto.getRandomValues) { + let nodeCrypto: any = undefined; + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: + nodeCrypto = await import(/*! webpackIgnore: true */"node:crypto"); + } catch (err: any) { + // Noop, error throwing polyfill provided bellow + } + + if (!nodeCrypto) { + globalThis.crypto.getRandomValues = () => { + throw new Error("Using node without crypto support. To enable current operation, either provide polyfill for 'globalThis.crypto.getRandomValues' or enable 'node:crypto' module."); + }; + } else if (nodeCrypto.webcrypto) { + globalThis.crypto = nodeCrypto.webcrypto; + } else if (nodeCrypto.randomBytes) { + const getRandomValues = (buffer: Uint8Array) => { + if (buffer) { + buffer.set(nodeCrypto.randomBytes(buffer.length)); + } + }; + globalThis.crypto.getRandomValues = getRandomValues as any; + } + } + if (!globalThis.performance) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: + globalThis.performance = (await import(/*! webpackIgnore: true */"perf_hooks")).performance; + } + } + // WASM-TODO: performance polyfill for V8 +} + +export async function fetchLike(url: string, init?: RequestInit): Promise { + let node_fs: any | undefined = undefined; + let node_url: any | undefined = undefined; + try { + // this need to be detected only after we import node modules in onConfigLoaded + const hasFetch = typeof (globalThis.fetch) === "function"; + if (dotnetJSEngine.IS_NODE) { + const isFileUrl = url.startsWith("file://"); + if (!isFileUrl && hasFetch) { + return globalThis.fetch(url, init || { credentials: "same-origin" }); + } + if (!node_fs) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: + node_url = await import(/*! webpackIgnore: true */"url"); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: + node_fs = await import(/*! webpackIgnore: true */"fs"); + } + if (isFileUrl) { + url = node_url.fileURLToPath(url); + } + + const arrayBuffer = await node_fs.promises.readFile(url); + return { + ok: true, + headers: { + length: 0, + get: () => null + }, + url, + arrayBuffer: () => arrayBuffer, + json: () => JSON.parse(arrayBuffer), + text: () => { + throw new Error("NotImplementedException"); + } + }; + } else if (hasFetch) { + return globalThis.fetch(url, init || { credentials: "same-origin" }); + } else if (typeof (read) === "function") { + // note that it can't open files with unicode names, like Strae.xml + // https://bugs.chromium.org/p/v8/issues/detail?id=12541 + return { + ok: true, + url, + headers: { + length: 0, + get: () => null + }, + arrayBuffer: () => { + return new Uint8Array(read(url, "binary")); + }, + json: () => { + return JSON.parse(read(url, "utf8")); + }, + text: () => read(url, "utf8") + }; + } + } catch (e: any) { + return { + ok: false, + url, + status: 500, + headers: { + length: 0, + get: () => null + }, + statusText: "ERR28: " + e, + arrayBuffer: () => { + throw e; + }, + json: () => { + throw e; + }, + text: () => { + throw e; + } + }; + } + throw new Error("No fetch implementation available"); +} diff --git a/src/native/corehost/browserhost/loader/promise-controller.ts b/src/native/corehost/browserhost/loader/promise-controller.ts new file mode 100644 index 00000000000000..cce6bd67a6dd95 --- /dev/null +++ b/src/native/corehost/browserhost/loader/promise-controller.ts @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { ControllablePromise, PromiseController } from "./types"; + +/// a unique symbol used to mark a promise as controllable +export const promiseControlSymbol = Symbol.for("wasm promise control"); + +// WASM-TODO: PromiseCompletionSource + +/// Creates a new promise together with a controller that can be used to resolve or reject that promise. +/// Optionally takes callbacks to be called immediately after a promise is resolved or rejected. +export function createPromiseController(afterResolve?: () => void, afterReject?: () => void): PromiseController { + let promiseControl: PromiseController = null as unknown as PromiseController; + const promise = new Promise((resolve, reject) => { + promiseControl = { + isDone: false, + promise: null as unknown as ControllablePromise, + resolve: (data: T | PromiseLike) => { + if (!promiseControl!.isDone) { + promiseControl!.isDone = true; + resolve(data); + if (afterResolve) { + afterResolve(); + } + } + }, + reject: (reason: any) => { + if (!promiseControl!.isDone) { + promiseControl!.isDone = true; + reject(reason); + if (afterReject) { + afterReject(); + } + } + }, + propagateFrom: (other: Promise) => { + other.then(promiseControl!.resolve).catch(promiseControl!.reject); + } + }; + }); + (promiseControl).promise = promise; + const controllablePromise = promise as ControllablePromise; + (controllablePromise as any)[promiseControlSymbol] = promiseControl; + return promiseControl; +} + +export function getPromiseController(promise: ControllablePromise): PromiseController; +export function getPromiseController(promise: Promise): PromiseController | undefined { + return (promise as any)[promiseControlSymbol]; +} + +export function isControllablePromise(promise: Promise): promise is ControllablePromise { + return (promise as any)[promiseControlSymbol] !== undefined; +} + diff --git a/src/native/corehost/browserhost/loader/run.ts b/src/native/corehost/browserhost/loader/run.ts new file mode 100644 index 00000000000000..2aff971d23c9da --- /dev/null +++ b/src/native/corehost/browserhost/loader/run.ts @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { Module } from "./cross-module"; +import { exit } from "./exit"; +import { createPromiseController } from "./promise-controller"; + +let CoreCLRInitialized = false; +const runMainPromiseController = createPromiseController(); + +export function BrowserHost_InitializeCoreCLR():void { + if (CoreCLRInitialized) { + return; + } + // int BrowserHost_InitializeCoreCLR(void) + // WASM-TODO: add more formal ccall wrapper like cwraps in Mono + const res = Module.ccall("BrowserHost_InitializeCoreCLR", "number") as number; + if (res != 0) { + const reason = new Error("Failed to initialize CoreCLR"); + runMainPromiseController.reject(reason); + exit(res, reason); + } + CoreCLRInitialized = true; +} + +export function resolveRunMainPromise(exitCode:number):void { + runMainPromiseController.resolve(exitCode); +} + +export function rejectRunMainPromise(reason:any):void { + runMainPromiseController.reject(reason); +} + +export function getRunMainPromise():Promise { + return runMainPromiseController.promise; +} diff --git a/src/native/corehost/browserhost/loader/runtime-list.ts b/src/native/corehost/browserhost/loader/runtime-list.ts new file mode 100644 index 00000000000000..32f65011ed63fa --- /dev/null +++ b/src/native/corehost/browserhost/loader/runtime-list.ts @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { RuntimeAPI } from "./types"; + +let runtimeList: RuntimeList; + +class RuntimeList { + private list: { [runtimeId: number]: WeakRef } = {}; + + public registerRuntime(api: RuntimeAPI): number { + if (api.runtimeId === undefined) { + api.runtimeId = Object.keys(this.list).length; + } + this.list[api.runtimeId] = new (globalThis as any).WeakRef(api); + return api.runtimeId; + } + + public getRuntime(runtimeId: number): RuntimeAPI | undefined { + const wr = this.list[runtimeId]; + return wr ? wr.deref() : undefined; + } +} + +export function registerRuntime(api: RuntimeAPI): number { + const globalThisAny = globalThis as any; + // this code makes it possible to find dotnet runtime on a page via global namespace, even when there are multiple runtimes at the same time + if (!globalThisAny.getDotnetRuntime) { + globalThisAny.getDotnetRuntime = (runtimeId: string) => globalThisAny.getDotnetRuntime.__list.getRuntime(runtimeId); + globalThisAny.getDotnetRuntime.__list = runtimeList = new RuntimeList(); + } else { + runtimeList = globalThisAny.getDotnetRuntime.__list; + } + + return runtimeList.registerRuntime(api); +} diff --git a/src/native/corehost/browserhost/loader/types.ts b/src/native/corehost/browserhost/loader/types.ts new file mode 100644 index 00000000000000..2786379af7369f --- /dev/null +++ b/src/native/corehost/browserhost/loader/types.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../types"; diff --git a/src/native/corehost/browserhost/sample/CMakeLists.txt b/src/native/corehost/browserhost/sample/CMakeLists.txt index ad0437cbdbdd0f..7f1d30ff7a099b 100644 --- a/src/native/corehost/browserhost/sample/CMakeLists.txt +++ b/src/native/corehost/browserhost/sample/CMakeLists.txt @@ -9,8 +9,10 @@ set(SAMPLE_ASSETS main.mjs dotnet.boot.js ${CLR_ARTIFACTS_OBJ_DIR}/coreclr/browser.wasm.${CMAKE_BUILD_TYPE}/corehost/dotnet.js + ${CLR_ARTIFACTS_OBJ_DIR}/coreclr/browser.wasm.${CMAKE_BUILD_TYPE}/corehost/dotnet.js.map ${CLR_ARTIFACTS_OBJ_DIR}/coreclr/browser.wasm.${CMAKE_BUILD_TYPE}/corehost/dotnet.d.ts ${CLR_ARTIFACTS_OBJ_DIR}/native/browser-${CMAKE_BUILD_TYPE}-wasm/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.js + ${CLR_ARTIFACTS_OBJ_DIR}/native/browser-${CMAKE_BUILD_TYPE}-wasm/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.js.map # Bring your own DLLs and update the dotnet.boot.js to match # HelloWorld.dll diff --git a/src/native/corehost/browserhost/types.ts b/src/native/corehost/browserhost/types.ts new file mode 100644 index 00000000000000..b586a5ddc79cad --- /dev/null +++ b/src/native/corehost/browserhost/types.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../../libs/Common/JavaScript/types"; diff --git a/src/native/libs/Common/JavaScript/cross-linked/index.ts b/src/native/libs/Common/JavaScript/cross-linked/index.ts new file mode 100644 index 00000000000000..f7be8c372ba8a8 --- /dev/null +++ b/src/native/libs/Common/JavaScript/cross-linked/index.ts @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { AssertType, EmscriptenModuleInternal, JSEngineType, LoggerType, LoaderExports, InternalExchange } from "../types"; + +// we want to use the cross-module symbols defined in closure of dotnet.native.js +// which are installed there by libSystem.Native.Browser.footer.js +// see also `reserved` in `rollup.config.defines.js` +declare global { + export const Module:EmscriptenModuleInternal; + export const dotnetAssert:AssertType; + export const dotnetLogger:LoggerType; + export const dotnetJSEngine:JSEngineType; + export const dotnetLoaderExports:LoaderExports; + export const dotnetSetInternals:(internals:Partial) => void; + export const dotnetUpdateAllInternals:() => void; + export const dotnetUpdateModuleInternals:() => void; +} diff --git a/src/native/libs/Common/JavaScript/cross-module/index.ts b/src/native/libs/Common/JavaScript/cross-module/index.ts new file mode 100644 index 00000000000000..affc6e791f0985 --- /dev/null +++ b/src/native/libs/Common/JavaScript/cross-module/index.ts @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +/** + * Common symbols shared between multiple JS modules. + * IMPORTANT: Anything you add into this folder could be duplicated into multiple JS bundles! + * Please keep it small and register it into emscripten as dependency. + */ + +import type { DotnetModuleInternal, InternalExchange, RuntimeExports, LoaderExports, RuntimeAPI, LoggerType, AssertType, JSEngineType, BrowserHostExports, InteropJavaScriptExports, LoaderExportsTable, RuntimeExportsTable, BrowserHostExportsTable, InteropJavaScriptExportsTable, NativeBrowserExports, NativeBrowserExportsTable } from "../types"; +import { InternalExchangeIndex } from "../types"; + +export let Module: DotnetModuleInternal; +export let dotnetApi: RuntimeAPI; +export let dotnetLogger: LoggerType = {} as any; +export let dotnetAssert: AssertType = {} as any; +export let dotnetJSEngine: JSEngineType = {}as any; +export let dotnetLoaderExports: LoaderExports = {} as any; +export let dotnetRuntimeExports: RuntimeExports = {} as any; +export let dotnetBrowserHostExports: BrowserHostExports = {} as any; +export let dotnetInteropJSExports: InteropJavaScriptExports = {} as any; +export let dotnetNativeBrowserExports: NativeBrowserExports = {} as any; +export let dotnetInternals: InternalExchange; + +export function dotnetGetInternals(): InternalExchange { + return dotnetInternals; +} + +export function dotnetSetInternals(internal: InternalExchange) { + dotnetInternals = internal; + dotnetApi = dotnetInternals[InternalExchangeIndex.RuntimeAPI]; + Module = dotnetApi.Module as any; + if (dotnetInternals[InternalExchangeIndex.InternalUpdatesCallbacks] === undefined) { + dotnetInternals[InternalExchangeIndex.InternalUpdatesCallbacks] = []; + } +} + +export function dotnetUpdateAllInternals() { + for (const updateImpl of dotnetInternals[InternalExchangeIndex.InternalUpdatesCallbacks]) { + updateImpl(); + } +} + +export function dotnetUpdateModuleInternals() { + /** + * Functions below allow our JS modules to exchange internal interfaces by passing tables of functions in known order instead of using string symbols. + * IMPORTANT: If you need to add more functions, make sure that you add them at the end of the table, so that the order of existing functions does not change. + */ + + if (Object.keys(dotnetLoaderExports).length === 0 && dotnetInternals[InternalExchangeIndex.LoaderExportsTable]) { + dotnetLoaderExports = {} as LoaderExports; + dotnetLogger = {} as LoggerType; + dotnetAssert = {} as AssertType; + dotnetJSEngine = {} as JSEngineType; + expandLoaderExports(dotnetInternals[InternalExchangeIndex.LoaderExportsTable], dotnetLogger, dotnetAssert, dotnetJSEngine, dotnetLoaderExports); + } + if (Object.keys(dotnetRuntimeExports).length === 0 && dotnetInternals[InternalExchangeIndex.RuntimeExportsTable]) { + dotnetRuntimeExports = {} as RuntimeExports; + expandRuntimeExports(dotnetInternals[InternalExchangeIndex.RuntimeExportsTable], dotnetRuntimeExports); + } + if (Object.keys(dotnetBrowserHostExports).length === 0 && dotnetInternals[InternalExchangeIndex.BrowserHostExportsTable]) { + dotnetBrowserHostExports = {} as BrowserHostExports; + expandBrowserHostExports(dotnetInternals[InternalExchangeIndex.BrowserHostExportsTable], dotnetBrowserHostExports); + } + if (Object.keys(dotnetInteropJSExports).length === 0 && dotnetInternals[InternalExchangeIndex.InteropJavaScriptExportsTable]) { + dotnetInteropJSExports = {} as InteropJavaScriptExports; + expandInteropJavaScriptExports(dotnetInternals[InternalExchangeIndex.InteropJavaScriptExportsTable], dotnetInteropJSExports); + } + if (Object.keys(dotnetNativeBrowserExports).length === 0 && dotnetInternals[InternalExchangeIndex.NativeBrowserExportsTable]) { + dotnetNativeBrowserExports = {} as NativeBrowserExports; + expandNativeBrowserExports(dotnetInternals[InternalExchangeIndex.NativeBrowserExportsTable], dotnetNativeBrowserExports); + } + + // keep in sync with tabulateRuntimeExports() + function expandRuntimeExports(table:RuntimeExportsTable, runtime:RuntimeExports):void { + Object.assign(runtime, { + utf16ToString: table[0], + stringToUTF16: table[1], + stringToUTF16Ptr: table[2], + }); + } + + // keep in sync with tabulateLoaderExports() + function expandLoaderExports(table:LoaderExportsTable, logger:LoggerType, assert:AssertType, jsEngine:JSEngineType, dotnetLoaderExports:LoaderExports):void { + const loggerLocal :LoggerType = { + info: table[0], + warn: table[1], + error: table[2], + }; + const assertLocal :AssertType = { + check: table[3], + }; + const loaderExportsLocal :LoaderExports = { + ENVIRONMENT_IS_NODE: table[4], + ENVIRONMENT_IS_SHELL: table[5], + ENVIRONMENT_IS_WEB: table[6], + ENVIRONMENT_IS_WORKER: table[7], + ENVIRONMENT_IS_SIDECAR: table[8], + resolveRunMainPromise: table[9], + rejectRunMainPromise: table[10], + getRunMainPromise: table[11], + }; + const jsEngineLocal :JSEngineType = { + IS_NODE: loaderExportsLocal.ENVIRONMENT_IS_NODE(), + IS_SHELL: loaderExportsLocal.ENVIRONMENT_IS_SHELL(), + IS_WEB: loaderExportsLocal.ENVIRONMENT_IS_WEB(), + IS_WORKER: loaderExportsLocal.ENVIRONMENT_IS_WORKER(), + IS_SIDECAR: loaderExportsLocal.ENVIRONMENT_IS_SIDECAR(), + }; + Object.assign(dotnetLoaderExports, loaderExportsLocal); + Object.assign(logger, loggerLocal); + Object.assign(assert, assertLocal); + Object.assign(jsEngine, jsEngineLocal); + } + + // keep in sync with tabulateBrowserHostExports() + function expandBrowserHostExports(table:BrowserHostExportsTable, native:BrowserHostExports):void { + const nativeLocal :BrowserHostExports = { + registerDllBytes: table[0], + isSharedArrayBuffer: table[1], + }; + Object.assign(native, nativeLocal); + } + + // keep in sync with tabulateInteropJavaScriptExports() + function expandInteropJavaScriptExports(table:InteropJavaScriptExportsTable, interop:InteropJavaScriptExports):void { + const interopLocal :InteropJavaScriptExports = { + }; + Object.assign(interop, interopLocal); + } + + // keep in sync with tabulateNativeBrowserExports() + function expandNativeBrowserExports(table:NativeBrowserExportsTable, interop:NativeBrowserExports):void { + const interopLocal :NativeBrowserExports = { + }; + Object.assign(interop, interopLocal); + } +} diff --git a/src/native/libs/Common/JavaScript/types/exchange.ts b/src/native/libs/Common/JavaScript/types/exchange.ts index da388fb591910f..ac630558e2e93f 100644 --- a/src/native/libs/Common/JavaScript/types/exchange.ts +++ b/src/native/libs/Common/JavaScript/types/exchange.ts @@ -4,9 +4,10 @@ import type { registerDllBytes } from "../../../../corehost/browserhost/host/host"; import type { isSharedArrayBuffer } from "../../../../corehost/browserhost/host/memory"; import type { check, error, info, warn } from "../../../../corehost/browserhost/loader/logging"; -import type { browserHostResolveMain, browserHostRejectMain, getRunMainPromise } from "../../../../corehost/browserhost/loader/run"; +import type { resolveRunMainPromise, rejectRunMainPromise, getRunMainPromise } from "../../../../corehost/browserhost/loader/run"; +import type { stringToUTF16, stringToUTF16Ptr, utf16ToString } from "../../../System.Runtime.InteropServices.JavaScript.Native/interop/strings"; -export type EnvironmentType = { +export type JSEngineType = { IS_NODE: boolean, IS_SHELL: boolean, IS_WEB: boolean, @@ -15,9 +16,15 @@ export type EnvironmentType = { } export type RuntimeExports = { + utf16ToString: typeof utf16ToString, + stringToUTF16: typeof stringToUTF16, + stringToUTF16Ptr: typeof stringToUTF16Ptr, } export type RuntimeExportsTable = [ + typeof utf16ToString, + typeof stringToUTF16, + typeof stringToUTF16Ptr, ] export type LoggerType = { @@ -36,8 +43,8 @@ export type LoaderExports = { ENVIRONMENT_IS_WEB: ()=> boolean, ENVIRONMENT_IS_WORKER: ()=> boolean, ENVIRONMENT_IS_SIDECAR: ()=> boolean, - browserHostResolveMain:typeof browserHostResolveMain, - browserHostRejectMain:typeof browserHostRejectMain, + resolveRunMainPromise:typeof resolveRunMainPromise, + rejectRunMainPromise:typeof rejectRunMainPromise, getRunMainPromise:typeof getRunMainPromise, } @@ -51,25 +58,25 @@ export type LoaderExportsTable = [ ()=> boolean, ()=> boolean, ()=> boolean, - typeof browserHostResolveMain, - typeof browserHostRejectMain, + typeof resolveRunMainPromise, + typeof rejectRunMainPromise, typeof getRunMainPromise, ] -export type HostNativeExports = { +export type BrowserHostExports = { isSharedArrayBuffer : typeof isSharedArrayBuffer, registerDllBytes: typeof registerDllBytes } -export type HostNativeExportsTable = [ +export type BrowserHostExportsTable = [ typeof registerDllBytes, typeof isSharedArrayBuffer, ] -export type InteropJavaScriptNativeExports = { +export type InteropJavaScriptExports = { } -export type InteropJavaScriptNativeExportsTable = [ +export type InteropJavaScriptExportsTable = [ ] export type NativeBrowserExports = { diff --git a/src/native/libs/Common/JavaScript/types/internal.ts b/src/native/libs/Common/JavaScript/types/internal.ts index d05e1f8f0e8305..3eb0f4da630e0a 100644 --- a/src/native/libs/Common/JavaScript/types/internal.ts +++ b/src/native/libs/Common/JavaScript/types/internal.ts @@ -3,7 +3,7 @@ import type { DotnetModuleConfig, RuntimeAPI, AssetEntry, LoaderConfig, LoadingResource } from "./public-api"; import type { CharPtr, EmscriptenModule, ManagedPointer, NativePointer, VoidPtr } from "./emscripten"; -import { InteropJavaScriptNativeExportsTable, LoaderExportsTable, HostNativeExportsTable, RuntimeExportsTable, NativeBrowserExportsTable } from "./exchange"; +import { InteropJavaScriptExportsTable as InteropJavaScriptExportsTable, LoaderExportsTable, BrowserHostExportsTable, RuntimeExportsTable, NativeBrowserExportsTable } from "./exchange"; export type GCHandle = { __brand: "GCHandle" @@ -106,18 +106,28 @@ export interface PromiseController { } -export type InternalApis = { - runtimeApi: RuntimeAPI, - runtimeExportsTable: RuntimeExportsTable, - loaderExportsTable: LoaderExportsTable, - hostNativeExportsTable: HostNativeExportsTable, - interopJavaScriptNativeExportsTable: InteropJavaScriptNativeExportsTable, - nativeBrowserExportsTable: NativeBrowserExportsTable, - config: LoaderConfigInternal, - updates: (() => void)[], +export type InternalExchange = [ + RuntimeAPI, //0 + (() => void)[], //1 + LoaderConfigInternal, //2 + RuntimeExportsTable, //3 + LoaderExportsTable, //4 + BrowserHostExportsTable, //5 + InteropJavaScriptExportsTable, //6 + NativeBrowserExportsTable, //7 +] +export const enum InternalExchangeIndex { + RuntimeAPI = 0, + InternalUpdatesCallbacks = 1, + LoaderConfig = 2, + RuntimeExportsTable = 3, + LoaderExportsTable = 4, + BrowserHostExportsTable = 5, + InteropJavaScriptExportsTable = 6, + NativeBrowserExportsTable = 7, } export type JsModuleExports = { - initialize(internals: InternalApis): Promise; + dotnetInitializeModule(internals: InternalExchange): Promise; }; diff --git a/src/native/libs/System.Native.Browser/CMakeLists.txt b/src/native/libs/System.Native.Browser/CMakeLists.txt index ef53459c67af5a..9ba68b404a0cb0 100644 --- a/src/native/libs/System.Native.Browser/CMakeLists.txt +++ b/src/native/libs/System.Native.Browser/CMakeLists.txt @@ -13,29 +13,33 @@ add_library(System.Native.Browser-Static set_target_properties(System.Native.Browser-Static PROPERTIES OUTPUT_NAME System.Native.Browser CLEAN_DIRECT_OUTPUT 1) install(TARGETS System.Native.Browser-Static DESTINATION ${STATIC_LIB_DESTINATION} COMPONENT libs) -# WASM-TODO *.ts files instead -set(ROLLUP_TS_SOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/../../corehost/browserhost/loader/dotnet.js" - "${CMAKE_CURRENT_SOURCE_DIR}/../../corehost/browserhost/loader/dotnet.d.ts" - "${CMAKE_CURRENT_SOURCE_DIR}/../../corehost/browserhost/libBrowserHost.js" - "${CMAKE_CURRENT_SOURCE_DIR}/libSystem.Native.Browser.js" - "${CMAKE_CURRENT_SOURCE_DIR}/libSystem.Native.Browser.extpost.js" - "${CMAKE_CURRENT_SOURCE_DIR}/../System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.js" - "${CMAKE_CURRENT_SOURCE_DIR}/../System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.js" - "${CMAKE_CURRENT_SOURCE_DIR}/./libSystem.Native.Browser.extpost.js" - "${CMAKE_CURRENT_SOURCE_DIR}/./libSystem.Native.Browser.js" + +file(GLOB_RECURSE ROLLUP_TS_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/*.ts" + "${CMAKE_CURRENT_SOURCE_DIR}/*.js" + "${CMAKE_CURRENT_SOURCE_DIR}/../System.Runtime.InteropServices.JavaScript.Native/*.ts" + "${CMAKE_CURRENT_SOURCE_DIR}/../System.Runtime.InteropServices.JavaScript.Native/*.js" + "${CMAKE_CURRENT_SOURCE_DIR}/../Common/JavaScript/*.ts" + "${CMAKE_CURRENT_SOURCE_DIR}/../Common/JavaScript/*.js" + "${CMAKE_CURRENT_SOURCE_DIR}/../../corehost/browserhost/*.ts" + "${CMAKE_CURRENT_SOURCE_DIR}/../../corehost/browserhost/*.js" "${CMAKE_CURRENT_SOURCE_DIR}/../../package.json" "${CMAKE_CURRENT_SOURCE_DIR}/../../tsconfig.json" + "${CMAKE_CURRENT_SOURCE_DIR}/../../node_modules/.npm-stamp" ) -# WASM-TODO *.js.map files set(ROLLUP_OUTPUTS "${CLR_ARTIFACTS_OBJ_DIR}/coreclr/browser.wasm.${CMAKE_BUILD_TYPE}/corehost/dotnet.js" + "${CLR_ARTIFACTS_OBJ_DIR}/coreclr/browser.wasm.${CMAKE_BUILD_TYPE}/corehost/dotnet.js.map" "${CLR_ARTIFACTS_OBJ_DIR}/coreclr/browser.wasm.${CMAKE_BUILD_TYPE}/corehost/dotnet.d.ts" "${CLR_ARTIFACTS_OBJ_DIR}/coreclr/browser.wasm.${CMAKE_BUILD_TYPE}/corehost/libBrowserHost.js" + "${CLR_ARTIFACTS_OBJ_DIR}/coreclr/browser.wasm.${CMAKE_BUILD_TYPE}/corehost/libBrowserHost.js.map" "${CLR_ARTIFACTS_OBJ_DIR}/native/browser-${CMAKE_BUILD_TYPE}-wasm/System.Native.Browser/libSystem.Native.Browser.js" + "${CLR_ARTIFACTS_OBJ_DIR}/native/browser-${CMAKE_BUILD_TYPE}-wasm/System.Native.Browser/libSystem.Native.Browser.js.map" "${CLR_ARTIFACTS_OBJ_DIR}/native/browser-${CMAKE_BUILD_TYPE}-wasm/System.Native.Browser/dotnet.runtime.js" + "${CLR_ARTIFACTS_OBJ_DIR}/native/browser-${CMAKE_BUILD_TYPE}-wasm/System.Native.Browser/dotnet.runtime.js.map" "${CLR_ARTIFACTS_OBJ_DIR}/native/browser-${CMAKE_BUILD_TYPE}-wasm/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.js" + "${CLR_ARTIFACTS_OBJ_DIR}/native/browser-${CMAKE_BUILD_TYPE}-wasm/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.js.map" ) set(ROLLUP_STAMP @@ -54,7 +58,7 @@ endif() add_custom_command( OUTPUT ${ROLLUP_STAMP} BYPRODUCTS ${ROLLUP_OUTPUTS} - COMMAND npm run rollup:stub -- ${CMAKE_BUILD_TYPE} ${PRODUCT_VERSION_JS} ${CI_BUILD_JS} + COMMAND npm run rollup:cmake -- "Configuration:${CMAKE_BUILD_TYPE},ProductVersion:${PRODUCT_VERSION_JS},ContinuousIntegrationBuild:${CI_BUILD_JS}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../.." COMMENT "Running 'npm run rollup' to generate JavaScript bundles" DEPENDS ${ROLLUP_TS_SOURCES} diff --git a/src/native/libs/System.Native.Browser/cross-module.ts b/src/native/libs/System.Native.Browser/cross-module.ts new file mode 100644 index 00000000000000..cc943b5b433b19 --- /dev/null +++ b/src/native/libs/System.Native.Browser/cross-module.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../Common/JavaScript/cross-module"; diff --git a/src/native/libs/System.Native.Browser/entrypoints.c b/src/native/libs/System.Native.Browser/entrypoints.c index de5c91cf87238b..7fb0248ef2e7a6 100644 --- a/src/native/libs/System.Native.Browser/entrypoints.c +++ b/src/native/libs/System.Native.Browser/entrypoints.c @@ -10,10 +10,12 @@ // implemented in JavaScript EXTERN_C int32_t SystemJS_RandomBytes(uint8_t* buffer, int32_t bufferLength); +EXTERN_C uint16_t* SystemJS_GetLocaleInfo (const uint16_t* locale, int32_t localeLength, const uint16_t* culture, int32_t cultureLength, const uint16_t* result, int32_t resultMaxLength, int *resultLength); static const Entry s_browserNative[] = { DllImportEntry(SystemJS_RandomBytes) + DllImportEntry(SystemJS_GetLocaleInfo) }; EXTERN_C const void* SystemJSResolveDllImport(const char* name); diff --git a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.extpost.js b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.extpost.js index 81df9991c0d16d..bc2e783ad756c2 100644 --- a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.extpost.js +++ b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.extpost.js @@ -1,10 +1,11 @@ +// // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -var fetch = fetch || undefined; var _nativeModuleLoaded = false; var dotnetInternals = null; -export function initialize(internals) { - if (_nativeModuleLoaded) throw new Error("Native module already loaded"); +var fetch = fetch || undefined; var netNativeModuleLoaded = false; var dotnetInternals = null; +export function dotnetInitializeModule(internals) { + if (netNativeModuleLoaded) throw new Error("Native module already loaded"); dotnetInternals = internals; - _nativeModuleLoaded = true; - return createDotnetRuntime(dotnetInternals.runtimeApi.Module); + netNativeModuleLoaded = true; + return createDotnetRuntime(dotnetInternals[0/*InternalExchangeIndex.RuntimeAPI*/].Module); } diff --git a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js new file mode 100644 index 00000000000000..14264e95eccd21 --- /dev/null +++ b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js @@ -0,0 +1,56 @@ +//! Licensed to the .NET Foundation under one or more agreements. +//! The .NET Foundation licenses this file to you under the MIT license. + +/** + * This is root of **Emscripten library** that would become part of `dotnet.native.js` + * It implements PAL for the VM/runtime. + */ + +(function (exports) { + function libFactory() { + // this executes the function at link time in order to capture exports + // this is what Emscripten does for linking JS libraries + // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#javascript-limits-in-library-files + // it would execute the code at link time and call .toString() on functions to move it to the final output + // this process would loose any closure references, unless they are passed to `__deps` and also explicitly given to the linker + // JS name mangling and minification also applies, see src\native\rollup.config.defines.js and `reserved` there + const exports = {}; + libNativeBrowser(exports); + + let commonDeps = []; + const lib = { + $DOTNET: { + selfInitialize: () => { + if (typeof dotnetInternals !== "undefined") { + DOTNET.dotnetInternals = dotnetInternals; + DOTNET.dotnetInitializeModule(dotnetInternals); + } + }, + dotnetInitializeModule: exports.dotnetInitializeModule, + }, + $DOTNET__deps: commonDeps, + $DOTNET__postset: "DOTNET.selfInitialize()", + }; + + // keep in sync with `reserved`+`keep_fnames` in src\native\rollup.config.defines.js + for (const exportName of Reflect.ownKeys(exports.cross)) { + const name = String(exportName); + if (name === "dotnetInternals") continue; + if (name === "Module") continue; + const emName = "$" + name; + lib[emName] = exports.cross[exportName]; + commonDeps.push(emName); + } + for (const exportName of Reflect.ownKeys(exports)) { + const name = String(exportName); + if (name === "cross") continue; + if (name === "dotnetInitializeModule") continue; + lib[name] = exports[name]; + } + + autoAddDeps(lib, "$DOTNET"); + addToLibrary(lib); + } + libFactory(); + return exports; +})({}); diff --git a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.js b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.js deleted file mode 100644 index 42dae127fd3568..00000000000000 --- a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.js +++ /dev/null @@ -1,279 +0,0 @@ -//! Licensed to the .NET Foundation under one or more agreements. -//! The .NET Foundation licenses this file to you under the MIT license. -//! This is generated file, see src/native/libs/Browser/rollup.config.defines.js - - -var libNativeBrowser = (function (exports) { - 'use strict'; - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - let Module; - let runtimeApi; - let Logger = {}; - let Assert = {}; - let JSEngine = {}; - let loaderExports = {}; - let runtimeExports = {}; - let hostExports = {}; - let interopExports = {}; - let nativeBrowserExports = {}; - let dotnetInternals; - function getInternals() { - return dotnetInternals; - } - function setInternals(internal) { - dotnetInternals = internal; - runtimeApi = dotnetInternals.runtimeApi; - Module = dotnetInternals.runtimeApi.Module; - } - function updateAllInternals() { - if (dotnetInternals.updates === undefined) { - dotnetInternals.updates = []; - } - for (const updateImpl of dotnetInternals.updates) { - updateImpl(); - } - } - function updateMyInternals() { - if (Object.keys(loaderExports).length === 0 && dotnetInternals.loaderExportsTable) { - loaderExports = {}; - Logger = {}; - Assert = {}; - JSEngine = {}; - expandLE(dotnetInternals.loaderExportsTable, Logger, Assert, JSEngine, loaderExports); - } - if (Object.keys(runtimeExports).length === 0 && dotnetInternals.runtimeExportsTable) { - runtimeExports = {}; - expandRE(dotnetInternals.runtimeExportsTable, runtimeExports); - } - if (Object.keys(hostExports).length === 0 && dotnetInternals.hostNativeExportsTable) { - hostExports = {}; - expandHE(dotnetInternals.hostNativeExportsTable, hostExports); - } - if (Object.keys(interopExports).length === 0 && dotnetInternals.interopJavaScriptNativeExportsTable) { - interopExports = {}; - expandJSNE(dotnetInternals.interopJavaScriptNativeExportsTable, interopExports); - } - if (Object.keys(nativeBrowserExports).length === 0 && dotnetInternals.nativeBrowserExportsTable) { - nativeBrowserExports = {}; - expandNBE(dotnetInternals.nativeBrowserExportsTable, nativeBrowserExports); - } - } - /** - * Functions below allow our JS modules to exchange internal interfaces by passing tables of functions in known order instead of using string symbols. - * IMPORTANT: If you need to add more functions, make sure that you add them at the end of the table, so that the order of existing functions does not change. - */ - function tabulateLE(logger, assert, loaderExports) { - return [ - logger.info, - logger.warn, - logger.error, - assert.check, - loaderExports.ENVIRONMENT_IS_NODE, - loaderExports.ENVIRONMENT_IS_SHELL, - loaderExports.ENVIRONMENT_IS_WEB, - loaderExports.ENVIRONMENT_IS_WORKER, - loaderExports.ENVIRONMENT_IS_SIDECAR, - loaderExports.browserHostResolveMain, - loaderExports.browserHostRejectMain, - loaderExports.getRunMainPromise, - ]; - } - function expandLE(table, logger, assert, jsEngine, loaderExports) { - const loggerLocal = { - info: table[0], - warn: table[1], - error: table[2], - }; - const assertLocal = { - check: table[3], - }; - const loaderExportsLocal = { - ENVIRONMENT_IS_NODE: table[4], - ENVIRONMENT_IS_SHELL: table[5], - ENVIRONMENT_IS_WEB: table[6], - ENVIRONMENT_IS_WORKER: table[7], - ENVIRONMENT_IS_SIDECAR: table[8], - browserHostResolveMain: table[9], - browserHostRejectMain: table[10], - getRunMainPromise: table[11], - }; - const jsEngineLocal = { - IS_NODE: loaderExportsLocal.ENVIRONMENT_IS_NODE(), - IS_SHELL: loaderExportsLocal.ENVIRONMENT_IS_SHELL(), - IS_WEB: loaderExportsLocal.ENVIRONMENT_IS_WEB(), - IS_WORKER: loaderExportsLocal.ENVIRONMENT_IS_WORKER(), - IS_SIDECAR: loaderExportsLocal.ENVIRONMENT_IS_SIDECAR(), - }; - Object.assign(loaderExports, loaderExportsLocal); - Object.assign(logger, loggerLocal); - Object.assign(assert, assertLocal); - Object.assign(jsEngine, jsEngineLocal); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function tabulateRE(map) { - return []; - } - function expandRE(table, runtime) { - Object.assign(runtime, {}); - } - function tabulateHE(map) { - return [ - map.registerDllBytes, - map.isSharedArrayBuffer, - ]; - } - function expandHE(table, native) { - const nativeLocal = { - registerDllBytes: table[0], - isSharedArrayBuffer: table[1], - }; - Object.assign(native, nativeLocal); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function tabulateJSNE(map) { - return []; - } - function expandJSNE(table, interop) { - const interopLocal = {}; - Object.assign(interop, interopLocal); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function tabulateNBE(map) { - return []; - } - function expandNBE(table, interop) { - const interopLocal = {}; - Object.assign(interop, interopLocal); - } - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - - var crossModule = /*#__PURE__*/Object.freeze({ - __proto__: null, - get Assert() { return Assert; }, - get JSEngine() { return JSEngine; }, - get Logger() { return Logger; }, - get Module() { return Module; }, - get dotnetInternals() { return dotnetInternals; }, - expandHE: expandHE, - expandJSNE: expandJSNE, - expandLE: expandLE, - expandNBE: expandNBE, - expandRE: expandRE, - getInternals: getInternals, - get hostExports() { return hostExports; }, - get interopExports() { return interopExports; }, - get loaderExports() { return loaderExports; }, - get nativeBrowserExports() { return nativeBrowserExports; }, - get runtimeApi() { return runtimeApi; }, - get runtimeExports() { return runtimeExports; }, - setInternals: setInternals, - tabulateHE: tabulateHE, - tabulateJSNE: tabulateJSNE, - tabulateLE: tabulateLE, - tabulateNBE: tabulateNBE, - tabulateRE: tabulateRE, - updateAllInternals: updateAllInternals, - updateMyInternals: updateMyInternals - }); - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - function SystemJS_RandomBytes(bufferPtr, bufferLength) { - // batchedQuotaMax is the max number of bytes as specified by the api spec. - // If the byteLength of array is greater than 65536, throw a QuotaExceededError and terminate the algorithm. - // https://www.w3.org/TR/WebCryptoAPI/#Crypto-method-getRandomValues - const batchedQuotaMax = 65536; - if (!globalThis.crypto || !globalThis.crypto.getRandomValues) { - if (!globalThis["cryptoWarnOnce"]) { - Logger.warn("This engine doesn't support crypto.getRandomValues. Please use a modern version or provide polyfill for 'globalThis.crypto.getRandomValues'."); - globalThis["cryptoWarnOnce"] = true; - } - return -1; - } - const memoryView = runtimeApi.localHeapViewU8(); - const targetView = memoryView.subarray(bufferPtr, bufferPtr + bufferLength); - // When threading is enabled, Chrome doesn't want SharedArrayBuffer to be passed to crypto APIs - const needsCopy = hostExports.isSharedArrayBuffer(memoryView.buffer); - const targetBuffer = needsCopy - ? new Uint8Array(bufferLength) - : targetView; - // fill the targetBuffer in batches of batchedQuotaMax - for (let i = 0; i < bufferLength; i += batchedQuotaMax) { - const targetBatch = targetBuffer.subarray(i, i + Math.min(bufferLength - i, batchedQuotaMax)); - globalThis.crypto.getRandomValues(targetBatch); - } - if (needsCopy) { - targetView.set(targetBuffer); - } - return 0; - } - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - function initialize(internals) { - const nativeBrowserExportsLocal = {}; - setInternals(internals); - internals.nativeBrowserExportsTable = [...tabulateNBE(nativeBrowserExportsLocal)]; - internals.updates.push(updateMyInternals); - updateAllInternals(); - } - - exports.SystemJS_RandomBytes = SystemJS_RandomBytes; - exports.cross = crossModule; - exports.initialize = initialize; - - return exports; - -}); -//! Licensed to the .NET Foundation under one or more agreements. -//! The .NET Foundation licenses this file to you under the MIT license. - -/** - * This is root of **Emscripten library** that would become part of `dotnet.native.js` - * It implements PAL for the VM/runtime. - */ - -(function (exports) { - function libFactory() { - const lib = { - $DOTNET: { - selfInitialize: () => { - if (typeof dotnetInternals !== "undefined") { - DOTNET.dotnetInternals = dotnetInternals; - DOTNET.initialize(dotnetInternals); - } - }, - }, - "$DOTNET__postset": "DOTNET.selfInitialize();", - }; - - // this executes the function at compile time in order to capture exports - const exports = libNativeBrowser({}); - let commonDeps = []; - for (const exportName of Reflect.ownKeys(exports.cross)) { - const name = String(exportName); - if (name === "dotnetInternals") continue; - if (name === "Module") continue; - const emName = "$" + name; - lib[emName] = exports.cross[exportName]; - commonDeps.push(emName); - } - for (const exportName of Reflect.ownKeys(exports)) { - const name = String(exportName); - if (name === "cross") continue; - if (name === "initialize") continue; - lib[name] = exports[name]; - } - lib["$DOTNET__deps"] = commonDeps; - lib.$DOTNET.initialize = exports.initialize; - - autoAddDeps(lib, "$DOTNET"); - addToLibrary(lib); - } - libFactory(); - return exports; -})({}); diff --git a/src/native/libs/System.Native.Browser/native/cross-linked.ts b/src/native/libs/System.Native.Browser/native/cross-linked.ts new file mode 100644 index 00000000000000..41615262167952 --- /dev/null +++ b/src/native/libs/System.Native.Browser/native/cross-linked.ts @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { + dotnetAssert, dotnetLogger, Module, + dotnetInternals, dotnetLoaderExports, dotnetApi, dotnetNativeBrowserExports, dotnetRuntimeExports, dotnetJSEngine, dotnetBrowserHostExports, dotnetInteropJSExports, + dotnetGetInternals, dotnetSetInternals, dotnetUpdateAllInternals, dotnetUpdateModuleInternals, +} from "../cross-module"; + +import { } from "../../Common/JavaScript/cross-linked"; + +export function crossLink() { + return [ + dotnetAssert, dotnetLogger, Module, + dotnetInternals, dotnetLoaderExports, dotnetApi, dotnetNativeBrowserExports, dotnetRuntimeExports, dotnetJSEngine, dotnetBrowserHostExports, dotnetInteropJSExports, + dotnetGetInternals, dotnetSetInternals, dotnetUpdateAllInternals, dotnetUpdateModuleInternals, + ]; +} diff --git a/src/native/libs/System.Native.Browser/native/crypto.ts b/src/native/libs/System.Native.Browser/native/crypto.ts new file mode 100644 index 00000000000000..1b3b13763a756c --- /dev/null +++ b/src/native/libs/System.Native.Browser/native/crypto.ts @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnetApi, dotnetLogger, dotnetBrowserHostExports } from "../cross-module"; + +export function SystemJS_RandomBytes(bufferPtr: number, bufferLength: number): number { + // batchedQuotaMax is the max number of bytes as specified by the api spec. + // If the byteLength of array is greater than 65536, throw a QuotaExceededError and terminate the algorithm. + // https://www.w3.org/TR/WebCryptoAPI/#Crypto-method-getRandomValues + const batchedQuotaMax = 65536; + + if (!globalThis.crypto || !globalThis.crypto.getRandomValues) { + if (!(globalThis as any)["cryptoWarnOnce"]) { + dotnetLogger.warn("This engine doesn't support crypto.getRandomValues. Please use a modern version or provide polyfill for 'globalThis.crypto.getRandomValues'."); + (globalThis as any)["cryptoWarnOnce"] = true; + } + return -1; + } + + const memoryView = dotnetApi.localHeapViewU8(); + const targetView = memoryView.subarray(bufferPtr, bufferPtr + bufferLength); + + // When threading is enabled, Chrome doesn't want SharedArrayBuffer to be passed to crypto APIs + const needsCopy = dotnetBrowserHostExports.isSharedArrayBuffer(memoryView.buffer); + const targetBuffer = needsCopy + ? new Uint8Array(bufferLength) + : targetView; + + // fill the targetBuffer in batches of batchedQuotaMax + for (let i = 0; i < bufferLength; i += batchedQuotaMax) { + const targetBatch = targetBuffer.subarray(i, i + Math.min(bufferLength - i, batchedQuotaMax)); + globalThis.crypto.getRandomValues(targetBatch); + } + + if (needsCopy) { + targetView.set(targetBuffer); + } + + return 0; +} diff --git a/src/native/libs/System.Native.Browser/native/globalization-locale.ts b/src/native/libs/System.Native.Browser/native/globalization-locale.ts new file mode 100644 index 00000000000000..eed5b625307c94 --- /dev/null +++ b/src/native/libs/System.Native.Browser/native/globalization-locale.ts @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnetApi, dotnetRuntimeExports } from "../cross-module"; +import { Int32Ptr, VoidPtr, VoidPtrNull } from "../types"; + +// char16_t* SystemJS_GetLocaleInfo (const uint16_t* locale, int32_t localeLength, const uint16_t* culture, int32_t cultureLength, const uint16_t* result, int32_t resultMaxLength, int *resultLength); +export function SystemJS_GetLocaleInfo(culture: number, cultureLength: number, locale: number, localeLength: number, dst: number, dstMaxLength: number, dstLength: Int32Ptr): VoidPtr { + const OUTER_SEPARATOR = "##"; + try { + const localeNameOriginal = dotnetRuntimeExports.utf16ToString(locale, (locale + 2 * localeLength)); + const localeName = normalizeLocale(localeNameOriginal); + if (!localeName && localeNameOriginal) { + // handle non-standard or malformed locales by forwarding the locale code + dotnetRuntimeExports.stringToUTF16(dst, dst + 2 * localeNameOriginal.length, localeNameOriginal); + dotnetApi.setHeapI32( dstLength, localeNameOriginal.length); + return VoidPtrNull; + } + const cultureNameOriginal = dotnetRuntimeExports.utf16ToString(culture, (culture + 2 * cultureLength)); + const cultureName = normalizeLocale(cultureNameOriginal); + + if (!localeName || !cultureName) + throw new Error(`Locale or culture name is null or empty. localeName=${localeName}, cultureName=${cultureName}`); + + const localeParts = localeName.split("-"); + // cultureName can be in a form of: + // 1) "language", e.g. "zh" + // 2) "language-region", e.g. "zn-CN" + // 3) "language-script-region", e.g. "zh-Hans-CN" + // 4) "language-script", e.g. "zh-Hans" (served in the catch block below) + let languageName, regionName; + try { + const region = localeParts.length > 1 ? localeParts.pop() : undefined; + // this line might fail if form 4 from the comment above is used: + regionName = region ? new Intl.DisplayNames([cultureName], { type: "region" }).of(region) : undefined; + const language = localeParts.join("-"); + languageName = new Intl.DisplayNames([cultureName], { type: "language" }).of(language); + } catch (error) { + if (error instanceof RangeError) { + // if it failed from this reason then cultureName is in a form "language-script", without region + try { + languageName = new Intl.DisplayNames([cultureName], { type: "language" }).of(localeName); + } catch (error) { + if (error instanceof RangeError && localeNameOriginal) { + // handle non-standard or malformed locales by forwarding the locale code, e.g. "xx-u-xx" + dotnetRuntimeExports.stringToUTF16(dst, dst + 2 * localeNameOriginal.length, localeNameOriginal); + dotnetApi.setHeapI32(dstLength, localeNameOriginal.length); + return VoidPtrNull; + } + throw error; + } + } else { + throw error; + } + } + const localeInfo = { + LanguageName: languageName, + RegionName: regionName, + }; + const result = Object.values(localeInfo).join(OUTER_SEPARATOR); + + if (!result) + throw new Error(`Locale info for locale=${localeName} is null or empty.`); + + if (result.length > dstMaxLength) + throw new Error(`Locale info for locale=${localeName} exceeds length of ${dstMaxLength}.`); + + dotnetRuntimeExports.stringToUTF16(dst, dst + 2 * result.length, result); + dotnetApi.setHeapI32(dstLength, result.length); + return VoidPtrNull; + } catch (ex: any) { + dotnetApi.setHeapI32(dstLength, -1); + return dotnetRuntimeExports.stringToUTF16Ptr(ex.toString()); + } + + function normalizeLocale(locale: string | null) { + if (!locale) + return undefined; + try { + locale = locale.toLocaleLowerCase().replace("_", "-"); + if (locale.startsWith("zh-")) { + // browser does not recognize "zh-chs" and "zh-cht" as equivalents of "zh-Hans" "zh-Hant", we are helping, otherwise + // it would throw on getCanonicalLocales with "RangeError: Incorrect locale information provided" + locale = locale.replace("-chs", "-Hans").replace("-cht", "-Hant"); + } + const canonicalLocales = (Intl as any).getCanonicalLocales(locale); + return canonicalLocales.length > 0 ? canonicalLocales[0] : undefined; + } catch { + return undefined; + } + } +} diff --git a/src/native/libs/System.Native.Browser/native/index.ts b/src/native/libs/System.Native.Browser/native/index.ts new file mode 100644 index 00000000000000..db18156ac4d32f --- /dev/null +++ b/src/native/libs/System.Native.Browser/native/index.ts @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { InternalExchange, NativeBrowserExports, NativeBrowserExportsTable } from "../types"; +import { InternalExchangeIndex } from "../types"; + +export { SystemJS_RandomBytes } from "./crypto"; + +export function dotnetInitializeModule(internals: InternalExchange): void { + const nativeBrowserExportsLocal: NativeBrowserExports = { + }; + dotnetSetInternals(internals); + internals[InternalExchangeIndex.NativeBrowserExportsTable] = tabulateNativeBrowserExports(nativeBrowserExportsLocal); + const updates = internals[InternalExchangeIndex.InternalUpdatesCallbacks]; + if (!updates.includes(dotnetUpdateModuleInternals)) updates.push(dotnetUpdateModuleInternals); + dotnetUpdateAllInternals(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function tabulateNativeBrowserExports(map:NativeBrowserExports):NativeBrowserExportsTable { + // keep in sync with dotnetUpdateModuleInternals() + return [ + ]; + } +} + + +// see also `reserved` in `rollup.config.defines.js` +export * as cross from "../cross-module"; diff --git a/src/native/libs/System.Native.Browser/types.ts b/src/native/libs/System.Native.Browser/types.ts new file mode 100644 index 00000000000000..c8fb6fdaccc4d2 --- /dev/null +++ b/src/native/libs/System.Native.Browser/types.ts @@ -0,0 +1,5 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../Common/JavaScript/types"; + diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.js b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.js deleted file mode 100644 index eb2e761bf9984b..00000000000000 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.js +++ /dev/null @@ -1,177 +0,0 @@ -//! Licensed to the .NET Foundation under one or more agreements. -//! The .NET Foundation licenses this file to you under the MIT license. -//! This is generated file, see src/native/libs/Browser/rollup.config.defines.js - - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -let Module; -let runtimeApi; -let Logger = {}; -let Assert = {}; -let JSEngine = {}; -let loaderExports = {}; -let runtimeExports = {}; -let hostExports = {}; -let interopExports = {}; -let nativeBrowserExports = {}; -let dotnetInternals; -function getInternals() { - return dotnetInternals; -} -function setInternals(internal) { - dotnetInternals = internal; - runtimeApi = dotnetInternals.runtimeApi; - Module = dotnetInternals.runtimeApi.Module; -} -function updateAllInternals() { - if (dotnetInternals.updates === undefined) { - dotnetInternals.updates = []; - } - for (const updateImpl of dotnetInternals.updates) { - updateImpl(); - } -} -function updateMyInternals() { - if (Object.keys(loaderExports).length === 0 && dotnetInternals.loaderExportsTable) { - loaderExports = {}; - Logger = {}; - Assert = {}; - JSEngine = {}; - expandLE(dotnetInternals.loaderExportsTable, Logger, Assert, JSEngine, loaderExports); - } - if (Object.keys(runtimeExports).length === 0 && dotnetInternals.runtimeExportsTable) { - runtimeExports = {}; - expandRE(dotnetInternals.runtimeExportsTable, runtimeExports); - } - if (Object.keys(hostExports).length === 0 && dotnetInternals.hostNativeExportsTable) { - hostExports = {}; - expandHE(dotnetInternals.hostNativeExportsTable, hostExports); - } - if (Object.keys(interopExports).length === 0 && dotnetInternals.interopJavaScriptNativeExportsTable) { - interopExports = {}; - expandJSNE(dotnetInternals.interopJavaScriptNativeExportsTable, interopExports); - } - if (Object.keys(nativeBrowserExports).length === 0 && dotnetInternals.nativeBrowserExportsTable) { - nativeBrowserExports = {}; - expandNBE(dotnetInternals.nativeBrowserExportsTable, nativeBrowserExports); - } -} -/** - * Functions below allow our JS modules to exchange internal interfaces by passing tables of functions in known order instead of using string symbols. - * IMPORTANT: If you need to add more functions, make sure that you add them at the end of the table, so that the order of existing functions does not change. - */ -function tabulateLE(logger, assert, loaderExports) { - return [ - logger.info, - logger.warn, - logger.error, - assert.check, - loaderExports.ENVIRONMENT_IS_NODE, - loaderExports.ENVIRONMENT_IS_SHELL, - loaderExports.ENVIRONMENT_IS_WEB, - loaderExports.ENVIRONMENT_IS_WORKER, - loaderExports.ENVIRONMENT_IS_SIDECAR, - loaderExports.browserHostResolveMain, - loaderExports.browserHostRejectMain, - loaderExports.getRunMainPromise, - ]; -} -function expandLE(table, logger, assert, jsEngine, loaderExports) { - const loggerLocal = { - info: table[0], - warn: table[1], - error: table[2], - }; - const assertLocal = { - check: table[3], - }; - const loaderExportsLocal = { - ENVIRONMENT_IS_NODE: table[4], - ENVIRONMENT_IS_SHELL: table[5], - ENVIRONMENT_IS_WEB: table[6], - ENVIRONMENT_IS_WORKER: table[7], - ENVIRONMENT_IS_SIDECAR: table[8], - browserHostResolveMain: table[9], - browserHostRejectMain: table[10], - getRunMainPromise: table[11], - }; - const jsEngineLocal = { - IS_NODE: loaderExportsLocal.ENVIRONMENT_IS_NODE(), - IS_SHELL: loaderExportsLocal.ENVIRONMENT_IS_SHELL(), - IS_WEB: loaderExportsLocal.ENVIRONMENT_IS_WEB(), - IS_WORKER: loaderExportsLocal.ENVIRONMENT_IS_WORKER(), - IS_SIDECAR: loaderExportsLocal.ENVIRONMENT_IS_SIDECAR(), - }; - Object.assign(loaderExports, loaderExportsLocal); - Object.assign(logger, loggerLocal); - Object.assign(assert, assertLocal); - Object.assign(jsEngine, jsEngineLocal); -} -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function tabulateRE(map) { - return []; -} -function expandRE(table, runtime) { - Object.assign(runtime, {}); -} -function tabulateHE(map) { - return [ - map.registerDllBytes, - map.isSharedArrayBuffer, - ]; -} -function expandHE(table, native) { - const nativeLocal = { - registerDllBytes: table[0], - isSharedArrayBuffer: table[1], - }; - Object.assign(native, nativeLocal); -} -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function tabulateJSNE(map) { - return []; -} -function expandJSNE(table, interop) { - const interopLocal = {}; - Object.assign(interop, interopLocal); -} -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function tabulateNBE(map) { - return []; -} -function expandNBE(table, interop) { - const interopLocal = {}; - Object.assign(interop, interopLocal); -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -function initialize(internals) { - const runtimeApiLocal = { - getAssemblyExports, - setModuleImports, - }; - const runtimeExportsLocal = {}; - setInternals(internals); - Object.assign(internals.runtimeApi, runtimeApiLocal); - internals.runtimeExportsTable = [...tabulateRE(runtimeExportsLocal)]; - internals.updates.push(updateMyInternals); - updateAllInternals(); -} -// eslint-disable-next-line @typescript-eslint/no-unused-vars -async function getAssemblyExports(assemblyName) { - throw new Error("Not implemented"); -} -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function setModuleImports(moduleName, moduleImports) { - throw new Error("Not implemented"); -} - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -export { initialize }; diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.ts new file mode 100644 index 00000000000000..09cd302590cf6f --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export { dotnetInitializeModule } from "./interop"; diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/cross-module.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/cross-module.ts new file mode 100644 index 00000000000000..8e72db213b830d --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/cross-module.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../../Common/JavaScript/cross-module"; diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/index.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/index.ts new file mode 100644 index 00000000000000..d66b3b98beb0e8 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/index.ts @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { InternalExchange, RuntimeAPI, RuntimeExports, RuntimeExportsTable } from "./types"; +import { InternalExchangeIndex } from "../types"; +import { dotnetSetInternals, dotnetUpdateAllInternals, dotnetUpdateModuleInternals } from "./cross-module"; +import { stringToUTF16, stringToUTF16Ptr, utf16ToString } from "./strings"; + +export function dotnetInitializeModule(internals: InternalExchange): void { + const runtimeApiLocal: Partial = { + getAssemblyExports, + setModuleImports, + }; + const runtimeExportsLocal: RuntimeExports = { + utf16ToString, + stringToUTF16, + stringToUTF16Ptr, + }; + dotnetSetInternals(internals); + Object.assign(internals[InternalExchangeIndex.RuntimeAPI], runtimeApiLocal); + internals[InternalExchangeIndex.RuntimeExportsTable] = tabulateRuntimeExports(runtimeExportsLocal); + internals[InternalExchangeIndex.InternalUpdatesCallbacks].push(dotnetUpdateModuleInternals); + dotnetUpdateAllInternals(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function tabulateRuntimeExports(map:RuntimeExports):RuntimeExportsTable { + // keep in sync with dotnetUpdateModuleInternals() + return [ + map.utf16ToString, + map.stringToUTF16, + map.stringToUTF16Ptr, + ]; + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function getAssemblyExports(assemblyName: string): Promise { + throw new Error("Not implemented"); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function setModuleImports(moduleName: string, moduleImports: any): void { + throw new Error("Not implemented"); +} + diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/memory.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/memory.ts new file mode 100644 index 00000000000000..97f980f22499d0 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/memory.ts @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { CharPtr, MemOffset, VoidPtr } from "./types"; +import { dotnetBrowserHostExports, dotnetApi } from "./cross-module"; + +// does not check for growable heap +export function getU16Local(localView: Uint16Array, offset: MemOffset): number { + return localView[offset >>> 1]; +} + +// does not check for growable heap +export function setU16Local(localView: Uint16Array, offset: MemOffset, value: number): void { + assertIntInRange(value, 0, 0xFFFF); + localView[offset >>> 1] = value; +} + +// When threading is enabled, TextDecoder does not accept a view of a +// SharedArrayBuffer, we must make a copy of the array first. +// See https://github.com/whatwg/encoding/issues/172 +export function viewOrCopy(view: Uint8Array, start: CharPtr, end: CharPtr): Uint8Array { + // this condition should be eliminated by rollup on non-threading builds + const needsCopy = dotnetBrowserHostExports.isSharedArrayBuffer(view.buffer); + return needsCopy + ? view.slice(start, end) + : view.subarray(start, end); +} + +export function assertIntInRange(value: Number, min: Number, max: Number) { + dotnetAssert.check(Number.isSafeInteger(value), () => `Value is not an integer: ${value} (${typeof (value)})`); + dotnetAssert.check(value >= min && value <= max, () => `Overflow: value ${value} is out of ${min} ${max} range`); +} + +export function ZeroRegion(byteOffset: VoidPtr, sizeBytes: number): void { + dotnetApi.localHeapViewU8().fill(0, byteOffset, byteOffset + sizeBytes); +} + diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/strings.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/strings.ts new file mode 100644 index 00000000000000..c17408341f5e70 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/strings.ts @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnetApi } from "./cross-module"; +import { getU16Local, setU16Local, viewOrCopy, ZeroRegion } from "./memory"; +import { VoidPtr } from "./types"; + +let textDecoderUtf16: TextDecoder | undefined | null; +let stringsInitialized = false; + +export function stringsInit(): void { + if (!stringsInitialized) { + // V8 does not provide TextDecoder + if (typeof TextDecoder !== "undefined") { + textDecoderUtf16 = new TextDecoder("utf-16le"); + } + stringsInitialized = true; + } +} + +export function stringToUTF16(dstPtr: number, endPtr: number, text: string) { + const heapI16 = dotnetApi.localHeapViewU16(); + const len = text.length; + for (let i = 0; i < len; i++) { + setU16Local(heapI16, dstPtr, text.charCodeAt(i)); + dstPtr += 2; + if (dstPtr >= endPtr) break; + } +} + +export function stringToUTF16Ptr(str: string): VoidPtr { + const bytes = (str.length + 1) * 2; + const ptr = Module._malloc(bytes) as any; + ZeroRegion(ptr, str.length * 2); + stringToUTF16(ptr, ptr + bytes, str); + return ptr; +} + +export function utf16ToString(startPtr: number, endPtr: number): string { + stringsInit(); + if (textDecoderUtf16) { + const subArray = viewOrCopy(dotnetApi.localHeapViewU8(), startPtr as any, endPtr as any); + return textDecoderUtf16.decode(subArray); + } else { + return utf16ToStringLoop(startPtr, endPtr); + } +} + +// V8 does not provide TextDecoder +export function utf16ToStringLoop(startPtr: number, endPtr: number): string { + let str = ""; + const heapU16 = dotnetApi.localHeapViewU16(); + for (let i = startPtr; i < endPtr; i += 2) { + const char = getU16Local(heapU16, i); + str += String.fromCharCode(char); + } + return str; +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/types.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/types.ts new file mode 100644 index 00000000000000..95021536b19918 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/types.ts @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { NativePointer } from "../types"; + +export interface JSMarshalerArguments extends NativePointer { + __brand: "JSMarshalerArguments" +} + +export interface JSFunctionSignature extends NativePointer { + __brand: "JSFunctionSignatures" +} + +export interface JSMarshalerType extends NativePointer { + __brand: "JSMarshalerType" +} + +export interface JSMarshalerArgument extends NativePointer { + __brand: "JSMarshalerArgument" +} + +export * from "../types"; diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js new file mode 100644 index 00000000000000..faa07191f95527 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js @@ -0,0 +1,46 @@ +//! Licensed to the .NET Foundation under one or more agreements. +//! The .NET Foundation licenses this file to you under the MIT license. + +/** + * This is root of **Emscripten library** that would become part of `dotnet.native.js` + * It implements interop between JS and .NET + */ + +(function (exports) { + function libFactory() { + // this executes the function at link time in order to capture exports + // this is what Emscripten does for linking JS libraries + // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#javascript-limits-in-library-files + // it would execute the code at link time and call .toString() on functions to move it to the final output + // this process would loose any closure references, unless they are passed to `__deps` and also explicitly given to the linker + // JS name mangling and minification also applies, see src\native\rollup.config.defines.js and `reserved` there + const exports = {}; + libInteropJavaScriptNative(exports); + + let commonDeps = ["$DOTNET"]; + const lib = { + $DOTNET_INTEROP: { + selfInitialize: () => { + if (typeof dotnetInternals !== "undefined") { + DOTNET_INTEROP.dotnetInternals = dotnetInternals; + DOTNET_INTEROP.dotnetInitializeModule(dotnetInternals); + } + }, + dotnetInitializeModule: exports.dotnetInitializeModule, + }, + $DOTNET_INTEROP__postset: "DOTNET_INTEROP.selfInitialize()", + $DOTNET_INTEROP__deps: commonDeps, + }; + + for (const exportName of Reflect.ownKeys(exports)) { + const name = String(exportName); + if (name === "dotnetInitializeModule") continue; + lib[name] = exports[name]; + } + + autoAddDeps(lib, "$DOTNET_INTEROP"); + addToLibrary(lib); + } + libFactory(); + return exports; +})({}); diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.js b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.js deleted file mode 100644 index 84efa97500a1ec..00000000000000 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.js +++ /dev/null @@ -1,253 +0,0 @@ -//! Licensed to the .NET Foundation under one or more agreements. -//! The .NET Foundation licenses this file to you under the MIT license. -//! This is generated file, see src/native/libs/Browser/rollup.config.defines.js - - -var libInteropJavaScriptNative = (function (exports) { - 'use strict'; - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - let Module; - let runtimeApi; - let Logger = {}; - let Assert = {}; - let JSEngine = {}; - let loaderExports = {}; - let runtimeExports = {}; - let hostExports = {}; - let interopExports = {}; - let nativeBrowserExports = {}; - let dotnetInternals; - function getInternals() { - return dotnetInternals; - } - function setInternals(internal) { - dotnetInternals = internal; - runtimeApi = dotnetInternals.runtimeApi; - Module = dotnetInternals.runtimeApi.Module; - } - function updateAllInternals() { - if (dotnetInternals.updates === undefined) { - dotnetInternals.updates = []; - } - for (const updateImpl of dotnetInternals.updates) { - updateImpl(); - } - } - function updateMyInternals() { - if (Object.keys(loaderExports).length === 0 && dotnetInternals.loaderExportsTable) { - loaderExports = {}; - Logger = {}; - Assert = {}; - JSEngine = {}; - expandLE(dotnetInternals.loaderExportsTable, Logger, Assert, JSEngine, loaderExports); - } - if (Object.keys(runtimeExports).length === 0 && dotnetInternals.runtimeExportsTable) { - runtimeExports = {}; - expandRE(dotnetInternals.runtimeExportsTable, runtimeExports); - } - if (Object.keys(hostExports).length === 0 && dotnetInternals.hostNativeExportsTable) { - hostExports = {}; - expandHE(dotnetInternals.hostNativeExportsTable, hostExports); - } - if (Object.keys(interopExports).length === 0 && dotnetInternals.interopJavaScriptNativeExportsTable) { - interopExports = {}; - expandJSNE(dotnetInternals.interopJavaScriptNativeExportsTable, interopExports); - } - if (Object.keys(nativeBrowserExports).length === 0 && dotnetInternals.nativeBrowserExportsTable) { - nativeBrowserExports = {}; - expandNBE(dotnetInternals.nativeBrowserExportsTable, nativeBrowserExports); - } - } - /** - * Functions below allow our JS modules to exchange internal interfaces by passing tables of functions in known order instead of using string symbols. - * IMPORTANT: If you need to add more functions, make sure that you add them at the end of the table, so that the order of existing functions does not change. - */ - function tabulateLE(logger, assert, loaderExports) { - return [ - logger.info, - logger.warn, - logger.error, - assert.check, - loaderExports.ENVIRONMENT_IS_NODE, - loaderExports.ENVIRONMENT_IS_SHELL, - loaderExports.ENVIRONMENT_IS_WEB, - loaderExports.ENVIRONMENT_IS_WORKER, - loaderExports.ENVIRONMENT_IS_SIDECAR, - loaderExports.browserHostResolveMain, - loaderExports.browserHostRejectMain, - loaderExports.getRunMainPromise, - ]; - } - function expandLE(table, logger, assert, jsEngine, loaderExports) { - const loggerLocal = { - info: table[0], - warn: table[1], - error: table[2], - }; - const assertLocal = { - check: table[3], - }; - const loaderExportsLocal = { - ENVIRONMENT_IS_NODE: table[4], - ENVIRONMENT_IS_SHELL: table[5], - ENVIRONMENT_IS_WEB: table[6], - ENVIRONMENT_IS_WORKER: table[7], - ENVIRONMENT_IS_SIDECAR: table[8], - browserHostResolveMain: table[9], - browserHostRejectMain: table[10], - getRunMainPromise: table[11], - }; - const jsEngineLocal = { - IS_NODE: loaderExportsLocal.ENVIRONMENT_IS_NODE(), - IS_SHELL: loaderExportsLocal.ENVIRONMENT_IS_SHELL(), - IS_WEB: loaderExportsLocal.ENVIRONMENT_IS_WEB(), - IS_WORKER: loaderExportsLocal.ENVIRONMENT_IS_WORKER(), - IS_SIDECAR: loaderExportsLocal.ENVIRONMENT_IS_SIDECAR(), - }; - Object.assign(loaderExports, loaderExportsLocal); - Object.assign(logger, loggerLocal); - Object.assign(assert, assertLocal); - Object.assign(jsEngine, jsEngineLocal); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function tabulateRE(map) { - return []; - } - function expandRE(table, runtime) { - Object.assign(runtime, {}); - } - function tabulateHE(map) { - return [ - map.registerDllBytes, - map.isSharedArrayBuffer, - ]; - } - function expandHE(table, native) { - const nativeLocal = { - registerDllBytes: table[0], - isSharedArrayBuffer: table[1], - }; - Object.assign(native, nativeLocal); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function tabulateJSNE(map) { - return []; - } - function expandJSNE(table, interop) { - const interopLocal = {}; - Object.assign(interop, interopLocal); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function tabulateNBE(map) { - return []; - } - function expandNBE(table, interop) { - const interopLocal = {}; - Object.assign(interop, interopLocal); - } - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - - var crossModule = /*#__PURE__*/Object.freeze({ - __proto__: null, - get Assert() { return Assert; }, - get JSEngine() { return JSEngine; }, - get Logger() { return Logger; }, - get Module() { return Module; }, - get dotnetInternals() { return dotnetInternals; }, - expandHE: expandHE, - expandJSNE: expandJSNE, - expandLE: expandLE, - expandNBE: expandNBE, - expandRE: expandRE, - getInternals: getInternals, - get hostExports() { return hostExports; }, - get interopExports() { return interopExports; }, - get loaderExports() { return loaderExports; }, - get nativeBrowserExports() { return nativeBrowserExports; }, - get runtimeApi() { return runtimeApi; }, - get runtimeExports() { return runtimeExports; }, - setInternals: setInternals, - tabulateHE: tabulateHE, - tabulateJSNE: tabulateJSNE, - tabulateLE: tabulateLE, - tabulateNBE: tabulateNBE, - tabulateRE: tabulateRE, - updateAllInternals: updateAllInternals, - updateMyInternals: updateMyInternals - }); - - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function SystemInteropJS_InvokeJSImportST(function_handle, args) { - // WASMTODO implementation - Logger.error("SystemInteropJS_InvokeJSImportST called"); - return -1; - } - function initialize(internals) { - const interopJavaScriptNativeExportsLocal = {}; - setInternals(internals); - internals.interopJavaScriptNativeExportsTable = [...tabulateJSNE(interopJavaScriptNativeExportsLocal)]; - internals.updates.push(updateMyInternals); - updateAllInternals(); - } - - exports.SystemInteropJS_InvokeJSImportST = SystemInteropJS_InvokeJSImportST; - exports.cross = crossModule; - exports.initialize = initialize; - - return exports; - -}); -//! Licensed to the .NET Foundation under one or more agreements. -//! The .NET Foundation licenses this file to you under the MIT license. - -/** - * This is root of **Emscripten library** that would become part of `dotnet.native.js` - * It implements interop between JS and .NET - */ - -(function (exports) { - function libFactory() { - const lib = { - $DOTNET_INTEROP: { - selfInitialize: () => { - if (typeof dotnetInternals !== "undefined") { - DOTNET_INTEROP.dotnetInternals = dotnetInternals; - DOTNET_INTEROP.initialize(dotnetInternals); - } - }, - }, - "$DOTNET_INTEROP__postset": "DOTNET_INTEROP.selfInitialize();", - }; - - // this executes the function at compile time in order to capture exports - const exports = libInteropJavaScriptNative({}); - let commonDeps = ["$DOTNET"]; - for (const exportName of Reflect.ownKeys(exports.cross)) { - const name = String(exportName); - if (name === "dotnetInternals") continue; - if (name === "Module") continue; - const emName = "$" + name; - commonDeps.push(emName); - } - for (const exportName of Reflect.ownKeys(exports)) { - const name = String(exportName); - if (name === "cross") continue; - if (name === "initialize") continue; - lib[name] = exports[name]; - } - - lib["$DOTNET_INTEROP__deps"] = commonDeps; - lib.$DOTNET_INTEROP.initialize = exports.initialize; - - autoAddDeps(lib, "$DOTNET_INTEROP"); - addToLibrary(lib); - } - libFactory(); - return exports; -})({}); diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/cross-linked.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/cross-linked.ts new file mode 100644 index 00000000000000..ea10f4707c6a6c --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/cross-linked.ts @@ -0,0 +1,5 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { } from "../../Common/JavaScript/cross-linked"; + diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/index.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/index.ts new file mode 100644 index 00000000000000..0c29a9c89976ca --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/index.ts @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { InternalExchange, InteropJavaScriptExports, InteropJavaScriptExportsTable, JSFnHandle, JSMarshalerArguments } from "../interop/types"; +import { InternalExchangeIndex } from "../types"; +import { } from "./cross-linked"; // ensure ambient symbols are declared + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function SystemInteropJS_InvokeJSImportST(function_handle: JSFnHandle, args: JSMarshalerArguments) { + // WASM-TODO implementation + dotnetLogger.error("SystemInteropJS_InvokeJSImportST called"); + return - 1; +} + +export function dotnetInitializeModule(internals: InternalExchange): void { + const interopJavaScriptNativeExportsLocal: InteropJavaScriptExports = { + }; + dotnetSetInternals(internals); + internals[InternalExchangeIndex.InteropJavaScriptExportsTable] = tabulateInteropJavaScriptExports(interopJavaScriptNativeExportsLocal); + const updates = internals[InternalExchangeIndex.InternalUpdatesCallbacks]; + if (!updates.includes(dotnetUpdateModuleInternals)) updates.push(dotnetUpdateModuleInternals); + dotnetUpdateAllInternals(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function tabulateInteropJavaScriptExports(map:InteropJavaScriptExports):InteropJavaScriptExportsTable { + // keep in sync with dotnetUpdateModuleInternals() + return [ + ]; + } +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/types.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/types.ts new file mode 100644 index 00000000000000..3ff5080c66dfd1 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/types.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../Common/JavaScript/types"; diff --git a/src/native/rollup.config.defines.js b/src/native/rollup.config.defines.js new file mode 100644 index 00000000000000..b89850e338abcd --- /dev/null +++ b/src/native/rollup.config.defines.js @@ -0,0 +1,50 @@ +//! Licensed to the .NET Foundation under one or more agreements. +//! The .NET Foundation licenses this file to you under the MIT license. + +import gitCommitInfo from "git-commit-info"; + +if (process.env.ContinuousIntegrationBuild === undefined) { + throw new Error("ContinuousIntegrationBuild environment variable is not defined"); +} +if (process.env.Configuration === undefined) { + throw new Error("Configuration environment variable is not defined"); +} +if (process.env.ProductVersion === undefined) { + throw new Error("ProductVersion environment variable is not defined"); +} + +export const configuration = process.env.Configuration; +export const productVersion = process.env.ProductVersion; +export const isContinuousIntegrationBuild = process.env.ContinuousIntegrationBuild === "true" ? true : false; + +console.log(`Rollup configuration: Configuration=${configuration}, ProductVersion=${productVersion}, ContinuousIntegrationBuild=${isContinuousIntegrationBuild}`); + +export const banner = "//! Licensed to the .NET Foundation under one or more agreements.\n//! The .NET Foundation licenses this file to you under the MIT license.\n//! This is generated file, see src/native/libs/Browser/rollup.config.defines.js\n\n"; +export const banner_dts = banner + "//! This is not considered public API with backward compatibility guarantees. \n"; +export const keep_classnames = /(ManagedObject|ManagedError|Span|ArraySegment)/; +export const keep_fnames = /(dotnetSetInternals|dotnetUpdateAllInternals|dotnetUpdateModuleInternals)/; +export const reserved = [ + "Module", "dotnetApi", + "dotnetInternals", "dotnetLogger", "dotnetAssert", "dotnetJSEngine", + "dotnetSetInternals", "dotnetUpdateAllInternals", "dotnetUpdateModuleInternals", "dotnetInitializeModule", + "dotnetLoaderExports", "dotnetRuntimeExports", "dotnetBrowserHostExports", "dotnetInteropJSExports", "dotnetNativeBrowserExports", +]; + +export const externalDependencies = ["module", "process", "perf_hooks", "node:crypto"]; +export const artifactsObjDir = "../../artifacts/obj"; +export const isDebug = process.env.Configuration !== "Release"; + +export let gitHash; +try { + const gitInfo = gitCommitInfo(); + gitHash = gitInfo.hash; +} catch (e) { + gitHash = "unknown"; +} + +export const envConstants = { + productVersion, + configuration, + gitHash, + isContinuousIntegrationBuild, +}; diff --git a/src/native/rollup.config.js b/src/native/rollup.config.js new file mode 100644 index 00000000000000..f0f6e7bf4415e4 --- /dev/null +++ b/src/native/rollup.config.js @@ -0,0 +1,161 @@ +//! Licensed to the .NET Foundation under one or more agreements. +//! The .NET Foundation licenses this file to you under the MIT license. + +import { defineConfig } from "rollup"; +import typescript from "@rollup/plugin-typescript"; +import { nodeResolve } from "@rollup/plugin-node-resolve"; +import dts from "rollup-plugin-dts"; +import { + externalDependencies, isDebug, artifactsObjDir, envConstants, banner, banner_dts, configuration, + keep_classnames, keep_fnames, reserved +} from "./rollup.config.defines.js" +import { terserPlugin, writeOnChangePlugin, consts, onwarn, alwaysLF, iife2fe, sourcemapPathTransform } from "./rollup.config.plugins.js" +import { promises as fs } from "fs"; + +const dotnetDTS = { + input: "./libs/Common/JavaScript/types/export-api.ts", + output: [ + { + format: "es", + file: artifactsObjDir + `/coreclr/browser.wasm.${configuration}/corehost/dotnet.d.ts`, + banner: banner_dts, + plugins: [writeOnChangePlugin()], + }, + ...(isDebug ? [{ + format: "es", + file: "./corehost/browserhost/loader/dotnet.d.ts", + banner: banner_dts, + plugins: [alwaysLF(), writeOnChangePlugin()], + }] : []) + ], + external: externalDependencies, + plugins: [dts()], + onwarn +}; + +const dotnetJS = configure({ + input: "./corehost/browserhost/loader/dotnet.ts", + output: [{ + file: artifactsObjDir + `/coreclr/browser.wasm.${configuration}/corehost/dotnet.js`, + intro: "/*! bundlerFriendlyImports */", + }], + terser: { + compress: { + module: true, + }, mangle: { + module: true, + } + } +}); + +const libNativeBrowser = configure({ + input: "./libs/System.Native.Browser/native/index.ts", + output: [{ + name: "libNativeBrowser", + format: "iife", + file: artifactsObjDir + `/native/browser-${configuration}-wasm/System.Native.Browser/libSystem.Native.Browser.js`, + footer: await fs.readFile("./libs/System.Native.Browser/libSystem.Native.Browser.footer.js"), + }], + terser: { + compress: { + toplevel: true, + keep_fnames, + }, mangle: { + toplevel: true, + keep_fnames, + reserved, + } + } +}); + +const dotnetRuntimeJS = configure({ + input: "./libs/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.ts", + output: [{ + file: artifactsObjDir + `/native/browser-${configuration}-wasm/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.js`, + }], + terser: { + compress: { + module: true, + }, mangle: { + module: true, + keep_classnames, + } + } +}); + +const libInteropJavaScriptNative = configure({ + input: "./libs/System.Runtime.InteropServices.JavaScript.Native/native/index.ts", + output: [{ + name: "libInteropJavaScriptNative", + format: "iife", + file: artifactsObjDir + `/native/browser-${configuration}-wasm/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.js`, + footer: await fs.readFile("./libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js"), + }], + terser: { + compress: { + toplevel: true, + keep_fnames, + }, mangle: { + toplevel: true, + keep_fnames, + reserved, + } + } +}); + +const libBrowserHost = configure({ + input: "./corehost/browserhost/host/index.ts", + output: [{ + name: "libBrowserHost", + format: "iife", + file: artifactsObjDir + `/coreclr/browser.wasm.${configuration}/corehost/libBrowserHost.js`, + footer: await fs.readFile("./corehost/browserhost/libBrowserHost.footer.js"), + }], + terser: { + compress: { + toplevel: true, + keep_fnames, + }, mangle: { + toplevel: true, + keep_fnames, + reserved, + } + } +}); + +export default defineConfig([ + dotnetJS, + dotnetDTS, + libNativeBrowser, + dotnetRuntimeJS, + libInteropJavaScriptNative, + libBrowserHost, +]); + +function configure({ input, output, terser }) { + return { + treeshake: !isDebug, + input, + output: output.map(o => { + return { + banner, + format: "es", + plugins: isDebug + ? [iife2fe(), writeOnChangePlugin()] + : [terserPlugin(terser), iife2fe(), writeOnChangePlugin()], + sourcemap: true, //isDebug ? true : "hidden", + sourcemapPathTransform, + ...o + } + }), + external: externalDependencies, + plugins: [ + nodeResolve(), + consts(envConstants), + typescript({ + tsconfig: "./tsconfig.json", + }) + ], + onwarn, + } +} diff --git a/src/native/rollup.config.plugins.js b/src/native/rollup.config.plugins.js new file mode 100644 index 00000000000000..f9146efcdd8eb0 --- /dev/null +++ b/src/native/rollup.config.plugins.js @@ -0,0 +1,153 @@ +//! Licensed to the .NET Foundation under one or more agreements. +//! The .NET Foundation licenses this file to you under the MIT license. + +import { createHash } from "crypto"; +import { readFile, writeFile, mkdir } from "fs/promises"; +import virtual from "@rollup/plugin-virtual"; +import * as fs from "fs"; +import * as path from "path"; +import terser from "@rollup/plugin-terser"; + +import { isContinuousIntegrationBuild, gitHash } from "./rollup.config.defines.js" + +export const terserPlugin = (terserOptions) => { + let { compress, mangle } = terserOptions || {}; + compress = compress || {}; + mangle = mangle || {}; + return terser({ + // keep in sync with src/native/tsconfig.json + ecma: "2020", + compress: { + defaults: true, + passes: 2, + drop_debugger: false, // we invoke debugger + drop_console: false, // we log to console + ...compress + }, + mangle: { + ...mangle, + }, + }) +} + +// this would create .sha256 file next to the output file, so that we do not touch datetime of the file if it's same -> faster incremental build. +export const writeOnChangePlugin = () => ({ + name: "writeOnChange", + generateBundle: writeWhenChanged +}); + +// Drop invocation from IIFE +export function iife2fe() { + return { + name: "iife2fe", + generateBundle: (options, bundle) => { + const name = Object.keys(bundle)[0]; + if (name.endsWith(".map")) return; + const asset = bundle[name]; + const code = asset.code; + //throw new Error("iife2fe " + code); + asset.code = code + .replace(/}\({}\);/, "};") // }({}); ->}; + .replace(/}\)\({}\);/, "});") // })({}); ->}); + ; + } + }; +} + +// force always unix line ending +export const alwaysLF = () => ({ + name: "writeOnChange", + generateBundle: (options, bundle) => { + const name = Object.keys(bundle)[0]; + const asset = bundle[name]; + const code = asset.code; + asset.code = code.replace(/\r/g, ""); + } +}); + +async function writeWhenChanged(options, bundle) { + try { + const name = Object.keys(bundle)[0]; + const asset = bundle[name]; + const code = asset.code; + const hashFileName = options.file + ".sha256"; + const oldHashExists = await checkFileExists(hashFileName); + const oldFileExists = await checkFileExists(options.file); + + const newHash = createHash("sha256").update(code).digest("hex"); + + let isOutputChanged = true; + if (oldHashExists && oldFileExists) { + const oldHash = await readFile(hashFileName, { encoding: "ascii" }); + isOutputChanged = oldHash !== newHash; + } + if (isOutputChanged) { + const dir = path.dirname(options.file); + if (!await checkFileExists(dir)) { + await mkdir(dir, { recursive: true }); + } + await writeFile(hashFileName, newHash); + } else { + // this.warn('No change in ' + options.file) + delete bundle[name]; + } + } catch (ex) { + this.warn(ex.toString()); + } +} + +function checkFileExists(file) { + return fs.promises.access(file, fs.constants.F_OK) + .then(() => true) + .catch(() => false); +} + +export function onwarn(warning) { + if (warning.code === "CIRCULAR_DEPENDENCY") { + return; + } + + if (warning.code === "UNRESOLVED_IMPORT" && warning.exporter === "process") { + return; + } + + if (warning.code === "PLUGIN_WARNING" && warning.message.indexOf("sourcemap") !== -1) { + return; + } + + // eslint-disable-next-line no-console + console.warn(`(!) ${warning.toString()} ${warning.code}`); +} + +export function consts(dict) { + // implement rollup-plugin-const in terms of @rollup/plugin-virtual + // It's basically the same thing except "consts" names all its modules with a "consts:" prefix, + // and the virtual module always exports a single default binding (the const value). + + let newDict = {}; + for (const k in dict) { + const newKey = "consts:" + k; + const newVal = JSON.stringify(dict[k]); + newDict[newKey] = `export default ${newVal}`; + } + return virtual(newDict); +} + +const locationCache = {}; +export function sourcemapPathTransform(relativeSourcePath, sourcemapPath) { + let res = locationCache[relativeSourcePath]; + if (res === undefined) { + if (!isContinuousIntegrationBuild) { + const sourcePath = path.resolve( + path.dirname(sourcemapPath), + relativeSourcePath + ); + res = `file:///${sourcePath.replace(/\\/g, "/")}`; + } else { + relativeSourcePath = relativeSourcePath.substring(12); + res = `https://raw.githubusercontent.com/dotnet/runtime/${gitHash}/${relativeSourcePath}`; + } + locationCache[relativeSourcePath] = res; + } + return res; +} diff --git a/src/native/rollup.stub.js b/src/native/rollup.stub.js deleted file mode 100644 index 1b59e429abce59..00000000000000 --- a/src/native/rollup.stub.js +++ /dev/null @@ -1,45 +0,0 @@ -//! Licensed to the .NET Foundation under one or more agreements. -//! The .NET Foundation licenses this file to you under the MIT license. - -import { promises as fs } from "fs"; -import path from "path"; - -/** - * This would be replace by real rollup with TypeScript compilation in next iteration - */ - - -const artifactsObjDir = "../../artifacts/obj"; -const configuration = process.argv[2] || "Debug"; -const productVersion = process.argv[3] || "10.0.0-dev"; -const isContinuousIntegrationBuild = process.argv[4] === "true" ? true : false; - -console.log(`Rollup configuration: Configuration=${configuration}, ProductVersion=${productVersion}, ContinuousIntegrationBuild=${isContinuousIntegrationBuild}`); - -const copies = [ - ["./corehost/browserhost/loader/dotnet.d.ts", - `${artifactsObjDir}/coreclr/browser.wasm.${configuration}/corehost/dotnet.d.ts`], - ["./corehost/browserhost/loader/dotnet.js", - `${artifactsObjDir}/coreclr/browser.wasm.${configuration}/corehost/dotnet.js`], - ["./corehost/browserhost/libBrowserHost.js", - `${artifactsObjDir}/coreclr/browser.wasm.${configuration}/corehost/libBrowserHost.js`], - ["./libs/System.Native.Browser/libSystem.Native.Browser.js", - `${artifactsObjDir}/native/browser-${configuration}-wasm/System.Native.Browser/libSystem.Native.Browser.js`], - ["./libs/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.js", - `${artifactsObjDir}/native/browser-${configuration}-wasm/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.js`], - ["./libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.js", - `${artifactsObjDir}/native/browser-${configuration}-wasm/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.js`] -]; - -const now = new Date(); -for (const [src, dest] of copies) { - const absoluteSrc = path.resolve(src); - const absoluteDest = path.resolve(dest); - const destDir = path.resolve(path.dirname(dest)); - console.log(`Copying ${absoluteSrc} to ${destDir}`); - await fs.mkdir(destDir, { recursive: true }); - await fs.copyFile(absoluteSrc, absoluteDest); - // TODO-WASM: make rollup incremental - // await fs.utimes(absoluteDest, now, now); -} -await fs.writeFile(`${artifactsObjDir}/coreclr/browser.wasm.${configuration}/corehost/.rollup.stamp`, new Date().toISOString()); diff --git a/src/native/tsconfig.json b/src/native/tsconfig.json index 6709400b3bc508..9ef2a54d8676bf 100644 --- a/src/native/tsconfig.json +++ b/src/native/tsconfig.json @@ -5,7 +5,8 @@ "removeComments": false, "module": "esnext", "sourceMap": true, - "target": "ES2018", + // keep in sync with src/native/rollup.config.defines.js + "target": "ES2020", "moduleResolution": "Node", "strict": true, "lib": [ diff --git a/src/tests/build.cmd b/src/tests/build.cmd index c500bcb1cc30d4..37d195c62266e5 100644 --- a/src/tests/build.cmd +++ b/src/tests/build.cmd @@ -72,6 +72,8 @@ if /i "%1" == "--" (set processedArgs=!processedArgs! %1&s if /i "%1" == "x64" (set __BuildArch=x64&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) if /i "%1" == "x86" (set __BuildArch=x86&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) if /i "%1" == "arm64" (set __BuildArch=arm64&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) +if /i "%1" == "wasm" (set __BuildArch=wasm&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) +if /i "%1" == "browser" (set __TargetOS=browser&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) if /i "%1" == "debug" (set __BuildType=Debug&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) if /i "%1" == "release" (set __BuildType=Release&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) @@ -374,7 +376,7 @@ echo All arguments are optional and case-insensitive, and the '-' prefix is opti echo. echo.-? -h --help: View this message. echo. -echo Build architecture: one of "x64", "x86", "arm64" ^(default: x64^). +echo Build architecture: one of "x64", "x86", "arm64", "wasm" ^(default: x64^). echo Build type: one of "Debug", "Checked", "Release" ^(default: Debug^). echo. echo -Rebuild: Clean up all test artifacts prior to building tests.