From 7cf5a6357552a2e2487ffadbfa30cbabf2b23b45 Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Thu, 21 Jan 2021 01:59:20 -0800 Subject: [PATCH 1/3] This is a combination of 236 commits. Add test coverage for date marshaling Add uri test Disable some log statements Returning structs works Returning custom classes works Add null checks to describe_value Remove invalid cwrap Add trimming annotations Restore old benchmark html Reuse result and exception roots Cleaner codegen Disambiguate converters in devtools Transition back to static methods, add signature checks and more error handling Implement a simple bound method cache so that automated tests don't exhaust the scratch root buffer; make the scratch root buffer smaller Fix a LMF leak Fix C warnings Hard-coded table of marshalers is no longer needed Shorter wrapper function names for better stacks Add linker exclusions for custom marshalers Hack around bug in linker/runtime that causes System.Uri forwarder to break Rework class lookup Support passing numeric values as DateTime Transition some null returns to asserts Clean up conditionals Don't use ISO strings Change pre/post filter syntax to require an explicit "return" so it can contain multiple statements Allow ToJavaScript to accept a pointer instead of an 'in' reference Support managed pointer return/parameter types in more places Add marshal_type for pointers Add tests verifying that you can marshal structs by address and then unpack them Initial gc safety work Fix formatting rule Rename pre/post filters Fix type error in driver.c remove _pick_result_chara_for_marshal_type Add library build descriptor to ensure marshalers are not stripped when the BCL is trimmed Attempt to fix the linker stripping test code Better version of no-configured-marshaler warning Annotate SetupJSContinuation Annotate safehandle APIs and add null checks Update targets file to pass custom marshaler msbuild items through to the helix proxy project (requires another change to work) Add tests for Task and ValueTask marshaling Fix unboxing for generic structs Rebase cleanups Correct datetime test to use UTC for comparison Eliminate use of MONO. and BINDING. in closures Optimize out a js->c call Normalize some APIs to take MonoType instead of MonoClass Repair merge damage Address PR feedback Move some types around Repair merge damage Type system fixes Rework create_named_function so that it can handle larger numbers of closure keys more efficiently Remove unnecessary test instrumentation Fix closure variables being generated in the wrong place Use a single memory slab for temp_malloc Checkpoint span support Fix unbuffered filters, support ReadOnlySpan Fix auto signatures for primitives and add test Use 4-chara unicode escapes since the x escapes are not officially permitted in JSON Checkpoint C# implementation of converter generator Align everything by 8 when constructing argument buffers because if you don't do that, the runtime passes corrupt data to C# functions Don't shove raw mono pointers into root buffers since we weren't doing it before (it might be nice to do it though) Fully transition over to having C# generate signature converters C# implementation of bind_method codegen Clean up the bindings named closure table management Add some comments Don't allocate a root for the this-ref when binding methods if the this-ref is null Don't generate dead code for void signatures Remove some dead code and refactor bound method generator Generate more specialized result handling code for bound methods. Align C# and C's idea of marshal types better. Fix GC safety issue Fix tsc wasting 70 seconds on every build Add basic build profiling information Fix root index being incorrect Rename MemOffset and MemValue Remove bind_method this_arg support Move some stuff around Change nested type descriptor syntax --- eng/testing/tests.wasm.targets | 5 + .../ILLink.Descriptors.LibraryBuild.xml | 9 + .../src/ILLink/ILLink.Descriptors.xml | 8 + ....Runtime.InteropServices.JavaScript.csproj | 5 + .../InteropServices/JavaScript/Codegen.cs | 474 +++++++++++++ .../JavaScript/DateTimeMarshaler.cs | 36 + .../JavaScript/DateTimeOffsetMarshaler.cs | 26 + .../JavaScript/JSObject.References.cs | 6 +- .../InteropServices/JavaScript/Runtime.cs | 492 +++++++++---- .../InteropServices/JavaScript/Types.cs | 114 +++ .../JavaScript/UriMarshaler.cs | 26 + .../tests/ILLink.Descriptors.xml | 8 + ...me.InteropServices.JavaScript.Tests.csproj | 12 + .../JavaScript/HelperMarshal.cs | 189 ++++- .../JavaScript/JavaScriptTests.cs | 4 +- .../JavaScript/MarshalTests.cs | 233 +++++- src/mono/mono/metadata/class-accessors.c | 4 +- src/mono/wasm/build/README.md | 10 + src/mono/wasm/build/WasmApp.targets | 11 +- src/mono/wasm/runtime/corebindings.ts | 18 +- src/mono/wasm/runtime/cs-to-js.ts | 62 +- src/mono/wasm/runtime/custom-marshaler.ts | 371 ++++++++++ src/mono/wasm/runtime/cwraps.ts | 4 +- src/mono/wasm/runtime/debug.ts | 2 +- src/mono/wasm/runtime/dotnet.d.ts | 50 +- src/mono/wasm/runtime/driver.c | 141 ++-- src/mono/wasm/runtime/exports.ts | 11 +- src/mono/wasm/runtime/js-to-cs.ts | 9 +- src/mono/wasm/runtime/memory.ts | 78 +- src/mono/wasm/runtime/method-binding.ts | 669 ++++++------------ src/mono/wasm/runtime/method-calls.ts | 64 +- src/mono/wasm/runtime/startup.ts | 26 +- src/mono/wasm/runtime/strings.ts | 4 +- src/mono/wasm/runtime/types.ts | 70 +- src/tasks/WasmAppBuilder/WasmAppBuilder.cs | 43 +- 35 files changed, 2443 insertions(+), 851 deletions(-) create mode 100644 src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/ILLink/ILLink.Descriptors.LibraryBuild.xml create mode 100644 src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/ILLink/ILLink.Descriptors.xml create mode 100644 src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Codegen.cs create mode 100644 src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeMarshaler.cs create mode 100644 src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeOffsetMarshaler.cs create mode 100644 src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Types.cs create mode 100644 src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/UriMarshaler.cs create mode 100644 src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/ILLink.Descriptors.xml create mode 100644 src/mono/wasm/runtime/custom-marshaler.ts diff --git a/eng/testing/tests.wasm.targets b/eng/testing/tests.wasm.targets index 273ab76ef90a3f..d017b726c32772 100644 --- a/eng/testing/tests.wasm.targets +++ b/eng/testing/tests.wasm.targets @@ -163,6 +163,11 @@ <_WasmVFSFilesToCopy Include="@(WasmFilesToIncludeInFileSystem)" /> <_WasmVFSFilesToCopy TargetPath="%(FileName)%(Extension)" Condition="'%(TargetPath)' == ''" /> + + <_WasmItemsToPass Include="@(WasmMarshaledType)" OriginalItemName__="WasmMarshaledType" /> + + + + + + + diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/ILLink/ILLink.Descriptors.xml b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/ILLink/ILLink.Descriptors.xml new file mode 100644 index 00000000000000..2b22b327ed2468 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/ILLink/ILLink.Descriptors.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System.Private.Runtime.InteropServices.JavaScript.csproj b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System.Private.Runtime.InteropServices.JavaScript.csproj index e6991801ba5c99..afe692c9f46b6b 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System.Private.Runtime.InteropServices.JavaScript.csproj +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System.Private.Runtime.InteropServices.JavaScript.csproj @@ -7,6 +7,7 @@ + @@ -31,6 +32,10 @@ + + + + diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Codegen.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Codegen.cs new file mode 100644 index 00000000000000..ad4f1588554862 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Codegen.cs @@ -0,0 +1,474 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript +{ + public static unsafe class Codegen + { + public static readonly int PointerSize = sizeof(IntPtr); + // HACK: Unless we align all the argument values in the heap by this amount, + // certain parameter types will be garbled when received by target C# functions + public const int IndirectAddressAlignment = 8; + + private static readonly Dictionary FastUnboxHandlers = new Dictionary { + { MarshalType.INT, "getI32(unboxBuffer)" }, + { MarshalType.POINTER, "getU32(unboxBuffer)" }, // FIXME: Is this right? + { MarshalType.UINT32, "getU32(unboxBuffer)" }, + { MarshalType.FP32, "getF32(unboxBuffer)" }, + { MarshalType.FP64, "getF64(unboxBuffer)" }, + { MarshalType.BOOL, "getI32(unboxBuffer) !== 0" }, + { MarshalType.CHAR, "String.fromCharCode(getI32(unboxBuffer))" }, + }; + + public abstract class BuilderStateBase { + public MarshalString MarshalString; + public StringBuilder Output = new StringBuilder(); + public HashSet ClosureReferences = new HashSet(); + } + + public class MarshalBuilderState : BuilderStateBase { + public HashSet<(string, int)> TypeReferences = new HashSet<(string, int)>(); + public StringBuilder Phase2 = new StringBuilder(); + public Dictionary Closure = new Dictionary(); + public int ArgIndex, RootIndex, DirectOffset, IndirectOffset; + + public string ArgKey => $"arg{ArgIndex}"; + + public MarshalBuilderState () { + ClosureReferences = new HashSet { + "_malloc", + "_error", + }; + } + } + + public class BoundMethodBuilderState : BuilderStateBase { + public string? FriendlyName; + public MethodInfo Method; + + public BoundMethodBuilderState (MethodInfo method) { + Method = method; + ClosureReferences = new HashSet { + "_error", + "mono_wasm_new_root", + "_create_temp_frame", + "_get_args_root_buffer_for_method_call", + "_get_buffer_for_method_call", + "_handle_exception_for_call", + "_teardown_after_call", + "mono_wasm_try_unbox_primitive_and_get_type", + "_unbox_mono_obj_root_with_known_nonprimitive_type", + "invoke_method", + "getI32", + "getU32", + "getF32", + "getF64", + }; + } + } + + private static string ToJsBool (bool b) => b ? "true" : "false"; + + public static void GenerateSignatureConverter (MarshalBuilderState state) { + int length = state.MarshalString.ArgumentCount; + var debugName = string.Concat("converter_", state.MarshalString.Key); + var variadicName = string.Concat("varConverter_", state.MarshalString.Key); + + // First we generate the individual steps that pack each argument into the buffer and + // place pointers to each argument into the args list that is passed when invoking a method. + var output = state.Output; + for (int i = 0; i < length; i++) { + state.ArgIndex = i; + var ch = state.MarshalString[i]; + EmitMarshalStep(state, ch); + } + + // Now we capture that list of steps so we can put stuff above it. Generating the list of + // steps produced valuable information like how large our buffer needs to be. + var temp = output.ToString(); + output.Clear(); + + // This special comment assigns a URL to this generated function in browser debuggers + output.AppendLine($"//# sourceURL=https://mono-wasm.invalid/signature/{state.MarshalString.Key}"); + output.AppendLine("\"use strict\";"); + + var alignmentMinusOne = IndirectAddressAlignment - 1; + // HACK: We have to pad out both buffers to ensure that all addresses will have an alignment of 8 + // If we don't do this, passing values to C# functions can fail (typically for doubles) + var directSize = (state.DirectOffset + alignmentMinusOne) / IndirectAddressAlignment * IndirectAddressAlignment; + var indirectSize = (state.IndirectOffset + alignmentMinusOne) / IndirectAddressAlignment * IndirectAddressAlignment; + var totalBufferSize = directSize + indirectSize + IndirectAddressAlignment; + output.AppendLine($"// '{state.MarshalString.Signature}' {length} argument(s)"); + output.AppendLine($"// direct buffer {state.DirectOffset} byte(s), indirect {state.IndirectOffset} byte(s)"); + + if (length > 0) { + // Now we scan through all the closure references that were generated while emitting + // the marshal steps, and pull them out of the closure table into local variables in + // the scope of the outer function. This will make them visible to the two inner + // inner functions we're generating (which are the actual signature converter + its + // variadic wrapper), eliminating any need to do table lookups on every invocation. + // FIXME: It's possible to end up with a cyclic dependency between converters this way + + // TODO: Sort this for consistent code + foreach (var key in state.ClosureReferences) + output.AppendLine($"const {key} = get_api('{key}');"); + foreach (var tup in state.TypeReferences) + output.AppendLine($"const {tup.Item1} = get_type_converter({tup.Item2});"); + } + + output.AppendLine(""); + output.Append($"function {debugName} (buffer, rootBuffer, methodPtr"); + for (int i = 0; i < length; i++) + output.Append($", arg{i}"); + output.AppendLine(") {"); + + if (length > 0) { + output.AppendLine(" if (!methodPtr) _error('no method provided');"); + if (state.RootIndex > 0) + state.Output.AppendLine($" if (!rootBuffer) _error('no root buffer provided');"); + // When a signature converter is called it may be passed an existing buffer for reuse, but + // if not it will allocate one on the fly. The caller is responsible for freeing it. + output.AppendLine($" if (!buffer) buffer = _malloc({totalBufferSize});"); + // FIXME: While we're aligning the size of the direct buffer, it's possible 'buffer' itself is not + // properly aligned, which would mean indirectBuffer will also not be properly aligned. + // In my testing emscripten's malloc always produces aligned addresses, but we may want to + // detect and handle this by shifting indirectBuffer forward to align it. + output.AppendLine($" const directBuffer = buffer, indirectBuffer = directBuffer + {directSize};"); + output.AppendLine(temp); + + // Some marshaling operations need to occur in two phases, so we append the second phase + // code right at the end before returning + if (state.Phase2.Length > 0) + output.AppendLine(state.Phase2.ToString()); + + output.AppendLine(" return buffer;"); + } else { + output.AppendLine(" return 0;"); + } + output.AppendLine("};"); + + // Generate a small dispatcher function that will unpack an arguments array to pass + // the individual arguments to the signature converter. This is much slower than + // taking arguments directly so it is only available as a fallback + output.AppendLine(""); + output.AppendLine($"function {variadicName} (buffer, rootBuffer, methodPtr, args) {{"); + output.AppendLine($" if (args.length !== {length}) _error('Expected {length} argument(s)');"); + if (length > 0) { + output.Append($" return {debugName}(buffer, rootBuffer, methodPtr"); + for (int i = 0; i < length; i++) + output.Append($", args[{i}]"); + output.AppendLine(");"); + } else { + output.Append(" return 0;"); + } + output.AppendLine("};"); + + var pMethod = state.MarshalString.Method?.MethodHandle.Value ?? IntPtr.Zero; + var method = state.MarshalString.ContainsAuto + ? pMethod.ToInt32().ToString() + : "null"; + + // At the end our wrapper function returns the two nested closures along with information + // on the signature they're for, so that the JS bindings layer can store everything away + // and do relevant setup (allocating the correct sized buffer, etc.) + output.AppendLine(""); + output.AppendLine("return {"); + output.AppendLine($" arg_count: {length}, "); + output.AppendLine($" args_marshal: '{state.MarshalString.Signature}', "); + output.AppendLine($" compiled_function: {debugName}, "); + output.AppendLine($" compiled_variadic_function: {variadicName}, "); + output.AppendLine($" contains_auto: {ToJsBool(state.MarshalString.ContainsAuto)}, "); + output.AppendLine($" is_result_definitely_unmarshaled: {ToJsBool(state.MarshalString.RawReturnValue)}, "); + output.AppendLine($" method: {method}, "); + output.AppendLine($" name: '{state.MarshalString.Key}', "); + output.AppendLine($" needs_root_buffer: {ToJsBool(state.RootIndex > 0)}, "); + output.AppendLine($" root_buffer_size: {state.RootIndex}, "); + output.AppendLine($" scratchBuffer: 0, "); + output.AppendLine($" scratchRootBuffer: null, "); + output.AppendLine($" size: {totalBufferSize}, "); + output.AppendLine("};"); + } + + public static void EmitPrimitiveMarshalStep (MarshalBuilderState state, string setterName) { + state.ClosureReferences.Add(setterName); + state.ClosureReferences.Add("setU32"); + var offsetKey = $"offset{state.ArgIndex}"; + state.Output.AppendLine($" let {offsetKey} = indirectBuffer + {state.IndirectOffset};"); + state.Output.AppendLine($" {setterName}({offsetKey}, {state.ArgKey});"); + state.Output.AppendLine($" setU32(directBuffer + {state.DirectOffset}, {offsetKey});"); + state.IndirectOffset += IndirectAddressAlignment; + state.DirectOffset += PointerSize; + } + + public static void EmitRawPointerMarshalStep (MarshalBuilderState state) { + state.ClosureReferences.Add("setU32"); + state.Output.AppendLine($" setU32(directBuffer + {state.DirectOffset}, {state.ArgKey});"); + state.DirectOffset += PointerSize; + } + + public static void EmitManagedMarshalStep (MarshalBuilderState state, string? converter) { + state.ClosureReferences.Add("setU32"); + + var key = state.ArgKey; + if (converter != null) { + key = $"converted{state.ArgIndex}"; + // Converters can either be a bare function name or raw 'foo(x, ..., y)' JS, where we will replace the '...' + var parenIndex = converter.IndexOf('('); + if (parenIndex >= 0) { + state.ClosureReferences.Add(converter.Substring(0, parenIndex)); + state.Output.AppendLine($" const {key} = {converter.Replace("...", state.ArgKey)};"); + } else { + state.ClosureReferences.Add(converter); + state.Output.AppendLine($" const {key} = {converter}({state.ArgKey});"); + } + } + + state.Output.AppendLine($" rootBuffer.set({state.RootIndex}, {key});"); + state.Output.AppendLine($" setU32(directBuffer + {state.DirectOffset}, {key});"); + state.RootIndex += 1; + state.DirectOffset += PointerSize; + } + + private static void EmitCustomMarshalStep (MarshalBuilderState state, Type argType) { + state.ClosureReferences.Add("setU32"); + var typePtr = argType.TypeHandle.Value; + var converterKey = $"type{typePtr.ToInt32()}"; + state.TypeReferences.Add((converterKey, typePtr.ToInt32())); + + var callArgs = $"{state.ArgKey}, methodPtr, {state.ArgIndex}"; + state.Output.AppendLine($" rootBuffer.set({state.RootIndex}, {converterKey}({callArgs}));"); + + if (argType.IsValueType) { + state.ClosureReferences.Add("mono_wasm_unbox_rooted"); + var unboxedKey = $"unboxed{state.ArgIndex}"; + // HACK: We need to do all these unboxes last after all the transform steps have run, + // because invoking a converter or creating a string instance could cause a GC and move + // the rooted object to a new location, invalidating the unbox_rooted return value. + state.Phase2.AppendLine($" const {unboxedKey} = mono_wasm_unbox_rooted(rootBuffer.get({state.RootIndex}));"); + state.Phase2.AppendLine($" setU32(directBuffer + {state.DirectOffset}, {unboxedKey});"); + } else { + // Note that even though we aren't unboxing, we still read the object address back from + // the root buffer, because the conversion steps may have caused a GC and moved the + // object after we initially created it. + state.Phase2.AppendLine($" setU32(directBuffer + {state.DirectOffset}, rootBuffer.get({state.RootIndex}));"); + } + + state.RootIndex += 1; + state.DirectOffset += PointerSize; + } + + public static void EmitMarshalStep (MarshalBuilderState state, ArgsMarshalCharacter ch) { + // If this slot in the signature uses the Auto type ('a'), we need to select an + // appropriate type for the parameter based on the target method's type info + if (ch == ArgsMarshalCharacter.Auto) { + var method = state.MarshalString.Method; + if (method == null) + // This either means no method was provided, or we failed to resolve a method + // from the method handle we were provided (this can happen if it's generic) + throw new Exception("No method provided when compiling converter"); + var parms = method.GetParameters(); + if (state.ArgIndex >= parms.Length) + throw new Exception($"Too many signature characters ({state.MarshalString.ArgumentCount}) for method ({parms.Length} args)"); + + var parm = parms[state.ArgIndex]; + var pName = string.IsNullOrEmpty(parm.Name) + ? $"#{state.ArgIndex}" + : parm.Name; + var argType = parm.ParameterType; + var autoMarshalType = Runtime.GetMarshalTypeFromType(argType); + + state.Output.AppendLine($"// #{state.ArgIndex} Auto {argType} {pName} -> {autoMarshalType}"); + + switch (autoMarshalType) { + // For basic types, we can just select an appropriate MarshalType for them, and then + // use the corresponding signature character as a replacement for the one we're missing + default: + ch = (ArgsMarshalCharacter)(int)Runtime.GetCallSignatureCharacterForMarshalType(autoMarshalType, null); + break; + // If the marshal type selector produced bare ValueType or Object, it needs custom marshaling + case MarshalType.VT: + EmitCustomMarshalStep(state, argType); + return; + case MarshalType.OBJECT: + // Though if it's just bare 'object', we cannot identify the marshaler at compile time here, + // and we need to let the regular js_to_mono_obj path below run to do it at run time. + if (argType != typeof(object)) { + EmitCustomMarshalStep(state, argType); + return; + } else { + ch = ArgsMarshalCharacter.JSObj; + break; + } + } + } else { + state.Output.AppendLine($"// #{state.ArgIndex} {ch}"); + } + + switch (ch) { + case ArgsMarshalCharacter.Int32: + EmitPrimitiveMarshalStep(state, "setI32"); + return; + case ArgsMarshalCharacter.Int64: + EmitPrimitiveMarshalStep(state, "setI64"); + return; + case ArgsMarshalCharacter.Float32: + EmitPrimitiveMarshalStep(state, "setF32"); + return; + case ArgsMarshalCharacter.Float64: + EmitPrimitiveMarshalStep(state, "setF64"); + return; + case ArgsMarshalCharacter.ByteSpan: + EmitPrimitiveMarshalStep(state, "_setSpan"); + return; + case ArgsMarshalCharacter.MONOObj: + EmitRawPointerMarshalStep(state); + return; + case ArgsMarshalCharacter.String: + EmitManagedMarshalStep(state, "js_string_to_mono_string"); + return; + case ArgsMarshalCharacter.InternedString: + EmitManagedMarshalStep(state, "js_string_to_mono_string_interned"); + return; + case ArgsMarshalCharacter.Int32Enum: + state.Output.AppendLine($" if (typeof({state.ArgKey}) !== 'number') _error(`Expected numeric value for enum argument, got '${{{state.ArgKey}}}'`);"); + EmitPrimitiveMarshalStep(state, "setI32"); + return; + case ArgsMarshalCharacter.JSObj: + EmitManagedMarshalStep(state, "_js_to_mono_obj(false, ...)"); + return; + case ArgsMarshalCharacter.Uri: + EmitManagedMarshalStep(state, "_js_to_mono_uri(false, ...)"); + return; + case ArgsMarshalCharacter.Auto: + state.Output.AppendLine($" _error('Automatic type selection failed');"); + return; + default: + throw new NotImplementedException(ch.ToString()); + } + } + + private static void GenerateFastUnboxCase (BoundMethodBuilderState state, MarshalType type, string? expression) { + var output = state.Output; + output.AppendLine($" case {(int)type}:"); + output.AppendLine($" return {expression};"); + } + + private static void GenerateFastUnboxBlock (BoundMethodBuilderState state) { + var output = state.Output; + var methodReturnType = Runtime.GetMarshalTypeFromType(state.Method.ReturnType); + bool hasPrimitiveType = FastUnboxHandlers.TryGetValue(methodReturnType, out string? fastHandler); + // For the common scenario where the return type is a primitive, we want to try and unbox it directly + // into our existing heap allocation and then read it out of the heap. Doing this all in one operation + // means that we only need to enter a gc safe region twice (instead of 3+ times with the normal, + // slower check-type-and-then-unbox flow which has extra checks since unbox verifies the type). + if (!hasPrimitiveType) { + output.AppendLine(" if (resultRoot.value === 0)"); + output.AppendLine(" return undefined;"); + } + output.AppendLine( " let resultType = mono_wasm_try_unbox_primitive_and_get_type(resultRoot.value, unboxBuffer, unboxBufferSize);"); + output.AppendLine( " switch (resultType) {"); + // If we know the return type of this method and it's a primitive, we only need to generate the unbox handler for that type + if (hasPrimitiveType) { + GenerateFastUnboxCase(state, methodReturnType, fastHandler); + // This default case should never be hit, but the runtime is returning a boxed object so it's possible if something horrible happens + output.AppendLine( " default:"); + output.AppendLine($" throw new Error('expected method return value to be of type {methodReturnType} but it was ' + resultType);"); + } else { + // The return type is something we can't fast-unbox or is unknown (i.e. object) + foreach (var kvp in FastUnboxHandlers) + GenerateFastUnboxCase(state, kvp.Key, kvp.Value); + output.AppendLine( " default:"); + output.AppendLine( " return _unbox_mono_obj_root_with_known_nonprimitive_type(resultRoot, resultType, unboxBuffer);"); + } + output.AppendLine( " }"); + } + + public static void GenerateBoundMethod (BoundMethodBuilderState state) { + // input arguments: + // get_api, token + + int length = state.MarshalString.ArgumentCount; + var handle = state.Method.MethodHandle.Value; + var name = state.FriendlyName ?? $"clr_{handle.ToInt32()}"; + var output = state.Output; + + // This special comment assigns a URL to this generated function in browser debuggers + output.AppendLine($"//# sourceURL=https://mono-wasm.invalid/bound_method/{handle.ToInt32()}"); + output.AppendLine("\"use strict\";"); + output.AppendLine($"//{state.Method?.DeclaringType?.FullName}::{state.Method?.Name}"); + + // Unpack various closure values into locals in the outer function that returns the actual + // bound method, so that the property lookup doesn't have to occur on every call + output.AppendLine("const method = token.method;"); + output.AppendLine("const converter = token.converter;"); + output.AppendLine($"const converter_{state.MarshalString.Key} = converter.compiled_function;"); + output.AppendLine("const unboxBuffer = token.unboxBuffer;"); + output.AppendLine("const unboxBufferSize = token.unboxBufferSize;"); + // get_api here will also ensure that every function we reference is available and do + // the check now at construction time instead of later when the bound method is called + foreach (var key in state.ClosureReferences) + output.AppendLine($"const {key} = get_api('{key}');"); + + output.Append($"function {name} ("); + for (int i = 0; i < length; i++) { + if (i < (length - 1)) + output.Append($"arg{i}, "); + else + output.AppendLine($"arg{i}) {{"); + } + if (length == 0) + output.AppendLine(") {"); + + output.AppendLine(" _create_temp_frame();"); + output.AppendLine(" let resultRoot = token.scratchResultRoot;"); + output.AppendLine(" let exceptionRoot = token.scratchExceptionRoot;"); + output.AppendLine(" token.scratchResultRoot = null;"); + output.AppendLine(" token.scratchExceptionRoot = null;"); + output.AppendLine(" if (resultRoot === null)"); + output.AppendLine(" resultRoot = mono_wasm_new_root();"); + output.AppendLine(" if (exceptionRoot === null)"); + output.AppendLine(" exceptionRoot = mono_wasm_new_root();"); + output.AppendLine(); + + output.AppendLine( " let argsRootBuffer = _get_args_root_buffer_for_method_call(converter, token);"); + output.AppendLine( " let scratchBuffer = _get_buffer_for_method_call(converter, token);"); + output.AppendLine( " let buffer = 0;"); + output.AppendLine( " try {"); + output.AppendLine($" buffer = converter_{state.MarshalString.Key}("); + output.AppendLine( " scratchBuffer, argsRootBuffer, method,"); + for (int i = 0; i < length; i++) { + if (i < (length - 1)) + output.AppendLine($" arg{i},"); + else + output.AppendLine($" arg{i}"); + } + output.AppendLine(" );"); + output.AppendLine(); + + output.AppendLine(" resultRoot.value = invoke_method(method, 0, buffer, exceptionRoot.get_address());"); + output.AppendLine(" _handle_exception_for_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer);"); + output.AppendLine(); + + if (state.MarshalString.RawReturnValue) + output.AppendLine(" return resultRoot.value;"); + else if ((state.Method?.ReturnType ?? typeof(void)) == typeof(void)) + output.AppendLine(" return;"); + else + GenerateFastUnboxBlock(state); + + output.AppendLine(" } finally {"); + output.AppendLine(" _teardown_after_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer);"); + output.AppendLine(" }"); + output.AppendLine("};"); + output.AppendLine(); + output.AppendLine($"return {name};"); + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeMarshaler.cs new file mode 100644 index 00000000000000..c8e0a12090e63b --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeMarshaler.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript +{ + public static class DateTimeMarshaler + { + public static string JavaScriptToInterchangeTransform => @" + switch (typeof (value)) { + case 'number': + return value; + default: + if (value instanceof Date) { + return value.valueOf(); + } else + throw new Error('Value must be a number (msecs since unix epoch), or a Date'); + } +"; + public static string InterchangeToJavaScriptTransform => "return new Date(value)"; + + public static DateTime FromJavaScript (double msecsSinceEpoch) + { + return DateTimeOffset.FromUnixTimeMilliseconds((long)msecsSinceEpoch).UtcDateTime; + } + + public static double ToJavaScript (in DateTime dt) + { + return (double)new DateTimeOffset(dt).ToUnixTimeMilliseconds(); + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeOffsetMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeOffsetMarshaler.cs new file mode 100644 index 00000000000000..971de1de202d50 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeOffsetMarshaler.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript +{ + public static class DateTimeOffsetMarshaler + { + public static string JavaScriptToInterchangeTransform => DateTimeMarshaler.JavaScriptToInterchangeTransform; + public static string InterchangeToJavaScriptTransform => DateTimeMarshaler.InterchangeToJavaScriptTransform; + + public static DateTimeOffset FromJavaScript (double msecsSinceEpoch) + { + return DateTimeOffset.FromUnixTimeMilliseconds((long)msecsSinceEpoch); + } + + public static double ToJavaScript (in DateTimeOffset dto) + { + return (double)dto.ToUnixTimeMilliseconds(); + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.References.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.References.cs index c9b443e9c82ce1..d31f1543ce76b9 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.References.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.References.cs @@ -48,7 +48,7 @@ internal void AddInFlight() InFlightCounter++; if (InFlightCounter == 1) { - Debug.Assert(InFlight == null); + Debug.Assert(InFlight == null, "InFlight == null"); InFlight = GCHandle.Alloc(this, GCHandleType.Normal); } } @@ -61,12 +61,12 @@ internal void ReleaseInFlight() { lock (this) { - Debug.Assert(InFlightCounter != 0); + Debug.Assert(InFlightCounter != 0, "InFlightCounter != 0"); InFlightCounter--; if (InFlightCounter == 0) { - Debug.Assert(InFlight.HasValue); + Debug.Assert(InFlight.HasValue, "InFlight.HasValue"); InFlight.Value.Free(); InFlight = null; } diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Runtime.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Runtime.cs index 60f694e2b7cf39..fc4d081860e687 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Runtime.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Runtime.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text; +using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Threading.Tasks; @@ -49,129 +52,361 @@ private struct IntPtrAndHandle internal IntPtr ptr; [FieldOffset(0)] - internal RuntimeMethodHandle handle; + internal RuntimeMethodHandle methodHandle; [FieldOffset(0)] internal RuntimeTypeHandle typeHandle; } - // see src/mono/wasm/driver.c MARSHAL_TYPE_xxx - public enum MarshalType : int { - NULL = 0, - INT = 1, - FP64 = 2, - STRING = 3, - VT = 4, - DELEGATE = 5, - TASK = 6, - OBJECT = 7, - BOOL = 8, - ENUM = 9, - URI = 22, - SAFEHANDLE = 23, - ARRAY_BYTE = 10, - ARRAY_UBYTE = 11, - ARRAY_UBYTE_C = 12, - ARRAY_SHORT = 13, - ARRAY_USHORT = 14, - ARRAY_INT = 15, - ARRAY_UINT = 16, - ARRAY_FLOAT = 17, - ARRAY_DOUBLE = 18, - FP32 = 24, - UINT32 = 25, - INT64 = 26, - UINT64 = 27, - CHAR = 28, - STRING_INTERNED = 29, - VOID = 30, - ENUM64 = 31, - POINTER = 32 + private static RuntimeMethodHandle GetMethodHandleFromIntPtr (IntPtr ptr) { + var temp = new IntPtrAndHandle { ptr = ptr }; + return temp.methodHandle; } - // see src/mono/wasm/driver.c MARSHAL_ERROR_xxx - public enum MarshalError : int { - BUFFER_TOO_SMALL = 512, - NULL_CLASS_POINTER = 513, - NULL_TYPE_POINTER = 514, - UNSUPPORTED_TYPE = 515, - FIRST = BUFFER_TOO_SMALL + private static RuntimeTypeHandle GetTypeHandleFromIntPtr (IntPtr ptr) { + var temp = new IntPtrAndHandle { ptr = ptr }; + return temp.typeHandle; } - public static string GetCallSignature(IntPtr methodHandle, object objForRuntimeType) - { - IntPtrAndHandle tmp = default(IntPtrAndHandle); - tmp.ptr = methodHandle; + private static string MakeMarshalTypeRecord (Type type, MarshalType mtype) { + var result = $"{{ \"marshalType\": {(int)mtype}, " + + $"\"typePtr\": {type.TypeHandle.Value}, " + + $"\"signatureChar\": \"{GetCallSignatureCharacterForMarshalType(mtype, 'a')}\" }}"; + return result; + } - MethodBase? mb = objForRuntimeType == null ? MethodBase.GetMethodFromHandle(tmp.handle) : MethodBase.GetMethodFromHandle(tmp.handle, Type.GetTypeHandle(objForRuntimeType)); - if (mb == null) - return string.Empty; + private static MethodBase? MethodFromPointers (IntPtr typePtr, IntPtr methodPtr) { + if (methodPtr == IntPtr.Zero) + return null; - ParameterInfo[] parms = mb.GetParameters(); - int parmsLength = parms.Length; - if (parmsLength == 0) - return string.Empty; + var methodHandle = GetMethodHandleFromIntPtr(methodPtr); - char[] res = new char[parmsLength]; + if (typePtr != IntPtr.Zero) { + var typeHandle = GetTypeHandleFromIntPtr(typePtr); + return MethodBase.GetMethodFromHandle(methodHandle, typeHandle); + } else { + return MethodBase.GetMethodFromHandle(methodHandle); + } + } - for (int c = 0; c < parmsLength; c++) - { - Type t = parms[c].ParameterType; - switch (Type.GetTypeCode(t)) - { + public static unsafe string? MakeMarshalSignatureInfo (IntPtr typePtr, IntPtr methodPtr) { + var mb = MethodFromPointers(typePtr, methodPtr); + if (mb is null) + return null; + + var returnType = (mb as MethodInfo)?.ReturnType ?? typeof(void); + var returnMtype = GetMarshalTypeFromType(returnType); + var sb = new StringBuilder(); + sb.Append("{ "); + sb.Append("\"result\": "); + sb.Append(MakeMarshalTypeRecord(returnType, returnMtype)); + sb.Append(", \"typePtr\": "); + sb.Append(typePtr.ToInt32()); + sb.Append(", \"methodPtr\": "); + sb.Append(methodPtr.ToInt32()); + sb.Append(", \"parameters\": ["); + + int i = 0; + foreach (var p in mb.GetParameters()) { + if (i > 0) + sb.Append(", "); + sb.Append(MakeMarshalTypeRecord(p.ParameterType, GetMarshalTypeFromType(p.ParameterType))); + i++; + } + + sb.Append("] }"); + + return sb.ToString(); + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern", + Justification = "Trimming doesn't affect types eligible for marshalling. Different exception for invalid inputs doesn't matter.")] + private static unsafe string GetAndEscapeJavascriptLiteralProperty (Type type, string name) { + var info = type.GetProperty( + name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic + ); + + var value = info?.GetValue(null) as string; + if (value is null) + return "null"; + + var sb = new StringBuilder(); + sb.Append('\"'); + foreach (var ch in value) { + switch (ch) { + case '\'': + sb.Append('\''); + continue; + case '"': + sb.Append('\"'); + continue; + case '\\': + sb.Append("\\\\"); + continue; + case '\n': + sb.Append("\\n"); + continue; + } + + if (ch < ' ') { + sb.Append("\\u"); + sb.Append(((int)ch).ToString("X4")); + } else { + sb.Append(ch); + } + } + sb.Append('\"'); + + return sb.ToString(); + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern", + Justification = "Trimming doesn't affect types eligible for marshalling. Different exception for invalid inputs doesn't matter.")] + private static unsafe IntPtr GetMarshalMethodPointer (Type type, string name, out Type? returnType, out Type parameterType, bool hasScratchBuffer) { + var info = type.GetMethod( + name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic + ); + if (info is null) + throw new WasmInteropException($"{type.Name} must have a static {name} method"); + + var p = info.GetParameters(); + int expectedLength = hasScratchBuffer ? 2 : 1; + if ((p.Length != expectedLength) || (p[0].ParameterType is null)) + throw new WasmInteropException($"Method {type.Name}.{name} must accept exactly {expectedLength} parameter(s)"); + + if (hasScratchBuffer) { + if ((info.ReturnType != null) && (info.ReturnType != typeof(void))) + throw new WasmInteropException($"Method {type.Name}.{name} must not have a return value"); + if ((p[1].ParameterType != typeof(Span)) && (p[1].ParameterType != typeof(ReadOnlySpan))) + throw new WasmInteropException($"Method {type.Name}.{name}'s second parameter must be of type Span or ReadOnlySpan"); + } else { + if (info.ReturnType is null) + throw new WasmInteropException($"Method {type.Name}.{name} must have a return value"); + } + + parameterType = p[0].ParameterType; + returnType = info.ReturnType; + + return info.MethodHandle.Value; + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:UnrecognizedReflectionPattern", + Justification = "Trimming doesn't affect types eligible for marshalling. Different exception for invalid inputs doesn't matter.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", + Justification = "Trimming doesn't affect types eligible for marshalling. Different exception for invalid inputs doesn't matter.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2057:UnrecognizedReflectionPattern", + Justification = "Trimming doesn't affect types eligible for marshalling. Different exception for invalid inputs doesn't matter.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", + Justification = "Trimming doesn't affect types eligible for marshalling. Different exception for invalid inputs doesn't matter.")] + public static unsafe string GetCustomMarshalerInfoForType (IntPtr typePtr, string? marshalerFullName) { + if ((typePtr == IntPtr.Zero) || string.IsNullOrEmpty(marshalerFullName)) + return "null"; + + var typeHandle = GetTypeHandleFromIntPtr(typePtr); + + var type = Type.GetTypeFromHandle(typeHandle); + if (type is null) + return "null"; + var marshalerType = Type.GetType(marshalerFullName) ?? type.Assembly.GetType(marshalerFullName); + if (marshalerType is null) + return "null"; + + var scratchInfo = marshalerType.GetProperty("ScratchBufferSize", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + var _scratchBufferSize = scratchInfo?.GetValue(null); + var scratchBufferSize = _scratchBufferSize != null + ? (int)_scratchBufferSize + : (int?)null; + + var jsToInterchange = GetAndEscapeJavascriptLiteralProperty(marshalerType, "JavaScriptToInterchangeTransform"); + var interchangeToJs = GetAndEscapeJavascriptLiteralProperty(marshalerType, "InterchangeToJavaScriptTransform"); + + if (scratchBufferSize.HasValue) { + if ((jsToInterchange == "null") || (interchangeToJs == "null")) + throw new WasmInteropException($"{marshalerType.Name} must provide interchange transforms if it has a scratch buffer"); + } + + var inputPtr = GetMarshalMethodPointer(marshalerType, "FromJavaScript", out Type? fromReturnType, out Type fromParameterType, false); + var outputPtr = GetMarshalMethodPointer(marshalerType, "ToJavaScript", out Type? toReturnType, out Type toParameterType, scratchBufferSize.HasValue); + + if (fromReturnType != type) + throw new WasmInteropException($"{marshalerType.Name}.FromJavaScript's return type must be {type.Name} but was {fromReturnType}"); + + if (type.IsValueType) { + var typeMatches = toParameterType.GetElementType() == type; + if (!typeMatches || !(toParameterType.IsPointer || toParameterType.IsByRef)) + throw new WasmInteropException($"{marshalerType.Name}.ToJavaScript's parameter must be 'in {type.Name}' or '{type.Name}*' but was {toParameterType}"); + } else { + if (toParameterType != type) + throw new WasmInteropException($"{marshalerType.Name}.ToJavaScript's parameter must be of type {type.Name} but was {toParameterType}"); + } + + var result = new StringBuilder(); + result.AppendLine("{"); + result.AppendLine($"\"typePtr\": {typePtr},"); + if (scratchBufferSize.HasValue) + result.AppendLine($"\"scratchBufferSize\": {scratchBufferSize.Value},"); + result.AppendLine($"\"jsToInterchange\": {jsToInterchange},"); + result.AppendLine($"\"interchangeToJs\": {interchangeToJs},"); + result.AppendLine($"\"inputPtr\": {inputPtr},"); + result.AppendLine($"\"outputPtr\": {outputPtr}"); + result.AppendLine("}"); + return result.ToString(); + } + + internal static MarshalType GetMarshalTypeFromType (Type? type) { + if (type is null) + return MarshalType.VOID; + + var typeCode = Type.GetTypeCode(type); + if (type.IsEnum) { + switch (typeCode) { + case TypeCode.Int32: + case TypeCode.UInt32: + return MarshalType.ENUM; + case TypeCode.Int64: + case TypeCode.UInt64: + return MarshalType.ENUM64; + default: + throw new WasmInteropException($"Unsupported enum underlying type {typeCode}"); + } + } + + switch (typeCode) { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.UInt16: + case TypeCode.Int32: + return MarshalType.INT; + case TypeCode.UInt32: + return MarshalType.UINT32; + case TypeCode.Boolean: + return MarshalType.BOOL; + case TypeCode.Int64: + return MarshalType.INT64; + case TypeCode.UInt64: + return MarshalType.UINT64; + case TypeCode.Single: + return MarshalType.FP32; + case TypeCode.Double: + return MarshalType.FP64; + case TypeCode.String: + return MarshalType.STRING; + case TypeCode.Char: + return MarshalType.CHAR; + } + + if (type.IsArray) { + if (!type.IsSZArray) + throw new WasmInteropException("Only single-dimensional arrays with a zero lower bound can be marshaled to JS"); + + var elementType = type.GetElementType(); + switch (Type.GetTypeCode(elementType)) { case TypeCode.Byte: + return MarshalType.ARRAY_UBYTE; case TypeCode.SByte: + return MarshalType.ARRAY_BYTE; case TypeCode.Int16: + return MarshalType.ARRAY_SHORT; case TypeCode.UInt16: + return MarshalType.ARRAY_USHORT; case TypeCode.Int32: + return MarshalType.ARRAY_INT; case TypeCode.UInt32: - case TypeCode.Boolean: - // Enums types have the same code as their underlying numeric types - if (t.IsEnum) - res[c] = 'j'; - else - res[c] = 'i'; - break; - case TypeCode.Int64: - case TypeCode.UInt64: - // Enums types have the same code as their underlying numeric types - if (t.IsEnum) - res[c] = 'k'; - else - res[c] = 'l'; - break; + return MarshalType.ARRAY_UINT; case TypeCode.Single: - res[c] = 'f'; - break; + return MarshalType.ARRAY_FLOAT; case TypeCode.Double: - res[c] = 'd'; - break; - case TypeCode.String: - res[c] = 's'; - break; + return MarshalType.ARRAY_DOUBLE; default: - if (t == typeof(IntPtr)) - { - res[c] = 'i'; - } - else if (t == typeof(Uri)) - { - res[c] = 'u'; - } - else if (t == typeof(SafeHandle)) - { - res[c] = 'h'; - } - else - { - if (t.IsValueType) - throw new NotSupportedException(SR.ValueTypeNotSupported); - res[c] = 'o'; - } - break; + throw new WasmInteropException($"Unsupported array element type {elementType}"); } + } else if (type == typeof(IntPtr)) + return MarshalType.POINTER; + else if (type == typeof(UIntPtr)) + return MarshalType.POINTER; + else if (type == typeof(SafeHandle)) + return MarshalType.SAFEHANDLE; + else if (typeof(Delegate).IsAssignableFrom(type)) + return MarshalType.DELEGATE; + else if ((type == typeof(Task)) || typeof(Task).IsAssignableFrom(type)) + return MarshalType.TASK; + // HACK: You could theoretically inherit from Uri, but I consider this out of scope. + // If you really need to marshal a custom Uri, define a custom marshaler for it + else if (typeof(Uri) == type) + return MarshalType.URI; + else if ((type == typeof(Span)) || (type == typeof(ReadOnlySpan))) + return MarshalType.SPAN_BYTE; + else if (type.IsPointer) + return MarshalType.POINTER; + + if (type.IsValueType) + return MarshalType.VT; + else + return MarshalType.OBJECT; + } + + internal static char GetCallSignatureCharacterForMarshalType (MarshalType t, char? defaultValue) { + switch (t) { + case MarshalType.BOOL: + case MarshalType.INT: + case MarshalType.UINT32: + case MarshalType.POINTER: + return 'i'; + case MarshalType.UINT64: + case MarshalType.INT64: + return 'l'; + case MarshalType.FP32: + return 'f'; + case MarshalType.FP64: + return 'd'; + case MarshalType.STRING: + return 's'; + case MarshalType.URI: + return 'u'; + case MarshalType.SAFEHANDLE: + return 'h'; + case MarshalType.ENUM: + return 'j'; + case MarshalType.ENUM64: + return 'k'; + case MarshalType.TASK: + case MarshalType.DELEGATE: + case MarshalType.OBJECT: + return 'o'; + case MarshalType.VT: + return 'a'; + case MarshalType.SPAN_BYTE: + return 'b'; + default: + if (defaultValue.HasValue) + return defaultValue.Value; + else + throw new WasmInteropException($"Unsupported marshal type {t}"); + } + } + + public static string GetCallSignature(IntPtr _methodHandle, object? objForRuntimeType) + { + var methodHandle = GetMethodHandleFromIntPtr(_methodHandle); + + MethodBase? mb = objForRuntimeType is null ? MethodBase.GetMethodFromHandle(methodHandle) : MethodBase.GetMethodFromHandle(methodHandle, Type.GetTypeHandle(objForRuntimeType)); + if (mb is null) + return string.Empty; + + ParameterInfo[] parms = mb.GetParameters(); + int parmsLength = parms.Length; + if (parmsLength == 0) + return string.Empty; + + var result = new char[parmsLength]; + for (int i = 0; i < parmsLength; i++) { + Type t = parms[i].ParameterType; + var mt = GetMarshalTypeFromType(t); + result[i] = GetCallSignatureCharacterForMarshalType(mt, null); } - return new string(res); + + return new string(result); } /// @@ -198,33 +433,9 @@ public static string GetCallSignature(IntPtr methodHandle, object objForRuntimeT return null; } - public static string ObjectToString(object o) + public static string ObjectToString(object? o) { - return o.ToString() ?? string.Empty; - } - - public static double GetDateValue(object dtv) - { - if (dtv == null) - throw new ArgumentNullException(nameof(dtv)); - if (!(dtv is DateTime dt)) - throw new InvalidCastException(SR.Format(SR.UnableCastObjectToType, dtv.GetType(), typeof(DateTime))); - if (dt.Kind == DateTimeKind.Local) - dt = dt.ToUniversalTime(); - else if (dt.Kind == DateTimeKind.Unspecified) - dt = new DateTime(dt.Ticks, DateTimeKind.Utc); - return new DateTimeOffset(dt).ToUnixTimeMilliseconds(); - } - - public static DateTime CreateDateTime(double ticks) - { - DateTimeOffset unixTime = DateTimeOffset.FromUnixTimeMilliseconds((long)ticks); - return unixTime.DateTime; - } - - public static Uri CreateUri(string uri) - { - return new Uri(uri); + return o?.ToString() ?? string.Empty; } public static void CancelPromise(int promiseJSHandle) @@ -296,5 +507,36 @@ public static void WebSocketAbort(JSObject webSocket) if (exception != 0) throw new JSException(res); } + + public static string GenerateArgsMarshaler (IntPtr typeHandle, IntPtr methodHandle, string signature) { + MethodBase? method; + try { + // It's generally harmless for this to fail unless the signature contains an 'a', so we log it and continue + method = MethodFromPointers(typeHandle, methodHandle); + } catch (Exception exc) { + Debug.WriteLine($"Failed to resolve method when generating marshaler: {exc.Message}"); + method = null; + } + + var state = new Codegen.MarshalBuilderState { + MarshalString = new MarshalString(signature, method) + }; + Codegen.GenerateSignatureConverter(state); + return state.Output.ToString(); + } + + public static string GenerateBoundMethod (IntPtr typeHandle, IntPtr methodHandle, string signature, string? friendlyName) { + MethodBase? method; + method = MethodFromPointers(typeHandle, methodHandle); + if (method == null) + throw new Exception("Failed to resolve method"); + + var state = new Codegen.BoundMethodBuilderState((MethodInfo)method) { + MarshalString = new MarshalString(signature, method), + FriendlyName = friendlyName, + }; + Codegen.GenerateBoundMethod(state); + return state.Output.ToString(); + } } } diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Types.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Types.cs new file mode 100644 index 00000000000000..3db975f78c4337 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Types.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript +{ + // see src/mono/wasm/driver.c MARSHAL_TYPE_xxx + public enum MarshalType : int { + NULL = 0, + INT = 1, + FP64 = 2, + STRING = 3, + VT = 4, + DELEGATE = 5, + TASK = 6, + OBJECT = 7, + BOOL = 8, + ENUM = 9, + URI = 22, + SAFEHANDLE = 23, + ARRAY_BYTE = 10, + ARRAY_UBYTE = 11, + ARRAY_UBYTE_C = 12, + ARRAY_SHORT = 13, + ARRAY_USHORT = 14, + ARRAY_INT = 15, + ARRAY_UINT = 16, + ARRAY_FLOAT = 17, + ARRAY_DOUBLE = 18, + FP32 = 24, + UINT32 = 25, + INT64 = 26, + UINT64 = 27, + CHAR = 28, + STRING_INTERNED = 29, + VOID = 30, + ENUM64 = 31, + POINTER = 32, + SPAN_BYTE = 33, + } + + // see src/mono/wasm/driver.c MARSHAL_ERROR_xxx + public enum MarshalError : int { + BUFFER_TOO_SMALL = 512, + NULL_CLASS_POINTER = 513, + NULL_TYPE_POINTER = 514, + UNSUPPORTED_TYPE = 515, + FIRST = BUFFER_TOO_SMALL + } + + public enum ArgsMarshalCharacter { + Int32 = 'i', // int32 + Int32Enum = 'j', // int32 - Enum with underlying type of int32 + Int64 = 'l', // int64 + Int64Enum = 'k', // int64 - Enum with underlying type of int64 + Float32 = 'f', // float + Float64 = 'd', // double + String = 's', // string + InternedString = 'S', // interned string + Uri = 'u', + JSObj = 'o', // js object will be converted to a C# object (this will box numbers/bool/promises) + MONOObj = 'm', // raw mono object. Don't use it unless you know what you're doing + Auto = 'a', // the bindings layer will select an appropriate converter based on the C# method signature + ByteSpan = 'b', // Span + } + + public struct MarshalString { + public string Signature { get; private set; } + public string Key { get; private set; } + public MethodBase? Method { get; private set; } + public int ArgumentCount { get; private set; } + public bool RawReturnValue { get; private set; } + public bool ContainsAuto { get; private set; } + + public MarshalString (string s, MethodBase? method = null) { + Signature = s; + Method = method; + RawReturnValue = s.EndsWith("!"); + ArgumentCount = Signature.Length; + ContainsAuto = s.Contains((char)(int)ArgsMarshalCharacter.Auto); + + if (RawReturnValue) + ArgumentCount -= 1; + + var keySig = Signature.Replace("!", "_result_unmarshaled"); + if (keySig.Length == 0) + keySig = "$void"; + + if (ContainsAuto && (Method != null)) + Key = $"{keySig}_m{Method.MethodHandle.Value.ToInt32()}"; + else + Key = keySig; + } + + public ArgsMarshalCharacter this [int index] => + (ArgsMarshalCharacter)(int)Signature[index]; + } + + public class WasmInteropException : Exception { + public WasmInteropException (string message) + : base (message) { + } + + public WasmInteropException (string message, Exception innerException) + : base (message, innerException) { + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/UriMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/UriMarshaler.cs new file mode 100644 index 00000000000000..8f319e4f4ade65 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/UriMarshaler.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System; +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript +{ + public static class UriMarshaler + { + public static Uri FromJavaScript (string s) + { + return new Uri(s); + } + + public static string ToJavaScript (Uri u) + { + // FIXME: Uri.ToString() escapes certain characters in URIs. + // This may not be desirable, but the old marshaler seems to have had this limitation too. + return u.ToString(); + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/ILLink.Descriptors.xml b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/ILLink.Descriptors.xml new file mode 100644 index 00000000000000..a6b235ad8f42db --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/ILLink.Descriptors.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj index f1afdecab341e2..af19b9664ca542 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj @@ -4,7 +4,19 @@ $(NetCoreAppCurrent)-Browser true $(WasmXHarnessArgs) --engine-arg=--expose-gc --web-server-use-cop + ILLink.Descriptors.xml + ILLink.Descriptors.xml + + + + + + diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/HelperMarshal.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/HelperMarshal.cs index f612dcb76be527..95c0290e9c6f62 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/HelperMarshal.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/HelperMarshal.cs @@ -1,17 +1,127 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Runtime.CompilerServices; using Xunit; namespace System.Runtime.InteropServices.JavaScript.Tests { public static class HelperMarshal { + public static class CustomClassMarshaler { + public static CustomClass FromJavaScript (double d) { + return new CustomClass { D = d }; + } + + public static double ToJavaScript (CustomClass ct) { + return ct?.D ?? -1; + } + } + + public class CustomClass { + public double D; + } + + public static class CustomStructMarshaler { + public static CustomStruct FromJavaScript (double d) { + return new CustomStruct { D = d }; + } + + public static double ToJavaScript (in CustomStruct ct) { + return ct.D; + } + } + + public struct CustomStruct { + public double D; + } + + public static class CustomDateMarshaler { + public static string JavaScriptToInterchangeTransform => "return value.toISOString()"; + public static string InterchangeToJavaScriptTransform => "return new Date(value)"; + + public static CustomDate FromJavaScript (string s) { + var newDate = DateTime.Parse(s).ToUniversalTime(); + return new CustomDate { + Date = newDate + }; + } + + public static string ToJavaScript (in CustomDate cd) { + var result = cd.Date.ToString("o"); + return result; + } + } + + public struct CustomDate { + public DateTime Date; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CustomVector3 { + public float X, Y, Z; + + public override string ToString () { + return $"[{X}, {Y}, {Z}]"; + } + } + + public static unsafe class CustomVector3Marshaler { + public static int ScratchBufferSize => sizeof(CustomVector3); + public static string JavaScriptToInterchangeTransform => + @" + if (bufferSize !== 12) + throw new Error('Invalid buffer size'); + if (!Array.isArray(value) || (value.length !== 3)) + throw new Error('Invalid vector3, expected [f, f, f]'); + setF32(buffer + 0, value[0]); + setF32(buffer + 4, value[1]); + setF32(buffer + 8, value[2]); + "; + + public static string InterchangeToJavaScriptTransform => + @" + if (bufferSize !== 12) + throw new Error('Invalid buffer size'); + return [getF32(buffer + 0), getF32(buffer + 4), getF32(buffer + 8)]; + "; + + public static void ToJavaScript (ref CustomVector3 value, Span buffer) { + MemoryMarshal.Write(buffer, ref value); + } + + public static CustomVector3 FromJavaScript (ReadOnlySpan buffer) { + return MemoryMarshal.AsRef(buffer); + } + } + internal const string INTEROP_CLASS = "[System.Private.Runtime.InteropServices.JavaScript.Tests]System.Runtime.InteropServices.JavaScript.Tests.HelperMarshal:"; + + internal static CustomClass _ccValue; + private static void InvokeCustomClass(CustomClass cc) + { + _ccValue = cc; + } + private static CustomClass ReturnCustomClass(CustomClass cc) + { + return cc; + } + + internal static CustomStruct _csValue; + private unsafe static void InvokeCustomStruct(CustomStruct cs) + { + _csValue = cs; + } + private static CustomStruct ReturnCustomStruct(CustomStruct cs) + { + return cs; + } + internal static int _i32Value; private static void InvokeI32(int a, int b) { @@ -104,6 +214,61 @@ private static object InvokeObj2(object obj) return obj; } + internal static DateTime _dateTimeValue; + private static void InvokeDateTime(object boxed) + { + _dateTimeValue = (DateTime)boxed; + } + private static void InvokeDateTimeOffset(DateTimeOffset dto) + { + // FIXME + _dateTimeValue = dto.DateTime; + } + private static void InvokeDateTimeByValue(DateTime dt) + { + _dateTimeValue = dt; + } + private static void InvokeCustomDate(CustomDate cd) + { + _dateTimeValue = cd.Date; + } + private static CustomDate ReturnCustomDate(CustomDate cd) + { + return cd; + } + + internal static CustomVector3 _vec3Value; + private static void InvokeCustomVector3(CustomVector3 cv3) + { + _vec3Value = cv3; + } + private static CustomVector3 MakeCustomVector3(float x, float y, float z) + { + return new CustomVector3 { + X = x, + Y = y, + Z = z + }; + } + private static CustomVector3 ReturnCustomVector3(CustomVector3 cv3) + { + return cv3; + } + private static CustomVector3 AddCustomVector3(CustomVector3 lhs, CustomVector3 rhs) + { + return new CustomVector3 { + X = lhs.X + rhs.X, + Y = lhs.Y + rhs.Y, + Z = lhs.Z + rhs.Z + }; + } + + internal static System.Uri _uriValue; + private static void InvokeUri(System.Uri uri) + { + _uriValue = uri; + } + internal static object _marshalledObject; private static object InvokeMarshalObj() { @@ -642,65 +807,65 @@ private static Func> CreateFunctionAcceptingArray() }; } - public static Task SynchronousTask() + public static Task SynchronousTask() { return Task.CompletedTask; } - public static async Task AsynchronousTask() + public static async Task AsynchronousTask() { await Task.Yield(); } - public static Task SynchronousTaskInt(int i) + public static Task SynchronousTaskInt(int i) { return Task.FromResult(i); } - public static async Task AsynchronousTaskInt(int i) + public static async Task AsynchronousTaskInt(int i) { await Task.Yield(); return i; } - public static Task FailedSynchronousTask() + public static Task FailedSynchronousTask() { return Task.FromException(new Exception()); } - public static async Task FailedAsynchronousTask() + public static async Task FailedAsynchronousTask() { await Task.Yield(); throw new Exception(); } - public static async ValueTask AsynchronousValueTask() + public static async ValueTask AsynchronousValueTask() { await Task.Yield(); } - public static ValueTask SynchronousValueTask() + public static ValueTask SynchronousValueTask() { return ValueTask.CompletedTask; } - public static ValueTask SynchronousValueTaskInt(int i) + public static ValueTask SynchronousValueTaskInt(int i) { return ValueTask.FromResult(i); } - public static async ValueTask AsynchronousValueTaskInt(int i) + public static async ValueTask AsynchronousValueTaskInt(int i) { await Task.Yield(); return i; } - public static ValueTask FailedSynchronousValueTask() + public static ValueTask FailedSynchronousValueTask() { return ValueTask.FromException(new Exception()); } - public static async ValueTask FailedAsynchronousValueTask() + public static async ValueTask FailedAsynchronousValueTask() { await Task.Yield(); throw new Exception(); diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTests.cs index 7158c38899c13a..d97a0b807b83b5 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTests.cs @@ -464,7 +464,9 @@ public static void RoundtripCSDate() var obj = (JSObject)factory.Call(null, date); var dummy = (DateTime)obj.GetObjectProperty("dummy"); - Assert.Equal(date, dummy); + // HACK: JS Dates do not contain timezone information, so date marshaling converts all dates to + // UTC dates. + Assert.Equal(date.ToUniversalTime(), dummy.ToUniversalTime()); } [Fact] diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs index f8955384981e0e..d40d955fac4924 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs @@ -69,7 +69,7 @@ public static void MarshalArrayBuffer2Int2() for (var i = 0; i < int32View.length; i++) { int32View[i] = i * 2; } - App.call_test_method (""MarshalByteBufferToInts"", [ buffer ]); + App.call_test_method (""MarshalByteBufferToInts"", [ buffer ]); "); Assert.Equal(4, HelperMarshal._intBuffer.Length); @@ -344,7 +344,7 @@ public static void GetObjectProperties() { Runtime.InvokeJS(@" var obj = {myInt: 100, myDouble: 4.5, myString: ""Hic Sunt Dracones"", myBoolean: true}; - App.call_test_method (""RetrieveObjectProperties"", [ obj ]); + App.call_test_method (""RetrieveObjectProperties"", [ obj ]); "); Assert.Equal(100, HelperMarshal._jsProperties[0]); @@ -358,8 +358,8 @@ public static void SetObjectProperties() { Runtime.InvokeJS(@" var obj = {myInt: 200, myDouble: 0, myString: ""foo"", myBoolean: false}; - App.call_test_method (""PopulateObjectProperties"", [ obj, false ]); - App.call_test_method (""RetrieveObjectProperties"", [ obj ]); + App.call_test_method (""PopulateObjectProperties"", [ obj, false ]); + App.call_test_method (""RetrieveObjectProperties"", [ obj ]); "); Assert.Equal(100, HelperMarshal._jsProperties[0]); @@ -374,8 +374,8 @@ public static void SetObjectPropertiesIfNotExistsFalse() // This test will not create the properties if they do not already exist Runtime.InvokeJS(@" var obj = {myInt: 200}; - App.call_test_method (""PopulateObjectProperties"", [ obj, false ]); - App.call_test_method (""RetrieveObjectProperties"", [ obj ]); + App.call_test_method (""PopulateObjectProperties"", [ obj, false ]); + App.call_test_method (""RetrieveObjectProperties"", [ obj ]); "); Assert.Equal(100, HelperMarshal._jsProperties[0]); @@ -387,7 +387,7 @@ public static void SetObjectPropertiesIfNotExistsFalse() [Fact] public static void SetObjectPropertiesIfNotExistsTrue() { - // This test will set the value of the property if it exists and will create and + // This test will set the value of the property if it exists and will create and // set the value if it does not exists Runtime.InvokeJS(@" var obj = {myInt: 200}; @@ -407,7 +407,7 @@ public static void MarshalTypedArray() Runtime.InvokeJS(@" var buffer = new ArrayBuffer(16); var uint8View = new Uint8Array(buffer); - App.call_test_method (""MarshalByteBuffer"", [ uint8View ]); + App.call_test_method (""MarshalByteBuffer"", [ uint8View ]); "); Assert.Equal(16, HelperMarshal._byteBuffer.Length); @@ -437,7 +437,7 @@ public static void MarshalTypedArray2Float() { Runtime.InvokeJS(@" var typedArray = new Float32Array([1, 2.1334, 3, 4.2, 5]); - App.call_test_method (""MarshalFloat32Array"", [ typedArray ]); + App.call_test_method (""MarshalFloat32Array"", [ typedArray ]); "); Assert.Equal(1, HelperMarshal._floatBuffer[0]); @@ -456,7 +456,7 @@ public static void MarshalArrayBuffer2Float2() for (var i = 0; i < float32View.length; i++) { float32View[i] = i * 2.5; } - App.call_test_method (""MarshalArrayBufferToFloat32Array"", [ buffer ]); + App.call_test_method (""MarshalArrayBufferToFloat32Array"", [ buffer ]); "); Assert.Equal(4, HelperMarshal._floatBuffer.Length); @@ -471,7 +471,7 @@ public static void MarshalTypedArray2Double() { Runtime.InvokeJS(@" var typedArray = new Float64Array([1, 2.1334, 3, 4.2, 5]); - App.call_test_method (""MarshalFloat64Array"", [ typedArray ]); + App.call_test_method (""MarshalFloat64Array"", [ typedArray ]); "); Assert.Equal(1, HelperMarshal._doubleBuffer[0]); @@ -490,7 +490,7 @@ public static void MarshalArrayBuffer2Double() for (var i = 0; i < float64View.length; i++) { float64View[i] = i * 2.5; } - App.call_test_method (""MarshalByteBufferToDoubles"", [ buffer ]); + App.call_test_method (""MarshalByteBufferToDoubles"", [ buffer ]); "); Assert.Equal(4, HelperMarshal._doubleBuffer.Length); @@ -509,7 +509,7 @@ public static void MarshalArrayBuffer2Double2() for (var i = 0; i < float64View.length; i++) { float64View[i] = i * 2.5; } - App.call_test_method (""MarshalArrayBufferToFloat64Array"", [ buffer ]); + App.call_test_method (""MarshalArrayBufferToFloat64Array"", [ buffer ]); "); Assert.Equal(4, HelperMarshal._doubleBuffer.Length); @@ -621,7 +621,7 @@ public static void TestFunctionApply() "); Assert.Equal(2, HelperMarshal._minValue); } - + [Fact] public static void BoundStaticMethodMissingArgs() { @@ -636,7 +636,7 @@ public static void BoundStaticMethodMissingArgs() "); Assert.Equal(0, HelperMarshal._intValue); } - + [Fact] public static void BoundStaticMethodExtraArgs() { @@ -647,7 +647,7 @@ public static void BoundStaticMethodExtraArgs() "); Assert.Equal(200, HelperMarshal._intValue); } - + [Fact] public static void BoundStaticMethodArgumentTypeCoercion() { @@ -667,7 +667,7 @@ public static void BoundStaticMethodArgumentTypeCoercion() "); Assert.Equal(400, HelperMarshal._intValue); } - + [Fact] public static void BoundStaticMethodUnpleasantArgumentTypeCoercion() { @@ -697,7 +697,7 @@ public static void PassUintArgument() Assert.Equal(0xFFFFFFFEu, HelperMarshal._uintValue); } - + [Fact] public static void ReturnUintEnum () { @@ -711,7 +711,7 @@ public static void ReturnUintEnum () "); Assert.Equal((uint)TestEnum.BigValue, HelperMarshal._uintValue); } - + [Fact] public static void PassUintEnumByValue () { @@ -722,7 +722,7 @@ public static void PassUintEnumByValue () "); Assert.Equal(TestEnum.BigValue, HelperMarshal._enumValue); } - + [Fact] public static void PassUintEnumByValueMasqueradingAsInt () { @@ -735,12 +735,12 @@ public static void PassUintEnumByValueMasqueradingAsInt () "); Assert.Equal(TestEnum.BigValue, HelperMarshal._enumValue); } - + [Fact] public static void PassUintEnumByNameIsNotImplemented () { HelperMarshal._enumValue = TestEnum.Zero; - var exc = Assert.Throws( () => + var exc = Assert.Throws( () => Runtime.InvokeJS(@$" var set_enum = INTERNAL.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}SetEnumValue"", ""j""); set_enum (""BigValue""); @@ -748,11 +748,11 @@ public static void PassUintEnumByNameIsNotImplemented () ); Assert.StartsWith("Error: Expected numeric value for enum argument, got 'BigValue'", exc.Message); } - + [Fact] public static void CannotUnboxUint64 () { - var exc = Assert.Throws( () => + var exc = Assert.Throws( () => Runtime.InvokeJS(@$" var get_u64 = INTERNAL.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}GetUInt64"", """"); var u64 = get_u64(); @@ -860,6 +860,9 @@ public static void SymbolsAreMarshaledAsStrings() Assert.True(Object.ReferenceEquals(HelperMarshal._stringResource, HelperMarshal._stringResource2)); } + const string ExpectedDateString = "1937-07-02T05:35:02.0000000Z"; + static readonly DateTime ExpectedDateTime = DateTime.Parse(ExpectedDateString).ToUniversalTime(); + [Fact] public static void InternedStringReturnValuesWork() { @@ -902,16 +905,16 @@ public static void InvokeJSNotInGlobalScope() Assert.Null(result); } - private static async Task MarshalTask(string helperMethodName, string helperMethodArgs = "", string resolvedBody = "") + private static async Task MarshalTask(string helperMethodName, string helperMethodArgs = "", string resolvedBody = "") { Runtime.InvokeJS( @"globalThis.__test_promise_completed = false; " + @"globalThis.__test_promise_resolved = false; " + @"globalThis.__test_promise_failed = false; " + $@"var t = App.call_test_method ('{helperMethodName}', [ {helperMethodArgs} ], 'i'); " + - "t.then(result => { globalThis.__test_promise_resolved = true; " + resolvedBody + " })" + + "t.then(result => { globalThis.__test_promise_resolved = true; " + resolvedBody + " })" + " .catch(e => { globalThis.__test_promise_failed = true; })" + - " .finally(result => { globalThis.__test_promise_completed = true; }); " + + " .finally(result => { globalThis.__test_promise_completed = true; }); " + "" ); @@ -1019,5 +1022,181 @@ public static async Task MarshalFailedAsynchronousValueTaskDoesNotWorkYet() bool success = await MarshalTask("FailedAsynchronousValueTask"); Assert.False(success, "FailedAsynchronousValueTask didn't failed."); } + + [Fact] + public static void MarshalDateTime() + { + HelperMarshal._dateTimeValue = default(DateTime); + Runtime.InvokeJS( + $"var dt = new Date('{ExpectedDateString}');\r\n" + + "App.call_test_method ('InvokeDateTime', [ dt ], 'o');" + ); + Assert.Equal(ExpectedDateTime, HelperMarshal._dateTimeValue); + } + + [Fact] + public static void MarshalDateTimeDefault() + { + HelperMarshal._dateTimeValue = default(DateTime); + Runtime.InvokeJS( + $"var dt = new Date('{ExpectedDateString}');\r\n" + + "App.call_test_method ('InvokeDateTime', [ dt ]);" + ); + Assert.Equal(ExpectedDateTime, HelperMarshal._dateTimeValue); + } + + [Fact] + public static void MarshalDateTimeAutomatic() + { + HelperMarshal._dateTimeValue = default(DateTime); + Runtime.InvokeJS( + $"var dt = new Date('{ExpectedDateString}');\r\n" + + "App.call_test_method ('InvokeDateTime', [ dt ], 'a');" + ); + Assert.Equal(ExpectedDateTime, HelperMarshal._dateTimeValue); + } + + [Fact] + public static void MarshalDateTimeOffsetAutomatic() + { + HelperMarshal._dateTimeValue = default(DateTime); + Runtime.InvokeJS( + $"var dt = new Date('{ExpectedDateString}');\r\n" + + "App.call_test_method ('InvokeDateTimeOffset', [ dt ], 'a');" + ); + // FIXME + Assert.Equal(ExpectedDateTime, HelperMarshal._dateTimeValue); + } + + [Fact] + public static void MarshalDateTimeByValueAutomatic() + { + HelperMarshal._dateTimeValue = default(DateTime); + Runtime.InvokeJS( + $"var dt = new Date('{ExpectedDateString}');\r\n" + + "App.call_test_method ('InvokeDateTimeByValue', [ dt.valueOf() ], 'a');" + ); + Assert.Equal(ExpectedDateTime, HelperMarshal._dateTimeValue); + } + + [Fact] + public static void MarshalUri() + { + var expected = new System.Uri("https://www.example.com/"); + HelperMarshal._uriValue = default(System.Uri); + Runtime.InvokeJS( + @"var uri = 'https://www.example.com/'; + App.call_test_method ('InvokeUri', [ uri ], 'u');" + ); + Assert.Equal(expected, HelperMarshal._uriValue); + } + + [Fact] + public static void MarshalCustomClassAutomatic() + { + HelperMarshal._ccValue = new HelperMarshal.CustomClass (); + Runtime.InvokeJS( + "App.call_test_method ('InvokeCustomClass', [ 4.13 ], 'a');" + ); + Assert.Equal(4.13, HelperMarshal._ccValue?.D); + } + + [Fact] + public static void MarshalCustomStructAutomatic() + { + HelperMarshal._csValue = default(HelperMarshal.CustomStruct); + Runtime.InvokeJS( + "App.call_test_method ('InvokeCustomStruct', [ 4.13 ], 'a');" + ); + Assert.Equal(4.13, HelperMarshal._csValue.D); + } + + [Fact] + public static void MarshalCustomDateAutomatic() + { + HelperMarshal._dateTimeValue = default(DateTime); + Runtime.InvokeJS( + $"var dt = new Date('{ExpectedDateString}');\r\n" + + "App.call_test_method ('InvokeCustomDate', [ dt ], 'a');" + ); + Assert.Equal(ExpectedDateTime, HelperMarshal._dateTimeValue); + } + + [Fact] + public static void ReturnCustomClass() + { + HelperMarshal._ccValue = new HelperMarshal.CustomClass (); + Runtime.InvokeJS( + "var cc = App.call_test_method ('ReturnCustomClass', [ 4.13 ], 'a');" + + "App.call_test_method ('InvokeCustomClass', [ cc ], 'a');" + ); + Assert.Equal(4.13, HelperMarshal._ccValue?.D); + } + + [Fact] + public static void ReturnCustomStruct() + { + HelperMarshal._csValue = default(HelperMarshal.CustomStruct); + Runtime.InvokeJS( + "var cs = App.call_test_method ('ReturnCustomStruct', [ 4.13 ], 'a');" + + "App.call_test_method ('InvokeCustomStruct', [ cs ], 'a');" + ); + Assert.Equal(4.13, HelperMarshal._csValue.D); + } + + [Fact] + public static void ReturnCustomDate() + { + HelperMarshal._dateTimeValue = default(DateTime); + Runtime.InvokeJS( + $"var dt = new Date('{ExpectedDateString}');\r\n" + + "var cd = App.call_test_method ('ReturnCustomDate', [ dt ], 'a');" + + "App.call_test_method ('InvokeCustomDate', [ cd ], 'a');" + ); + Assert.Equal(ExpectedDateTime, HelperMarshal._dateTimeValue); + } + + [Fact] + public static void InvokeCustomVector3() + { + HelperMarshal._vec3Value = default(HelperMarshal.CustomVector3); + Runtime.InvokeJS( + "App.call_test_method ('InvokeCustomVector3', [ [1, 2.5, 4] ], 'a');" + ); + Assert.Equal(1, HelperMarshal._vec3Value.X); + Assert.Equal(2.5, HelperMarshal._vec3Value.Y); + Assert.Equal(4, HelperMarshal._vec3Value.Z); + } + + [Fact] + public static void ReturnCustomVector3() + { + HelperMarshal._vec3Value = default(HelperMarshal.CustomVector3); + Runtime.InvokeJS( + "var cv3 = App.call_test_method ('ReturnCustomVector3', [ [1, 2.5, 4] ], 'a');" + + "App.call_test_method ('InvokeCustomVector3', [ cv3 ], 'a');" + ); + Assert.Equal(1, HelperMarshal._vec3Value.X); + Assert.Equal(2.5, HelperMarshal._vec3Value.Y); + Assert.Equal(4, HelperMarshal._vec3Value.Z); + } + + [Fact] + public static void AddCustomVector3() + { + HelperMarshal._stringResource = null; + HelperMarshal._vec3Value = default(HelperMarshal.CustomVector3); + Runtime.InvokeJS( + "var cva = App.call_test_method ('MakeCustomVector3', [1, 2.5, 4], 'aaa');" + + "var cvb = App.call_test_method ('MakeCustomVector3', [4, 3, 2], 'aaa');" + + "var res = App.call_test_method ('AddCustomVector3', [ cva, cvb ], 'aa');" + + "App.call_test_method ('InvokeCustomVector3', [ res ], 'a');" + + "App.call_test_method ('InvokeString', [ String(res) ], 'a');" + ); + Assert.Equal("5,5.5,6", HelperMarshal._stringResource); + Assert.Equal(5, HelperMarshal._vec3Value.X); + Assert.Equal(5.5, HelperMarshal._vec3Value.Y); + Assert.Equal(6, HelperMarshal._vec3Value.Z); + } } } diff --git a/src/mono/mono/metadata/class-accessors.c b/src/mono/mono/metadata/class-accessors.c index 76065cb45c456f..662c0493553ac9 100644 --- a/src/mono/mono/metadata/class-accessors.c +++ b/src/mono/mono/metadata/class-accessors.c @@ -67,7 +67,9 @@ mono_class_try_get_generic_class (MonoClass *klass) guint32 mono_class_get_flags (MonoClass *klass) { - switch (m_class_get_class_kind (klass)) { + g_assert (klass); + guint32 kind = m_class_get_class_kind (klass); + switch (kind) { case MONO_CLASS_DEF: case MONO_CLASS_GTD: return m_classdef_get_flags ((MonoClassDef*)klass); diff --git a/src/mono/wasm/build/README.md b/src/mono/wasm/build/README.md index faed3d05827f68..c6fb0a8681944b 100644 --- a/src/mono/wasm/build/README.md +++ b/src/mono/wasm/build/README.md @@ -132,3 +132,13 @@ them for the new task assembly. - `eng/testing/linker/trimmingTests.targets`, - `src/tests/Common/wasm-test-runner/WasmTestRunner.proj` - `src/tests/Directory.Build.targets` + +## Profiling build performance + +If encountering build performance issues, you can use the rollup `--perf` option and the typescript compiler `--generateCpuProfile` option to get build profile data, like so: + +```../emsdk/node/14.15.5_64bit/bin/npm run rollup --perf -- --perf --environment Configuration:Release,NativeBinDir:./rollup-test-data,ProductVersion:12.3.4``` + +```node node_modules/typescript/lib/tsc.js --generateCpuProfile dotnet-tsc.cpuprofile -p tsconfig.json ``` + +The .cpuprofile file generated by node can be opened in the Performance tab of Chrome or Edge's devtools. \ No newline at end of file diff --git a/src/mono/wasm/build/WasmApp.targets b/src/mono/wasm/build/WasmApp.targets index 145e5c8f7ee909..090290e516b63a 100644 --- a/src/mono/wasm/build/WasmApp.targets +++ b/src/mono/wasm/build/WasmApp.targets @@ -276,6 +276,13 @@ + + + + + + + + DebugLevel="$(WasmDebugLevel)" + MarshaledTypes="@(WasmMarshaledType)" + > diff --git a/src/mono/wasm/runtime/corebindings.ts b/src/mono/wasm/runtime/corebindings.ts index 8f64ad00dbaf85..88dc910e8ed82d 100644 --- a/src/mono/wasm/runtime/corebindings.ts +++ b/src/mono/wasm/runtime/corebindings.ts @@ -1,11 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { JSHandle, GCHandle, MonoObject } from "./types"; +import { JSHandle, GCHandle, MonoObject, MonoType, MonoMethod } from "./types"; import { PromiseControl } from "./cancelable-promise"; import { runtimeHelpers } from "./imports"; -const fn_signatures: [jsname: string, csname: string, signature: string/*ArgsMarshalString*/][] = [ +const fn_signatures: [jsname: string, csname: string, signature: string][] = [ ["_get_cs_owned_object_by_js_handle", "GetCSOwnedObjectByJSHandle", "ii!"], ["_get_cs_owned_object_js_handle", "GetCSOwnedObjectJSHandle", "mi"], ["_try_get_cs_owned_object_js_handle", "TryGetCSOwnedObjectJSHandle", "mi"], @@ -23,10 +23,10 @@ const fn_signatures: [jsname: string, csname: string, signature: string/*ArgsMar ["_setup_js_cont", "SetupJSContinuation", "mo"], ["_object_to_string", "ObjectToString", "m"], - ["_get_date_value", "GetDateValue", "m"], - ["_create_date_time", "CreateDateTime", "d!"], - ["_create_uri", "CreateUri", "s!"], ["_is_simple_array", "IsSimpleArray", "m"], + + ["make_marshal_signature_info", "MakeMarshalSignatureInfo", "ii"], + ["get_custom_marshaler_info", "GetCustomMarshalerInfoForType", "is"], ]; export interface t_CSwraps { @@ -48,10 +48,12 @@ export interface t_CSwraps { _setup_js_cont(task: MonoObject, continuation: PromiseControl): MonoObject _object_to_string(obj: MonoObject): string; - _get_date_value(obj: MonoObject): number; - _create_date_time(ticks: number): MonoObject; - _create_uri(uri: string): MonoObject; _is_simple_array(obj: MonoObject): boolean; + + make_marshal_signature_info(typePtr: MonoType, methodPtr: MonoMethod): string; + get_custom_marshaler_info(typePtr: MonoType, marshalerFullName: string | null): string; + + generate_args_marshaler(signature: string, methodPtr: MonoMethod): string; } const wrapped_cs_functions: t_CSwraps = {}; diff --git a/src/mono/wasm/runtime/cs-to-js.ts b/src/mono/wasm/runtime/cs-to-js.ts index 2a7646d85973fd..eb28a7942273cb 100644 --- a/src/mono/wasm/runtime/cs-to-js.ts +++ b/src/mono/wasm/runtime/cs-to-js.ts @@ -5,7 +5,7 @@ import { mono_wasm_new_root, WasmRoot } from "./roots"; import { GCHandle, JSHandleDisposed, MonoArray, MonoArrayNull, MonoObject, MonoObjectNull, MonoString, - MonoType, MonoTypeNull + MonoType, MonoTypeNull, MarshalType, MarshalError } from "./types"; import { runtimeHelpers } from "./imports"; import { conv_string } from "./strings"; @@ -16,50 +16,8 @@ import { mono_method_get_call_signature, call_method, wrap_error } from "./metho import { _js_to_mono_obj } from "./js-to-cs"; import { _are_promises_supported, _create_cancelable_promise } from "./cancelable-promise"; import { getU32, getI32, getF32, getF64 } from "./memory"; -import { Int32Ptr, VoidPtr } from "./types/emscripten"; - -// see src/mono/wasm/driver.c MARSHAL_TYPE_xxx and Runtime.cs MarshalType -export enum MarshalType { - NULL = 0, - INT = 1, - FP64 = 2, - STRING = 3, - VT = 4, - DELEGATE = 5, - TASK = 6, - OBJECT = 7, - BOOL = 8, - ENUM = 9, - URI = 22, - SAFEHANDLE = 23, - ARRAY_BYTE = 10, - ARRAY_UBYTE = 11, - ARRAY_UBYTE_C = 12, - ARRAY_SHORT = 13, - ARRAY_USHORT = 14, - ARRAY_INT = 15, - ARRAY_UINT = 16, - ARRAY_FLOAT = 17, - ARRAY_DOUBLE = 18, - FP32 = 24, - UINT32 = 25, - INT64 = 26, - UINT64 = 27, - CHAR = 28, - STRING_INTERNED = 29, - VOID = 30, - ENUM64 = 31, - POINTER = 32 -} - -// see src/mono/wasm/driver.c MARSHAL_ERROR_xxx and Runtime.cs -export enum MarshalError { - BUFFER_TOO_SMALL = 512, - NULL_CLASS_POINTER = 513, - NULL_TYPE_POINTER = 514, - UNSUPPORTED_TYPE = 515, - FIRST = BUFFER_TOO_SMALL -} +import { extract_js_obj_root_with_converter, extract_js_obj_root_with_possible_converter } from "./custom-marshaler"; +import { VoidPtr, Int32Ptr } from "./types/emscripten"; const delegate_invoke_symbol = Symbol.for("wasm delegate_invoke"); const delegate_invoke_signature_symbol = Symbol.for("wasm delegate_invoke_signature"); @@ -84,8 +42,7 @@ function _unbox_cs_owned_root_as_js_object(root: WasmRoot) { return js_obj; } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function _unbox_mono_obj_root_with_known_nonprimitive_type_impl(root: WasmRoot, type: MarshalType, typePtr: MonoType, unbox_buffer: VoidPtr): any { +function _unbox_mono_obj_root_with_known_nonprimitive_type_impl(root: WasmRoot, type: MarshalType, typePtr: MonoType, unbox_buffer: VoidPtr) : any { //See MARSHAL_TYPE_ defines in driver.c switch (type) { case MarshalType.INT64: @@ -96,13 +53,13 @@ function _unbox_mono_obj_root_with_known_nonprimitive_type_impl(root: WasmRoot20: // clr .NET DateTime - return new Date(corebindings._get_date_value(root.value)); case 21: // clr .NET DateTimeOffset - return corebindings._object_to_string(root.value); + throw new Error("Deprecated type (DATETIME / DATETIMEOFFSET)"); case MarshalType.URI: return corebindings._object_to_string(root.value); case MarshalType.SAFEHANDLE: @@ -128,7 +84,7 @@ function _unbox_mono_obj_root_with_known_nonprimitive_type_impl(root: WasmRoot, type: MarshalType, unbox_buffer: VoidPtr): any { +export function _unbox_mono_obj_root_with_known_nonprimitive_type(root: WasmRoot, type: MarshalType, unbox_buffer: VoidPtr) : any { if (type >= MarshalError.FIRST) throw new Error(`Got marshaling error ${type} when attempting to unbox object at address ${root.value} (root located at ${root.get_address()})`); @@ -142,7 +98,7 @@ export function _unbox_mono_obj_root_with_known_nonprimitive_type(root: WasmRoot return _unbox_mono_obj_root_with_known_nonprimitive_type_impl(root, type, typePtr, unbox_buffer); } -export function _unbox_mono_obj_root(root: WasmRoot): any { +export function _unbox_mono_obj_root(root: WasmRoot) : any { if (root.value === 0) return undefined; diff --git a/src/mono/wasm/runtime/custom-marshaler.ts b/src/mono/wasm/runtime/custom-marshaler.ts new file mode 100644 index 00000000000000..c195b6b016bc38 --- /dev/null +++ b/src/mono/wasm/runtime/custom-marshaler.ts @@ -0,0 +1,371 @@ +import { Module, MONO, BINDING, runtimeHelpers } from "./imports"; +import cwraps from "./cwraps"; +import { WasmRoot } from "./roots"; +import { + MonoMethod, MonoObject, MonoObjectNull, + MonoType, MonoTypeNull, + MarshalType, MarshalTypeRecord, CustomMarshalerInfo +} from "./types"; +import { + mono_bind_method, _create_named_function, + _get_type_aqn, _get_type_name, + get_method_signature_info, bindings_named_closures, + TypeConverter +} from "./method-binding"; +import { + temp_malloc, _create_temp_frame, _release_temp_frame, + getI8, getI16, getI32, getI64, + getU8, getU16, getU32, + getF32, getF64, + setI8, setI16, setI32, setI64, + setU8, setU16, setU32, + setF32, setF64, +} from "./memory"; +import { _unbox_ref_type_root_as_js_object } from "./cs-to-js"; +import { js_to_mono_obj } from "./js-to-cs"; +import cswraps from "./corebindings"; +import { VoidPtr } from "./types/emscripten"; + +const _custom_marshaler_info_cache = new Map(); +const _struct_unboxer_cache = new Map(); +const _automatic_converter_table = new Map(); +export const _custom_marshaler_name_table : { [key: string] : string } = {}; +const _temp_unbox_buffer_cache = new Map(); +let _has_logged_custom_marshaler_table = false; + +function extract_js_obj_root_with_converter_impl (root : WasmRoot, typePtr : MonoType, unbox_buffer : VoidPtr, optional: boolean) : any { + if (root.value === MonoObjectNull) + return null; + + const converter = _get_struct_unboxer_for_type (typePtr); + + if (converter) { + let buffer_is_temporary = false; + if (!unbox_buffer) { + buffer_is_temporary = true; + if (_temp_unbox_buffer_cache.has(typePtr)) { + unbox_buffer = _temp_unbox_buffer_cache.get(typePtr); + _temp_unbox_buffer_cache.delete(typePtr); + } else { + unbox_buffer = Module._malloc(runtimeHelpers._unbox_buffer_size); + } + // TODO: Verify the MarshalType return value? + cwraps.mono_wasm_try_unbox_primitive_and_get_type(root.value, unbox_buffer, runtimeHelpers._unbox_buffer_size); + } + const objectSize = getI32(unbox_buffer + 4); + const pUnboxedData = unbox_buffer + 8; + _create_temp_frame(); + try { + // Reftypes have no size because they cannot be copied into the unbox buffer, + // so we pass their managed address directly to the converter + if (objectSize <= 0) + return converter(root.value); + else + return converter(pUnboxedData); + } finally { + _release_temp_frame(); + if (buffer_is_temporary) { + if (_temp_unbox_buffer_cache.has(typePtr)) + Module._free(unbox_buffer); + else + _temp_unbox_buffer_cache.set(typePtr, unbox_buffer); + } + } + } else if (optional) + return _unbox_ref_type_root_as_js_object (root); + else + throw new Error (`No CustomJavaScriptMarshaler found for type ${_get_type_name(typePtr)}`); +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function extract_js_obj_root_with_converter (root : WasmRoot, typePtr : MonoType, unbox_buffer : VoidPtr) : any { + return extract_js_obj_root_with_converter_impl(root, typePtr, unbox_buffer, false); +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function extract_js_obj_root_with_possible_converter (root : WasmRoot, typePtr : MonoType, unbox_buffer : VoidPtr) : any { + return extract_js_obj_root_with_converter_impl(root, typePtr, unbox_buffer, true); +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function box_js_obj_with_converter (js_obj : any, typePtr : MonoType) : MonoObject { + if ((js_obj === null) || (js_obj === undefined)) + return MonoObjectNull; + + if (!typePtr) + throw new Error("No type pointer provided"); + + const converter = _pick_automatic_converter_for_type(typePtr); + if (!converter) + throw new Error (`No CustomJavaScriptMarshaler found for type ${_get_type_name(typePtr)}`); + + _create_temp_frame(); + try { + return converter(js_obj); + } finally { + _release_temp_frame(); + } +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +function _create_interchange_closure (typePtr : MonoType) : any { + return { + // Put binding/mono API namespaces in the closure so that interchange filters can use them + Module, + MONO, + BINDING, + // RuntimeTypeHandle for the type so that type-oriented APIs can be used easily + typePtr, + // Special interchange-only API for temporary allocations + alloca: temp_malloc, + // Memory accessors + getI8, getI16, getI32, getI64, + getU8, getU16, getU32, + getF32, getF64, + setI8, setI16, setI32, setI64, + setU8, setU16, setU32, + setF32, setF64, + }; +} + +function _compile_interchange_to_js (typePtr : MonoType, boundConverter : Function, js : string | undefined, info : CustomMarshalerInfo) : Function { + if (!js) + return boundConverter; + + const closure = _create_interchange_closure(typePtr); + const hasScratchBuffer = (info.scratchBufferSize || 0) > 0; + + let converterKey = boundConverter.name || "boundConverter"; + if (converterKey in closure) + converterKey += "_"; + closure[converterKey] = boundConverter; + + const filterParams = hasScratchBuffer + ? ["buffer", "bufferSize"] + : ["value"]; + + const filterName = "interchange_to_js_filter_for_type" + typePtr; + + const filterExpression = _create_named_function( + filterName, filterParams, js, closure + ); + closure[filterName] = filterExpression; + + let bodyJs : string; + if (hasScratchBuffer) { + bodyJs = `let buffer = alloca(${info.scratchBufferSize});\r\n` + + `${converterKey}(value, [buffer, ${info.scratchBufferSize}]);\r\n` + + `let filteredValue = ${filterName}(buffer, ${info.scratchBufferSize});\r\n` + + "return filteredValue;"; + } else { + bodyJs = `let convertedValue = ${converterKey}(value), filteredValue = ${filterName}(convertedValue);\r\n` + + "return filteredValue;"; + } + const functionName = "interchange_to_js_for_type" + typePtr; + const result = _create_named_function( + functionName, ["value"], bodyJs, closure + ); + + return result; +} + +function _get_custom_marshaler_info_for_type (typePtr : MonoType) : CustomMarshalerInfo | null { + if (!typePtr) + return null; + if (!_custom_marshaler_name_table) + return null; + + let result; + if (!_custom_marshaler_info_cache.has (typePtr)) { + const aqn = _get_type_aqn (typePtr); + if (!aqn.startsWith("System.Object, System.Private.CoreLib, ")) { + let marshalerAQN = _custom_marshaler_name_table[aqn]; + if (!marshalerAQN) { + for (const k in _custom_marshaler_name_table) { + // Perform a loose match against the assembly-qualified type names, + // because in some cases it is not possible or convenient to + // include the full string (i.e. version, culture, etc) + const isMatch = k.startsWith(aqn) || aqn.startsWith(k); + if (isMatch) { + marshalerAQN = _custom_marshaler_name_table[k]; + break; + } + } + } + + if (!marshalerAQN) { + if (!_has_logged_custom_marshaler_table) { + _has_logged_custom_marshaler_table = true; + console.log(`WARNING: Type "${aqn}" has no registered custom marshaler. A dump of the marshaler table follows:`); + for (const k in _custom_marshaler_name_table) + console.log(` ${k}: ${_custom_marshaler_name_table[k]}`); + } + _custom_marshaler_info_cache.set(typePtr, null); + return null; + } + const json = cswraps.get_custom_marshaler_info (typePtr, marshalerAQN); + result = JSON.parse(json); + if (!result) + throw new Error (`Configured custom marshaler for ${aqn} could not be loaded: ${marshalerAQN}`); + } else { + result = null; + } + + _custom_marshaler_info_cache.set (typePtr, result); + } else { + result = _custom_marshaler_info_cache.get (typePtr); + } + + return result || null; +} + +function _get_struct_unboxer_for_type (typePtr : MonoType) : Function | null { + if (!typePtr) + throw new Error("no type"); + + if (!_struct_unboxer_cache.has (typePtr)) { + const info = _get_custom_marshaler_info_for_type (typePtr); + if (!info) { + _struct_unboxer_cache.set (typePtr, null); + return null; + } + + if (info.error) + console.error(`Error while configuring automatic converter for type ${_get_type_name(typePtr)}: ${info.error}`); + + const interchangeToJs = info.interchangeToJs; + + const convMethod = info.outputPtr; + if (!convMethod) { + if (info.typePtr) + console.error(`Automatic converter for type ${_get_type_name(typePtr)} has no suitable ToJavaScript method`); + // We explicitly store null in the cache so that lookups are not performed again for this type + _struct_unboxer_cache.set (typePtr, null); + } else { + const typeName = _get_type_name(typePtr); + const signature = (info.scratchBufferSize || 0) > 0 + ? "mb" + : "m"; + const boundConverter = mono_bind_method ( + convMethod, null, signature, typeName + "$ToJavaScript" + ); + + _struct_unboxer_cache.set (typePtr, _compile_interchange_to_js (typePtr, boundConverter, interchangeToJs, info)); + } + } + + return _struct_unboxer_cache.get (typePtr) || null; +} + +function _compile_js_to_interchange (typePtr : MonoType, boundConverter : Function, js : string | undefined, info : CustomMarshalerInfo) : Function { + if (!js) + return boundConverter; + + const closure = _create_interchange_closure(typePtr); + const hasScratchBuffer = (info.scratchBufferSize || 0) > 0; + + let converterKey = boundConverter.name || "boundConverter"; + if (converterKey in closure) + converterKey += "_"; + closure[converterKey] = boundConverter; + + const filterParams = hasScratchBuffer + ? ["value", "buffer", "bufferSize"] + : ["value"]; + + const filterName = "js_to_interchange_filter_for_type" + typePtr; + const filterExpression = _create_named_function( + filterName, filterParams, js, closure + ); + + closure[filterName] = filterExpression; + const functionName = "js_to_interchange_for_type" + typePtr; + + let bodyJs : string; + if (hasScratchBuffer) { + bodyJs = `let buffer = alloca(${info.scratchBufferSize});\r\n` + + `${filterName}(value, buffer, ${info.scratchBufferSize});\r\n` + + `let span = [buffer, ${info.scratchBufferSize}];\r\n` + + `let convertedResult = ${converterKey}(span, method, parmIdx);\r\n` + + "return convertedResult;"; + } else { + bodyJs = `let filteredValue = ${filterName}(value);\r\n` + + `let convertedResult = ${converterKey}(filteredValue, method, parmIdx);\r\n` + + "return convertedResult;"; + } + + const result = _create_named_function( + functionName, + ["value", "method", "parmIdx"], bodyJs, closure + ); + + return result; +} + +export function _pick_automatic_converter_for_type (typePtr : MonoType) : Function | null { + if (!typePtr) + throw new Error("typePtr is null or undefined"); + + if (!_automatic_converter_table.has(typePtr)) { + let info = _get_custom_marshaler_info_for_type(typePtr); + // HACK + if (!info) + info = {}; + if (info.error) + console.error(`Error while configuring automatic converter for type ${_get_type_name(typePtr)}: ${info.error}`); + + const jsToInterchange = info.jsToInterchange; + + const convMethod = info.inputPtr; + if (!convMethod) { + if (info.typePtr) + console.error(`Automatic converter for type ${_get_type_name(typePtr)} has no suitable FromJavaScript method`); + _automatic_converter_table.set(typePtr, null); + return null; + } + + // FIXME + const sigInfo = get_method_signature_info(MonoTypeNull, convMethod); + if (sigInfo.parameters.length < 1) + throw new Error("Expected at least one parameter"); + // Return unboxed so it can go directly into the arguments list + const signature = sigInfo.parameters[0].signatureChar + "!"; + const methodName = _get_type_name(typePtr) + "$FromJavaScript"; + const boundConverter = mono_bind_method( + convMethod, null, signature, methodName + ); + + const result = _compile_js_to_interchange(typePtr, boundConverter, jsToInterchange, info); + + _automatic_converter_table.set(typePtr, result); + bindings_named_closures.set(`type${typePtr}`, result); + } + + return _automatic_converter_table.get(typePtr) || null; +} + +export function _pick_automatic_converter (methodPtr : MonoMethod, args_marshal : string, paramRecord : MarshalTypeRecord) : TypeConverter { + const needs_unbox = (paramRecord.marshalType === MarshalType.VT); + + if ( + (paramRecord.marshalType === MarshalType.VT) || + (paramRecord.marshalType === MarshalType.OBJECT) + ) { + const res = _pick_automatic_converter_for_type (paramRecord.typePtr); + if (res) { + return { + convert: res, + needs_root: !needs_unbox, + needs_unbox + }; + } + if (needs_unbox) + throw new Error(`found no automatic converter for type ${_get_type_name(paramRecord.typePtr)}`); + } + + return { + convert: js_to_mono_obj, + needs_root: !needs_unbox, + needs_unbox + }; +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/cwraps.ts b/src/mono/wasm/runtime/cwraps.ts index 98dd8f8e4e9bb5..3351c8d67b85a1 100644 --- a/src/mono/wasm/runtime/cwraps.ts +++ b/src/mono/wasm/runtime/cwraps.ts @@ -57,6 +57,7 @@ const fn_signatures: [ident: string, returnType: string | null, argTypes?: strin ["mono_wasm_type_get_class", "number", ["number"]], ["mono_wasm_get_type_name", "string", ["number"]], ["mono_wasm_get_type_aqn", "string", ["number"]], + ["mono_wasm_get_class_for_bind_or_invoke", "number", ["number", "number"]], ["mono_wasm_unbox_rooted", "number", ["number"]], //DOTNET @@ -97,7 +98,7 @@ export interface t_Cwraps { mono_wasm_find_corlib_type(namespace: string, name: string): MonoType; mono_wasm_assembly_find_type(assembly: MonoAssembly, namespace: string, name: string): MonoType; mono_wasm_assembly_find_method(klass: MonoClass, name: string, args: number): MonoMethod; - mono_wasm_invoke_method(method: MonoMethod, this_arg: MonoObject, params: VoidPtr, out_exc: MonoObject): MonoObject; + mono_wasm_invoke_method(method: MonoMethod, this_arg: MonoObject, params: VoidPtr, out_exc: VoidPtr): MonoObject; mono_wasm_string_get_utf8(str: MonoString): CharPtr; mono_wasm_string_from_utf16(str: CharPtr, len: number): MonoString; mono_wasm_get_obj_type(str: MonoObject): number; @@ -117,6 +118,7 @@ export interface t_Cwraps { mono_wasm_type_get_class(ty: MonoType): MonoClass; mono_wasm_get_type_name(ty: MonoType): string; mono_wasm_get_type_aqn(ty: MonoType): string; + mono_wasm_get_class_for_bind_or_invoke(this_arg: MonoObject, method: MonoMethod): MonoClass; mono_wasm_unbox_rooted(obj: MonoObject): VoidPtr; //DOTNET diff --git a/src/mono/wasm/runtime/debug.ts b/src/mono/wasm/runtime/debug.ts index 8eb60b46e5478e..2aa8f121a55b80 100644 --- a/src/mono/wasm/runtime/debug.ts +++ b/src/mono/wasm/runtime/debug.ts @@ -179,7 +179,7 @@ export function mono_wasm_call_function_on(request: CallRequest): CFOResponse { const fn_args = request.arguments != undefined ? request.arguments.map(a => JSON.stringify(a.value)) : []; - const fn_body_template = `var fn = ${request.functionDeclaration}; return fn.apply(proxy, [${fn_args}]);`; + const fn_body_template = `const fn = ${request.functionDeclaration}; return fn.apply(proxy, [${fn_args}]);`; const fn_defn = new Function("proxy", fn_body_template); const fn_res = fn_defn(proxy); diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index b939d337aab85f..8687c0769e4ae1 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -1,7 +1,7 @@ //! Licensed to the .NET Foundation under one or more agreements. //! The .NET Foundation licenses this file to you under the MIT license. -//! -//! This is generated file, see src/mono/wasm/runtime/rollup.config.js +//! +//! This is generated file, see src/mono/wasm/runtime/rollup.config.js declare interface ManagedPointer { __brandManagedPointer: "ManagedPointer"; @@ -139,6 +139,9 @@ declare type MonoConfig = { aot_profiler_options?: AOTProfilerOptions; coverage_profiler_options?: CoverageProfilerOptions; ignore_pdb_load_errors?: boolean; + custom_marshalers?: { + [key: string]: string | undefined; + }; }; declare type MonoConfigError = { isError: true; @@ -223,6 +226,7 @@ declare type DotnetModuleConfigImports = { declare function mono_wasm_runtime_ready(): void; declare function mono_wasm_setenv(name: string, value: string): void; +declare function mono_wasm_register_custom_marshaler(aqn: string, marshalerAQN: string): void; declare function mono_load_runtime_and_bcl_args(config: MonoConfig | MonoConfigError | undefined): Promise; declare function mono_wasm_load_data_archive(data: Uint8Array, prefix: string): boolean; /** @@ -237,7 +241,7 @@ declare function mono_wasm_load_config(configFilePath: string): Promise; declare function mono_wasm_load_icu_data(offset: VoidPtr): boolean; declare function conv_string(mono_obj: MonoString): string | null; -declare function js_string_to_mono_string(string: string): MonoString | null; +declare function js_string_to_mono_string(string: string): MonoString; declare function js_to_mono_obj(js_obj: any): MonoObject; declare function js_typed_array_to_array(js_obj: any): MonoArray; @@ -250,25 +254,26 @@ declare function mono_call_assembly_entry_point(assembly: string, args?: any[], declare function mono_wasm_load_bytes_into_heap(bytes: Uint8Array): VoidPtr; -declare type _MemOffset = number | VoidPtr | NativePointer; -declare function setU8(offset: _MemOffset, value: number): void; -declare function setU16(offset: _MemOffset, value: number): void; -declare function setU32(offset: _MemOffset, value: number): void; -declare function setI8(offset: _MemOffset, value: number): void; -declare function setI16(offset: _MemOffset, value: number): void; -declare function setI32(offset: _MemOffset, value: number): void; -declare function setI64(offset: _MemOffset, value: number): void; -declare function setF32(offset: _MemOffset, value: number): void; -declare function setF64(offset: _MemOffset, value: number): void; -declare function getU8(offset: _MemOffset): number; -declare function getU16(offset: _MemOffset): number; -declare function getU32(offset: _MemOffset): number; -declare function getI8(offset: _MemOffset): number; -declare function getI16(offset: _MemOffset): number; -declare function getI32(offset: _MemOffset): number; -declare function getI64(offset: _MemOffset): number; -declare function getF32(offset: _MemOffset): number; -declare function getF64(offset: _MemOffset): number; +declare type DotnetMemOffset = number | NativePointer; +declare type DotnetMemValue = number | NativePointer | ManagedPointer; +declare function setU8(offset: DotnetMemOffset, value: number): void; +declare function setU16(offset: DotnetMemOffset, value: number): void; +declare function setU32(offset: DotnetMemOffset, value: DotnetMemValue): void; +declare function setI8(offset: DotnetMemOffset, value: number): void; +declare function setI16(offset: DotnetMemOffset, value: number): void; +declare function setI32(offset: DotnetMemOffset, value: number): void; +declare function setI64(offset: DotnetMemOffset, value: number): void; +declare function setF32(offset: DotnetMemOffset, value: number): void; +declare function setF64(offset: DotnetMemOffset, value: number): void; +declare function getU8(offset: DotnetMemOffset): number; +declare function getU16(offset: DotnetMemOffset): number; +declare function getU32(offset: DotnetMemOffset): number; +declare function getI8(offset: DotnetMemOffset): number; +declare function getI16(offset: DotnetMemOffset): number; +declare function getI32(offset: DotnetMemOffset): number; +declare function getI64(offset: DotnetMemOffset): number; +declare function getF32(offset: DotnetMemOffset): number; +declare function getF64(offset: DotnetMemOffset): number; declare function mono_run_main_and_exit(main_assembly_name: string, args: string[]): Promise; declare function mono_run_main(main_assembly_name: string, args: string[]): Promise; @@ -286,6 +291,7 @@ declare const MONO: { mono_wasm_release_roots: typeof mono_wasm_release_roots; mono_run_main: typeof mono_run_main; mono_run_main_and_exit: typeof mono_run_main_and_exit; + mono_wasm_register_custom_marshaler: typeof mono_wasm_register_custom_marshaler; mono_wasm_add_assembly: (name: string, data: VoidPtr, size: number) => number; mono_wasm_load_runtime: (unused: string, debug_level: number) => void; config: MonoConfig | MonoConfigError; diff --git a/src/mono/wasm/runtime/driver.c b/src/mono/wasm/runtime/driver.c index dbe9800b9f12de..425d39fa1b7a4e 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/runtime/driver.c @@ -90,21 +90,21 @@ char *mono_method_get_full_name (MonoMethod *method); #define MARSHAL_TYPE_VOID 30 #define MARSHAL_TYPE_POINTER 32 +// Used for passing spans to C# from the JS bindings. Since spans have type restrictions, +// no boxed value will ever have this type and driver.c does not ever produce it +#define MARSHAL_TYPE_SPAN_BYTE 33 + // errors #define MARSHAL_ERROR_BUFFER_TOO_SMALL 512 #define MARSHAL_ERROR_NULL_CLASS_POINTER 513 #define MARSHAL_ERROR_NULL_TYPE_POINTER 514 -static MonoClass* datetime_class; -static MonoClass* datetimeoffset_class; static MonoClass* uri_class; static MonoClass* task_class; static MonoClass* safehandle_class; static MonoClass* voidtaskresult_class; -static int resolved_datetime_class = 0, - resolved_datetimeoffset_class = 0, - resolved_uri_class = 0, +static int resolved_uri_class = 0, resolved_task_class = 0, resolved_safehandle_class = 0, resolved_voidtaskresult_class = 0; @@ -158,7 +158,7 @@ mono_wasm_register_root (char *start, size_t size, const char *name) return mono_gc_register_root (start, size, (MonoGCDescriptor)NULL, MONO_ROOT_SOURCE_EXTERNAL, NULL, name ? name : "mono_wasm_register_root"); } -EMSCRIPTEN_KEEPALIVE void +EMSCRIPTEN_KEEPALIVE void mono_wasm_deregister_root (char *addr) { mono_gc_deregister_root (addr); @@ -575,7 +575,7 @@ mono_wasm_assembly_load (const char *name) return res; } -EMSCRIPTEN_KEEPALIVE MonoAssembly* +EMSCRIPTEN_KEEPALIVE MonoAssembly* mono_wasm_get_corlib () { return mono_image_get_assembly (mono_get_corlib()); @@ -656,7 +656,7 @@ mono_wasm_assembly_get_entry_point (MonoAssembly *assembly) uint32_t entry = mono_image_get_entry_point (image); if (!entry) return NULL; - + mono_domain_ensure_entry_assembly (root_domain, assembly); method = mono_get_method (image, entry, NULL); @@ -752,14 +752,6 @@ MonoClass* mono_get_uri_class(MonoException** exc) void mono_wasm_ensure_classes_resolved () { - if (!datetime_class && !resolved_datetime_class) { - datetime_class = mono_class_from_name (mono_get_corlib(), "System", "DateTime"); - resolved_datetime_class = 1; - } - if (!datetimeoffset_class && !resolved_datetimeoffset_class) { - datetimeoffset_class = mono_class_from_name (mono_get_corlib(), "System", "DateTimeOffset"); - resolved_datetimeoffset_class = 1; - } if (!uri_class && !resolved_uri_class) { MonoException** exc = NULL; uri_class = mono_get_uri_class(exc); @@ -784,7 +776,8 @@ mono_wasm_marshal_type_from_mono_type (int mono_type, MonoClass *klass, MonoType return MARSHAL_TYPE_VOID; case MONO_TYPE_BOOLEAN: return MARSHAL_TYPE_BOOL; - case MONO_TYPE_I: // IntPtr + case MONO_TYPE_I: // IntPtr + case MONO_TYPE_U: // UIntPtr case MONO_TYPE_PTR: return MARSHAL_TYPE_POINTER; case MONO_TYPE_I1: @@ -810,29 +803,29 @@ mono_wasm_marshal_type_from_mono_type (int mono_type, MonoClass *klass, MonoType return MARSHAL_TYPE_STRING; case MONO_TYPE_SZARRAY: { // simple zero based one-dim-array if (klass) { - MonoClass *eklass = mono_class_get_element_class (klass); - MonoType *etype = mono_class_get_type (eklass); - - switch (mono_type_get_type (etype)) { - case MONO_TYPE_U1: - return MARSHAL_ARRAY_UBYTE; - case MONO_TYPE_I1: - return MARSHAL_ARRAY_BYTE; - case MONO_TYPE_U2: - return MARSHAL_ARRAY_USHORT; - case MONO_TYPE_I2: - return MARSHAL_ARRAY_SHORT; - case MONO_TYPE_U4: - return MARSHAL_ARRAY_UINT; - case MONO_TYPE_I4: - return MARSHAL_ARRAY_INT; - case MONO_TYPE_R4: - return MARSHAL_ARRAY_FLOAT; - case MONO_TYPE_R8: - return MARSHAL_ARRAY_DOUBLE; - default: - return MARSHAL_TYPE_OBJECT; - } + MonoClass *eklass = mono_class_get_element_class (klass); + MonoType *etype = mono_class_get_type (eklass); + + switch (mono_type_get_type (etype)) { + case MONO_TYPE_U1: + return MARSHAL_ARRAY_UBYTE; + case MONO_TYPE_I1: + return MARSHAL_ARRAY_BYTE; + case MONO_TYPE_U2: + return MARSHAL_ARRAY_USHORT; + case MONO_TYPE_I2: + return MARSHAL_ARRAY_SHORT; + case MONO_TYPE_U4: + return MARSHAL_ARRAY_UINT; + case MONO_TYPE_I4: + return MARSHAL_ARRAY_INT; + case MONO_TYPE_R4: + return MARSHAL_ARRAY_FLOAT; + case MONO_TYPE_R8: + return MARSHAL_ARRAY_DOUBLE; + default: + return MARSHAL_TYPE_OBJECT; + } } else { return MARSHAL_TYPE_OBJECT; } @@ -841,24 +834,20 @@ mono_wasm_marshal_type_from_mono_type (int mono_type, MonoClass *klass, MonoType mono_wasm_ensure_classes_resolved (); if (klass) { - if (klass == datetime_class) - return MARSHAL_TYPE_DATE; - if (klass == datetimeoffset_class) - return MARSHAL_TYPE_DATEOFFSET; - if (uri_class && mono_class_is_assignable_from(uri_class, klass)) - return MARSHAL_TYPE_URI; - if (klass == voidtaskresult_class) - return MARSHAL_TYPE_VOID; - if (mono_class_is_enum (klass)) - return MARSHAL_TYPE_ENUM; + if (uri_class && mono_class_is_assignable_from(uri_class, klass)) + return MARSHAL_TYPE_URI; + if (klass == voidtaskresult_class) + return MARSHAL_TYPE_VOID; + if (mono_class_is_enum (klass)) + return MARSHAL_TYPE_ENUM; if (type && !mono_type_is_reference (type)) //vt - return MARSHAL_TYPE_VT; - if (mono_class_is_delegate (klass)) - return MARSHAL_TYPE_DELEGATE; - if (class_is_task(klass)) - return MARSHAL_TYPE_TASK; + return MARSHAL_TYPE_VT; + if (mono_class_is_delegate (klass)) + return MARSHAL_TYPE_DELEGATE; + if (class_is_task(klass)) + return MARSHAL_TYPE_TASK; if (safehandle_class && (klass == safehandle_class || mono_class_is_subclass_of(klass, safehandle_class, 0))) - return MARSHAL_TYPE_SAFEHANDLE; + return MARSHAL_TYPE_SAFEHANDLE; } return MARSHAL_TYPE_OBJECT; @@ -929,7 +918,7 @@ mono_wasm_try_unbox_primitive_and_get_type (MonoObject *obj, void *result, int r MonoType *type = mono_class_get_type (klass), *original_type = type; if (!type) return MARSHAL_ERROR_NULL_TYPE_POINTER; - + if ((klass == mono_get_string_class ()) && mono_string_instance_is_interned ((MonoString *)obj)) { *resultL = 0; @@ -939,19 +928,29 @@ mono_wasm_try_unbox_primitive_and_get_type (MonoObject *obj, void *result, int r if (mono_class_is_enum (klass)) type = mono_type_get_underlying_type (type); - + if (!type) return MARSHAL_ERROR_NULL_TYPE_POINTER; - + + if (!type) + return MARSHAL_ERROR_NULL_TYPE_POINTER; + int mono_type = mono_type_get_type (type); - + + if (mono_type == MONO_TYPE_GENERICINST) { + // HACK: While the 'any other type' fallback is valid for classes, it will do the + // wrong thing for structs, so we need to make sure the valuetype handler is used + if (mono_type_generic_inst_is_valuetype (type)) + mono_type = MONO_TYPE_VALUETYPE; + } + if (mono_type == MONO_TYPE_GENERICINST) { - // HACK: While the 'any other type' fallback is valid for classes, it will do the + // HACK: While the 'any other type' fallback is valid for classes, it will do the // wrong thing for structs, so we need to make sure the valuetype handler is used if (mono_type_generic_inst_is_valuetype (type)) mono_type = MONO_TYPE_VALUETYPE; } - + // FIXME: We would prefer to unbox once here but it will fail if the value isn't unboxable switch (mono_type) { @@ -994,7 +993,7 @@ mono_wasm_try_unbox_primitive_and_get_type (MonoObject *obj, void *result, int r break; case MONO_TYPE_VALUETYPE: { - int obj_size = mono_object_get_size (obj), + int obj_size = mono_object_get_size (obj), required_size = (sizeof (int)) + (sizeof (MonoType *)) + obj_size; // Check whether this struct has special-case marshaling @@ -1104,7 +1103,7 @@ mono_wasm_enable_on_demand_gc (int enable) } EMSCRIPTEN_KEEPALIVE MonoString * -mono_wasm_intern_string (MonoString *string) +mono_wasm_intern_string (MonoString *string) { return mono_string_intern (string); } @@ -1156,12 +1155,22 @@ mono_wasm_unbox_rooted (MonoObject *obj) return mono_object_unbox (obj); } -EMSCRIPTEN_KEEPALIVE char * +EMSCRIPTEN_KEEPALIVE MonoClass * +mono_wasm_get_class_for_bind_or_invoke (MonoObject *this_arg, MonoMethod *method) { + if (this_arg) + return mono_object_get_class (this_arg); + else if (method) + return mono_method_get_class (method); + else + return NULL; +} + +EMSCRIPTEN_KEEPALIVE char * mono_wasm_get_type_name (MonoType * typePtr) { return mono_type_get_name_full (typePtr, MONO_TYPE_NAME_FORMAT_REFLECTION); } -EMSCRIPTEN_KEEPALIVE char * +EMSCRIPTEN_KEEPALIVE char * mono_wasm_get_type_aqn (MonoType * typePtr) { return mono_type_get_name_full (typePtr, MONO_TYPE_NAME_FORMAT_ASSEMBLY_QUALIFIED); } diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index d960519e9b072b..d734abadb4fee6 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -33,7 +33,8 @@ import { mono_wasm_load_data_archive, mono_wasm_asm_loaded, mono_wasm_pre_init, mono_wasm_runtime_is_initialized, - mono_wasm_on_runtime_initialized + mono_wasm_on_runtime_initialized, + mono_wasm_register_custom_marshaler } from "./startup"; import { mono_set_timeout, schedule_background_exec } from "./scheduling"; import { mono_wasm_load_icu_data, mono_wasm_get_icudt_name } from "./icu"; @@ -49,7 +50,7 @@ import { mono_wasm_get_by_index, mono_wasm_get_global_object, mono_wasm_get_object_property, mono_wasm_invoke_js, mono_wasm_invoke_js_blazor, - mono_wasm_invoke_js_with_args, mono_wasm_set_by_index, mono_wasm_set_object_property + mono_wasm_invoke_js_with_args, mono_wasm_set_by_index, mono_wasm_set_object_property, } from "./method-calls"; import { mono_wasm_typed_array_copy_to, mono_wasm_typed_array_from, mono_wasm_typed_array_copy_from, mono_wasm_load_bytes_into_heap } from "./buffers"; import { mono_wasm_cancel_promise } from "./cancelable-promise"; @@ -83,6 +84,8 @@ const MONO = { mono_run_main, mono_run_main_and_exit, + mono_wasm_register_custom_marshaler, + // for Blazor's future! mono_wasm_add_assembly: cwraps.mono_wasm_add_assembly, mono_wasm_load_runtime: cwraps.mono_wasm_load_runtime, @@ -218,7 +221,7 @@ function initializeImportsAndExports( // backward compatibility // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - module.mono_bind_static_method = (fqn: string, signature: string/*ArgsMarshalString*/): Function => { + module.mono_bind_static_method = (fqn: string, signature: string): Function => { console.warn("Module.mono_bind_static_method is obsolete, please use BINDING.bind_static_method instead"); return mono_bind_static_method(fqn, signature); }; @@ -266,7 +269,7 @@ function initializeImportsAndExports( // if onRuntimeInitialized is set it's probably Blazor, we let them to do their own init sequence if (!module.onRuntimeInitialized) { - // note this would keep running in async-parallel with emscripten's `run()` and `postRun()` + // note this would keep running in async-parallel with emscripten's `run()` and `postRun()` // because it's loading files asynchronously and the emscripten is not awaiting onRuntimeInitialized // execution order == [1] == module.onRuntimeInitialized = () => mono_wasm_on_runtime_initialized(); diff --git a/src/mono/wasm/runtime/js-to-cs.ts b/src/mono/wasm/runtime/js-to-cs.ts index 8bafaec669cd8f..78f1603dfd4a12 100644 --- a/src/mono/wasm/runtime/js-to-cs.ts +++ b/src/mono/wasm/runtime/js-to-cs.ts @@ -17,6 +17,8 @@ import { has_backing_array_buffer } from "./buffers"; import { JSHandle, MonoArray, MonoMethod, MonoObject, MonoObjectNull, MonoString, wasm_type_symbol } from "./types"; import { setI32, setU32, setF64 } from "./memory"; import { Int32Ptr, TypedArray } from "./types/emscripten"; +import { box_js_obj_with_converter } from "./custom-marshaler"; +import { find_corlib_type, find_type_in_assembly } from "./class-loader"; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function _js_to_mono_uri(should_add_in_flight: boolean, js_obj: any): MonoObject { @@ -26,12 +28,11 @@ export function _js_to_mono_uri(should_add_in_flight: boolean, js_obj: any): Mon return MonoObjectNull; case typeof js_obj === "symbol": case typeof js_obj === "string": - return corebindings._create_uri(js_obj); + return box_js_obj_with_converter(js_obj, find_type_in_assembly ("System.Private.Uri", "System", "Uri", true)); default: return _extract_mono_obj(should_add_in_flight, js_obj); } } - // this is only used from Blazor // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function js_to_mono_obj(js_obj: any): MonoObject { @@ -70,7 +71,7 @@ export function _js_to_mono_obj(should_add_in_flight: boolean, js_obj: any): Mon } case js_obj.constructor.name === "Date": // getTime() is always UTC - return corebindings._create_date_time(js_obj.getTime()); + return box_js_obj_with_converter(js_obj, find_corlib_type("System", "DateTime", true)); default: return _extract_mono_obj(should_add_in_flight, js_obj); } @@ -203,7 +204,7 @@ export function _wrap_js_thenable_as_task(thenable: Promise): { // ideally, this should be hold alive by lifespan of the resulting C# Task, but this is good cheap aproximation const thenable_js_handle = mono_wasm_get_js_handle(thenable); - // Note that we do not implement promise/task roundtrip. + // Note that we do not implement promise/task roundtrip. // With more complexity we could recover original instance when this Task is marshaled back to JS. // TODO optimization: return the tcs.Task on this same call instead of _get_tcs_task const tcs_gc_handle = corebindings._create_tcs(); diff --git a/src/mono/wasm/runtime/memory.ts b/src/mono/wasm/runtime/memory.ts index e8a89bd11a574a..85677d0e9e8a57 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -1,108 +1,116 @@ import { Module } from "./imports"; -import { VoidPtr, NativePointer } from "./types/emscripten"; +import { VoidPtr, NativePointer, ManagedPointer } from "./types/emscripten"; -const _temp_mallocs: Array | null> = []; +const alloca_stack: Array = []; +const alloca_buffer_size = 32 * 1024; +let alloca_base: VoidPtr, alloca_offset: VoidPtr, alloca_limit: VoidPtr; + +function _ensure_allocated(): void { + if (alloca_base) + return; + alloca_base = Module._malloc(alloca_buffer_size); + alloca_offset = alloca_base; + alloca_limit = (alloca_base + alloca_buffer_size); +} export function temp_malloc(size: number): VoidPtr { - if (!_temp_mallocs || !_temp_mallocs.length) + _ensure_allocated(); + if (!alloca_stack.length) throw new Error("No temp frames have been created at this point"); - const frame = _temp_mallocs[_temp_mallocs.length - 1] || []; - const result = Module._malloc(size); - frame.push(result); - _temp_mallocs[_temp_mallocs.length - 1] = frame; + const result = alloca_offset; + alloca_offset += size; + if (alloca_offset >= alloca_limit) + throw new Error("Out of temp storage space"); return result; } export function _create_temp_frame(): void { - _temp_mallocs.push(null); + _ensure_allocated(); + alloca_stack.push(alloca_offset); } export function _release_temp_frame(): void { - if (!_temp_mallocs.length) + if (!alloca_stack.length) throw new Error("No temp frames have been created at this point"); - const frame = _temp_mallocs.pop(); - if (!frame) - return; - - for (let i = 0, l = frame.length; i < l; i++) - Module._free(frame[i]); + alloca_offset = alloca_stack.pop(); } -type _MemOffset = number | VoidPtr | NativePointer; +type DotnetMemOffset = number | NativePointer; +type DotnetMemValue = number | NativePointer | ManagedPointer; -export function setU8(offset: _MemOffset, value: number): void { +export function setU8(offset: DotnetMemOffset, value: number): void { Module.HEAPU8[offset] = value; } -export function setU16(offset: _MemOffset, value: number): void { +export function setU16(offset: DotnetMemOffset, value: number): void { Module.HEAPU16[offset >>> 1] = value; } -export function setU32(offset: _MemOffset, value: number): void { - Module.HEAPU32[offset >>> 2] = value; +export function setU32 (offset: DotnetMemOffset, value: DotnetMemValue) : void { + Module.HEAPU32[offset >>> 2] = value; } -export function setI8(offset: _MemOffset, value: number): void { +export function setI8(offset: DotnetMemOffset, value: number): void { Module.HEAP8[offset] = value; } -export function setI16(offset: _MemOffset, value: number): void { +export function setI16(offset: DotnetMemOffset, value: number): void { Module.HEAP16[offset >>> 1] = value; } -export function setI32(offset: _MemOffset, value: number): void { +export function setI32(offset: DotnetMemOffset, value: number): void { Module.HEAP32[offset >>> 2] = value; } // NOTE: Accepts a number, not a BigInt, so values over Number.MAX_SAFE_INTEGER will be corrupted -export function setI64(offset: _MemOffset, value: number): void { +export function setI64(offset: DotnetMemOffset, value: number): void { Module.setValue(offset, value, "i64"); } -export function setF32(offset: _MemOffset, value: number): void { +export function setF32(offset: DotnetMemOffset, value: number): void { Module.HEAPF32[offset >>> 2] = value; } -export function setF64(offset: _MemOffset, value: number): void { +export function setF64(offset: DotnetMemOffset, value: number): void { Module.HEAPF64[offset >>> 3] = value; } -export function getU8(offset: _MemOffset): number { +export function getU8(offset: DotnetMemOffset): number { return Module.HEAPU8[offset]; } -export function getU16(offset: _MemOffset): number { +export function getU16(offset: DotnetMemOffset): number { return Module.HEAPU16[offset >>> 1]; } -export function getU32(offset: _MemOffset): number { +export function getU32(offset: DotnetMemOffset): number { return Module.HEAPU32[offset >>> 2]; } -export function getI8(offset: _MemOffset): number { +export function getI8(offset: DotnetMemOffset): number { return Module.HEAP8[offset]; } -export function getI16(offset: _MemOffset): number { +export function getI16(offset: DotnetMemOffset): number { return Module.HEAP16[offset >>> 1]; } -export function getI32(offset: _MemOffset): number { +export function getI32(offset: DotnetMemOffset): number { return Module.HEAP32[offset >>> 2]; } // NOTE: Returns a number, not a BigInt. This means values over Number.MAX_SAFE_INTEGER will be corrupted -export function getI64(offset: _MemOffset): number { +export function getI64(offset: DotnetMemOffset): number { return Module.getValue(offset, "i64"); } -export function getF32(offset: _MemOffset): number { +export function getF32(offset: DotnetMemOffset): number { return Module.HEAPF32[offset >>> 2]; } -export function getF64(offset: _MemOffset): number { +export function getF64(offset: DotnetMemOffset): number { return Module.HEAPF64[offset >>> 3]; } diff --git a/src/mono/wasm/runtime/method-binding.ts b/src/mono/wasm/runtime/method-binding.ts index 4827766f88c490..2739c3058006d8 100644 --- a/src/mono/wasm/runtime/method-binding.ts +++ b/src/mono/wasm/runtime/method-binding.ts @@ -2,27 +2,36 @@ // The .NET Foundation licenses this file to you under the MIT license. import { WasmRoot, WasmRootBuffer, mono_wasm_new_root } from "./roots"; -import { MonoClass, MonoMethod, MonoObject, coerceNull, VoidPtrNull, MonoType } from "./types"; -import { BINDING, Module, runtimeHelpers } from "./imports"; +import { Module, runtimeHelpers } from "./imports"; import { js_to_mono_enum, _js_to_mono_obj, _js_to_mono_uri } from "./js-to-cs"; -import { js_string_to_mono_string, js_string_to_mono_string_interned } from "./strings"; -import { MarshalType, _unbox_mono_obj_root_with_known_nonprimitive_type } from "./cs-to-js"; +import { _unbox_mono_obj_root_with_known_nonprimitive_type } from "./cs-to-js"; +import { + MonoClass, MonoMethod, MonoObject, coerceNull, MonoString, MonoObjectNull, + VoidPtrNull, MonoType, MarshalSignatureInfo, MonoTypeNull +} from "./types"; +import { js_string_to_mono_string, js_string_to_mono_string_interned, conv_string } from "./strings"; import { _create_temp_frame, getI32, getU32, getF32, getF64, setI32, setU32, setF32, setF64, setI64, } from "./memory"; +import { _pick_automatic_converter_for_type } from "./custom-marshaler"; import { _get_args_root_buffer_for_method_call, _get_buffer_for_method_call, - _handle_exception_for_call, _teardown_after_call + _handle_exception_for_call, _teardown_after_call, + _convert_exception_for_method_call, } from "./method-calls"; import cwraps from "./cwraps"; import { VoidPtr } from "./types/emscripten"; +import cswraps from "./corebindings"; -const primitiveConverters = new Map(); -const _signature_converters = new Map(); +const _signature_converters = new Map>(); const _method_descriptions = new Map(); +const _method_signature_info_table = new Map(); +const _bound_method_cache = new Map(); +export const bindings_named_closures = new Map(); +let bindings_named_closures_initialized = false; export function _get_type_name(typePtr: MonoType): string { if (!typePtr) @@ -65,22 +74,13 @@ export function bind_runtime_method(method_name: string, signature: string): Fun // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function _create_named_function(name: string, argumentNames: string[], body: string, closure: any): Function { - let result = null; - let closureArgumentList: any[] | null = null; let closureArgumentNames = null; - if (closure) { + if (closure) closureArgumentNames = Object.keys(closure); - closureArgumentList = new Array(closureArgumentNames.length); - for (let i = 0, l = closureArgumentNames.length; i < l; i++) - closureArgumentList[i] = closure[closureArgumentNames[i]]; - } const constructor = _create_rebindable_named_function(name, argumentNames, body, closureArgumentNames); - // eslint-disable-next-line prefer-spread - result = constructor.apply(null, closureArgumentList); - - return result; + return constructor(closure); } export function _create_rebindable_named_function(name: string, argumentNames: string[], body: string, closureArgNames: string[] | null): Function { @@ -94,462 +94,271 @@ export function _create_rebindable_named_function(name: string, argumentNames: s escapedFunctionIdentifier = "unnamed"; } + let closurePrefix = ""; + if (closureArgNames) { + for (let i = 0; i < closureArgNames.length; i++) { + const argName = closureArgNames[i]; + closurePrefix += `const ${argName} = __closure__.${argName};\r\n`; + } + closurePrefix += "\r\n"; + } + + let rawFunctionText = "function " + escapedFunctionIdentifier + "(" + argumentNames.join(", ") + - ") {\r\n" + - body + - "\r\n};\r\n"; + ") {\r\n"; + + rawFunctionText += body + "\r\n"; const lineBreakRE = /\r(\n?)/g; rawFunctionText = - uriPrefix + strictPrefix + + uriPrefix + strictPrefix + closurePrefix + rawFunctionText.replace(lineBreakRE, "\r\n ") + - ` return ${escapedFunctionIdentifier};\r\n`; - - let result = null, keys = null; + `};\r\nreturn ${escapedFunctionIdentifier};\r\n`; - if (closureArgNames) { - keys = closureArgNames.concat([rawFunctionText]); - } else { - keys = [rawFunctionText]; - } + /* + console.log(rawFunctionText); + console.log(""); + */ - result = Function.apply(Function, keys); - return result; -} - -export function _create_primitive_converters(): void { - const result = primitiveConverters; - result.set("m", { steps: [{}], size: 0 }); - result.set("s", { steps: [{ convert: js_string_to_mono_string.bind(BINDING) }], size: 0, needs_root: true }); - result.set("S", { steps: [{ convert: js_string_to_mono_string_interned.bind(BINDING) }], size: 0, needs_root: true }); - // note we also bind first argument to false for both _js_to_mono_obj and _js_to_mono_uri, - // because we will root the reference, so we don't need in-flight reference - // also as those are callback arguments and we don't have platform code which would release the in-flight reference on C# end - result.set("o", { steps: [{ convert: _js_to_mono_obj.bind(BINDING, false) }], size: 0, needs_root: true }); - result.set("u", { steps: [{ convert: _js_to_mono_uri.bind(BINDING, false) }], size: 0, needs_root: true }); - - // result.set ('k', { steps: [{ convert: js_to_mono_enum.bind (this), indirect: 'i64'}], size: 8}); - result.set("j", { steps: [{ convert: js_to_mono_enum.bind(BINDING), indirect: "i32" }], size: 8 }); - - result.set("i", { steps: [{ indirect: "i32" }], size: 8 }); - result.set("l", { steps: [{ indirect: "i64" }], size: 8 }); - result.set("f", { steps: [{ indirect: "float" }], size: 8 }); - result.set("d", { steps: [{ indirect: "double" }], size: 8 }); + return new Function("__closure__", rawFunctionText); } -function _create_converter_for_marshal_string(args_marshal: string/*ArgsMarshalString*/): Converter { - const steps = []; - let size = 0; - let is_result_definitely_unmarshaled = false, - is_result_possibly_unmarshaled = false, - result_unmarshaled_if_argc = -1, - needs_root_buffer = false; - - for (let i = 0; i < args_marshal.length; ++i) { - const key = args_marshal[i]; - - if (i === args_marshal.length - 1) { - if (key === "!") { - is_result_definitely_unmarshaled = true; - continue; - } else if (key === "m") { - is_result_possibly_unmarshaled = true; - result_unmarshaled_if_argc = args_marshal.length - 1; - } - } else if (key === "!") - throw new Error("! must be at the end of the signature"); - - const conv = primitiveConverters.get(key); - if (!conv) - throw new Error("Unknown parameter type " + key); - - const localStep = Object.create(conv.steps[0]); - localStep.size = conv.size; - if (conv.needs_root) - needs_root_buffer = true; - localStep.needs_root = conv.needs_root; - localStep.key = key; - steps.push(localStep); - size += conv.size; +export function get_method_signature_info (typePtr : MonoType, methodPtr : MonoMethod) : MarshalSignatureInfo { + if (!methodPtr) + throw new Error("Method ptr not provided"); + + let result = _method_signature_info_table.get(methodPtr); + const classMismatch = !!result && (result.typePtr !== typePtr); + if (!result) { + const typeName = _get_type_name(typePtr); + const json = cswraps.make_marshal_signature_info(typePtr, methodPtr); + if (!json) + throw new Error(`MakeMarshalSignatureInfo failed for type ${typeName}`); + + result = JSON.parse(json); + result.typePtr = typePtr; + + if (classMismatch) + console.log("WARNING: Class ptr mismatch for signature info, so caching is disabled"); + else + _method_signature_info_table.set(methodPtr, result); } - - return { - steps, size, args_marshal, - is_result_definitely_unmarshaled, - is_result_possibly_unmarshaled, - result_unmarshaled_if_argc, - needs_root_buffer - }; + return result; } -function _get_converter_for_marshal_string(args_marshal: string/*ArgsMarshalString*/): Converter { +function _get_converter_for_marshal_string(typePtr: MonoType, method: MonoMethod, args_marshal: string): SignatureConverter | undefined { let converter = _signature_converters.get(args_marshal); - if (!converter) { - converter = _create_converter_for_marshal_string(args_marshal); - _signature_converters.set(args_marshal, converter); + let map : Map | null = null; + if (converter instanceof Map) { + map = converter; + converter = map.get(method); } - return converter; } -export function _compile_converter_for_marshal_string(args_marshal: string/*ArgsMarshalString*/): Converter { - const converter = _get_converter_for_marshal_string(args_marshal); - if (typeof (converter.args_marshal) !== "string") - throw new Error("Corrupt converter for '" + args_marshal + "'"); +function _setSpan (offset : VoidPtr, span : Array) : void { + if (!Array.isArray(span) || (span.length !== 2)) + throw new Error(`Span must be an array of shape [offset, length_in_elements] but was ${span}`); + setU32(offset, span[0]); + setU32(offset + 4, span[1]); +} - if (converter.compiled_function && converter.compiled_variadic_function) - return converter; +function _bindingsError (message : string) : void { + throw new Error(message); +} - const converterName = args_marshal.replace("!", "_result_unmarshaled"); - converter.name = converterName; +function _generate_args_marshaler (typePtr: MonoType, method: MonoMethod, args_marshal: string): string { + const argsRoot = mono_wasm_new_root(), + resultRoot = mono_wasm_new_root(), + exceptionRoot = mono_wasm_new_root(); + const generatorMethod = get_method("GenerateArgsMarshaler"); + const buffer = Module._malloc(64); - let body = []; - let argumentNames = ["buffer", "rootBuffer", "method"]; + try { + argsRoot.value = js_string_to_mono_string(args_marshal); + + // Manually assemble an arguments buffer + // (RuntimeTypeHandle, RuntimeMethodHandle, string) + setU32(buffer + 16, typePtr); + setU32(buffer + 32, method); + setU32(buffer + 0, buffer + 16); + setU32(buffer + 4, buffer + 32); + setU32(buffer + 8, argsRoot.value); + + // Invoke the managed method + resultRoot.value = cwraps.mono_wasm_invoke_method(generatorMethod, MonoObjectNull, buffer, exceptionRoot.get_address()); + // If it threw an exception, this will yield us a JS Error instance to throw + const exc = _convert_exception_for_method_call(resultRoot.value, exceptionRoot.value); + if (exc) + throw exc; + // Otherwise it returned a managed String containing the JS for our new function + return conv_string(resultRoot.value); + } finally { + resultRoot.release(); + exceptionRoot.release(); + argsRoot.release(); + Module._free(buffer); + } +} - // worst-case allocation size instead of allocating dynamically, plus padding - const bufferSizeBytes = converter.size + (args_marshal.length * 4) + 16; +function _generate_bound_method (typePtr: MonoType, method: MonoMethod, args_marshal: string, friendly_name: string): string { + const argsRoot = mono_wasm_new_root(), + nameRoot = mono_wasm_new_root(), + resultRoot = mono_wasm_new_root(), + exceptionRoot = mono_wasm_new_root(); + const generatorMethod = get_method("GenerateBoundMethod"); + const buffer = Module._malloc(64); - // ensure the indirect values are 8-byte aligned so that aligned loads and stores will work - const indirectBaseOffset = ((((args_marshal.length * 4) + 7) / 8) | 0) * 8; + try { + argsRoot.value = js_string_to_mono_string(args_marshal); + nameRoot.value = js_string_to_mono_string(friendly_name); + + // Manually assemble an arguments buffer + // (RuntimeTypeHandle, RuntimeMethodHandle, string) + setU32(buffer + 16, typePtr); + setU32(buffer + 32, method); + setU32(buffer + 0, buffer + 16); + setU32(buffer + 4, buffer + 32); + setU32(buffer + 8, argsRoot.value); + setU32(buffer + 12, nameRoot.value); + + // Invoke the managed method + resultRoot.value = cwraps.mono_wasm_invoke_method(generatorMethod, MonoObjectNull, buffer, exceptionRoot.get_address()); + // If it threw an exception, this will yield us a JS Error instance to throw + const exc = _convert_exception_for_method_call(resultRoot.value, exceptionRoot.value); + if (exc) + throw exc; + // Otherwise it returned a managed String containing the JS for our new function + return conv_string(resultRoot.value); + } finally { + resultRoot.release(); + exceptionRoot.release(); + nameRoot.release(); + argsRoot.release(); + Module._free(buffer); + } +} +function _initialize_bindings_named_closures () : void { + // HACK: Populate the lookup table used by compiled closures const closure: any = { - Module, + _create_temp_frame, + _error: _bindingsError, + _get_args_root_buffer_for_method_call, + _get_buffer_for_method_call, + _handle_exception_for_call, + _js_to_mono_obj, + _js_to_mono_uri, _malloc: Module._malloc, + _pick_automatic_converter_for_type, + _setSpan, + _teardown_after_call, + _unbox_mono_obj_root_with_known_nonprimitive_type, + invoke_method: cwraps.mono_wasm_invoke_method, + js_string_to_mono_string_interned, + js_string_to_mono_string, + js_to_mono_enum, + mono_wasm_new_root, + mono_wasm_try_unbox_primitive_and_get_type: cwraps.mono_wasm_try_unbox_primitive_and_get_type, mono_wasm_unbox_rooted: cwraps.mono_wasm_unbox_rooted, - setI32, - setU32, + getF32, + getF64, + getI32, + getU32, setF32, setF64, - setI64 - }; - let indirectLocalOffset = 0; - - body.push( - "if (!method) throw new Error('no method provided');", - `if (!buffer) buffer = _malloc (${bufferSizeBytes});`, - `let indirectStart = buffer + ${indirectBaseOffset};`, - "" - ); - - for (let i = 0; i < converter.steps.length; i++) { - const step = converter.steps[i]; - const closureKey = "step" + i; - const valueKey = "value" + i; - - const argKey = "arg" + i; - argumentNames.push(argKey); - - if (step.convert) { - closure[closureKey] = step.convert; - body.push(`let ${valueKey} = ${closureKey}(${argKey}, method, ${i});`); - } else { - body.push(`let ${valueKey} = ${argKey};`); - } - - if (step.needs_root) { - body.push("if (!rootBuffer) throw new Error('no root buffer provided');"); - body.push(`rootBuffer.set (${i}, ${valueKey});`); - } - - // HACK: needs_unbox indicates that we were passed a pointer to a managed object, and either - // it was already rooted by our caller or (needs_root = true) by us. Now we can unbox it and - // pass the raw address of its boxed value into the callee. - // FIXME: I don't think this is GC safe - if (step.needs_unbox) - body.push(`${valueKey} = mono_wasm_unbox_rooted (${valueKey});`); - - if (step.indirect) { - const offsetText = `(indirectStart + ${indirectLocalOffset})`; - - switch (step.indirect) { - case "u32": - body.push(`setU32(${offsetText}, ${valueKey});`); - break; - case "i32": - body.push(`setI32(${offsetText}, ${valueKey});`); - break; - case "float": - body.push(`setF32(${offsetText}, ${valueKey});`); - break; - case "double": - body.push(`setF64(${offsetText}, ${valueKey});`); - break; - case "i64": - body.push(`setI64(${offsetText}, ${valueKey});`); - break; - default: - throw new Error("Unimplemented indirect type: " + step.indirect); - } - - body.push(`setU32(buffer + (${i} * 4), ${offsetText});`); - indirectLocalOffset += step.size!; - } else { - body.push(`setI32(buffer + (${i} * 4), ${valueKey});`); - indirectLocalOffset += 4; - } - body.push(""); - } - - body.push("return buffer;"); - - let bodyJs = body.join("\r\n"), compiledFunction = null, compiledVariadicFunction = null; - try { - compiledFunction = _create_named_function("converter_" + converterName, argumentNames, bodyJs, closure); - converter.compiled_function = compiledFunction; - } catch (exc) { - converter.compiled_function = null; - console.warn("compiling converter failed for", bodyJs, "with error", exc); - throw exc; - } - - - argumentNames = ["existingBuffer", "rootBuffer", "method", "args"]; - const variadicClosure = { - converter: compiledFunction + setI32, + setI64, + setU32, }; - body = [ - "return converter(", - " existingBuffer, rootBuffer, method," - ]; - - for (let i = 0; i < converter.steps.length; i++) { - body.push( - " args[" + i + - ( - (i == converter.steps.length - 1) - ? "]" - : "], " - ) - ); - } - - body.push(");"); - - bodyJs = body.join("\r\n"); - try { - compiledVariadicFunction = _create_named_function("variadic_converter_" + converterName, argumentNames, bodyJs, variadicClosure); - converter.compiled_variadic_function = compiledVariadicFunction; - } catch (exc) { - converter.compiled_variadic_function = null; - console.warn("compiling converter failed for", bodyJs, "with error", exc); - throw exc; - } - - converter.scratchRootBuffer = null; - converter.scratchBuffer = VoidPtrNull; - - return converter; + for (const k in closure) + bindings_named_closures.set(k, closure[k]); } -function _maybe_produce_signature_warning(converter: Converter) { - if (converter.has_warned_about_signature) - return; +function _get_api (key: string): Function { + if (!bindings_named_closures_initialized) { + bindings_named_closures_initialized = true; + _initialize_bindings_named_closures(); + } - console.warn("MONO_WASM: Deprecated raw return value signature: '" + converter.args_marshal + "'. End the signature with '!' instead of 'm'."); - converter.has_warned_about_signature = true; + const result = bindings_named_closures.get(key); + if (!result || typeof(result) !== "function") + throw new Error(`Expected ${key} to be a function but was '${result}'`); + return result; } -export function _decide_if_result_is_marshaled(converter: Converter, argc: number): boolean { - if (!converter) - return true; - - if ( - converter.is_result_possibly_unmarshaled && - (argc === converter.result_unmarshaled_if_argc) - ) { - if (argc < converter.result_unmarshaled_if_argc) - throw new Error(`Expected >= ${converter.result_unmarshaled_if_argc} argument(s) but got ${argc} for signature '${converter.args_marshal}'`); - - _maybe_produce_signature_warning(converter); - return false; - } else { - if (argc < converter.steps.length) - throw new Error(`Expected ${converter.steps.length} argument(s) but got ${argc} for signature '${converter.args_marshal}'`); +export function _compile_converter_for_marshal_string(typePtr: MonoType, method: MonoMethod, args_marshal: string): SignatureConverter { + const converter = _get_converter_for_marshal_string(typePtr, method, args_marshal); + if (converter && converter.compiled_function && converter.compiled_variadic_function) + return converter; - return !converter.is_result_definitely_unmarshaled; - } + let csFuncResult : any = null; + // HACK: We invoke this method directly instead of using the cswraps. version, since that wrapper relies on this function + const js = _generate_args_marshaler(typePtr, method, args_marshal); + const csFunc = new Function("get_api", "get_type_converter", js); + csFuncResult = csFunc(_get_api, _pick_automatic_converter_for_type); + return csFuncResult; } -export function mono_bind_method(method: MonoMethod, this_arg: MonoObject | null, args_marshal: string/*ArgsMarshalString*/, friendly_name: string): Function { +export function mono_bind_method(method: MonoMethod, this_arg: 0 | null, args_marshal: string, friendly_name: string): Function { if (typeof (args_marshal) !== "string") throw new Error("args_marshal argument invalid, expected string"); - this_arg = coerceNull(this_arg); - let converter: Converter | null = null; - if (typeof (args_marshal) === "string") { - converter = _compile_converter_for_marshal_string(args_marshal); + if (this_arg) + throw new Error("this_arg must be 0"); + + // We implement a simple lookup cache here to prevent repeated bind_method calls on the same target + // from exhausting the set of available scratch roots. This is mostly useful for automated tests, + // but it may also save some naive callers from rare runtime failures + const cacheKey = `m${method}_a${args_marshal}`; + if (_bound_method_cache.has(cacheKey)) { + const cacheHit = _bound_method_cache.get(cacheKey); + return cacheHit; } // FIXME - const unbox_buffer_size = 8192; - const unbox_buffer = Module._malloc(unbox_buffer_size); + const unboxBufferSize = 8192; const token: BoundMethodToken = { - friendlyName: friendly_name, method, - converter, + converter: null, // Initialized later + unboxBuffer: Module._malloc(unboxBufferSize), + unboxBufferSize, scratchRootBuffer: null, scratchBuffer: VoidPtrNull, scratchResultRoot: mono_wasm_new_root(), scratchExceptionRoot: mono_wasm_new_root() }; - const closure: any = { - Module, - mono_wasm_new_root, - _create_temp_frame, - _get_args_root_buffer_for_method_call, - _get_buffer_for_method_call, - _handle_exception_for_call, - _teardown_after_call, - mono_wasm_try_unbox_primitive_and_get_type: cwraps.mono_wasm_try_unbox_primitive_and_get_type, - _unbox_mono_obj_root_with_known_nonprimitive_type, - invoke_method: cwraps.mono_wasm_invoke_method, - method, - this_arg, - token, - unbox_buffer, - unbox_buffer_size, - getI32, - getU32, - getF32, - getF64 - }; - const converterKey = converter ? "converter_" + converter.name : ""; - if (converter) - closure[converterKey] = converter; - - const argumentNames = []; - const body = [ - "_create_temp_frame();", - "let resultRoot = token.scratchResultRoot, exceptionRoot = token.scratchExceptionRoot;", - "token.scratchResultRoot = null;", - "token.scratchExceptionRoot = null;", - "if (resultRoot === null)", - " resultRoot = mono_wasm_new_root ();", - "if (exceptionRoot === null)", - " exceptionRoot = mono_wasm_new_root ();", - "" - ]; - - if (converter) { - body.push( - `let argsRootBuffer = _get_args_root_buffer_for_method_call(${converterKey}, token);`, - `let scratchBuffer = _get_buffer_for_method_call(${converterKey}, token);`, - `let buffer = ${converterKey}.compiled_function(`, - " scratchBuffer, argsRootBuffer, method," - ); - - for (let i = 0; i < converter.steps.length; i++) { - const argName = "arg" + i; - argumentNames.push(argName); - body.push( - " " + argName + - ( - (i == converter.steps.length - 1) - ? "" - : ", " - ) - ); - } + let typePtr : MonoType = MonoTypeNull; - body.push(");"); - - } else { - body.push("let argsRootBuffer = null, buffer = 0;"); - } - - if (converter && converter.is_result_definitely_unmarshaled) { - body.push("let is_result_marshaled = false;"); - } else if (converter && converter.is_result_possibly_unmarshaled) { - body.push(`let is_result_marshaled = arguments.length !== ${converter.result_unmarshaled_if_argc};`); - } else { - body.push("let is_result_marshaled = true;"); - } - - // We inline a bunch of the invoke and marshaling logic here in order to eliminate the GC pressure normally - // created by the unboxing part of the call process. Because unbox_mono_obj(_root) can return non-numeric - // types, v8 and spidermonkey allocate and store its result on the heap (in the nursery, to be fair). - // For a bound method however, we know the result will always be the same type because C# methods have known - // return types. Inlining the invoke and marshaling logic means that even though the bound method has logic - // for handling various types, only one path through the method (for its appropriate return type) will ever - // be taken, and the JIT will see that the 'result' local and thus the return value of this function are - // always of the exact same type. All of the branches related to this end up being predicted and low-cost. - // The end result is that bound method invocations don't always allocate, so no more nursery GCs. Yay! -kg - body.push( - "", - "resultRoot.value = invoke_method (method, this_arg, buffer, exceptionRoot.get_address ());", - `_handle_exception_for_call (${converterKey}, token, buffer, resultRoot, exceptionRoot, argsRootBuffer);`, - "", - "let resultPtr = resultRoot.value, result = undefined;" - ); - - if (converter) { - if (converter.is_result_possibly_unmarshaled) - body.push("if (!is_result_marshaled) "); - - if (converter.is_result_definitely_unmarshaled || converter.is_result_possibly_unmarshaled) - body.push(" result = resultPtr;"); - - if (!converter.is_result_definitely_unmarshaled) - body.push( - "if (is_result_marshaled && (resultPtr !== 0)) {", - // For the common scenario where the return type is a primitive, we want to try and unbox it directly - // into our existing heap allocation and then read it out of the heap. Doing this all in one operation - // means that we only need to enter a gc safe region twice (instead of 3+ times with the normal, - // slower check-type-and-then-unbox flow which has extra checks since unbox verifies the type). - " let resultType = mono_wasm_try_unbox_primitive_and_get_type (resultPtr, unbox_buffer, unbox_buffer_size);", - " switch (resultType) {", - ` case ${MarshalType.INT}:`, - " result = getI32(unbox_buffer); break;", - ` case ${MarshalType.POINTER}:`, // FIXME: Is this right? - ` case ${MarshalType.UINT32}:`, - " result = getU32(unbox_buffer); break;", - ` case ${MarshalType.FP32}:`, - " result = getF32(unbox_buffer); break;", - ` case ${MarshalType.FP64}:`, - " result = getF64(unbox_buffer); break;", - ` case ${MarshalType.BOOL}:`, - " result = getI32(unbox_buffer) !== 0; break;", - ` case ${MarshalType.CHAR}:`, - " result = String.fromCharCode(getI32(unbox_buffer)); break;", - " default:", - " result = _unbox_mono_obj_root_with_known_nonprimitive_type (resultRoot, resultType, unbox_buffer); break;", - " }", - "}" - ); - } else { - throw new Error("No converter"); + let converter: SignatureConverter | null = null; + if (typeof (args_marshal) === "string") { + const classPtr = cwraps.mono_wasm_get_class_for_bind_or_invoke(MonoObjectNull, method); + if (!classPtr) + throw new Error(`Could not get class ptr for bind_method with method (${method})`); + typePtr = cwraps.mono_wasm_class_get_type(classPtr); + converter = _compile_converter_for_marshal_string(typePtr, method, args_marshal); } + token.converter = converter; if (friendly_name) { const escapeRE = /[^A-Za-z0-9_$]/g; friendly_name = friendly_name.replace(escapeRE, "_"); } - let displayName = friendly_name || ("clr_" + method); + const bodyJs = _generate_bound_method(typePtr, method, args_marshal, friendly_name); + const ctor = new Function("get_api", "token", bodyJs); + const result = ctor(_get_api, token); - if (this_arg) - displayName += "_this" + this_arg; - - body.push( - `_teardown_after_call (${converterKey}, token, buffer, resultRoot, exceptionRoot, argsRootBuffer);`, - "return result;" - ); - - const bodyJs = body.join("\r\n"); - - const result = _create_named_function(displayName, argumentNames, bodyJs, closure); + _bound_method_cache.set(cacheKey, result); return result; } -/* -We currently don't use these types because it makes typeScript compiler very slow. - -declare const enum ArgsMarshal { +export enum ArgsMarshal { Int32 = "i", // int32 Int32Enum = "j", // int32 - Enum with underlying type of int32 Int64 = "l", // int64 @@ -557,56 +366,44 @@ declare const enum ArgsMarshal { Float32 = "f", // float Float64 = "d", // double String = "s", // string - Char = "s", // interned string + InternedString = "S", // interned string + Uri = "u", JSObj = "o", // js object will be converted to a C# object (this will box numbers/bool/promises) MONOObj = "m", // raw mono object. Don't use it unless you know what you're doing + Auto = "a", // the bindings layer will select an appropriate converter based on the C# method signature + ByteSpan = "b", // Span +} + +export type TypeConverter = { + needs_unbox: boolean; + needs_root: boolean; + convert: Function; } -// to suppress marshaling of the return value, place '!' at the end of args_marshal, i.e. 'ii!' instead of 'ii' -type _ExtraArgsMarshalOperators = "!" | ""; - -export type ArgsMarshalString = "" - | `${ArgsMarshal}${_ExtraArgsMarshalOperators}` - | `${ArgsMarshal}${ArgsMarshal}${_ExtraArgsMarshalOperators}` - | `${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${_ExtraArgsMarshalOperators}` - | `${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${_ExtraArgsMarshalOperators}`; -*/ - -type ConverterStepIndirects = "u32" | "i32" | "float" | "double" | "i64" - -export type Converter = { - steps: { - convert?: boolean | Function; - needs_root?: boolean; - needs_unbox?: boolean; - indirect?: ConverterStepIndirects; - size?: number; - }[]; +export type SignatureConverter = { + arg_count: number; size: number; - args_marshal?: string/*ArgsMarshalString*/; + args_marshal?: string; is_result_definitely_unmarshaled?: boolean; - is_result_possibly_unmarshaled?: boolean; - result_unmarshaled_if_argc?: number; needs_root_buffer?: boolean; key?: string; name?: string; - needs_root?: boolean; - needs_unbox?: boolean; compiled_variadic_function?: Function | null; compiled_function?: Function | null; scratchRootBuffer?: WasmRootBuffer | null; scratchBuffer?: VoidPtr; has_warned_about_signature?: boolean; - convert?: Function | null; method?: MonoMethod | null; + root_buffer_size?: number; } export type BoundMethodToken = { - friendlyName: string; method: MonoMethod; - converter: Converter | null; + converter: SignatureConverter | null; scratchRootBuffer: WasmRootBuffer | null; scratchBuffer: VoidPtr; + unboxBuffer: VoidPtr; + unboxBufferSize: number; scratchResultRoot: WasmRoot; scratchExceptionRoot: WasmRoot; } \ No newline at end of file diff --git a/src/mono/wasm/runtime/method-calls.ts b/src/mono/wasm/runtime/method-calls.ts index 3bd1057c1c154a..299637a06eceff 100644 --- a/src/mono/wasm/runtime/method-calls.ts +++ b/src/mono/wasm/runtime/method-calls.ts @@ -12,10 +12,9 @@ import { _mono_array_root_to_js_array, _unbox_mono_obj_root } from "./cs-to-js"; import { get_js_obj, mono_wasm_get_jsobj_from_js_handle } from "./gc-handles"; import { js_array_to_mono_array, _box_js_bool, _js_to_mono_obj } from "./js-to-cs"; import { - mono_bind_method, - Converter, _compile_converter_for_marshal_string, - _decide_if_result_is_marshaled, find_method, - BoundMethodToken + mono_bind_method, SignatureConverter, + _compile_converter_for_marshal_string, + find_method, BoundMethodToken } from "./method-binding"; import { conv_string, js_string_to_mono_string } from "./strings"; import cwraps from "./cwraps"; @@ -23,7 +22,7 @@ import { bindings_lazy_init } from "./startup"; import { _create_temp_frame, _release_temp_frame } from "./memory"; import { VoidPtr, Int32Ptr, EmscriptenModule } from "./types/emscripten"; -function _verify_args_for_method_call(args_marshal: string/*ArgsMarshalString*/, args: any) { +function _verify_args_for_method_call(args_marshal: string, args: any) : boolean { const has_args = args && (typeof args === "object") && args.length > 0; const has_args_marshal = typeof args_marshal === "string"; @@ -37,7 +36,7 @@ function _verify_args_for_method_call(args_marshal: string/*ArgsMarshalString*/, return has_args_marshal && has_args; } -export function _get_buffer_for_method_call(converter: Converter, token: BoundMethodToken | null): VoidPtr | undefined { +export function _get_buffer_for_method_call(converter: SignatureConverter, token: BoundMethodToken | null): VoidPtr | undefined { if (!converter) return VoidPtrNull; @@ -52,7 +51,7 @@ export function _get_buffer_for_method_call(converter: Converter, token: BoundMe return result; } -export function _get_args_root_buffer_for_method_call(converter: Converter, token: BoundMethodToken | null): WasmRootBuffer | undefined { +export function _get_args_root_buffer_for_method_call(converter: SignatureConverter, token: BoundMethodToken | null): WasmRootBuffer | undefined { if (!converter) return undefined; @@ -73,7 +72,7 @@ export function _get_args_root_buffer_for_method_call(converter: Converter, toke // mono_wasm_new_root_buffer_from_pointer instead. Not that important // at present because the scratch buffer will be reused unless we are // recursing through a re-entrant call - result = mono_wasm_new_root_buffer(converter.steps.length); + result = mono_wasm_new_root_buffer(converter.root_buffer_size); // FIXME (result).converter = converter; } @@ -82,8 +81,8 @@ export function _get_args_root_buffer_for_method_call(converter: Converter, toke } function _release_args_root_buffer_from_method_call( - converter?: Converter, token?: BoundMethodToken | null, argsRootBuffer?: WasmRootBuffer -) { + converter?: SignatureConverter, token?: BoundMethodToken | null, argsRootBuffer?: WasmRootBuffer +) : void { if (!argsRootBuffer || !converter) return; @@ -100,8 +99,8 @@ function _release_args_root_buffer_from_method_call( } function _release_buffer_from_method_call( - converter: Converter | undefined, token?: BoundMethodToken | null, buffer?: VoidPtr -) { + converter: SignatureConverter | undefined, token?: BoundMethodToken | null, buffer?: VoidPtr +) : void { if (!converter || !buffer) return; @@ -113,7 +112,7 @@ function _release_buffer_from_method_call( Module._free(buffer); } -function _convert_exception_for_method_call(result: MonoString, exception: MonoObject) { +export function _convert_exception_for_method_call(result: MonoString, exception: MonoObject) : Error | null { if (exception === MonoObjectNull) return null; @@ -140,7 +139,7 @@ m: raw mono object. Don't use it unless you know what you're doing to suppress marshaling of the return value, place '!' at the end of args_marshal, i.e. 'ii!' instead of 'ii' */ -export function call_method(method: MonoMethod, this_arg: MonoObject | undefined, args_marshal: string/*ArgsMarshalString*/, args: ArrayLike): any { +export function call_method(method: MonoMethod, this_arg: MonoObject | undefined, args_marshal: string, args: ArrayLike): any { // HACK: Sometimes callers pass null or undefined, coerce it to 0 since that's what wasm expects this_arg = coerceNull(this_arg); @@ -160,9 +159,14 @@ export function call_method(method: MonoMethod, this_arg: MonoObject | undefined // check if the method signature needs argument mashalling if (needs_converter) { - converter = _compile_converter_for_marshal_string(args_marshal); + const classPtr = cwraps.mono_wasm_get_class_for_bind_or_invoke(this_arg, method); + if (!classPtr) + throw new Error (`Could not get class ptr for call_method with this (${this_arg}) and method (${method})`); - is_result_marshaled = _decide_if_result_is_marshaled(converter, args.length); + const typePtr = cwraps.mono_wasm_class_get_type(classPtr); + converter = _compile_converter_for_marshal_string(typePtr, method, args_marshal); + + is_result_marshaled = !converter.is_result_definitely_unmarshaled; argsRootBuffer = _get_args_root_buffer_for_method_call(converter, null); @@ -175,11 +179,11 @@ export function call_method(method: MonoMethod, this_arg: MonoObject | undefined export function _handle_exception_for_call( - converter: Converter | undefined, token: BoundMethodToken | null, - buffer: VoidPtr, resultRoot: WasmRoot, + converter: SignatureConverter | undefined, token: BoundMethodToken | null, + buffer: VoidPtr, resultRoot: WasmRoot, exceptionRoot: WasmRoot, argsRootBuffer?: WasmRootBuffer ): void { - const exc = _convert_exception_for_method_call(resultRoot.value, exceptionRoot.value); + const exc = _convert_exception_for_method_call(resultRoot.value, exceptionRoot.value); if (!exc) return; @@ -188,8 +192,8 @@ export function _handle_exception_for_call( } function _handle_exception_and_produce_result_for_call( - converter: Converter | undefined, token: BoundMethodToken | null, - buffer: VoidPtr, resultRoot: WasmRoot, + converter: SignatureConverter | undefined, token: BoundMethodToken | null, + buffer: VoidPtr, resultRoot: WasmRoot, exceptionRoot: WasmRoot, argsRootBuffer: WasmRootBuffer | undefined, is_result_marshaled: boolean ): any { @@ -205,7 +209,7 @@ function _handle_exception_and_produce_result_for_call( } export function _teardown_after_call( - converter: Converter | undefined, token: BoundMethodToken | null, + converter: SignatureConverter | undefined, token: BoundMethodToken | null, buffer: VoidPtr, resultRoot: WasmRoot, exceptionRoot: WasmRoot, argsRootBuffer?: WasmRootBuffer ): void { @@ -230,16 +234,16 @@ export function _teardown_after_call( } function _call_method_with_converted_args( - method: MonoMethod, this_arg: MonoObject, converter: Converter | undefined, + method: MonoMethod, this_arg: MonoObject, converter: SignatureConverter | undefined, token: BoundMethodToken | null, buffer: VoidPtr, is_result_marshaled: boolean, argsRootBuffer?: WasmRootBuffer ): any { - const resultRoot = mono_wasm_new_root(), exceptionRoot = mono_wasm_new_root(); - resultRoot.value = cwraps.mono_wasm_invoke_method(method, this_arg, buffer, exceptionRoot.get_address()); + const resultRoot = mono_wasm_new_root(), exceptionRoot = mono_wasm_new_root(); + resultRoot.value = cwraps.mono_wasm_invoke_method(method, this_arg, buffer, exceptionRoot.get_address()); return _handle_exception_and_produce_result_for_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer, is_result_marshaled); } -export function call_static_method(fqn: string, args: any[], signature: string/*ArgsMarshalString*/): any { +export function call_static_method(fqn: string, args: any[], signature: string): any { bindings_lazy_init();// TODO remove this once Blazor does better startup const method = mono_method_resolve(fqn); @@ -250,7 +254,7 @@ export function call_static_method(fqn: string, args: any[], signature: string/* return call_method(method, undefined, signature, args); } -export function mono_bind_static_method(fqn: string, signature?: string/*ArgsMarshalString*/): Function { +export function mono_bind_static_method(fqn: string, signature?: string): Function { bindings_lazy_init();// TODO remove this once Blazor does better startup const method = mono_method_resolve(fqn); @@ -261,7 +265,7 @@ export function mono_bind_static_method(fqn: string, signature?: string/*ArgsMar return mono_bind_method(method, null, signature!, fqn); } -export function mono_bind_assembly_entry_point(assembly: string, signature?: string/*ArgsMarshalString*/): Function { +export function mono_bind_assembly_entry_point(assembly: string, signature?: string): Function { bindings_lazy_init();// TODO remove this once Blazor does better startup const asm = cwraps.mono_wasm_assembly_load(assembly); @@ -282,7 +286,7 @@ export function mono_bind_assembly_entry_point(assembly: string, signature?: str }; } -export function mono_call_assembly_entry_point(assembly: string, args?: any[], signature?: string/*ArgsMarshalString*/): number { +export function mono_call_assembly_entry_point(assembly: string, args?: any[], signature?: string): number { if (!args) { args = [[]]; } @@ -471,7 +475,7 @@ export function wrap_error(is_exception: Int32Ptr | null, ex: any): MonoString { return js_string_to_mono_string(res)!; } -export function mono_method_get_call_signature(method: MonoMethod, mono_obj?: MonoObject): string/*ArgsMarshalString*/ { +export function mono_method_get_call_signature(method: MonoMethod, mono_obj?: MonoObject): string { const instanceRoot = mono_wasm_new_root(mono_obj); try { return call_method(runtimeHelpers.get_call_sig, undefined, "im", [method, instanceRoot.value]); diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index cc115366db6db1..fe65b912246253 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -1,15 +1,22 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { AllAssetEntryTypes, AssetEntry, CharPtrNull, DotnetModule, GlobalizationMode, MonoConfig, MonoConfigError, wasm_type_symbol } from "./types"; -import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, INTERNAL, locateFile, Module, MONO, runtimeHelpers } from "./imports"; +import { + AllAssetEntryTypes, AssetEntry, CharPtrNull, DotnetModule, + MonoConfig, MonoConfigError, wasm_type_symbol, GlobalizationMode +} from "./types"; +import { + ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, INTERNAL, + locateFile, Module, MONO, runtimeHelpers +} from "./imports"; import cwraps from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; import { mono_wasm_globalization_init, mono_wasm_load_icu_data } from "./icu"; import { toBase64StringImpl } from "./base64"; import { mono_wasm_init_aot_profiler, mono_wasm_init_coverage_profiler } from "./profiler"; import { mono_wasm_load_bytes_into_heap } from "./buffers"; -import { bind_runtime_method, get_method, _create_primitive_converters } from "./method-binding"; +import { bind_runtime_method, get_method } from "./method-binding"; +import { _custom_marshaler_name_table } from "./custom-marshaler"; import { find_corlib_class } from "./class-loader"; import { VoidPtr, CharPtr } from "./types/emscripten"; @@ -149,6 +156,12 @@ function _handle_fetched_asset(ctx: MonoInitContext, asset: AssetEntry, url: str } } +export function mono_wasm_register_custom_marshaler (aqn: string, marshalerAQN: string): void { + if (_custom_marshaler_name_table[aqn]) + throw new Error(`A custom marshaler for ${aqn} is already registered.`); + _custom_marshaler_name_table[aqn] = marshalerAQN; +} + function _apply_configuration_from_args(config: MonoConfig) { for (const k in (config.environment_variables || {})) mono_wasm_setenv(k, config.environment_variables![k]); @@ -161,6 +174,9 @@ function _apply_configuration_from_args(config: MonoConfig) { if (config.coverage_profiler_options) mono_wasm_init_coverage_profiler(config.coverage_profiler_options); + + if (config.custom_marshalers) + Object.assign(_custom_marshaler_name_table, config.custom_marshalers); } function finalize_startup(config: MonoConfig | MonoConfigError | undefined): void { @@ -296,8 +312,6 @@ export function bindings_lazy_init(): void { runtimeHelpers.get_call_sig = get_method("GetCallSignature"); if (!runtimeHelpers.get_call_sig) throw "Can't find GetCallSignature method"; - - _create_primitive_converters(); } // Initializes the runtime and loads assemblies, debug information, and other files. @@ -532,4 +546,4 @@ export type MonoInitContext = { loaded_assets: { [id: string]: [VoidPtr, number] }, createPath: Function, createDataFile: Function -} \ No newline at end of file +} diff --git a/src/mono/wasm/runtime/strings.ts b/src/mono/wasm/runtime/strings.ts index 2ac05a90e4d92d..02c3b1fa302e89 100644 --- a/src/mono/wasm/runtime/strings.ts +++ b/src/mono/wasm/runtime/strings.ts @@ -163,9 +163,9 @@ export function js_string_to_mono_string_interned(string: string | symbol): Mono return ptr; } -export function js_string_to_mono_string(string: string): MonoString | null { +export function js_string_to_mono_string(string: string): MonoString { if (string === null) - return null; + return MonoStringNull; else if (typeof (string) === "symbol") return js_string_to_mono_string_interned(string); else if (typeof (string) !== "string") diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 4e0ea4e7b3bef8..9e3aa1c42dae8f 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -62,7 +62,8 @@ export type MonoConfig = { runtime_options?: string[], // array of runtime options as strings aot_profiler_options?: AOTProfilerOptions, // dictionary-style Object. If omitted, aot profiler will not be initialized. coverage_profiler_options?: CoverageProfilerOptions, // dictionary-style Object. If omitted, coverage profiler will not be initialized. - ignore_pdb_load_errors?: boolean + ignore_pdb_load_errors?: boolean, + custom_marshalers?: { [key: string]: string | undefined }, }; export type MonoConfigError = { @@ -186,4 +187,71 @@ export type DotnetModuleConfigImports = { dirname?: (path: string) => string, }; url?: any; +} + + +// see src/mono/wasm/driver.c MARSHAL_TYPE_xxx and Runtime.cs MarshalType +export enum MarshalType { + NULL = 0, + INT = 1, + FP64 = 2, + STRING = 3, + VT = 4, + DELEGATE = 5, + TASK = 6, + OBJECT = 7, + BOOL = 8, + ENUM = 9, + URI = 22, + SAFEHANDLE = 23, + ARRAY_BYTE = 10, + ARRAY_UBYTE = 11, + ARRAY_UBYTE_C = 12, + ARRAY_SHORT = 13, + ARRAY_USHORT = 14, + ARRAY_INT = 15, + ARRAY_UINT = 16, + ARRAY_FLOAT = 17, + ARRAY_DOUBLE = 18, + FP32 = 24, + UINT32 = 25, + INT64 = 26, + UINT64 = 27, + CHAR = 28, + STRING_INTERNED = 29, + VOID = 30, + ENUM64 = 31, + POINTER = 32, + SPAN_BYTE = 33, +} + +// see src/mono/wasm/driver.c MARSHAL_ERROR_xxx and Runtime.cs +export enum MarshalError { + BUFFER_TOO_SMALL = 512, + NULL_CLASS_POINTER = 513, + NULL_TYPE_POINTER = 514, + UNSUPPORTED_TYPE = 515, + FIRST = BUFFER_TOO_SMALL +} + +export type MarshalTypeRecord = { + marshalType : MarshalType; + typePtr : MonoType; + signatureChar : string; +} + +export type MarshalSignatureInfo = { + typePtr : MonoType; + methodPtr : MonoMethod; + parameters : MarshalTypeRecord[]; +} + +export type CustomMarshalerInfo = { + typePtr : MonoType; + jsToInterchange? : string; + interchangeToJs? : string; + inputPtr? : MonoMethod; + outputPtr? : MonoMethod; + error? : string; + scratchBufferSize? : number; } \ No newline at end of file diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index e90f74a40109f4..e327f15b0f9f6c 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -46,18 +46,29 @@ public class WasmAppBuilder : Task public bool InvariantGlobalization { get; set; } public ITaskItem[]? ExtraFilesToDeploy { get; set; } - // - // Extra json elements to add to mono-config.json - // - // Metadata: - // - Value: can be a number, bool, quoted string, or json string - // - // Examples: - // - // - // - // - // + /// + /// A list of managed types along with their associated marshaler type + /// + /// Metadata: + /// - MarshalerType: the fully-qualified type name of the managed type marshaler for this type + /// + /// Examples: + /// + /// + public ITaskItem[]? MarshaledTypes { get; set; } + + /// + /// Extra json elements to add to mono-config.json + /// + /// Metadata: + /// - Value: can be a number, bool, quoted string, or json string + /// + /// Examples: + /// + /// + /// + /// + /// public ITaskItem[]? ExtraConfig { get; set; } private sealed class WasmAppConfig @@ -70,6 +81,8 @@ private sealed class WasmAppConfig public List Assets { get; } = new List(); [JsonPropertyName("remote_sources")] public List RemoteSources { get; set; } = new List(); + [JsonPropertyName("custom_marshalers")] + public Dictionary CustomMarshalers { get; set; } = new(); [JsonExtensionData] public Dictionary Extra { get; set; } = new(); } @@ -274,6 +287,12 @@ private bool ExecuteInternal () config.RemoteSources.Add(source.ItemSpec); } + if (MarshaledTypes?.Length > 0) + { + foreach (var mt in MarshaledTypes) + config.CustomMarshalers.Add(mt.ItemSpec, mt.GetMetadata("MarshalerType")); + } + foreach (ITaskItem extra in ExtraConfig ?? Enumerable.Empty()) { string name = extra.ItemSpec; From 9794c823ef77d9b1f02e7264af7443f533cf83e7 Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Mon, 20 Dec 2021 17:01:41 -0800 Subject: [PATCH 2/3] Exception handling fixes - prevent errors in _teardown_after_call from hiding other errors, and avoid double teardown when managed code throws --- .../InteropServices/JavaScript/Codegen.cs | 14 +++++++++++++- src/mono/wasm/runtime/method-calls.ts | 16 +++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Codegen.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Codegen.cs index ad4f1588554862..fa0f54299b73a7 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Codegen.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Codegen.cs @@ -440,6 +440,7 @@ public static void GenerateBoundMethod (BoundMethodBuilderState state) { output.AppendLine( " let argsRootBuffer = _get_args_root_buffer_for_method_call(converter, token);"); output.AppendLine( " let scratchBuffer = _get_buffer_for_method_call(converter, token);"); output.AppendLine( " let buffer = 0;"); + output.AppendLine( " let abnormalExit = true;"); output.AppendLine( " try {"); output.AppendLine($" buffer = converter_{state.MarshalString.Key}("); output.AppendLine( " scratchBuffer, argsRootBuffer, method,"); @@ -454,6 +455,7 @@ public static void GenerateBoundMethod (BoundMethodBuilderState state) { output.AppendLine(" resultRoot.value = invoke_method(method, 0, buffer, exceptionRoot.get_address());"); output.AppendLine(" _handle_exception_for_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer);"); + output.AppendLine(" abnormalExit = false;"); output.AppendLine(); if (state.MarshalString.RawReturnValue) @@ -464,7 +466,17 @@ public static void GenerateBoundMethod (BoundMethodBuilderState state) { GenerateFastUnboxBlock(state); output.AppendLine(" } finally {"); - output.AppendLine(" _teardown_after_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer);"); + // An error can occur during a managed method call, in which case we will hit the finally block without having fully + // cleaned up from the call. In this case, allowing _teardown_after_call to throw a new exception (due to corrupt + // state, etc) would silence the original exception that caused the failure, so we turn it into a log message + output.AppendLine(" if (abnormalExit) {"); + output.AppendLine(" try {"); + output.AppendLine(" _teardown_after_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer);"); + output.AppendLine(" } catch (exc) {"); + output.AppendLine(" console.error(`Unhandled error while tearing down after failed managed method call: ${exc}`);"); + output.AppendLine(" }"); + output.AppendLine(" } else "); + output.AppendLine(" _teardown_after_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer);"); output.AppendLine(" }"); output.AppendLine("};"); output.AppendLine(); diff --git a/src/mono/wasm/runtime/method-calls.ts b/src/mono/wasm/runtime/method-calls.ts index 299637a06eceff..b3e7e4f0c435d8 100644 --- a/src/mono/wasm/runtime/method-calls.ts +++ b/src/mono/wasm/runtime/method-calls.ts @@ -187,7 +187,6 @@ export function _handle_exception_for_call( if (!exc) return; - _teardown_after_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer); throw exc; } @@ -197,15 +196,18 @@ function _handle_exception_and_produce_result_for_call( exceptionRoot: WasmRoot, argsRootBuffer: WasmRootBuffer | undefined, is_result_marshaled: boolean ): any { - _handle_exception_for_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer); + try { + _handle_exception_for_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer); - let result: any = resultRoot.value; + let result: any = resultRoot.value; - if (is_result_marshaled) - result = _unbox_mono_obj_root(resultRoot); + if (is_result_marshaled) + result = _unbox_mono_obj_root(resultRoot); - _teardown_after_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer); - return result; + return result; + } finally { + _teardown_after_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer); + } } export function _teardown_after_call( From 85eb3497e0535609a8a8828d478ac49e90fda3cb Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Wed, 5 Jan 2022 21:02:01 -0800 Subject: [PATCH 3/3] Fix compiled signature converters not being cached --- src/mono/wasm/runtime/method-binding.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/mono/wasm/runtime/method-binding.ts b/src/mono/wasm/runtime/method-binding.ts index 2739c3058006d8..e58b75c1fda298 100644 --- a/src/mono/wasm/runtime/method-binding.ts +++ b/src/mono/wasm/runtime/method-binding.ts @@ -299,6 +299,19 @@ export function _compile_converter_for_marshal_string(typePtr: MonoType, method: const js = _generate_args_marshaler(typePtr, method, args_marshal); const csFunc = new Function("get_api", "get_type_converter", js); csFuncResult = csFunc(_get_api, _pick_automatic_converter_for_type); + + if (csFuncResult.contains_auto) { + let map = >_signature_converters.get(args_marshal); + if (!map) { + map = new Map(); + _signature_converters.set(args_marshal, map); + } + + map.set(method, csFuncResult); + } else { + _signature_converters.set(args_marshal, csFuncResult); + } + return csFuncResult; }