diff --git a/src/mono/sample/wasm/browser/Wasm.Browser.Sample.csproj b/src/mono/sample/wasm/browser/Wasm.Browser.Sample.csproj index 2b05836627673..c4d606cb32964 100644 --- a/src/mono/sample/wasm/browser/Wasm.Browser.Sample.csproj +++ b/src/mono/sample/wasm/browser/Wasm.Browser.Sample.csproj @@ -1,7 +1,6 @@ true - runtime.js diff --git a/src/mono/sample/wasm/browser2/Makefile b/src/mono/sample/wasm/browser2/Makefile new file mode 100644 index 0000000000000..f02f650ccb0a4 --- /dev/null +++ b/src/mono/sample/wasm/browser2/Makefile @@ -0,0 +1,11 @@ +TOP=../../../../.. + +include ../wasm.mk + +ifneq ($(AOT),) +override MSBUILD_ARGS+=/p:RunAOTCompilation=true +endif + +PROJECT_NAME=Wasm.Browser2.Sample.csproj + +run: run-browser diff --git a/src/mono/sample/wasm/browser2/Program.cs b/src/mono/sample/wasm/browser2/Program.cs new file mode 100644 index 0000000000000..743f481896fcb --- /dev/null +++ b/src/mono/sample/wasm/browser2/Program.cs @@ -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. + +using System; +using System.Runtime.CompilerServices; + +namespace Sample +{ + public class Test + { + public static void Main(string[] args) + { + Console.WriteLine ("Hello, World!"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int TestMeaning() + { + return 42; + } + } +} diff --git a/src/mono/sample/wasm/browser2/Wasm.Browser2.Sample.csproj b/src/mono/sample/wasm/browser2/Wasm.Browser2.Sample.csproj new file mode 100644 index 0000000000000..f60a4a27dd537 --- /dev/null +++ b/src/mono/sample/wasm/browser2/Wasm.Browser2.Sample.csproj @@ -0,0 +1,17 @@ + + + true + main.js + + + + + + + + + <_SampleProject>Wasm.Browser2.Sample.csproj + + + + diff --git a/src/mono/sample/wasm/browser2/declarations/main.d.ts b/src/mono/sample/wasm/browser2/declarations/main.d.ts new file mode 100644 index 0000000000000..62798c243e9ef --- /dev/null +++ b/src/mono/sample/wasm/browser2/declarations/main.d.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. + +// This file defines various types related to the main.js file (i.e. user code) diff --git a/src/mono/sample/wasm/browser2/index.html b/src/mono/sample/wasm/browser2/index.html new file mode 100644 index 0000000000000..cde50658b9bd8 --- /dev/null +++ b/src/mono/sample/wasm/browser2/index.html @@ -0,0 +1,16 @@ + + + + + + TESTS + + + + + + Result from Sample.Test.TestMeaning: + + + + \ No newline at end of file diff --git a/src/mono/sample/wasm/browser2/main.js b/src/mono/sample/wasm/browser2/main.js new file mode 100644 index 0000000000000..58dfb1381c04b --- /dev/null +++ b/src/mono/sample/wasm/browser2/main.js @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +"use strict"; // I am working on this in a separate PR (#54011) but am including it here as well + +import DotNet from "./dotnet.js"; // User can import DotNet so that there can even potentially be multiple instances in 1 app and we don't pollute global namespace + +// Called when the mono-config.js (soon to be mono-config.json via PR#53606) is loaded +function onConfigLoaded (config) { + // we can do some modification to config at runtime here + // then return it + // i.e. config.newProp = "test new prop"; + + return config; +} + +// Called when mono runtime is ready but before being started +function onRuntimeInitialized () { +} + +// Called when MONO runtime started +function onRuntimeStarted () { + SampleApp.init() +} + +// Called when MONO runtime or the config file didn't load correctly +function onInitError(error) { + test_exit(1); // test runner related + throw(error); +} + +DotNet.onConfigLoaded = onConfigLoaded; // optional +DotNet.onRuntimeInitialized = onRuntimeInitialized; // optional +DotNet.onRuntimeStarted = onRuntimeStarted; // required as it acts like the entry point into the program +DotNet.onInitError = onInitError; // required as it acts like the entry point into the program if loading the runtime fails +// They are called in the following order: onConfigLoaded, onRuntimeInitialized, onMonoRuntimeInitialized + + + + +// FROM HTML SCRIPT TAG (all js code should be in js files) //////////////////////////////////////////////////////////////////////////// +// Only modification was to use newer syntax. + +document.onreadystatechange = () => { + if (document.readyState === 'complete') { + onLoad(); + } +}; + +let is_testing = false; +function onLoad () { + const url = new URL(decodeURI(window.location.href)); + let args = url.searchParams.getAll('arg'); + is_testing = args !== undefined && (args.find(arg => arg == '--testing') !== undefined); +}; + +function test_exit (exit_code) { + if (!is_testing) { + console.log(`test_exit: ${exit_code}`); + return; + } + + /* Set result in a tests_done element, to be read by xharness */ + let tests_done_elem = document.createElement("label"); + tests_done_elem.id = "tests_done"; + tests_done_elem.innerHTML = exit_code.toString(); + document.body.appendChild(tests_done_elem); + + console.log(`WASM EXIT ${exit_code}`); +}; + +const SampleApp = { + init: function () { + const ret = BINDING.call_static_method("[Wasm.Browser.Sample] Sample.Test:TestMeaning", []); + const out = document.getElementById("out"); + if (out) { + out.innerHTML = ret; + } + + if (is_testing) + { + console.debug(`ret: ${ret}`); + let exit_code = ret == 42 ? 0 : 1; + test_exit(exit_code); + } + }, +}; diff --git a/src/mono/sample/wasm/browser2/tsconfig.json b/src/mono/sample/wasm/browser2/tsconfig.json new file mode 100644 index 0000000000000..8a4ebea26e479 --- /dev/null +++ b/src/mono/sample/wasm/browser2/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "noImplicitAny": false, + "noEmitOnError": true, + "removeComments": false, + "sourceMap": true, + "target": "es2019", + "lib": ["es2019", "dom"], + "strict": true, + + // TODO I added but we should remove when the transition to TS is done + "allowJs": true, + "checkJs": true, // this prevents TS from currently throwing errors in the files (set to true if you like the color red :)) + "typeRoots": [ + "./declarations/", + "../../../wasm/runtime/declarations/" // is this how this would be done? + ], + "outDir": "./build/" + }, + "exclude": [ "build" ] +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/declarations/interop.d.ts b/src/mono/wasm/runtime/declarations/interop.d.ts new file mode 100644 index 0000000000000..4866b7d63d98d --- /dev/null +++ b/src/mono/wasm/runtime/declarations/interop.d.ts @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// This file defines various types related with the interaction with Dotnet interop + +// TODO find better types than the anys, however it will do for now +// TODO move to DotNet namespace + +declare interface BINDING { // Should BINDING be renamed? + call_static_method: (method: string, params: any[]) => void, + mono_bindings_init: (binding: string) => void, + mono_bind_method: (method: string, this_arg: number, args_marshal: string?, friendly_name: string) => any, + mono_method_invoke: (method: number, this_arg: number, args_marshal: string, args: any) => any, + mono_method_get_call_signature: (method: string, mono_obj: any[]) => any, + mono_method_resolve: (fqn: string) => any, + mono_bind_static_method: (fqn: string, signature: string?) => any, + mono_call_static_method: (fqn: string, args: any, signature: string) => any, + mono_bind_assembly_entry_point: (assembly: any, signature: string?) => any, + mono_call_assembly_entry_point: (assembly: any, args: any, signature: string?) => any, + mono_intern_string: (string: string) => any, +} + +declare interface MONO { //should MONO be renamed? + pump_message: () => void, + mono_load_runtime_and_bcl: (unused_vfs_prefix /* unused - can it be removed? */ : any, deploy_prefix: String, debug_level: number, file_list, loaded_cb: () => void, fetch_file_cb: (asset: string) => void) => any, + mono_load_runtime_and_bcl_args: (args: MonoRuntimeArgs) => any, + mono_wasm_load_bytes_into_heap: (bytes: Uint8Array) => number, + mono_wasm_load_icu_data: (offset: number) => boolean, + mono_wasm_get_icudt_name: (culture: string) => any, // todo technically we can define culture as a set of valid cultures instead of just string + mono_wasm_globalization_init: (globalization_mode: GlobalizationMode) => void, + mono_wasm_get_loaded_files: () => LoadedFiles[], + mono_wasm_new_root_buffer: (capacity: number, msg: string) => WasmRootBuffer, + mono_wasm_new_root_buffer_from_pointer: (offset: number, capacity: number, msg: string) => WasmRootBuffer, + mono_wasm_new_root: (value: number) => WasmRoot, + mono_wasm_new_roots: (count_or_values: number[] | number) => number[], + mono_wasm_release_roots: () => void, +} + +type MonoRuntimeArgs = { + fetch_file_cb: (asset: string) => void, + loaded_cb: () => void, + debug_level: number, + assembly_root: string, + assets: { + name: string, + behavior: string + }[] +} + +export type LoadedFiles = { + url: string, + file: string +}[] + +type GlobalizationMode = "icu" | "invarient" | "auto"; + +type WasmRootBuffer = { + __offset: number, + __offset32: number, + __count: number, + length: number, + __handle: any, + __ownsAllocation: boolean + // TODO add the functions and change from a type to a class (?) +} + +type WasmRoot = { + get_address: () => number, + get_address_32: () => number, + get: () => number, + set: (value: any) => any, + valueOf: () => number, + clear: () => void, + toString: () => string, + release: any, + __buffer: any, + __index: number, +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/declarations/runtime.d.ts b/src/mono/wasm/runtime/declarations/runtime.d.ts new file mode 100644 index 0000000000000..ece843b4b91a7 --- /dev/null +++ b/src/mono/wasm/runtime/declarations/runtime.d.ts @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// This file defines various types related with the WASM API + +declare namespace DotNet { // TODO due to dotnet.js making everything public ther eis a `DOTNET` object as well as this one + type onConfigLoaded = (config: MonoConfig) => MonoConfig; + type onRuntimeInitialized = () => void; + type onMonoRuntimeInitialized = (error: Error) => void; +} + +export type MonoConfig = { + assembly_root: string, + debug_level: number, + assets: (AssetEntry | AssetEntry | SatelliteAssemblyEntry | VfsEntry | IcuData)[], + remote_sources: string[], + [index: string]: any, // overflow for the "Extra" sections +} + +// Types of assets that can be in the mono-config.js/mono-config.json file (taken from /src/tasks/WasmAppBuilder/WasmAppBuilder.cs) +type AssetEntry = { + behavior: string, + name: string +} + +interface AssemblyEntry extends AssetEntry { + name: "assembly" +} + +interface SatelliteAssemblyEntry extends AssetEntry { + name: "resource", + culture: string +} + +interface VfsEntry extends AssetEntry { + name: "vfs", + virtual_path: string +} + +interface IcuData extends AssetEntry { + name: "icu", + load_remote: boolean +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/js_support.js b/src/mono/wasm/runtime/js_support.js new file mode 100644 index 0000000000000..17ced1c9daaf3 --- /dev/null +++ b/src/mono/wasm/runtime/js_support.js @@ -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. + +// NOTE THAT THIS FILE IS A PRE-JS SO IT IS PREPENDED TO DOTNET.JS AND RUNS BEFORE ALMOST ALL OTHER THINGS + +// Since we load the mono config in preInit but don't want to force the user to +// need to use Module, we do it here. Note that the user will +var DotNet = { + // loads the config file + preInit: async function() { + const config = await MONO.mono_wasm_load_config("./mono-config.json"); + this.onConfigLoaded(config); + }, + + // sends various callbacks at different times to notify user code + onConfigLoaded(config) { + const onConfigLoadedCallback = DotNet['onConfigLoaded'] ?? (() => {}); + const onRuntimeStarted = DotNet['onRuntimeStarted'] ?? (() => {}); + const onInitError = DotNet['onInitError'] ?? (() => {}); + // Note: onRuntimeInitialized is called by emsdk in another location + + config.loaded_cb = function () { + // sends callback so that user knows that mono is loaded + onRuntimeStarted(); + }; + config.fetch_file_cb = function (asset) { + return fetch (asset, { credentials: 'same-origin' }); + } + + // sends callback so that user can modify the config as needed before sending it to mono but this is optional + // for user to handle + if (!config.error) { + config = onConfigLoadedCallback(config) ?? config; + } else { + // send error callback with the error message + onInitError(config.error); + return; // no point in continuing loading mono as config is incorrect + } + + try + { + MONO.mono_load_runtime_and_bcl_args (config); + } catch (error) { + onInitError(error); + } + }, +}; diff --git a/src/mono/wasm/runtime/library_mono.js b/src/mono/wasm/runtime/library_mono.js index 685aaaf375d85..4f8b5a4934cdb 100644 --- a/src/mono/wasm/runtime/library_mono.js +++ b/src/mono/wasm/runtime/library_mono.js @@ -86,6 +86,8 @@ var MonoSupportLib = { module ["mono_wasm_new_root"] = MONO.mono_wasm_new_root.bind(MONO); module ["mono_wasm_new_roots"] = MONO.mono_wasm_new_roots.bind(MONO); module ["mono_wasm_release_roots"] = MONO.mono_wasm_release_roots.bind(MONO); + module ["mono_wasm_load_config"] = MONO.mono_wasm_load_config.bind(MONO); + }, _base64Converter: { @@ -2323,6 +2325,29 @@ var MonoSupportLib = { console.debug('mono_wasm_debug_event_raised:aef14bca-5519-4dfe-b35a-f867abc123ae', JSON.stringify(event), JSON.stringify(args)); }, + + /** + * COPIED FROM PR#53606 + * Loads the mono config file (typically called mono-config.json) + * + * @param {string} configFilePath - relative path to the config file + * @throws Will throw an error if the config file loading fails + */ + mono_wasm_load_config: async function (configFilePath) { + try { + let config = null; + // NOTE: when we add nodejs make sure to include the nodejs fetch package + if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_NODE) { + const configRaw = await fetch(configFilePath); + config = await configRaw.json(); + } else { // shell or worker + config = JSON.parse(read(configFilePath)); // read is a v8 debugger command + } + return config; + } catch(e) { + return {message: "failed to load config file", error: e}; + } + } }, mono_wasm_add_typed_value: function (type, str_value, value) { diff --git a/src/mono/wasm/tsconfig.json b/src/mono/wasm/tsconfig.json new file mode 100644 index 0000000000000..4036eafcd0895 --- /dev/null +++ b/src/mono/wasm/tsconfig.json @@ -0,0 +1,21 @@ +{ // copied from Blazor (https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web.JS/tsconfig.json) + "compilerOptions": { + "moduleResolution": "node", + "noImplicitAny": false, + "noEmitOnError": true, + "removeComments": false, + "sourceMap": true, + "target": "es2019", + "lib": ["es2019", "dom"], + "strict": true, + + // TODO I added but we should remove when the transition to TS is done + "allowJs": true, + "checkJs": false, // this prevents TS from currently throwing errors in the files (set to true if you like the color red :)) + "typeRoots": [ + "./declarations" + ], + "outDir": "./build/" + }, + "exclude": [ "build" ] +} \ No newline at end of file diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index bcdf4eae40a7a..5eb00266b8f53 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -10,7 +10,7 @@ $([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'microsoft.netcore.app.runtime.$(PackageRID)', '$(Configuration)')) $([MSBuild]::NormalizeDirectory('$(MicrosoftNetCoreAppRuntimePackDir)', 'runtimes', '$(PackageRID)')) $([MSBuild]::NormalizeDirectory('$(MicrosoftNetCoreAppRuntimePackRidDir)', 'native')) - false + true false true emcc @@ -80,6 +80,8 @@ <_EmccCommonFlags Include="--source-map-base http://example.com" /> <_EmccCommonFlags Include="-emit-llvm" /> + <_EmccCommonFlags Include="-s EXPORT_NAME=DotNet" /> + <_EmccCommonFlags Include="-s MODULARIZE=1" Condition="'$(WasmEnableES6)' != 'false'" /> <_EmccCommonFlags Include="-s EXPORT_ES6=1" Condition="'$(WasmEnableES6)' != 'false'" />