From 45af31b2dfe778603c77c7d154f5aa1b6378cd21 Mon Sep 17 00:00:00 2001 From: Zoltan Varga Date: Wed, 13 May 2020 13:10:57 -0400 Subject: [PATCH] Add a WasmAppBuilder task and a sample. (#36338) --- src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.cs | 115 +++++++ .../msbuild/WasmAppBuilder/WasmAppBuilder.csproj | 1 + src/mono/netcore/sample/wasm/Makefile | 12 + src/mono/netcore/sample/wasm/Program.cs | 12 + src/mono/netcore/sample/wasm/WasmSample.csproj | 40 +++ src/mono/netcore/sample/wasm/runtime.js | 339 +++++++++++++++++++++ 6 files changed, 519 insertions(+) create mode 100644 src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.cs create mode 100644 src/mono/netcore/sample/wasm/Makefile create mode 100644 src/mono/netcore/sample/wasm/Program.cs create mode 100644 src/mono/netcore/sample/wasm/WasmSample.csproj create mode 100644 src/mono/netcore/sample/wasm/runtime.js diff --git a/src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.cs b/src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.cs new file mode 100644 index 0000000..77538e6 --- /dev/null +++ b/src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.cs @@ -0,0 +1,115 @@ +// -*- indent-tabs-mode: nil -*- +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public class WasmAppBuilder : Task +{ + // FIXME: Document + + [Required] + public string? AppDir { get; set; } + [Required] + public string? RuntimePackDir { get; set; } + [Required] + public string? MainAssembly { get; set; } + [Required] + public string? MainJS { get; set; } + [Required] + public ITaskItem[]? AssemblySearchPaths { get; set; } + + Dictionary? Assemblies; + Resolver? Resolver; + + public override bool Execute () { + if (!File.Exists (MainAssembly)) + throw new ArgumentException ($"File MainAssembly='{MainAssembly}' doesn't exist."); + if (!File.Exists (MainJS)) + throw new ArgumentException ($"File MainJS='{MainJS}' doesn't exist."); + + var paths = new List (); + Assemblies = new Dictionary (); + + // Collect and load assemblies used by the app + foreach (var v in AssemblySearchPaths!) { + var dir = v.ItemSpec; + if (!Directory.Exists (dir)) + throw new ArgumentException ($"Directory '{dir}' doesn't exist or not a directory."); + paths.Add (dir); + } + Resolver = new Resolver (paths); + var mlc = new MetadataLoadContext (Resolver, "System.Private.CoreLib"); + + var mainAssembly = mlc.LoadFromAssemblyPath (MainAssembly); + Add (mlc, mainAssembly); + + // Create app + Directory.CreateDirectory (AppDir!); + Directory.CreateDirectory (Path.Join (AppDir, "managed")); + foreach (var assembly in Assemblies!.Values) + File.Copy (assembly.Location, Path.Join (AppDir, "managed", Path.GetFileName (assembly.Location)), true); + foreach (var f in new string [] { "dotnet.wasm", "dotnet.js" }) + File.Copy (Path.Join (RuntimePackDir, "native", "wasm", "release", f), Path.Join (AppDir, f), true); + File.Copy (MainJS!, Path.Join (AppDir, Path.GetFileName (MainJS!)), true); + + using (var sw = File.CreateText (Path.Join (AppDir, "mono-config.js"))) { + sw.WriteLine ("config = {"); + sw.WriteLine ("\tvfs_prefix: \"managed\","); + sw.WriteLine ("\tdeploy_prefix: \"managed\","); + sw.WriteLine ("\tenable_debugging: 0,"); + sw.WriteLine ("\tfile_list: ["); + foreach (var assembly in Assemblies.Values) { + sw.Write ("\"" + Path.GetFileName (assembly.Location) + "\""); + sw.Write (", "); + } + sw.WriteLine ("],"); + sw.WriteLine ("}"); + } + + using (var sw = File.CreateText (Path.Join (AppDir, "run-v8.sh"))) { + sw.WriteLine ("v8 --expose_wasm runtime.js -- --run " + Path.GetFileName (MainAssembly)); + } + + return true; + } + + void Add (MetadataLoadContext mlc, Assembly assembly) { + Assemblies! [assembly.GetName ().Name!] = assembly; + foreach (var aname in assembly.GetReferencedAssemblies ()) { + var refAssembly = mlc.LoadFromAssemblyName (aname); + Add (mlc, refAssembly); + } + } +} + +class Resolver : MetadataAssemblyResolver +{ + List SearchPaths; + + public Resolver (List searchPaths) { + this.SearchPaths = searchPaths; + } + + public override Assembly? Resolve (MetadataLoadContext context, AssemblyName assemblyName) { + var name = assemblyName.Name; + foreach (var dir in SearchPaths) { + var path = Path.Combine (dir, name + ".dll"); + if (File.Exists (path)) { + Console.WriteLine (path); + return context.LoadFromAssemblyPath (path); + } + } + return null; + } +} diff --git a/src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.csproj b/src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.csproj index 271afb9..aa09da9 100644 --- a/src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.csproj +++ b/src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.csproj @@ -12,5 +12,6 @@ + diff --git a/src/mono/netcore/sample/wasm/Makefile b/src/mono/netcore/sample/wasm/Makefile new file mode 100644 index 0000000..09c42a4 --- /dev/null +++ b/src/mono/netcore/sample/wasm/Makefile @@ -0,0 +1,12 @@ +TOP=../../../../.. + +all: build + +build: + $(TOP)/.dotnet/dotnet build /p:TargetArchitecture=wasm /p:TargetOS=Browser /p:Configuration=Release + +clean: + rm -rf bin + +run: + cd bin/Release/publish && ~/.jsvu/v8 --expose_wasm runtime.js -- --run WasmSample.dll diff --git a/src/mono/netcore/sample/wasm/Program.cs b/src/mono/netcore/sample/wasm/Program.cs new file mode 100644 index 0000000..58a6f57 --- /dev/null +++ b/src/mono/netcore/sample/wasm/Program.cs @@ -0,0 +1,12 @@ +// -*- indent-tabs-mode: nil -*- +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +using System; + +public class Test +{ + public static void Main (String[] args) { + Console.WriteLine ("Hello, World!"); + } +} diff --git a/src/mono/netcore/sample/wasm/WasmSample.csproj b/src/mono/netcore/sample/wasm/WasmSample.csproj new file mode 100644 index 0000000..588f292 --- /dev/null +++ b/src/mono/netcore/sample/wasm/WasmSample.csproj @@ -0,0 +1,40 @@ + + + Exe + bin + false + $(NetCoreAppCurrent) + wasm + Browser + $(ArtifactsDir)bin\lib-runtime-packs\$(NetCoreAppCurrent)-$(TargetOS)-$(Configuration)-$(TargetArchitecture)\runtimes\browser-wasm + $(MSBuildThisFileDirectory)\obj\$(Configuration)\wasm + $(MSBuildThisFileDirectory)\bin\$(Configuration)\publish + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/mono/netcore/sample/wasm/runtime.js b/src/mono/netcore/sample/wasm/runtime.js new file mode 100644 index 0000000..10e4fa6f --- /dev/null +++ b/src/mono/netcore/sample/wasm/runtime.js @@ -0,0 +1,339 @@ +// -*- mode: js; js-indent-level: 4; -*- +// +// Run runtime tests under a JS shell or a browser +// + +//glue code to deal with the differences between chrome, ch, d8, jsc and sm. +var is_browser = typeof window != "undefined"; + +if (is_browser) { + // We expect to be run by tests/runtime/run.js which passes in the arguments using http parameters + var url = new URL (decodeURI (window.location)); + arguments = []; + for (var v of url.searchParams) { + if (v [0] == "arg") { + console.log ("URL ARG: " + v [0] + "=" + v [1]); + arguments.push (v [1]); + } + } +} + +if (is_browser || typeof print === "undefined") + print = console.log; + +// JavaScript core does not have a console defined +if (typeof console === "undefined") { + var Console = function () { + this.log = function(msg){ print(msg) }; + }; + console = new Console(); +} + +if (typeof console !== "undefined") { + if (!console.debug) + console.debug = console.log; + if (!console.trace) + console.trace = console.log; + if (!console.warn) + console.warn = console.log; +} + +if (typeof crypto == 'undefined') { + // /dev/random doesn't work on js shells, so define our own + // See library_fs.js:createDefaultDevices () + var crypto = { + getRandomValues: function (buffer) { + buffer[0] = (Math.random()*256)|0; + } + } +} + +try { + if (typeof arguments == "undefined") + arguments = WScript.Arguments; + load = WScript.LoadScriptFile; + read = WScript.LoadBinaryFile; +} catch (e) { +} + +try { + if (typeof arguments == "undefined") { + if (typeof scriptArgs !== "undefined") + arguments = scriptArgs; + } +} catch (e) { +} +//end of all the nice shell glue code. + +// set up a global variable to be accessed in App.init +var testArguments = arguments; + +function test_exit (exit_code) { + if (is_browser) { + // Notify the puppeteer script + Module.exit_code = exit_code; + print ("WASM EXIT " + exit_code); + } else { + Module.wasm_exit (exit_code); + } +} + +function fail_exec (reason) { + print (reason); + test_exit (1); +} + +function inspect_object (o) { + var r = ""; + for(var p in o) { + var t = typeof o[p]; + r += "'" + p + "' => '" + t + "', "; + } + return r; +} + +// Preprocess arguments +var args = testArguments; +print("Arguments: " + testArguments); +profilers = []; +setenv = {}; +runtime_args = []; +enable_gc = false; +enable_zoneinfo = false; +while (true) { + if (args [0].startsWith ("--profile=")) { + var arg = args [0].substring ("--profile=".length); + + profilers.push (arg); + + args = args.slice (1); + } else if (args [0].startsWith ("--setenv=")) { + var arg = args [0].substring ("--setenv=".length); + var parts = arg.split ('='); + if (parts.length != 2) + fail_exec ("Error: malformed argument: '" + args [0]); + setenv [parts [0]] = parts [1]; + args = args.slice (1); + } else if (args [0].startsWith ("--runtime-arg=")) { + var arg = args [0].substring ("--runtime-arg=".length); + runtime_args.push (arg); + args = args.slice (1); + } else if (args [0] == "--enable-gc") { + enable_gc = true; + args = args.slice (1); + } else if (args [0] == "--enable-zoneinfo") { + enable_zoneinfo = true; + args = args.slice (1); + } else { + break; + } +} +testArguments = args; + +if (typeof window == "undefined") + load ("mono-config.js"); + +var Module = { + mainScriptUrlOrBlob: "dotnet.js", + + print: function(x) { print ("WASM: " + x) }, + printErr: function(x) { print ("WASM-ERR: " + x) }, + + onAbort: function(x) { + print ("ABORT: " + x); + var err = new Error(); + print ("Stacktrace: \n"); + print (err.stack); + test_exit (1); + }, + + onRuntimeInitialized: function () { + // Have to set env vars here to enable setting MONO_LOG_LEVEL etc. + var wasm_setenv = Module.cwrap ('mono_wasm_setenv', 'void', ['string', 'string']); + for (var variable in setenv) { + MONO.mono_wasm_setenv (variable, setenv [variable]); + } + + if (enable_gc) { + var f = Module.cwrap ('mono_wasm_enable_on_demand_gc', 'void', []); + f (); + } + if (enable_zoneinfo) { + // Load the zoneinfo data into the VFS rooted at /zoneinfo + FS.mkdir("zoneinfo"); + Module['FS_createPath']('/', 'zoneinfo', true, true); + Module['FS_createPath']('/zoneinfo', 'Indian', true, true); + Module['FS_createPath']('/zoneinfo', 'Atlantic', true, true); + Module['FS_createPath']('/zoneinfo', 'US', true, true); + Module['FS_createPath']('/zoneinfo', 'Brazil', true, true); + Module['FS_createPath']('/zoneinfo', 'Pacific', true, true); + Module['FS_createPath']('/zoneinfo', 'Arctic', true, true); + Module['FS_createPath']('/zoneinfo', 'America', true, true); + Module['FS_createPath']('/zoneinfo/America', 'Indiana', true, true); + Module['FS_createPath']('/zoneinfo/America', 'Argentina', true, true); + Module['FS_createPath']('/zoneinfo/America', 'Kentucky', true, true); + Module['FS_createPath']('/zoneinfo/America', 'North_Dakota', true, true); + Module['FS_createPath']('/zoneinfo', 'Australia', true, true); + Module['FS_createPath']('/zoneinfo', 'Etc', true, true); + Module['FS_createPath']('/zoneinfo', 'Asia', true, true); + Module['FS_createPath']('/zoneinfo', 'Antarctica', true, true); + Module['FS_createPath']('/zoneinfo', 'Europe', true, true); + Module['FS_createPath']('/zoneinfo', 'Mexico', true, true); + Module['FS_createPath']('/zoneinfo', 'Africa', true, true); + Module['FS_createPath']('/zoneinfo', 'Chile', true, true); + Module['FS_createPath']('/zoneinfo', 'Canada', true, true); + var zoneInfoData = read ('zoneinfo.data', 'binary'); + var metadata = JSON.parse(read ("mono-webassembly-zoneinfo-fs-smd.js.metadata", 'utf-8')); + var files = metadata.files; + for (var i = 0; i < files.length; ++i) { + var byteArray = zoneInfoData.subarray(files[i].start, files[i].end); + var stream = FS.open(files[i].filename, 'w+'); + FS.write(stream, byteArray, 0, byteArray.length, 0); + FS.close(stream); + } + } + MONO.mono_load_runtime_and_bcl ( + config.vfs_prefix, + config.deploy_prefix, + config.enable_debugging, + config.file_list, + function () { + App.init (); + }, + function (asset) + { + if (typeof window != 'undefined') { + return fetch (asset, { credentials: 'same-origin' }); + } else { + // The default mono_load_runtime_and_bcl defaults to using + // fetch to load the assets. It also provides a way to set a + // fetch promise callback. + // Here we wrap the file read in a promise and fake a fetch response + // structure. + return new Promise((resolve, reject) => { + var response = { ok: true, url: asset, + arrayBuffer: function() { + return new Promise((resolve2, reject2) => { + resolve2(new Uint8Array (read (asset, 'binary'))); + } + )} + } + resolve(response) + }) + } + } + ); + }, +}; + +if (typeof window == "undefined") + load ("dotnet.js"); + +const IGNORE_PARAM_COUNT = -1; + +var App = { + init: function () { + + var assembly_load = Module.cwrap ('mono_wasm_assembly_load', 'number', ['string']) + var find_class = Module.cwrap ('mono_wasm_assembly_find_class', 'number', ['number', 'string', 'string']) + var find_method = Module.cwrap ('mono_wasm_assembly_find_method', 'number', ['number', 'string', 'number']) + var runtime_invoke = Module.cwrap ('mono_wasm_invoke_method', 'number', ['number', 'number', 'number', 'number']); + var string_from_js = Module.cwrap ('mono_wasm_string_from_js', 'number', ['string']); + var assembly_get_entry_point = Module.cwrap ('mono_wasm_assembly_get_entry_point', 'number', ['number']); + var string_get_utf8 = Module.cwrap ('mono_wasm_string_get_utf8', 'string', ['number']); + var string_array_new = Module.cwrap ('mono_wasm_string_array_new', 'number', ['number']); + var obj_array_set = Module.cwrap ('mono_wasm_obj_array_set', 'void', ['number', 'number', 'number']); + var exit = Module.cwrap ('mono_wasm_exit', 'void', ['number']); + var wasm_setenv = Module.cwrap ('mono_wasm_setenv', 'void', ['string', 'string']); + var wasm_set_main_args = Module.cwrap ('mono_wasm_set_main_args', 'void', ['number', 'number']); + var wasm_strdup = Module.cwrap ('mono_wasm_strdup', 'number', ['string']); + var unbox_int = Module.cwrap ('mono_unbox_int', 'number', ['number']); + + Module.wasm_exit = Module.cwrap ('mono_wasm_exit', 'void', ['number']); + + Module.print("Initializing....."); + + for (var i = 0; i < profilers.length; ++i) { + var init = Module.cwrap ('mono_wasm_load_profiler_' + profilers [i], 'void', ['string']) + + init (""); + } + + if (args[0] == "--regression") { + var exec_regression = Module.cwrap ('mono_wasm_exec_regression', 'number', ['number', 'string']) + + var res = 0; + try { + res = exec_regression (10, args[1]); + Module.print ("REGRESSION RESULT: " + res); + } catch (e) { + Module.print ("ABORT: " + e); + print (e.stack); + res = 1; + } + + if (res) + fail_exec ("REGRESSION TEST FAILED"); + + return; + } + + if (runtime_args.length > 0) + MONO.mono_wasm_set_runtime_options (runtime_args); + + if (args[0] == "--run") { + // Run an exe + if (args.length == 1) + fail_exec ("Error: Missing main executable argument."); + main_assembly = assembly_load (args[1]); + if (main_assembly == 0) + fail_exec ("Error: Unable to load main executable '" + args[1] + "'"); + main_method = assembly_get_entry_point (main_assembly); + if (main_method == 0) + fail_exec ("Error: Main (string[]) method not found."); + + var app_args = string_array_new (args.length - 2); + for (var i = 2; i < args.length; ++i) { + obj_array_set (app_args, i - 2, string_from_js (args [i])); + } + + var main_argc = args.length - 2 + 1; + var main_argv = Module._malloc (main_argc * 4); + aindex = 0; + Module.setValue (main_argv + (aindex * 4), wasm_strdup (args [1]), "i32") + aindex += 1; + for (var i = 2; i < args.length; ++i) { + Module.setValue (main_argv + (aindex * 4), wasm_strdup (args [i]), "i32"); + aindex += 1; + } + wasm_set_main_args (main_argc, main_argv); + + try { + var invoke_args = Module._malloc (4); + Module.setValue (invoke_args, app_args, "i32"); + var eh_exc = Module._malloc (4); + Module.setValue (eh_exc, 0, "i32"); + var res = runtime_invoke (main_method, 0, invoke_args, eh_exc); + var eh_res = Module.getValue (eh_exc, "i32"); + if (eh_res != 0) { + print ("Exception:" + string_get_utf8 (res)); + test_exit (1); + } + var exit_code = unbox_int (res); + if (exit_code != 0) + test_exit (exit_code); + } catch (ex) { + print ("JS exception: " + ex); + print (ex.stack); + test_exit (1); + } + + if (is_browser) + test_exit (0); + + return; + } else { + fail_exec ("Unhanded argument: " + args [0]); + } + }, +}; -- 2.7.4