Make Blazor apps actually start up Mono and execute the specified .NET entrypoint
This commit is contained in:
parent
4d764d78df
commit
4138b3a049
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,8 @@
|
|||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Blazor\Microsoft.Blazor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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$/, '');
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in New Issue