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..fa0f54299b73a7 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Codegen.cs @@ -0,0 +1,486 @@ +// 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( " let abnormalExit = true;"); + 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(" abnormalExit = false;"); + 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 {"); + // 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(); + 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..e58b75c1fda298 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,284 @@ 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; +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; - 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}'`); + 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); + + if (csFuncResult.contains_auto) { + let map = >_signature_converters.get(args_marshal); + if (!map) { + map = new Map(); + _signature_converters.set(args_marshal, map); + } - _maybe_produce_signature_warning(converter); - return false; + map.set(method, csFuncResult); } else { - if (argc < converter.steps.length) - throw new Error(`Expected ${converter.steps.length} argument(s) but got ${argc} for signature '${converter.args_marshal}'`); - - return !converter.is_result_definitely_unmarshaled; + _signature_converters.set(args_marshal, csFuncResult); } + + 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) - ? "" - : ", " - ) - ); - } - - 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;"); - } + let typePtr : MonoType = MonoTypeNull; - // 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 +379,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..b3e7e4f0c435d8 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,37 +179,39 @@ 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; - _teardown_after_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer); throw exc; } 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 { - _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( - converter: Converter | undefined, token: BoundMethodToken | null, + converter: SignatureConverter | undefined, token: BoundMethodToken | null, buffer: VoidPtr, resultRoot: WasmRoot, exceptionRoot: WasmRoot, argsRootBuffer?: WasmRootBuffer ): void { @@ -230,16 +236,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 +256,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 +267,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 +288,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 +477,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;