Make Blazor apps actually start up Mono and execute the specified .NET entrypoint

This commit is contained in:
Steve Sanderson 2017-12-08 17:06:40 +00:00
parent 4d764d78df
commit 4138b3a049
11 changed files with 317 additions and 4 deletions

View File

@ -40,6 +40,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostedInAspNet.Client", "sa
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostedInAspNet.Server", "samples\HostedInAspNet.Server\HostedInAspNet.Server.csproj", "{F8996835-41F7-4663-91DF-3B5652ADC37D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Blazor", "src\Microsoft.Blazor\Microsoft.Blazor.csproj", "{7FD8C650-74B3-4153-AEA1-00F4F6AF393D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -94,6 +96,10 @@ Global
{F8996835-41F7-4663-91DF-3B5652ADC37D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F8996835-41F7-4663-91DF-3B5652ADC37D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F8996835-41F7-4663-91DF-3B5652ADC37D}.Release|Any CPU.Build.0 = Release|Any CPU
{7FD8C650-74B3-4153-AEA1-00F4F6AF393D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7FD8C650-74B3-4153-AEA1-00F4F6AF393D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7FD8C650-74B3-4153-AEA1-00F4F6AF393D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7FD8C650-74B3-4153-AEA1-00F4F6AF393D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -113,6 +119,7 @@ Global
{4D367450-96E9-4C8C-8B56-EED8ADE3A20D} = {F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA}
{B4335F7C-4E86-4559-821F-F1B1C75F5FAE} = {4D367450-96E9-4C8C-8B56-EED8ADE3A20D}
{F8996835-41F7-4663-91DF-3B5652ADC37D} = {4D367450-96E9-4C8C-8B56-EED8ADE3A20D}
{7FD8C650-74B3-4153-AEA1-00F4F6AF393D} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}

View File

@ -4,4 +4,8 @@
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Blazor\Microsoft.Blazor.csproj" />
</ItemGroup>
</Project>

View File

@ -6,6 +6,6 @@
</head>
<body>
<h1>Hello</h1>
<script src="/_framework/blazor.js"></script>
<script src="/_framework/blazor.js" main="HostedInAspNet.Client.dll"></script>
</body>
</html>

View File

@ -1 +1,35 @@
console.log('Blazor is loading');
import { platform } from './Environment';
import { getAssemblyNameFromUrl } from './Platform/DotNet';
async function boot() {
// Read startup config from the <script> element that's importing this file
const allScriptElems = document.getElementsByTagName('script');
const thisScriptElem = allScriptElems[allScriptElems.length - 1];
const entryPoint = thisScriptElem.getAttribute('main');
if (!entryPoint) {
throw new Error('Missing "main" attribute on Blazor script tag.');
}
const entryPointAssemblyName = getAssemblyNameFromUrl(entryPoint);
const referenceAssembliesCommaSeparated = thisScriptElem.getAttribute('references') || '';
const referenceAssemblies = referenceAssembliesCommaSeparated
.split(',')
.map(s => s.trim())
.filter(s => !!s);
// Determine the URLs of the assemblies we want to load
const loadAssemblyUrls = [entryPoint]
.concat(referenceAssemblies) // Developer-specified references
.concat(['Microsoft.Blazor.dll']) // Standard references
.map(filename => `/_bin/${filename}`);
try {
await platform.start(loadAssemblyUrls);
} catch (ex) {
throw new Error(`Failed to start platform. Reason: ${ex}`);
}
// Start up the application
platform.callEntryPoint(entryPointAssemblyName, []);
}
boot();

View File

@ -0,0 +1,6 @@
// Expose an export called 'platform' of the interface type 'Platform',
// so that consumers can be agnostic about which implementation they use.
// Basic alternative to having an actual DI container.
import { Platform } from './Platform/Platform';
import { monoPlatform } from './Platform/Mono/MonoPlatform';
export const platform: Platform = monoPlatform;

View File

@ -0,0 +1,6 @@
export function getAssemblyNameFromUrl(url: string) {
const lastSegment = url.substring(url.lastIndexOf('/') + 1);
const queryStringStartPos = lastSegment.indexOf('?');
const filename = queryStringStartPos < 0 ? lastSegment : lastSegment.substring(0, queryStringStartPos);
return filename.replace(/\.dll$/, '');
}

View File

@ -0,0 +1,186 @@
import { MethodHandle, System_Object, System_String, Platform } from '../Platform';
import { getAssemblyNameFromUrl } from '../DotNet';
let assembly_load: (assemblyName: string) => number;
let find_class: (assemblyHandle: number, namespace: string, className: string) => number;
let find_method: (typeHandle: number, methodName: string, unknownArg: number) => MethodHandle;
let invoke_method: (method: MethodHandle, target: System_Object, argsArrayPtr: number, exceptionFlagIntPtr: number) => System_Object;
let mono_string_get_utf8: (managedString: System_String) => Mono.Utf8Ptr;
let mono_string: (jsString: string) => System_String;
export const monoPlatform: Platform = {
start: function start(loadAssemblyUrls: string[]) {
return new Promise<void>((resolve, reject) => {
// mono.js assumes the existence of this
window['Browser'] = {
init: () => { },
asyncLoad: asyncLoad
};
// Emscripten works by expecting the module config to be a global
window['Module'] = createEmscriptenModuleInstance(loadAssemblyUrls, resolve, reject);
addScriptTagsToDocument();
});
},
findMethod: function findMethod(assemblyName: string, namespace: string, className: string, methodName: string): MethodHandle {
// TODO: Cache the assembly_load outputs?
const assemblyHandle = assembly_load(assemblyName);
if (!assemblyHandle) {
throw new Error(`Could not find assembly "${assemblyName}"`);
}
const typeHandle = find_class(assemblyHandle, namespace, className);
if (!typeHandle) {
throw new Error(`Could not find type "${className}'" in namespace "${namespace}" in assembly "${assemblyName}"`);
}
const methodHandle = find_method(typeHandle, methodName, -1);
if (!methodHandle) {
throw new Error(`Could not find method "${methodName}" on type "${namespace}.${className}"`);
}
return methodHandle;
},
callEntryPoint: function callEntryPoint(assemblyName: string, args: System_Object[]): void {
// TODO: There should be a proper way of running whatever counts as the entrypoint without
// having to specify what method it is, but I haven't found it. The code here assumes
// that the entry point is "<assemblyname>.Program.Main" (i.e., namespace == assembly name).
const entryPointMethod = monoPlatform.findMethod(assemblyName, assemblyName, 'Program', 'Main');
monoPlatform.callMethod(entryPointMethod, null, args);
},
callMethod: function callMethod(method: MethodHandle, target: System_Object, args: System_Object[]): System_Object {
const stack = Module.Runtime.stackSave();
try {
const argsBuffer = Module.Runtime.stackAlloc(args.length);
const exceptionFlagManagedInt = Module.Runtime.stackAlloc(4);
for (var i = 0; i < args.length; ++i) {
Module.setValue(argsBuffer + i * 4, args[i], 'i32');
}
Module.setValue(exceptionFlagManagedInt, 0, 'i32');
const res = invoke_method(method, target, argsBuffer, exceptionFlagManagedInt);
if (Module.getValue(exceptionFlagManagedInt, 'i32') !== 0) {
// If the exception flag is set, the returned value is exception.ToString()
throw new Error(monoPlatform.toJavaScriptString(<System_String>res));
}
return res;
} finally {
Module.Runtime.stackRestore(stack);
}
},
toJavaScriptString: function toJavaScriptString(managedString: System_String) {
// Comments from original Mono sample:
//FIXME this is wastefull, we could remove the temp malloc by going the UTF16 route
//FIXME this is unsafe, cuz raw objects could be GC'd.
if (!managedString) {
return null;
}
const utf8 = mono_string_get_utf8(managedString);
const res = Module.UTF8ToString(utf8);
Module._free(utf8 as any);
return res;
},
toDotNetString: function toDotNetString(jsString: string): System_String {
return mono_string(jsString);
}
};
function addScriptTagsToDocument() {
// Load either the wasm or asm.js version of the Mono runtime
const browserSupportsNativeWebAssembly = typeof WebAssembly !== 'undefined' && WebAssembly.validate;
const monoRuntimeUrlBase = '/_framework/' + (browserSupportsNativeWebAssembly ? 'wasm' : 'asmjs');
const monoRuntimeScriptUrl = `${monoRuntimeUrlBase}/mono.js`;
if (!browserSupportsNativeWebAssembly) {
// In the asmjs case, the initial memory structure is in a separate file we need to download
const meminitXHR = Module['memoryInitializerRequest'] = new XMLHttpRequest();
meminitXHR.open('GET', `${monoRuntimeUrlBase}/mono.js.mem`);
meminitXHR.responseType = 'arraybuffer';
meminitXHR.send(null);
}
document.write(`<script defer src="${monoRuntimeScriptUrl}"></script>`);
}
function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: () => void, onError: (reason: any) => void) {
const module = {} as typeof Module;
module.print = line => console.log(`WASM: ${line}`);
module.printErr = line => console.error(`WASM: ${line}`);
module.wasmBinaryFile = '/_framework/wasm/mono.wasm';
module.asmjsCodeFile = '/_framework/asmjs/mono.asm.js';
module.preRun = [];
module.postRun = [];
module.preloadPlugins = [];
module.preRun.push(() => {
// By now, emscripten should be initialised enough that we can capture these methods for later use
assembly_load = Module.cwrap('mono_wasm_assembly_load', 'number', ['string']);
find_class = Module.cwrap('mono_wasm_assembly_find_class', 'number', ['number', 'string', 'string']);
find_method = Module.cwrap('mono_wasm_assembly_find_method', 'number', ['number', 'string', 'number']);
invoke_method = Module.cwrap('mono_wasm_invoke_method', 'number', ['number', 'number', 'number']);
mono_string_get_utf8 = Module.cwrap('mono_wasm_string_get_utf8', 'number', ['number']);
mono_string = Module.cwrap('mono_wasm_string_from_js', 'number', ['string']);
const loadBclAssemblies = [
'mscorlib',
'System',
'System.Core',
'Facades/netstandard',
'Facades/System.Console',
'Facades/System.Collections',
'Facades/System.Diagnostics.Debug',
'Facades/System.IO',
'Facades/System.Linq',
'Facades/System.Reflection',
'Facades/System.Reflection.Extensions',
'Facades/System.Runtime',
'Facades/System.Runtime.Extensions',
'Facades/System.Runtime.InteropServices',
'Facades/System.Threading',
'Facades/System.Threading.Tasks'
];
var allAssemblyUrls = loadAssemblyUrls
.concat(loadBclAssemblies.map(name => `_framework/bcl/${name}.dll`));
Module.FS_createPath('/', 'appBinDir', true, true);
allAssemblyUrls.forEach(url =>
FS.createPreloadedFile('appBinDir', `${getAssemblyNameFromUrl(url)}.dll`, url, true, false, null, <any>onError));
});
module.postRun.push(() => {
const load_runtime = Module.cwrap('mono_wasm_load_runtime', null, ['string']);
load_runtime('appBinDir');
onReady();
});
return module;
}
function asyncLoad(url, onload, onerror) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, /* async: */ true);
xhr.responseType = 'arraybuffer';
xhr.onload = function xhr_onload() {
if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
var asm = new Uint8Array(xhr.response);
onload(asm);
} else {
onerror(xhr);
}
};
xhr.onerror = onerror;
xhr.send(null);
}

View File

@ -0,0 +1,14 @@
declare namespace Module {
function UTF8ToString(utf8: Mono.Utf8Ptr): string;
var preloadPlugins: any[];
// These should probably be in @types/emscripten
var wasmBinaryFile: string;
var asmjsCodeFile: string;
function FS_createPath(parent, path, canRead, canWrite);
function FS_createDataFile(parent, name, data, canRead, canWrite, canOwn);
}
declare namespace Mono {
interface Utf8Ptr { Utf8Ptr__DO_NOT_IMPLEMENT: any }
}

View File

@ -0,0 +1,17 @@
export interface Platform {
start(loadAssemblyUrls: string[]): Promise<void>;
callEntryPoint(assemblyName: string, args: System_Object[]);
findMethod(assemblyName: string, namespace: string, className: string, methodName: string): MethodHandle;
callMethod(method: MethodHandle, target: System_Object, args: System_Object[]): System_Object;
toJavaScriptString(dotNetString: System_String): string;
toDotNetString(javaScriptString: string): System_String;
}
// We don't actually instantiate any of these at runtime. For perf it's preferable to
// use the original 'number' instances without any boxing. The definitions are just
// for compile-time checking, since TypeScript doesn't support nominal types.
export interface MethodHandle { MethodHandle__DO_NOT_IMPLEMENT: any };
export interface System_Object { System_Object__DO_NOT_IMPLEMENT: any };
export interface System_String extends System_Object { System_String__DO_NOT_IMPLEMENT: any }

View File

@ -2,9 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Mime;
namespace Microsoft.AspNetCore.Builder
{
@ -18,12 +22,40 @@ namespace Microsoft.AspNetCore.Builder
var sourcePath = Path.Combine(env.ContentRootPath, relativeSourcePath);
ServeWebRoot(applicationBuilder, sourcePath);
ServeClientBinDir(applicationBuilder, sourcePath);
}
private static void ServeWebRoot(IApplicationBuilder applicationBuilder, string sourcePath)
private static void ServeClientBinDir(IApplicationBuilder applicationBuilder, string clientAppSourceRoot)
{
var clientBinDirPath = FindClientBinDir(clientAppSourceRoot);
applicationBuilder.UseStaticFiles(new StaticFileOptions
{
RequestPath = "/_bin",
FileProvider = new PhysicalFileProvider(clientBinDirPath),
ContentTypeProvider = new FileExtensionContentTypeProvider(new Dictionary<string, string>
{
{ ".dll", MediaTypeNames.Application.Octet },
})
});
}
private static string FindClientBinDir(string clientAppSourceRoot)
{
var binDebugDir = Path.Combine(clientAppSourceRoot, "bin", "Debug");
var subdirectories = Directory.GetDirectories(binDebugDir);
if (subdirectories.Length != 1)
{
throw new InvalidOperationException($"Could not locate bin directory for Blazor app. " +
$"Expected to find exactly 1 subdirectory in '{binDebugDir}', but found {subdirectories.Length}.");
}
return Path.Combine(binDebugDir, subdirectories[0]);
}
private static void ServeWebRoot(IApplicationBuilder applicationBuilder, string clientAppSourceRoot)
{
var webRootFileProvider = new PhysicalFileProvider(
Path.Combine(sourcePath, "wwwroot"));
Path.Combine(clientAppSourceRoot, "wwwroot"));
applicationBuilder.UseDefaultFiles(new DefaultFilesOptions
{

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>