diff --git a/src/coreclr/nativeaot/BuildIntegration/DotNetJsApi.targets b/src/coreclr/nativeaot/BuildIntegration/DotNetJsApi.targets index ae5549d13825..7f64fcbe55e2 100644 --- a/src/coreclr/nativeaot/BuildIntegration/DotNetJsApi.targets +++ b/src/coreclr/nativeaot/BuildIntegration/DotNetJsApi.targets @@ -1,7 +1,8 @@ - $(LinkNativeDependsOn);PrepareDotNetJsApiForLinking + $(LinkNativeDependsOn);PrepareDotNetJsApiForLinking $(NativeOutputPath)dotnet.native.js + true @@ -52,7 +53,7 @@ <_DotNetJsLinkerFlag Include="-Wl,--export,__main_argc_argv" /> <_DotNetJsLinkerFlag Include="-s EXPORT_ES6=1" /> <_DotNetJsLinkerFlag Include="-s MODULARIZE=1" /> - <_DotNetJsLinkerFlag Include="-s INVOKE_RUN=0" /> + <_DotNetJsLinkerFlag Include="-s INVOKE_RUN=0" /> <_DotNetJsLinkerFlag Include="-s EXPORT_NAME="'createDotnetRuntime'"" /> <_DotNetJsLinkerFlag Include="-s ENVIRONMENT="'web,webview,worker,node,shell'"" /> <_DotNetJsLinkerFlag Condition="'$(EmccEnvironment)' != ''" Include="-s ENVIRONMENT="$(EmccEnvironment)"" /> @@ -72,18 +73,26 @@ <_DotNetJsLinkerFlag Include="-s DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=$(_EmccExportedLibraryFunction)" Condition="'$(_EmccExportedLibraryFunction)' != ''" /> <_DotNetJsLinkerFlag Include="-s EXPORTED_RUNTIME_METHODS=$(_EmccExportedRuntimeMethods)" /> - <_DotNetJsLinkerFlag Include="-s EXPORTED_FUNCTIONS=$(_EmccExportedFunctions)" /> + <_DotNetJsLinkerFlag Include="-s EXPORTED_FUNCTIONS=$(_EmccExportedFunctions)" Condition="'$(ExportsFile)' == ''" /> <_DotNetJsLinkerFlag Include="$(EmccExtraLDFlags)" /> + + + + + <_ExportsToAddToExportsFile Include="@(EmccExportedFunction)" /> + <_ExportsToAddToExportsFile Remove="@(_ExistingExports)" /> + + <_FilesToCopyToNative Include="$(IlcFrameworkNativePath)\dotnet*.js" /> <_FilesToCopyToNative Include="$(IlcFrameworkNativePath)\dotnet*.map" Condition="'$(WasmEmitSourceMap)' == 'true'" /> - <_FilesToCopyToNative Include="@(WasmExtraFilesToDeploy)" /> + <_FilesToCopyToNative Include="@(WasmExtraFilesToDeploy)" /> diff --git a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets index db21edcfdc3c..ad030a034ee2 100644 --- a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets +++ b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets @@ -56,6 +56,17 @@ The .NET Foundation licenses this file to you under the MIT license. false + + + IlcCompile + CompileWasmObjects + + LinkNativeSingle + LinkNativeLlvm + + + + .obj .o @@ -87,18 +98,12 @@ The .NET Foundation licenses this file to you under the MIT license. .exports $(NativeIntermediateOutputPath)$(TargetName)$(NativeObjectExt) - $(NativeOutputPath)$(TargetName)$(NativeBinaryExt) + $(NativeOutputPath)$(TargetName)$(NativeBinaryExt) true $(NativeIntermediateOutputPath)$(TargetName)$(ExportsFileExt) $(NativeObject) - IlcCompile - CompileWasmObjects - - LinkNativeSingle - LinkNativeLlvm - $(NativeOutputPath) $(NativeIntermediateOutputPath) @@ -492,8 +497,6 @@ The .NET Foundation licenses this file to you under the MIT license. strip -no_code_signature_warning $(_StripFlag) "$(NativeBinary)"" /> - - - + + + diff --git a/src/mono/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts index 65a0f160d7e3..5f5719424725 100644 --- a/src/mono/wasm/runtime/invoke-cs.ts +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +/* eslint-disable prefer-rest-params */ +import NativeAOT from "consts:nativeAOT"; import BuildConfiguration from "consts:configuration"; import MonoWasmThreads from "consts:monoWasmThreads"; @@ -13,6 +15,7 @@ import { } from "./marshal"; import { mono_wasm_new_external_root, mono_wasm_new_root } from "./roots"; import { monoStringToString } from "./strings"; +import { utf16ToString } from "./strings"; import { MonoObjectRef, MonoStringRef, MonoString, MonoObject, MonoMethod, JSMarshalerArguments, JSFunctionSignature, BoundMarshalerToCs, BoundMarshalerToJs, VoidPtrNull, MonoObjectRefNull, MonoObjectNull, MarshalerType } from "./types/internal"; import { Int32Ptr } from "./types/emscripten"; import cwraps from "./cwraps"; @@ -22,34 +25,48 @@ import { startMeasure, MeasuredBlock, endMeasure } from "./profiler"; import { mono_log_debug } from "./logging"; import { assert_synchronization_context } from "./pthreads/shared"; +// function mono_wasm_bind_cs_function_naot(fully_qualified_name: CharPtr, fully_qualified_name_length: number, signature_hash: number, signature: JSFunctionSignature, is_exception: Int32Ptr): void export function mono_wasm_bind_cs_function(fully_qualified_name: MonoStringRef, signature_hash: number, signature: JSFunctionSignature, is_exception: Int32Ptr, result_address: MonoObjectRef): void { assert_bindings(); const fqn_root = mono_wasm_new_external_root(fully_qualified_name), resultRoot = mono_wasm_new_external_root(result_address); const mark = startMeasure(); try { + if (NativeAOT) { + signature_hash = arguments[2]; + signature = arguments[3]; + is_exception = arguments[4]; + } const version = get_signature_version(signature); mono_assert(version === 2, () => `Signature version ${version} mismatch.`); const args_count = get_signature_argument_count(signature); - const js_fqn = monoStringToString(fqn_root)!; + const js_fqn = NativeAOT ? utf16ToString(arguments[0], arguments[0] + 2 * arguments[1]) : monoStringToString(fqn_root)!; mono_assert(js_fqn, "fully_qualified_name must be string"); mono_log_debug(`Binding [JSExport] ${js_fqn}`); const { assembly, namespace, classname, methodname } = parseFQN(js_fqn); - const asm = assembly_load(assembly); - if (!asm) - throw new Error("Could not find assembly: " + assembly); - - const klass = cwraps.mono_wasm_assembly_find_class(asm, namespace, classname); - if (!klass) - throw new Error("Could not find class: " + namespace + ":" + classname + " in assembly " + assembly); - - const wrapper_name = `__Wrapper_${methodname}_${signature_hash}`; - const method = cwraps.mono_wasm_assembly_find_method(klass, wrapper_name, -1); - if (!method) - throw new Error(`Could not find method: ${wrapper_name} in ${klass} [${assembly}]`); + let method = null; + if (NativeAOT) { + const wrapper_name = fixupSymbolName(`${js_fqn}_${signature_hash}`); + method = (Module as any)["_" + wrapper_name]; + if (!method) + throw new Error(`Could not find method: ${wrapper_name} in ${js_fqn}`); + } else { + const asm = assembly_load(assembly); + if (!asm) + throw new Error("Could not find assembly: " + assembly); + + const klass = cwraps.mono_wasm_assembly_find_class(asm, namespace, classname); + if (!klass) + throw new Error("Could not find class: " + namespace + ":" + classname + " in assembly " + assembly); + + const wrapper_name = `__Wrapper_${methodname}_${signature_hash}`; + method = cwraps.mono_wasm_assembly_find_method(klass, wrapper_name, -1); + if (!method) + throw new Error(`Could not find method: ${wrapper_name} in ${klass} [${assembly}]`); + } const arg_marshalers: (BoundMarshalerToCs)[] = new Array(args_count); for (let index = 0; index < args_count; index++) { @@ -122,6 +139,28 @@ export function mono_wasm_bind_cs_function(fully_qualified_name: MonoStringRef, } } +const s_charsToReplace = [".", "-", "+"]; + +function fixupSymbolName(name: string) { + // Sync with JSExportGenerator.FixupSymbolName + let result = ""; + for (let index = 0; index < name.length; index++) { + const b = name[index]; + if ((b >= "0" && b <= "9") || + (b >= "a" && b <= "z") || + (b >= "A" && b <= "Z") || + (b == "_")) { + result += b; + } else if( s_charsToReplace.includes(b)) { + result += "_"; + } else { + result += `_${b.charCodeAt(0).toString(16).toUpperCase()}_`; + } + } + + return result; +} + function bind_fn_0V(closure: BindingClosure) { const method = closure.method; const fqn = closure.fqn; @@ -266,7 +305,7 @@ type BindingClosure = { isDisposed: boolean, } -export function invoke_method_and_handle_exception(method: MonoMethod, args: JSMarshalerArguments): void { +function invoke_method_and_handle_exception_mono(method: MonoMethod, args: JSMarshalerArguments): void { assert_bindings(); const fail_root = mono_wasm_new_root(); try { @@ -282,6 +321,16 @@ export function invoke_method_and_handle_exception(method: MonoMethod, args: JSM } } +function invoke_method_and_handle_exception_naot(method: Function, args: JSMarshalerArguments): void { + method(args); + if (is_args_exception(args)) { + const exc = get_arg(args, 0); + throw marshal_exception_to_js(exc); + } +} + +export const invoke_method_and_handle_exception: (method: any, args: JSMarshalerArguments) => void = NativeAOT ? invoke_method_and_handle_exception_naot : invoke_method_and_handle_exception_mono; + export const exportsByAssembly: Map = new Map(); function _walk_exports_to_set_function(assembly: string, namespace: string, classname: string, methodname: string, signature_hash: number, fn: Function): void { const parts = `${namespace}.${classname}`.replace(/\//g, ".").split("."); @@ -312,7 +361,7 @@ function _walk_exports_to_set_function(assembly: string, namespace: string, clas scope[`${methodname}.${signature_hash}`] = fn; } -export async function mono_wasm_get_assembly_exports(assembly: string): Promise { +async function mono_wasm_get_assembly_exports_mono(assembly: string): Promise { assert_bindings(); const result = exportsByAssembly.get(assembly); if (!result) { @@ -351,6 +400,28 @@ export async function mono_wasm_get_assembly_exports(assembly: string): Promise< return exportsByAssembly.get(assembly) || {}; } +async function mono_wasm_get_assembly_exports_naot(assembly: string): Promise { + assert_bindings(); + const result = exportsByAssembly.get(assembly); + if (!result) { + const mark = startMeasure(); + + let assemblyWithoutExtension = assembly; + if (assemblyWithoutExtension.endsWith(".dll")) { + assemblyWithoutExtension = assemblyWithoutExtension.substring(0, assembly.length - 4); + } + const register = (Module as any)["_" + assemblyWithoutExtension + "__GeneratedInitializer" + "__Register_"]; + mono_assert(register, `Missing wasm export for JSExport registration function in assembly ${assembly}`); + register(); + + endMeasure(mark, MeasuredBlock.getAssemblyExports, assembly); + } + + return exportsByAssembly.get(assembly) || {}; +} + +export const mono_wasm_get_assembly_exports = NativeAOT ? mono_wasm_get_assembly_exports_naot : mono_wasm_get_assembly_exports_mono; + export function parseFQN(fqn: string) : { assembly: string, namespace: string, classname: string, methodname: string } { const assembly = fqn.substring(fqn.indexOf("[") + 1, fqn.indexOf("]")).trim(); diff --git a/src/mono/wasm/runtime/invoke-js.ts b/src/mono/wasm/runtime/invoke-js.ts index 2f75aa181d66..3e8760be7049 100644 --- a/src/mono/wasm/runtime/invoke-js.ts +++ b/src/mono/wasm/runtime/invoke-js.ts @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +/* eslint-disable prefer-rest-params */ +import NativeAOT from "consts:nativeAOT"; import MonoWasmThreads from "consts:monoWasmThreads"; import BuildConfiguration from "consts:configuration"; @@ -8,6 +10,7 @@ import { marshal_exception_to_cs, bind_arg_marshal_to_cs } from "./marshal-to-cs import { get_signature_argument_count, bound_js_function_symbol, get_sig, get_signature_version, get_signature_type, imported_js_function_symbol } from "./marshal"; import { setI32, setI32_unchecked, receiveWorkerHeapViews } from "./memory"; import { monoStringToString, stringToMonoStringRoot } from "./strings"; +import { utf16ToString } from "./strings"; import { MonoObject, MonoObjectRef, MonoString, MonoStringRef, JSFunctionSignature, JSMarshalerArguments, WasmRoot, BoundMarshalerToJs, JSFnHandle, BoundMarshalerToCs, JSHandle, MarshalerType } from "./types/internal"; import { Int32Ptr } from "./types/emscripten"; import { INTERNAL, Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; @@ -21,18 +24,25 @@ import { assert_synchronization_context } from "./pthreads/shared"; export const fn_wrapper_by_fn_handle: Function[] = [null];// 0th slot is dummy, main thread we free them on shutdown. On web worker thread we free them when worker is detached. +// function mono_wasm_bind_js_function_naot(function_name: CharPtr, function_name_length: number, module_name: CharPtr, module_name_length: number, signature: JSFunctionSignature, function_js_handle: Int32Ptr, is_exception: Int32Ptr): void export function mono_wasm_bind_js_function(function_name: MonoStringRef, module_name: MonoStringRef, signature: JSFunctionSignature, function_js_handle: Int32Ptr, is_exception: Int32Ptr, result_address: MonoObjectRef): void { assert_bindings(); const function_name_root = mono_wasm_new_external_root(function_name), module_name_root = mono_wasm_new_external_root(module_name), resultRoot = mono_wasm_new_external_root(result_address); try { + if (NativeAOT) { + signature = arguments[4]; + function_js_handle = arguments[5]; + is_exception = arguments[6]; + } + const version = get_signature_version(signature); mono_assert(version === 2, () => `Signature version ${version} mismatch.`); - const js_function_name = monoStringToString(function_name_root)!; + const js_function_name = NativeAOT ? utf16ToString(arguments[0], arguments[0] + 2 * arguments[1]) : monoStringToString(function_name_root)!; const mark = startMeasure(); - const js_module_name = monoStringToString(module_name_root)!; + const js_module_name = NativeAOT ? utf16ToString(arguments[2], arguments[2] + 2 * arguments[3]) : monoStringToString(module_name_root)!; mono_log_debug(`Binding [JSImport] ${js_function_name} from ${js_module_name} module`); const fn = mono_wasm_lookup_function(js_function_name, js_module_name); @@ -389,6 +399,10 @@ function _wrap_error_flag(is_exception: Int32Ptr | null, ex: any): string { export function wrap_error_root(is_exception: Int32Ptr | null, ex: any, result: WasmRoot): void { const res = _wrap_error_flag(is_exception, ex); + if (NativeAOT) { + return; + } + stringToMonoStringRoot(res, result); } diff --git a/src/mono/wasm/runtime/marshal-to-js.ts b/src/mono/wasm/runtime/marshal-to-js.ts index 21af5460110e..cbe9cf748f37 100644 --- a/src/mono/wasm/runtime/marshal-to-js.ts +++ b/src/mono/wasm/runtime/marshal-to-js.ts @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import NativeAOT from "consts:nativeAOT"; import MonoWasmThreads from "consts:monoWasmThreads"; import BuildConfiguration from "consts:configuration"; @@ -324,6 +325,9 @@ export function marshal_exception_to_js(arg: JSMarshalerArgument): Error | null if (type == MarshalerType.None) { return null; } + if (NativeAOT) { + return new Error("C# exception from NativeAOT"); // TODO-LLVM-JSInterop: Marshal exception message + } if (type == MarshalerType.JSException) { // this is JSException roundtrip const js_handle = get_arg_js_handle(arg); diff --git a/src/mono/wasm/runtime/roots.ts b/src/mono/wasm/runtime/roots.ts index 21f9f18077b7..e34e56f4c1b1 100644 --- a/src/mono/wasm/runtime/roots.ts +++ b/src/mono/wasm/runtime/roots.ts @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import NativeAOT from "consts:nativeAOT"; import cwraps from "./cwraps"; import { Module } from "./globals"; import { VoidPtr, ManagedPointer, NativePointer } from "./types/emscripten"; @@ -60,6 +61,15 @@ export function mono_wasm_new_root_buffer_from_pointer(offset: VoidPtr, capacity * Releasing this root will not de-allocate the root space. You still need to call .release(). */ export function mono_wasm_new_external_root(address: VoidPtr | MonoObjectRef): WasmRoot { + if (NativeAOT) { + return { + // eslint-disable-next-line @typescript-eslint/no-empty-function + release: () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + clear: () => {} + } as unknown as WasmRoot; + } + let result: WasmExternalRoot; if (!address) diff --git a/src/tests/nativeaot/SmokeTests/DotnetJs/DotnetJs.csproj b/src/tests/nativeaot/SmokeTests/DotnetJs/DotnetJs.csproj index 6f412ade2443..cd14c2a1310c 100644 --- a/src/tests/nativeaot/SmokeTests/DotnetJs/DotnetJs.csproj +++ b/src/tests/nativeaot/SmokeTests/DotnetJs/DotnetJs.csproj @@ -7,6 +7,9 @@ true + + + diff --git a/src/tests/nativeaot/SmokeTests/DotnetJs/Program.cs b/src/tests/nativeaot/SmokeTests/DotnetJs/Program.cs index cdaf3d606e2f..46609c2a278e 100644 --- a/src/tests/nativeaot/SmokeTests/DotnetJs/Program.cs +++ b/src/tests/nativeaot/SmokeTests/DotnetJs/Program.cs @@ -1,6 +1,9 @@ using System; +using System.Runtime.InteropServices.JavaScript; -class Program +namespace DotnetJsApp; + +partial class Program { static int Main(string[] args) { @@ -8,8 +11,30 @@ static int Main(string[] args) Console.WriteLine($"Args {String.Join(", ", args)}"); if (args.Length != 3 || args[0] != "A" || args[1] != "B" || args[2] != "C") - return 1; + return 11; + + var mathResult = Interop.Math(1, 2, 3); + Console.WriteLine($"Math result is '{mathResult}'"); + if (mathResult != 7) + return 12; return 100; } -} \ No newline at end of file + + static partial class Interop + { + [JSImport("interop.math", "main.js")] + internal static partial int Math(int a, int b, int c); + + [JSExport] + internal static int Square(int x) + { + var result = x * x; + Console.WriteLine($"Computing square of '{x}' with result '{result}'"); + return result; + } + + [JSExport] + internal static void Throw() => throw new Exception("This is a test exception"); + } +} diff --git a/src/tests/nativeaot/SmokeTests/DotnetJs/wwwroot/main.js b/src/tests/nativeaot/SmokeTests/DotnetJs/wwwroot/main.js index 3d91ca60294c..9dde8fbe6025 100644 --- a/src/tests/nativeaot/SmokeTests/DotnetJs/wwwroot/main.js +++ b/src/tests/nativeaot/SmokeTests/DotnetJs/wwwroot/main.js @@ -3,10 +3,30 @@ import { dotnet, exit } from './dotnet.js' -const { runMain } = await dotnet +const { runMain, setModuleImports, getAssemblyExports } = await dotnet .withApplicationArguments("A", "B", "C") .create(); -var result = await runMain(); +setModuleImports('main.js', { + interop: { + math: (a, b, c) => a + b * c, + } +}); + +let result = await runMain(); + +const exports = await getAssemblyExports("DotnetJs.dll"); +const square = exports.DotnetJsApp.Program.Interop.Square(5); +if (square != 25) { + result = 13; +} + +try { + exports.DotnetJsApp.Program.Interop.Throw(); + result = 14; +} catch (e) { + console.log(`Thrown expected exception: ${e}`); +} + console.log(`Exit code ${result}`); -exit(result); \ No newline at end of file +exit(result);