Add a WasmAppBuilder task and a sample. (#36338)
authorZoltan Varga <vargaz@gmail.com>
Wed, 13 May 2020 17:10:57 +0000 (13:10 -0400)
committerGitHub <noreply@github.com>
Wed, 13 May 2020 17:10:57 +0000 (19:10 +0200)
src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.cs [new file with mode: 0644]
src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.csproj
src/mono/netcore/sample/wasm/Makefile [new file with mode: 0644]
src/mono/netcore/sample/wasm/Program.cs [new file with mode: 0644]
src/mono/netcore/sample/wasm/WasmSample.csproj [new file with mode: 0644]
src/mono/netcore/sample/wasm/runtime.js [new file with mode: 0644]

diff --git a/src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.cs b/src/mono/msbuild/WasmAppBuilder/WasmAppBuilder.cs
new file mode 100644 (file)
index 0000000..77538e6
--- /dev/null
@@ -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<string, Assembly>? 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<string> ();
+        Assemblies = new Dictionary<string, Assembly> ();
+
+        // 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<String> SearchPaths;
+
+    public Resolver (List<string> 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;
+    }
+}
index 271afb9..aa09da9 100644 (file)
@@ -12,5 +12,6 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="PInvokeTableGenerator.cs" />
+    <Compile Include="WasmAppBuilder.cs" />
   </ItemGroup>
 </Project>
diff --git a/src/mono/netcore/sample/wasm/Makefile b/src/mono/netcore/sample/wasm/Makefile
new file mode 100644 (file)
index 0000000..09c42a4
--- /dev/null
@@ -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 (file)
index 0000000..58a6f57
--- /dev/null
@@ -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 (file)
index 0000000..588f292
--- /dev/null
@@ -0,0 +1,40 @@
+<Project Sdk="Microsoft.NET.Sdk" DefaultTargets="BuildApp">
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <OutputPath>bin</OutputPath>
+    <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
+    <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+    <TargetArchitecture>wasm</TargetArchitecture>
+    <TargetOS>Browser</TargetOS>
+    <RuntimePackDir>$(ArtifactsDir)bin\lib-runtime-packs\$(NetCoreAppCurrent)-$(TargetOS)-$(Configuration)-$(TargetArchitecture)\runtimes\browser-wasm</RuntimePackDir>
+    <BuildDir>$(MSBuildThisFileDirectory)\obj\$(Configuration)\wasm</BuildDir>
+    <AppDir>$(MSBuildThisFileDirectory)\bin\$(Configuration)\publish</AppDir>
+  </PropertyGroup>
+
+  <Target Name="RebuildWasmAppBuilder">
+    <MSBuild Projects="$(RepoRoot)src\mono\msbuild\WasmAppBuilder\WasmAppBuilder.csproj"
+             Properties="Configuration=$(Configuration)" Targets="Restore;Build;Publish" />
+  </Target>
+
+  <UsingTask TaskName="WasmAppBuilder" 
+      AssemblyFile="$(ArtifactsBinDir)\WasmAppBuilder\$(NetCoreAppCurrent)-$(Configuration)\publish\WasmAppBuilder.dll"/>
+
+  <Target Name="BuildApp" DependsOnTargets="RebuildWasmAppBuilder;Build">
+    <ItemGroup>
+      <AssemblySearchPaths Include="bin"/>
+      <AssemblySearchPaths Include="$(RuntimePackDir)\native"/>
+      <AssemblySearchPaths Include="$(RuntimePackDir)\lib\$(NetCoreAppCurrent)"/>
+    </ItemGroup>
+    <WasmAppBuilder
+      AppDir="$(AppDir)"
+      RuntimePackDir="$(RuntimePackDir)"
+      MainAssembly="bin\WasmSample.dll"
+      MainJS="runtime.js"
+      AssemblySearchPaths="@(AssemblySearchPaths)"/>
+    <Exec Command="chmod a+x $(AppDir)/run-v8.sh" />
+  </Target>
+
+  <ItemGroup>
+    <Compile Include="Program.cs" />
+  </ItemGroup>
+</Project>
diff --git a/src/mono/netcore/sample/wasm/runtime.js b/src/mono/netcore/sample/wasm/runtime.js
new file mode 100644 (file)
index 0000000..10e4fa6
--- /dev/null
@@ -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]);
+               }
+    },
+};